@coinbase/cds-mcp-server 8.47.1 → 8.47.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +8 -0
- package/mcp-docs/mobile/components/BarChart.txt +8 -8
- package/mcp-docs/mobile/components/CartesianChart.txt +85 -30
- package/mcp-docs/mobile/components/LineChart.txt +16 -16
- package/mcp-docs/mobile/components/MessagingCard.txt +18 -11
- package/mcp-docs/mobile/components/Numpad.txt +2 -2
- package/mcp-docs/mobile/components/Point.txt +2 -2
- package/mcp-docs/mobile/components/ReferenceLine.txt +151 -65
- package/mcp-docs/mobile/components/Scrubber.txt +12 -19
- package/mcp-docs/mobile/components/Select.txt +1 -1
- package/mcp-docs/mobile/components/SelectAlpha.txt +1 -1
- package/mcp-docs/mobile/components/SelectOption.txt +1 -1
- package/mcp-docs/mobile/components/SlideButton.txt +1 -1
- package/mcp-docs/mobile/components/SparklineInteractive.txt +239 -46
- package/mcp-docs/mobile/components/SparklineInteractiveHeader.txt +55 -13
- package/mcp-docs/mobile/components/XAxis.txt +4 -5
- package/mcp-docs/mobile/components/YAxis.txt +2 -2
- package/mcp-docs/mobile/getting-started/theming.txt +1 -1
- package/mcp-docs/web/components/BarChart.txt +40 -48
- package/mcp-docs/web/components/Carousel.txt +2 -2
- package/mcp-docs/web/components/CartesianChart.txt +82 -45
- package/mcp-docs/web/components/Combobox.txt +61 -61
- package/mcp-docs/web/components/LineChart.txt +87 -110
- package/mcp-docs/web/components/MediaQueryProvider.txt +10 -2
- package/mcp-docs/web/components/MessagingCard.txt +21 -12
- package/mcp-docs/web/components/PeriodSelector.txt +57 -39
- package/mcp-docs/web/components/Point.txt +3 -3
- package/mcp-docs/web/components/ReferenceLine.txt +341 -279
- package/mcp-docs/web/components/Scrubber.txt +48 -52
- package/mcp-docs/web/components/SelectChipAlpha.txt +1 -1
- package/mcp-docs/web/components/SparklineInteractive.txt +399 -54
- package/mcp-docs/web/components/SparklineInteractiveHeader.txt +368 -28
- package/mcp-docs/web/components/TabbedChipsAlpha.txt +1 -1
- package/mcp-docs/web/components/XAxis.txt +5 -6
- package/mcp-docs/web/components/YAxis.txt +2 -2
- package/mcp-docs/web/getting-started/theming.txt +1 -1
- package/mcp-docs/web/hooks/useBreakpoints.txt +5 -4
- package/mcp-docs/web/hooks/useMediaQuery.txt +10 -2
- package/package.json +1 -1
|
@@ -14,10 +14,6 @@ import { ReferenceLine } from '@coinbase/cds-web-visualization'
|
|
|
14
14
|
|
|
15
15
|
ReferenceLine can be used to add important details to a chart, such as a reference price or date. You can create horizontal lines using `dataY` or vertical lines using `dataX`.
|
|
16
16
|
|
|
17
|
-
#### Simple Reference Line
|
|
18
|
-
|
|
19
|
-
A minimal reference line without labels, useful for marking key thresholds:
|
|
20
|
-
|
|
21
17
|
```jsx live
|
|
22
18
|
<LineChart
|
|
23
19
|
showArea
|
|
@@ -178,73 +174,159 @@ Use `labelBoundsInset` to prevent labels from getting too close to chart edges.
|
|
|
178
174
|
</Box>
|
|
179
175
|
```
|
|
180
176
|
|
|
181
|
-
#### Custom
|
|
177
|
+
#### Custom Components
|
|
182
178
|
|
|
183
179
|
You can adjust the style of the label using a custom `LabelComponent`.
|
|
184
180
|
|
|
185
181
|
```jsx live
|
|
186
|
-
function
|
|
187
|
-
const
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
[]
|
|
202
|
-
|
|
182
|
+
function CustomLabelExample() {
|
|
183
|
+
const PriceLabel = memo((props) => (
|
|
184
|
+
<DefaultReferenceLineLabel
|
|
185
|
+
{...props}
|
|
186
|
+
background="var(--color-bgSecondary)"
|
|
187
|
+
borderRadius={12.5}
|
|
188
|
+
color="var(--color-fg)"
|
|
189
|
+
inset={{ top: 4, bottom: 4, left: 8, right: 8 }}
|
|
190
|
+
font="label1"
|
|
191
|
+
/>
|
|
192
|
+
));
|
|
193
|
+
|
|
194
|
+
function Example() {
|
|
195
|
+
const hourData = useMemo(() => sparklineInteractiveData.hour, []);
|
|
196
|
+
const startPrice = hourData[0].value;
|
|
197
|
+
const endPrice = hourData[hourData.length - 1].value;
|
|
198
|
+
const isPositive = endPrice >= startPrice;
|
|
199
|
+
const seriesColor = isPositive ? 'var(--color-fgPositive)' : 'var(--color-fgNegative)';
|
|
200
|
+
|
|
201
|
+
const formattedStartPrice = useMemo(
|
|
202
|
+
() =>
|
|
203
|
+
startPrice.toLocaleString('en-US', {
|
|
204
|
+
minimumFractionDigits: 2,
|
|
205
|
+
maximumFractionDigits: 2,
|
|
206
|
+
}),
|
|
207
|
+
[startPrice],
|
|
208
|
+
);
|
|
203
209
|
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
210
|
+
return (
|
|
211
|
+
<LineChart
|
|
212
|
+
enableScrubbing
|
|
213
|
+
showArea
|
|
214
|
+
areaType="dotted"
|
|
215
|
+
height={{ base: 200, tablet: 250, desktop: 300 }}
|
|
216
|
+
series={[
|
|
217
|
+
{
|
|
218
|
+
id: 'hourly-prices',
|
|
219
|
+
data: hourData.map((d) => d.value),
|
|
220
|
+
color: seriesColor,
|
|
221
|
+
},
|
|
222
|
+
]}
|
|
223
|
+
xAxis={{
|
|
224
|
+
range: ({ min, max }) => ({ min, max: max - 24 }),
|
|
225
|
+
}}
|
|
226
|
+
>
|
|
227
|
+
<Scrubber />
|
|
228
|
+
<ReferenceLine
|
|
229
|
+
LabelComponent={PriceLabel}
|
|
230
|
+
LineComponent={(props) => (
|
|
231
|
+
<DottedLine {...props} strokeDasharray="0 16" strokeWidth={3} />
|
|
232
|
+
)}
|
|
233
|
+
dataY={startPrice}
|
|
234
|
+
label={formattedStartPrice}
|
|
235
|
+
stroke="var(--color-fgMuted)"
|
|
236
|
+
labelDx={-12}
|
|
237
|
+
labelHorizontalAlignment="right"
|
|
216
238
|
/>
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
239
|
+
</LineChart>
|
|
240
|
+
);
|
|
241
|
+
}
|
|
220
242
|
|
|
221
|
-
return
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
243
|
+
return <Example />;
|
|
244
|
+
}
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
You can also optionally hide the label based on user scrubbing.
|
|
248
|
+
|
|
249
|
+
```jsx live
|
|
250
|
+
function StartPriceReferenceLine() {
|
|
251
|
+
const PriceLabel = memo((props) => {
|
|
252
|
+
const { scrubberPosition } = useScrubberContext();
|
|
253
|
+
const { getXScale, drawingArea } = useCartesianChartContext();
|
|
254
|
+
const isScrubbing = scrubberPosition !== undefined;
|
|
255
|
+
|
|
256
|
+
const fadeZone = 128;
|
|
257
|
+
|
|
258
|
+
const opacity = useMemo(() => {
|
|
259
|
+
if (!isScrubbing) return 0;
|
|
260
|
+
const xScale = getXScale();
|
|
261
|
+
if (!xScale) return 1;
|
|
262
|
+
const scrubX = xScale(scrubberPosition) ?? 0;
|
|
263
|
+
const rightEdge = drawingArea.x + drawingArea.width;
|
|
264
|
+
return rightEdge - scrubX >= fadeZone ? 1 : 0;
|
|
265
|
+
}, [isScrubbing, scrubberPosition, getXScale, drawingArea]);
|
|
266
|
+
|
|
267
|
+
return (
|
|
268
|
+
<DefaultReferenceLineLabel
|
|
269
|
+
{...props}
|
|
270
|
+
background="var(--color-bgSecondary)"
|
|
271
|
+
borderRadius={12.5}
|
|
272
|
+
color="var(--color-fg)"
|
|
273
|
+
inset={{ top: 4, bottom: 4, left: 8, right: 8 }}
|
|
274
|
+
font="label1"
|
|
275
|
+
styles={{ root: { opacity: opacity, transition: 'opacity 0.25s ease' } }}
|
|
245
276
|
/>
|
|
246
|
-
|
|
247
|
-
);
|
|
277
|
+
);
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
function Example() {
|
|
281
|
+
const hourData = useMemo(() => sparklineInteractiveData.hour, []);
|
|
282
|
+
const startPrice = hourData[0].value;
|
|
283
|
+
const endPrice = hourData[hourData.length - 1].value;
|
|
284
|
+
const isPositive = endPrice >= startPrice;
|
|
285
|
+
const seriesColor = isPositive ? 'var(--color-fgPositive)' : 'var(--color-fgNegative)';
|
|
286
|
+
|
|
287
|
+
const formattedStartPrice = useMemo(
|
|
288
|
+
() =>
|
|
289
|
+
startPrice.toLocaleString('en-US', {
|
|
290
|
+
minimumFractionDigits: 2,
|
|
291
|
+
maximumFractionDigits: 2,
|
|
292
|
+
}),
|
|
293
|
+
[startPrice],
|
|
294
|
+
);
|
|
295
|
+
|
|
296
|
+
return (
|
|
297
|
+
<LineChart
|
|
298
|
+
enableScrubbing
|
|
299
|
+
showArea
|
|
300
|
+
areaType="dotted"
|
|
301
|
+
height={{ base: 200, tablet: 250, desktop: 300 }}
|
|
302
|
+
series={[
|
|
303
|
+
{
|
|
304
|
+
id: 'hourly-prices',
|
|
305
|
+
data: hourData.map((d) => d.value),
|
|
306
|
+
color: seriesColor,
|
|
307
|
+
},
|
|
308
|
+
]}
|
|
309
|
+
xAxis={{
|
|
310
|
+
range: ({ min, max }) => ({ min, max: max - 24 }),
|
|
311
|
+
}}
|
|
312
|
+
>
|
|
313
|
+
<Scrubber />
|
|
314
|
+
<ReferenceLine
|
|
315
|
+
LabelComponent={PriceLabel}
|
|
316
|
+
LineComponent={(props) => (
|
|
317
|
+
<DottedLine {...props} strokeDasharray="0 16" strokeWidth={3} />
|
|
318
|
+
)}
|
|
319
|
+
dataY={startPrice}
|
|
320
|
+
label={formattedStartPrice}
|
|
321
|
+
stroke="var(--color-fgMuted)"
|
|
322
|
+
labelDx={-12}
|
|
323
|
+
labelHorizontalAlignment="right"
|
|
324
|
+
/>
|
|
325
|
+
</LineChart>
|
|
326
|
+
);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
return <Example />;
|
|
248
330
|
}
|
|
249
331
|
```
|
|
250
332
|
|
|
@@ -252,12 +334,10 @@ function LabelStyleExample() {
|
|
|
252
334
|
|
|
253
335
|
You can pair a ReferenceLine with a custom drag component to create a draggable price target.
|
|
254
336
|
|
|
255
|
-
```
|
|
337
|
+
```tsx live
|
|
256
338
|
function DraggablePriceTarget() {
|
|
257
|
-
const DragIcon = ({ x, y }
|
|
258
|
-
const DragCircle = (props
|
|
259
|
-
<circle {...props} fill="var(--color-fg)" r="1.5" />
|
|
260
|
-
);
|
|
339
|
+
const DragIcon = ({ x, y }) => {
|
|
340
|
+
const DragCircle = (props) => <circle {...props} fill="var(--color-fg)" r="1.5" />;
|
|
261
341
|
|
|
262
342
|
return (
|
|
263
343
|
<g transform={`translate(${x}, ${y})`}>
|
|
@@ -273,17 +353,7 @@ function DraggablePriceTarget() {
|
|
|
273
353
|
);
|
|
274
354
|
};
|
|
275
355
|
|
|
276
|
-
const TrendArrowIcon = ({
|
|
277
|
-
x,
|
|
278
|
-
y,
|
|
279
|
-
isPositive,
|
|
280
|
-
color,
|
|
281
|
-
}: {
|
|
282
|
-
x: number;
|
|
283
|
-
y: number;
|
|
284
|
-
isPositive: boolean;
|
|
285
|
-
color: string;
|
|
286
|
-
}) => {
|
|
356
|
+
const TrendArrowIcon = ({ x, y, isPositive, color }) => {
|
|
287
357
|
return (
|
|
288
358
|
<g transform={`translate(${x - 8}, ${y - 8})`}>
|
|
289
359
|
<g
|
|
@@ -302,221 +372,209 @@ function DraggablePriceTarget() {
|
|
|
302
372
|
);
|
|
303
373
|
};
|
|
304
374
|
|
|
305
|
-
const DynamicPriceLabel = memo(
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
),
|
|
318
|
-
);
|
|
375
|
+
const DynamicPriceLabel = memo(({ color, ...props }) => (
|
|
376
|
+
<DefaultReferenceLineLabel
|
|
377
|
+
{...props}
|
|
378
|
+
background={color}
|
|
379
|
+
borderRadius={4}
|
|
380
|
+
color="white"
|
|
381
|
+
dx={-12}
|
|
382
|
+
font="label1"
|
|
383
|
+
horizontalAlignment="right"
|
|
384
|
+
inset={{ top: 5, bottom: 5, left: 10, right: 10 }}
|
|
385
|
+
/>
|
|
386
|
+
));
|
|
319
387
|
|
|
320
|
-
const DraggableReferenceLine = memo(
|
|
321
|
-
(
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
})
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
}, []);
|
|
388
|
+
const DraggableReferenceLine = memo(({ baselineAmount, startAmount, chartRef }) => {
|
|
389
|
+
const theme = useTheme();
|
|
390
|
+
const { isPhone } = useBreakpoints();
|
|
391
|
+
|
|
392
|
+
const formatPrice = useCallback((value) => {
|
|
393
|
+
return `$${value.toLocaleString('en-US', {
|
|
394
|
+
minimumFractionDigits: 2,
|
|
395
|
+
maximumFractionDigits: 2,
|
|
396
|
+
})}`;
|
|
397
|
+
}, []);
|
|
398
|
+
|
|
399
|
+
const { getYScale, drawingArea } = useCartesianChartContext();
|
|
400
|
+
const [amount, setAmount] = useState(startAmount);
|
|
401
|
+
const [isDragging, setIsDragging] = useState(false);
|
|
402
|
+
const [textDimensions, setTextDimensions] = useState({ width: 0, height: 0 });
|
|
403
|
+
const color = amount >= baselineAmount ? 'var(--color-bgPositive)' : 'var(--color-bgNegative)';
|
|
404
|
+
|
|
405
|
+
const yScale = getYScale();
|
|
339
406
|
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
407
|
+
const labelComponent = useCallback(
|
|
408
|
+
(props) => <DynamicPriceLabel {...props} color={color} />,
|
|
409
|
+
[color],
|
|
410
|
+
);
|
|
411
|
+
|
|
412
|
+
// Set up persistent event listeners on the chart SVG element
|
|
413
|
+
useEffect(() => {
|
|
414
|
+
const element = chartRef.current;
|
|
415
|
+
|
|
416
|
+
if (!element || !yScale || !('invert' in yScale && typeof yScale.invert === 'function')) {
|
|
417
|
+
return;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
const updatePosition = (clientX, clientY) => {
|
|
421
|
+
const point = element.createSVGPoint();
|
|
422
|
+
point.x = clientX;
|
|
423
|
+
point.y = clientY;
|
|
424
|
+
|
|
425
|
+
const svgPoint = point.matrixTransform(element.getScreenCTM()?.inverse());
|
|
426
|
+
|
|
427
|
+
// Clamp the Y position to the chart area
|
|
428
|
+
const clampedY = Math.max(
|
|
429
|
+
drawingArea.y,
|
|
430
|
+
Math.min(drawingArea.y + drawingArea.height, svgPoint.y),
|
|
431
|
+
);
|
|
432
|
+
|
|
433
|
+
const rawAmount = yScale.invert(clampedY);
|
|
345
434
|
|
|
346
|
-
|
|
435
|
+
const rawPercentage = ((rawAmount - baselineAmount) / baselineAmount) * 100;
|
|
347
436
|
|
|
348
|
-
|
|
349
|
-
(props: React.ComponentProps<typeof DefaultReferenceLineLabel>) => (
|
|
350
|
-
<DynamicPriceLabel {...props} color={color} />
|
|
351
|
-
),
|
|
352
|
-
[color],
|
|
353
|
-
);
|
|
437
|
+
let targetPercentage = Math.round(rawPercentage);
|
|
354
438
|
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
439
|
+
if (targetPercentage === 0) {
|
|
440
|
+
targetPercentage = rawPercentage >= 0 ? 1 : -1;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
const newAmount = baselineAmount * (1 + targetPercentage / 100);
|
|
444
|
+
setAmount(newAmount);
|
|
445
|
+
};
|
|
446
|
+
|
|
447
|
+
const handleMouseMove = (event: MouseEvent) => {
|
|
448
|
+
if (!isDragging) {
|
|
449
|
+
return;
|
|
450
|
+
}
|
|
451
|
+
updatePosition(event.clientX, event.clientY);
|
|
452
|
+
};
|
|
358
453
|
|
|
359
|
-
|
|
454
|
+
const handleTouchMove = (event: TouchEvent) => {
|
|
455
|
+
if (!isDragging || event.touches.length === 0) {
|
|
360
456
|
return;
|
|
361
457
|
}
|
|
458
|
+
const touch = event.touches[0];
|
|
459
|
+
updatePosition(touch.clientX, touch.clientY);
|
|
460
|
+
};
|
|
362
461
|
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
point.x = clientX;
|
|
366
|
-
point.y = clientY;
|
|
367
|
-
|
|
368
|
-
const svgPoint = point.matrixTransform(element.getScreenCTM()?.inverse());
|
|
369
|
-
|
|
370
|
-
// Clamp the Y position to the chart area
|
|
371
|
-
const clampedY = Math.max(
|
|
372
|
-
drawingArea.y,
|
|
373
|
-
Math.min(drawingArea.y + drawingArea.height, svgPoint.y),
|
|
374
|
-
);
|
|
375
|
-
|
|
376
|
-
const rawAmount = yScale.invert(clampedY);
|
|
377
|
-
|
|
378
|
-
const rawPercentage = ((rawAmount - baselineAmount) / baselineAmount) * 100;
|
|
379
|
-
|
|
380
|
-
let targetPercentage = Math.round(rawPercentage);
|
|
381
|
-
|
|
382
|
-
if (targetPercentage === 0) {
|
|
383
|
-
targetPercentage = rawPercentage >= 0 ? 1 : -1;
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
const newAmount = baselineAmount * (1 + targetPercentage / 100);
|
|
387
|
-
setAmount(newAmount);
|
|
388
|
-
};
|
|
389
|
-
|
|
390
|
-
const handleMouseMove = (event: MouseEvent) => {
|
|
391
|
-
if (!isDragging) {
|
|
392
|
-
return;
|
|
393
|
-
}
|
|
394
|
-
updatePosition(event.clientX, event.clientY);
|
|
395
|
-
};
|
|
396
|
-
|
|
397
|
-
const handleTouchMove = (event: TouchEvent) => {
|
|
398
|
-
if (!isDragging || event.touches.length === 0) {
|
|
399
|
-
return;
|
|
400
|
-
}
|
|
401
|
-
const touch = event.touches[0];
|
|
402
|
-
updatePosition(touch.clientX, touch.clientY);
|
|
403
|
-
};
|
|
404
|
-
|
|
405
|
-
const handleMouseUp = () => {
|
|
406
|
-
setIsDragging(false);
|
|
407
|
-
};
|
|
408
|
-
|
|
409
|
-
const handleTouchEnd = () => {
|
|
410
|
-
setIsDragging(false);
|
|
411
|
-
};
|
|
412
|
-
|
|
413
|
-
const handleMouseLeave = () => {
|
|
414
|
-
setIsDragging(false);
|
|
415
|
-
};
|
|
416
|
-
|
|
417
|
-
element.addEventListener('mousemove', handleMouseMove);
|
|
418
|
-
element.addEventListener('mouseup', handleMouseUp);
|
|
419
|
-
element.addEventListener('mouseleave', handleMouseLeave);
|
|
420
|
-
element.addEventListener('touchmove', handleTouchMove);
|
|
421
|
-
element.addEventListener('touchend', handleTouchEnd);
|
|
422
|
-
element.addEventListener('touchcancel', handleTouchEnd);
|
|
423
|
-
|
|
424
|
-
return () => {
|
|
425
|
-
element.removeEventListener('mousemove', handleMouseMove);
|
|
426
|
-
element.removeEventListener('mouseup', handleMouseUp);
|
|
427
|
-
element.removeEventListener('mouseleave', handleMouseLeave);
|
|
428
|
-
element.removeEventListener('touchmove', handleTouchMove);
|
|
429
|
-
element.removeEventListener('touchend', handleTouchEnd);
|
|
430
|
-
element.removeEventListener('touchcancel', handleTouchEnd);
|
|
431
|
-
};
|
|
432
|
-
}, [isDragging, yScale, chartRef, baselineAmount, drawingArea.y, drawingArea.height]);
|
|
433
|
-
|
|
434
|
-
if (!yScale) return null;
|
|
435
|
-
|
|
436
|
-
const yPixel = yScale(amount);
|
|
437
|
-
|
|
438
|
-
if (yPixel === undefined || yPixel === null) return null;
|
|
439
|
-
|
|
440
|
-
const difference = amount - baselineAmount;
|
|
441
|
-
const percentageChange = Math.round((difference / baselineAmount) * 100);
|
|
442
|
-
const isPositive = difference > 0;
|
|
443
|
-
|
|
444
|
-
const percentageLabel = isPhone
|
|
445
|
-
? `${Math.abs(percentageChange)}%`
|
|
446
|
-
: `${Math.abs(percentageChange)}% (${formatPrice(Math.abs(difference))})`;
|
|
447
|
-
const dollarLabel = formatPrice(amount);
|
|
448
|
-
|
|
449
|
-
const handleMouseDown = (e: React.MouseEvent) => {
|
|
450
|
-
e.preventDefault();
|
|
451
|
-
setIsDragging(true);
|
|
462
|
+
const handleMouseUp = () => {
|
|
463
|
+
setIsDragging(false);
|
|
452
464
|
};
|
|
453
465
|
|
|
454
|
-
const
|
|
455
|
-
|
|
456
|
-
setIsDragging(true);
|
|
466
|
+
const handleTouchEnd = () => {
|
|
467
|
+
setIsDragging(false);
|
|
457
468
|
};
|
|
458
469
|
|
|
459
|
-
const
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
const iconGap = 8;
|
|
463
|
-
const totalPadding = padding * 2 + iconGap;
|
|
470
|
+
const handleMouseLeave = () => {
|
|
471
|
+
setIsDragging(false);
|
|
472
|
+
};
|
|
464
473
|
|
|
465
|
-
|
|
474
|
+
element.addEventListener('mousemove', handleMouseMove);
|
|
475
|
+
element.addEventListener('mouseup', handleMouseUp);
|
|
476
|
+
element.addEventListener('mouseleave', handleMouseLeave);
|
|
477
|
+
element.addEventListener('touchmove', handleTouchMove);
|
|
478
|
+
element.addEventListener('touchend', handleTouchEnd);
|
|
479
|
+
element.addEventListener('touchcancel', handleTouchEnd);
|
|
480
|
+
|
|
481
|
+
return () => {
|
|
482
|
+
element.removeEventListener('mousemove', handleMouseMove);
|
|
483
|
+
element.removeEventListener('mouseup', handleMouseUp);
|
|
484
|
+
element.removeEventListener('mouseleave', handleMouseLeave);
|
|
485
|
+
element.removeEventListener('touchmove', handleTouchMove);
|
|
486
|
+
element.removeEventListener('touchend', handleTouchEnd);
|
|
487
|
+
element.removeEventListener('touchcancel', handleTouchEnd);
|
|
488
|
+
};
|
|
489
|
+
}, [isDragging, yScale, chartRef, baselineAmount, drawingArea.y, drawingArea.height]);
|
|
466
490
|
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
491
|
+
if (!yScale) return null;
|
|
492
|
+
|
|
493
|
+
const yPixel = yScale(amount);
|
|
494
|
+
|
|
495
|
+
if (yPixel === undefined || yPixel === null) return null;
|
|
496
|
+
|
|
497
|
+
const difference = amount - baselineAmount;
|
|
498
|
+
const percentageChange = Math.round((difference / baselineAmount) * 100);
|
|
499
|
+
const isPositive = difference > 0;
|
|
500
|
+
|
|
501
|
+
const percentageLabel = isPhone
|
|
502
|
+
? `${Math.abs(percentageChange)}%`
|
|
503
|
+
: `${Math.abs(percentageChange)}% (${formatPrice(Math.abs(difference))})`;
|
|
504
|
+
const dollarLabel = formatPrice(amount);
|
|
505
|
+
|
|
506
|
+
const handleMouseDown = (e) => {
|
|
507
|
+
e.preventDefault();
|
|
508
|
+
setIsDragging(true);
|
|
509
|
+
};
|
|
510
|
+
|
|
511
|
+
const handleTouchStart = (e) => {
|
|
512
|
+
e.preventDefault();
|
|
513
|
+
setIsDragging(true);
|
|
514
|
+
};
|
|
515
|
+
|
|
516
|
+
const padding = 16;
|
|
517
|
+
const dragIconSize = 16;
|
|
518
|
+
const trendArrowIconSize = 16;
|
|
519
|
+
const iconGap = 8;
|
|
520
|
+
const totalPadding = padding * 2 + iconGap;
|
|
521
|
+
|
|
522
|
+
const rectWidth = textDimensions.width + totalPadding + dragIconSize + trendArrowIconSize;
|
|
523
|
+
|
|
524
|
+
return (
|
|
525
|
+
<>
|
|
526
|
+
<ReferenceLine
|
|
527
|
+
LabelComponent={labelComponent}
|
|
528
|
+
dataY={amount}
|
|
529
|
+
label={dollarLabel}
|
|
530
|
+
labelPosition="right"
|
|
531
|
+
/>
|
|
532
|
+
<g
|
|
533
|
+
onMouseDown={handleMouseDown}
|
|
534
|
+
onTouchStart={handleTouchStart}
|
|
535
|
+
style={{
|
|
536
|
+
cursor: isDragging ? 'grabbing' : 'grab',
|
|
537
|
+
opacity: textDimensions.width === 0 ? 0 : 1,
|
|
538
|
+
}}
|
|
539
|
+
>
|
|
540
|
+
<rect
|
|
541
|
+
fill="var(--color-bgSecondary)"
|
|
542
|
+
height={32}
|
|
543
|
+
rx={theme.borderRadius['400']}
|
|
544
|
+
ry={theme.borderRadius['400']}
|
|
545
|
+
width={rectWidth}
|
|
546
|
+
x={drawingArea.x}
|
|
547
|
+
y={yPixel - 16}
|
|
548
|
+
/>
|
|
549
|
+
<DragIcon x={drawingArea.x + padding} y={yPixel} />
|
|
550
|
+
<TrendArrowIcon
|
|
551
|
+
color={color}
|
|
552
|
+
isPositive={isPositive}
|
|
553
|
+
x={drawingArea.x + padding + dragIconSize + iconGap}
|
|
554
|
+
y={yPixel}
|
|
474
555
|
/>
|
|
475
|
-
<
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
556
|
+
<ChartText
|
|
557
|
+
disableRepositioning
|
|
558
|
+
color={color}
|
|
559
|
+
font="label1"
|
|
560
|
+
horizontalAlignment="left"
|
|
561
|
+
onDimensionsChange={(dimensions) => setTextDimensions(dimensions)}
|
|
562
|
+
verticalAlignment="middle"
|
|
563
|
+
x={drawingArea.x + padding + dragIconSize + iconGap + trendArrowIconSize}
|
|
564
|
+
y={yPixel + 1}
|
|
482
565
|
>
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
x={drawingArea.x}
|
|
490
|
-
y={yPixel - 16}
|
|
491
|
-
/>
|
|
492
|
-
<DragIcon x={drawingArea.x + padding} y={yPixel} />
|
|
493
|
-
<TrendArrowIcon
|
|
494
|
-
color={color}
|
|
495
|
-
isPositive={isPositive}
|
|
496
|
-
x={drawingArea.x + padding + dragIconSize + iconGap}
|
|
497
|
-
y={yPixel}
|
|
498
|
-
/>
|
|
499
|
-
<ChartText
|
|
500
|
-
disableRepositioning
|
|
501
|
-
color={color}
|
|
502
|
-
font="label1"
|
|
503
|
-
horizontalAlignment="left"
|
|
504
|
-
onDimensionsChange={(dimensions) => setTextDimensions(dimensions)}
|
|
505
|
-
verticalAlignment="middle"
|
|
506
|
-
x={drawingArea.x + padding + dragIconSize + iconGap + trendArrowIconSize}
|
|
507
|
-
y={yPixel + 1}
|
|
508
|
-
>
|
|
509
|
-
{percentageLabel}
|
|
510
|
-
</ChartText>
|
|
511
|
-
</g>
|
|
512
|
-
</>
|
|
513
|
-
);
|
|
514
|
-
},
|
|
515
|
-
);
|
|
566
|
+
{percentageLabel}
|
|
567
|
+
</ChartText>
|
|
568
|
+
</g>
|
|
569
|
+
</>
|
|
570
|
+
);
|
|
571
|
+
});
|
|
516
572
|
|
|
517
|
-
const BaselinePriceLabel = useMemo(
|
|
518
|
-
|
|
519
|
-
|
|
573
|
+
const BaselinePriceLabel = useMemo(
|
|
574
|
+
() =>
|
|
575
|
+
memo((props) => <DefaultReferenceLineLabel {...props} dx={8} horizontalAlignment="left" />),
|
|
576
|
+
[],
|
|
577
|
+
);
|
|
520
578
|
|
|
521
579
|
const PriceTargetChart = () => {
|
|
522
580
|
const priceData = useMemo(() => sparklineInteractiveData.year.map((d) => d.value), []);
|
|
@@ -524,7 +582,7 @@ function DraggablePriceTarget() {
|
|
|
524
582
|
|
|
525
583
|
const chartRef = useRef<SVGSVGElement>(null);
|
|
526
584
|
|
|
527
|
-
const formatPrice = useCallback((value
|
|
585
|
+
const formatPrice = useCallback((value) => {
|
|
528
586
|
return `$${value.toLocaleString('en-US', {
|
|
529
587
|
minimumFractionDigits: 2,
|
|
530
588
|
maximumFractionDigits: 2,
|
|
@@ -537,7 +595,11 @@ function DraggablePriceTarget() {
|
|
|
537
595
|
showArea
|
|
538
596
|
animate={false}
|
|
539
597
|
height={250}
|
|
540
|
-
inset={
|
|
598
|
+
inset={
|
|
599
|
+
isPhone
|
|
600
|
+
? { top: 16, bottom: 16, left: 0, right: 0 }
|
|
601
|
+
: { top: 16, bottom: 16, left: 8, right: 80 }
|
|
602
|
+
}
|
|
541
603
|
series={[
|
|
542
604
|
{
|
|
543
605
|
id: 'prices',
|
|
@@ -563,7 +625,7 @@ function DraggablePriceTarget() {
|
|
|
563
625
|
</LineChart>
|
|
564
626
|
);
|
|
565
627
|
};
|
|
566
|
-
return <PriceTargetChart
|
|
628
|
+
return <PriceTargetChart />;
|
|
567
629
|
}
|
|
568
630
|
```
|
|
569
631
|
|