Skip to content

Commit 1c176a2

Browse files
authored
Merge pull request #816 from nteract/integration-7
Improve temporal custom charts
2 parents 83829f1 + 5566cab commit 1c176a2

27 files changed

Lines changed: 463 additions & 104 deletions

.clinerules

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -326,6 +326,11 @@ Charts render with `role="group"` (outer interactive wrapper, keyboard/focus) an
326326

327327
## Performance
328328

329+
- **scalePadding**: Pixel inset on scale ranges to prevent glyph clipping at chart edges. Insets both X and Y ranges — edge data points map to N pixels from the chart boundary instead of 0px. Domain and tick values unchanged (rendered positions shift inward with the range). On `StreamXYFrame`, use `scalePadding` directly; on HOC charts, pass via `frameProps`:
330+
```jsx
331+
<StreamXYFrame chartType="candlestick" scalePadding={12} ... />
332+
<Scatterplot data={data} pointRadius={8} frameProps={{ scalePadding: 12 }} />
333+
```
329334
- **Range/dumbbell plot**: Use `chartType="candlestick"` on StreamXYFrame with only `highAccessor` + `lowAccessor` (omit `openAccessor`/`closeAccessor`). Auto-detects range mode: no body rect, endpoint dots, single `rangeColor` via `candlestickStyle={{ rangeColor: "#6366f1" }}`. When `bodyWidth === 0`, body rect is skipped entirely (no invisible DOM elements).
330335

331336
## Performance

.cursorrules

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -326,6 +326,11 @@ Charts render with `role="group"` (outer interactive wrapper, keyboard/focus) an
326326

327327
## Performance
328328

329+
- **scalePadding**: Pixel inset on scale ranges to prevent glyph clipping at chart edges. Insets both X and Y ranges — edge data points map to N pixels from the chart boundary instead of 0px. Domain and tick values unchanged (rendered positions shift inward with the range). On `StreamXYFrame`, use `scalePadding` directly; on HOC charts, pass via `frameProps`:
330+
```jsx
331+
<StreamXYFrame chartType="candlestick" scalePadding={12} ... />
332+
<Scatterplot data={data} pointRadius={8} frameProps={{ scalePadding: 12 }} />
333+
```
329334
- **Range/dumbbell plot**: Use `chartType="candlestick"` on StreamXYFrame with only `highAccessor` + `lowAccessor` (omit `openAccessor`/`closeAccessor`). Auto-detects range mode: no body rect, endpoint dots, single `rangeColor` via `candlestickStyle={{ rangeColor: "#6366f1" }}`. When `bodyWidth === 0`, body rect is skipped entirely (no invisible DOM elements).
330335

331336
## Performance

.github/copilot-instructions.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -326,6 +326,11 @@ Charts render with `role="group"` (outer interactive wrapper, keyboard/focus) an
326326

327327
## Performance
328328

329+
- **scalePadding**: Pixel inset on scale ranges to prevent glyph clipping at chart edges. Insets both X and Y ranges — edge data points map to N pixels from the chart boundary instead of 0px. Domain and tick values unchanged (rendered positions shift inward with the range). On `StreamXYFrame`, use `scalePadding` directly; on HOC charts, pass via `frameProps`:
330+
```jsx
331+
<StreamXYFrame chartType="candlestick" scalePadding={12} ... />
332+
<Scatterplot data={data} pointRadius={8} frameProps={{ scalePadding: 12 }} />
333+
```
329334
- **Range/dumbbell plot**: Use `chartType="candlestick"` on StreamXYFrame with only `highAccessor` + `lowAccessor` (omit `openAccessor`/`closeAccessor`). Auto-detects range mode: no body rect, endpoint dots, single `rangeColor` via `candlestickStyle={{ rangeColor: "#6366f1" }}`. When `bodyWidth === 0`, body rect is skipped entirely (no invisible DOM elements).
330335

331336
## Performance

.windsurfrules

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -326,6 +326,11 @@ Charts render with `role="group"` (outer interactive wrapper, keyboard/focus) an
326326

327327
## Performance
328328

329+
- **scalePadding**: Pixel inset on scale ranges to prevent glyph clipping at chart edges. Insets both X and Y ranges — edge data points map to N pixels from the chart boundary instead of 0px. Domain and tick values unchanged (rendered positions shift inward with the range). On `StreamXYFrame`, use `scalePadding` directly; on HOC charts, pass via `frameProps`:
330+
```jsx
331+
<StreamXYFrame chartType="candlestick" scalePadding={12} ... />
332+
<Scatterplot data={data} pointRadius={8} frameProps={{ scalePadding: 12 }} />
333+
```
329334
- **Range/dumbbell plot**: Use `chartType="candlestick"` on StreamXYFrame with only `highAccessor` + `lowAccessor` (omit `openAccessor`/`closeAccessor`). Auto-detects range mode: no body rect, endpoint dots, single `rangeColor` via `candlestickStyle={{ rangeColor: "#6366f1" }}`. When `bodyWidth === 0`, body rect is skipped entirely (no invisible DOM elements).
330335

331336
## Performance

CLAUDE.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -326,6 +326,11 @@ Charts render with `role="group"` (outer interactive wrapper, keyboard/focus) an
326326

327327
## Performance
328328

329+
- **scalePadding**: Pixel inset on scale ranges to prevent glyph clipping at chart edges. Insets both X and Y ranges — edge data points map to N pixels from the chart boundary instead of 0px. Domain and tick values unchanged (rendered positions shift inward with the range). On `StreamXYFrame`, use `scalePadding` directly; on HOC charts, pass via `frameProps`:
330+
```jsx
331+
<StreamXYFrame chartType="candlestick" scalePadding={12} ... />
332+
<Scatterplot data={data} pointRadius={8} frameProps={{ scalePadding: 12 }} />
333+
```
329334
- **Range/dumbbell plot**: Use `chartType="candlestick"` on StreamXYFrame with only `highAccessor` + `lowAccessor` (omit `openAccessor`/`closeAccessor`). Auto-detects range mode: no body rect, endpoint dots, single `rangeColor` via `candlestickStyle={{ rangeColor: "#6366f1" }}`. When `bodyWidth === 0`, body rect is skipped entirely (no invisible DOM elements).
330335

331336
## Performance

docs/public/llms-full.txt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -326,6 +326,11 @@ Charts render with `role="group"` (outer interactive wrapper, keyboard/focus) an
326326

327327
## Performance
328328

329+
- **scalePadding**: Pixel inset on scale ranges to prevent glyph clipping at chart edges. Insets both X and Y ranges — edge data points map to N pixels from the chart boundary instead of 0px. Domain and tick values unchanged (rendered positions shift inward with the range). On `StreamXYFrame`, use `scalePadding` directly; on HOC charts, pass via `frameProps`:
330+
```jsx
331+
<StreamXYFrame chartType="candlestick" scalePadding={12} ... />
332+
<Scatterplot data={data} pointRadius={8} frameProps={{ scalePadding: 12 }} />
333+
```
329334
- **Range/dumbbell plot**: Use `chartType="candlestick"` on StreamXYFrame with only `highAccessor` + `lowAccessor` (omit `openAccessor`/`closeAccessor`). Auto-detects range mode: no body rect, endpoint dots, single `rangeColor` via `candlestickStyle={{ rangeColor: "#6366f1" }}`. When `bodyWidth === 0`, body rect is skipped entirely (no invisible DOM elements).
330335

331336
## Performance

docs/src/examples/CandlestickChart.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -86,10 +86,11 @@ const frameProps = {
8686
lowAccessor: "low",
8787
closeAccessor: "close",
8888
candlestickStyle: {
89-
upColor: theme[2],
90-
downColor: theme[1],
89+
upColor: "#4caf50",
90+
downColor: "#e45050",
9191
wickColor: "#999",
9292
},
93+
scalePadding: 12,
9394
size: [700, 400],
9495
margin: { left: 80, bottom: 50, right: 10, top: 40 },
9596
showAxes: true,

docs/src/pages/charts/GaugeChartPage.js

Lines changed: 71 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -34,28 +34,60 @@ const gaugeProps = [
3434
]
3535

3636
function StreamingGaugeDemo() {
37+
// NYT-style election needle: value drifts around 50 with occasional swings
3738
const [value, setValue] = useState(50)
3839
useEffect(() => {
3940
const id = setInterval(() => {
40-
setValue(v => Math.max(0, Math.min(100, v + (Math.random() - 0.45) * 8)))
41-
}, 500)
41+
setValue(v => {
42+
const drift = (Math.random() - 0.48) * 3
43+
const revert = (52 - v) * 0.02
44+
return Math.max(40, Math.min(60, v + drift + revert))
45+
})
46+
}, 400)
4247
return () => clearInterval(id)
4348
}, [])
49+
50+
const lead = value - 50
51+
const absLead = Math.abs(lead)
52+
const leaderLabel = absLead < 2 ? "Toss-up" : lead > 0 ? "Candidate B" : "Candidate A"
53+
const leaderColor = absLead < 2 ? "#888" : lead > 0 ? "#4575b4" : "#d73027"
54+
4455
return (
45-
<div style={{ display: "flex", alignItems: "center", gap: 16, marginBottom: 16 }}>
56+
<div style={{ textAlign: "center", marginBottom: 16, display: "flex", flexDirection: "column", alignItems: "center" }}>
57+
<div style={{ fontSize: 11, textTransform: "uppercase", letterSpacing: 1, color: "var(--semiotic-text-secondary, #888)", marginBottom: 4 }}>
58+
Estimated chance of winning
59+
</div>
4660
<GaugeChart
4761
value={value}
48-
max={100}
62+
min={40}
63+
max={60}
64+
sweep={180}
65+
arcWidth={0.15}
66+
fillZones={false}
67+
showNeedle={true}
68+
needleColor="#222"
69+
showScaleLabels={false}
4970
thresholds={[
50-
{ value: 50, color: "#4caf50", label: "Normal" },
51-
{ value: 80, color: "#ff9800", label: "Warning" },
52-
{ value: 100, color: "#f44336", label: "Critical" },
71+
{ value: 45, color: "#d73027" },
72+
{ value: 48, color: "#fc8d59" },
73+
{ value: 52, color: "#ccc" },
74+
{ value: 55, color: "#91bfdb" },
75+
{ value: 60, color: "#4575b4" },
5376
]}
54-
width={250}
77+
centerContent={
78+
<div style={{ textAlign: "center", lineHeight: 1.1 }}>
79+
<div style={{ fontSize: 28, fontWeight: 700, color: leaderColor }}>
80+
{absLead < 2 ? "Even" : `+${absLead.toFixed(1)}`}
81+
</div>
82+
<div style={{ fontSize: 11, color: leaderColor, fontWeight: 600 }}>{leaderLabel}</div>
83+
</div>
84+
}
85+
width={360}
5586
height={200}
5687
/>
57-
<div style={{ fontSize: 12, color: "var(--semiotic-text-secondary, #666)" }}>
58-
Live value: <strong>{Math.round(value)}</strong>
88+
<div style={{ display: "flex", justifyContent: "space-between", width: 360, fontSize: 11, color: "var(--semiotic-text-secondary, #888)", padding: "4px 40px 0" }}>
89+
<span style={{ color: "#d73027", fontWeight: 600 }}>← Candidate A</span>
90+
<span style={{ color: "#4575b4", fontWeight: 600 }}>Candidate B →</span>
5991
</div>
6092
</div>
6193
)
@@ -284,24 +316,46 @@ export default function GaugeChartPage() {
284316
language="jsx"
285317
/>
286318

287-
<h3 id="streaming">Streaming Updates</h3>
319+
<h3 id="streaming">Streaming — Election Needle</h3>
288320
<p>
289-
Update the <code>value</code> prop to animate the gauge. The example
290-
below simulates a live metric that fluctuates every 500ms.
321+
The gauge below simulates a live election forecast in the style of the
322+
NYT needle. The value drifts around 50% with a slight lean,
323+
updating every 400ms. The diverging color scheme runs from red
324+
(Candidate A) through a grey toss-up zone to blue (Candidate B).
325+
Only the needle moves — the threshold zones stay fixed.
291326
</p>
292327

293328
<StreamingGaugeDemo />
294329

295330
<CodeBlock
296-
code={`function LiveGauge() {
331+
code={`function ElectionNeedle() {
297332
const [value, setValue] = useState(50)
298333
useEffect(() => {
299334
const id = setInterval(() => {
300-
setValue(v => Math.max(0, Math.min(100, v + (Math.random() - 0.45) * 8)))
301-
}, 500)
335+
setValue(v => {
336+
const drift = (Math.random() - 0.48) * 3
337+
const revert = (52 - v) * 0.02
338+
return Math.max(40, Math.min(60, v + drift + revert))
339+
})
340+
}, 400)
302341
return () => clearInterval(id)
303342
}, [])
304-
return <GaugeChart value={value} thresholds={[...]} />
343+
344+
return (
345+
<GaugeChart
346+
value={value} min={40} max={60} sweep={180}
347+
arcWidth={0.15} needleColor="#222"
348+
fillZones={false} // zones stay fixed, only needle moves
349+
thresholds={[
350+
{ value: 45, color: "#d73027" }, // strong A (dark red)
351+
{ value: 48, color: "#fc8d59" }, // lean A (light red)
352+
{ value: 52, color: "#ccc" }, // toss-up (grey)
353+
{ value: 55, color: "#91bfdb" }, // lean B (light blue)
354+
{ value: 60, color: "#4575b4" }, // strong B (dark blue)
355+
]}
356+
centerContent={...} // +{abs(value-50)} colored by leader
357+
/>
358+
)
305359
}`}
306360
language="jsx"
307361
/>

docs/src/pages/cookbook/CandlestickChartPage.js

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -95,8 +95,8 @@ function StreamingCandlestickDemo({ width }) {
9595
lowAccessor="low"
9696
closeAccessor="close"
9797
candlestickStyle={{
98-
upColor: "#4daf4a",
99-
downColor: "#e41a1c",
98+
upColor: "#4caf50",
99+
downColor: "#e45050",
100100
wickColor: "#999",
101101
}}
102102
windowSize={40}
@@ -160,9 +160,10 @@ export default function CandlestickChartPage() {
160160
highAccessor="high"
161161
lowAccessor="low"
162162
closeAccessor="close"
163+
scalePadding={12} // prevents edge candles from clipping
163164
candlestickStyle={{
164-
upColor: "#4daf4a", // close >= open
165-
downColor: "#e41a1c", // close < open
165+
upColor: "#4caf50", // close >= open
166+
downColor: "#e45050", // close < open
166167
wickColor: "#999",
167168
}}
168169
/>`}
@@ -262,6 +263,15 @@ export default function CandlestickChartPage() {
262263
range mode: no body rect, endpoint dots, and a single{" "}
263264
<code>rangeColor</code> instead of up/down coloring.
264265
</p>
266+
<p>
267+
Both examples on this page use <code>scalePadding</code> to prevent
268+
the edge glyphs (candle bodies, dumbbell dots) from being clipped at
269+
the chart boundary. <code>scalePadding={`{12}`}</code> insets the X and Y
270+
scale ranges by 12 pixels on each side — the data domain and tick
271+
values are unchanged, but their rendered positions shift inward
272+
because the pixel range is inset, so marks at the min/max data
273+
values have room to render fully.
274+
</p>
265275

266276
<div style={{ marginBottom: 24 }}>
267277
<StreamXYFrame
@@ -281,6 +291,7 @@ export default function CandlestickChartPage() {
281291
xAccessor="day"
282292
highAccessor="high"
283293
lowAccessor="low"
294+
scalePadding={12}
284295
candlestickStyle={{ rangeColor: "#6366f1", wickWidth: 2 }}
285296
showAxes={true}
286297
enableHover={true}
@@ -304,6 +315,7 @@ export default function CandlestickChartPage() {
304315
xAccessor="day"
305316
highAccessor="high"
306317
lowAccessor="low"
318+
scalePadding={12} // prevents edge glyph clipping
307319
candlestickStyle={{ rangeColor: "#6366f1", wickWidth: 2 }}
308320
showAxes enableHover
309321
tooltipContent={d => (

docs/src/process.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,7 @@ export const processNodes = [
152152
"amplitude",
153153
"runtimeMode",
154154
"extentPadding",
155+
"scalePadding",
155156
"nodeIDAccessor",
156157
"nodeIdAccessor",
157158
"sourceAccessor",

0 commit comments

Comments
 (0)