@coinbase/cds-mcp-server 8.21.8 → 8.22.2

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.
Files changed (153) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/mcp-docs/mobile/components/AreaChart.txt +39 -37
  3. package/mcp-docs/mobile/components/Avatar.txt +18 -18
  4. package/mcp-docs/mobile/components/AvatarButton.txt +19 -19
  5. package/mcp-docs/mobile/components/Banner.txt +62 -23
  6. package/mcp-docs/mobile/components/BarChart.txt +37 -35
  7. package/mcp-docs/mobile/components/Box.txt +18 -18
  8. package/mcp-docs/mobile/components/BrowserBar.txt +18 -18
  9. package/mcp-docs/mobile/components/Button.txt +19 -19
  10. package/mcp-docs/mobile/components/Carousel.txt +18 -18
  11. package/mcp-docs/mobile/components/CartesianChart.txt +75 -44
  12. package/mcp-docs/mobile/components/CheckboxCell.txt +19 -19
  13. package/mcp-docs/mobile/components/Chip.txt +20 -20
  14. package/mcp-docs/mobile/components/Coachmark.txt +18 -18
  15. package/mcp-docs/mobile/components/ContentCard.txt +18 -18
  16. package/mcp-docs/mobile/components/ContentCardBody.txt +18 -18
  17. package/mcp-docs/mobile/components/ContentCardFooter.txt +18 -18
  18. package/mcp-docs/mobile/components/ContentCardHeader.txt +18 -18
  19. package/mcp-docs/mobile/components/ContentCell.txt +18 -18
  20. package/mcp-docs/mobile/components/ControlGroup.txt +18 -18
  21. package/mcp-docs/mobile/components/DatePicker.txt +1 -1
  22. package/mcp-docs/mobile/components/Divider.txt +18 -18
  23. package/mcp-docs/mobile/components/DotCount.txt +1 -1
  24. package/mcp-docs/mobile/components/DotSymbol.txt +2 -2
  25. package/mcp-docs/mobile/components/Fallback.txt +18 -18
  26. package/mcp-docs/mobile/components/HStack.txt +18 -18
  27. package/mcp-docs/mobile/components/Icon.txt +6 -0
  28. package/mcp-docs/mobile/components/IconButton.txt +19 -19
  29. package/mcp-docs/mobile/components/InputChip.txt +20 -20
  30. package/mcp-docs/mobile/components/Interactable.txt +19 -19
  31. package/mcp-docs/mobile/components/LineChart.txt +1608 -898
  32. package/mcp-docs/mobile/components/Link.txt +18 -18
  33. package/mcp-docs/mobile/components/ListCell.txt +37 -19
  34. package/mcp-docs/mobile/components/Lottie.txt +18 -18
  35. package/mcp-docs/mobile/components/MediaChip.txt +20 -20
  36. package/mcp-docs/mobile/components/MultiContentModule.txt +18 -18
  37. package/mcp-docs/mobile/components/NavigationTitle.txt +18 -18
  38. package/mcp-docs/mobile/components/NavigationTitleSelect.txt +18 -18
  39. package/mcp-docs/mobile/components/Numpad.txt +18 -18
  40. package/mcp-docs/mobile/components/Overlay.txt +18 -18
  41. package/mcp-docs/mobile/components/PageFooter.txt +17 -17
  42. package/mcp-docs/mobile/components/PageHeader.txt +17 -17
  43. package/mcp-docs/mobile/components/PeriodSelector.txt +26 -26
  44. package/mcp-docs/mobile/components/Point.txt +203 -98
  45. package/mcp-docs/mobile/components/Pressable.txt +19 -19
  46. package/mcp-docs/mobile/components/ProgressBar.txt +1 -1
  47. package/mcp-docs/mobile/components/ProgressBarWithFixedLabels.txt +1 -1
  48. package/mcp-docs/mobile/components/ProgressBarWithFloatLabel.txt +1 -1
  49. package/mcp-docs/mobile/components/ProgressCircle.txt +1 -1
  50. package/mcp-docs/mobile/components/RadioCell.txt +19 -19
  51. package/mcp-docs/mobile/components/ReferenceLine.txt +197 -54
  52. package/mcp-docs/mobile/components/RollingNumber.txt +18 -18
  53. package/mcp-docs/mobile/components/Scrubber.txt +597 -79
  54. package/mcp-docs/mobile/components/SegmentedTabs.txt +18 -18
  55. package/mcp-docs/mobile/components/SelectAlpha.txt +1 -1
  56. package/mcp-docs/mobile/components/SelectChip.txt +20 -20
  57. package/mcp-docs/mobile/components/SlideButton.txt +19 -19
  58. package/mcp-docs/mobile/components/Spacer.txt +6 -6
  59. package/mcp-docs/mobile/components/SparklineInteractive.txt +3 -3
  60. package/mcp-docs/mobile/components/Spinner.txt +1 -1
  61. package/mcp-docs/mobile/components/Stepper.txt +18 -18
  62. package/mcp-docs/mobile/components/TabLabel.txt +18 -18
  63. package/mcp-docs/mobile/components/TabNavigation.txt +18 -18
  64. package/mcp-docs/mobile/components/TabbedChips.txt +18 -18
  65. package/mcp-docs/mobile/components/TabbedChipsAlpha.txt +1 -1
  66. package/mcp-docs/mobile/components/Tabs.txt +18 -18
  67. package/mcp-docs/mobile/components/Tag.txt +18 -18
  68. package/mcp-docs/mobile/components/Text.txt +18 -18
  69. package/mcp-docs/mobile/components/Toast.txt +18 -18
  70. package/mcp-docs/mobile/components/Tooltip.txt +17 -1
  71. package/mcp-docs/mobile/components/TopNavBar.txt +18 -18
  72. package/mcp-docs/mobile/components/VStack.txt +18 -18
  73. package/mcp-docs/mobile/components/XAxis.txt +86 -24
  74. package/mcp-docs/mobile/components/YAxis.txt +75 -17
  75. package/mcp-docs/mobile/routes.txt +1 -1
  76. package/mcp-docs/web/components/AreaChart.txt +523 -301
  77. package/mcp-docs/web/components/Avatar.txt +27 -27
  78. package/mcp-docs/web/components/AvatarButton.txt +28 -28
  79. package/mcp-docs/web/components/Banner.txt +71 -32
  80. package/mcp-docs/web/components/BarChart.txt +182 -313
  81. package/mcp-docs/web/components/Box.txt +28 -28
  82. package/mcp-docs/web/components/Button.txt +28 -28
  83. package/mcp-docs/web/components/Calendar.txt +27 -27
  84. package/mcp-docs/web/components/Carousel.txt +27 -27
  85. package/mcp-docs/web/components/CartesianChart.txt +62 -309
  86. package/mcp-docs/web/components/CheckboxCell.txt +25 -25
  87. package/mcp-docs/web/components/Chip.txt +27 -27
  88. package/mcp-docs/web/components/Coachmark.txt +27 -27
  89. package/mcp-docs/web/components/ContainedAssetCard.txt +27 -27
  90. package/mcp-docs/web/components/ContentCard.txt +28 -28
  91. package/mcp-docs/web/components/ContentCardBody.txt +28 -28
  92. package/mcp-docs/web/components/ContentCardFooter.txt +28 -28
  93. package/mcp-docs/web/components/ContentCardHeader.txt +28 -28
  94. package/mcp-docs/web/components/ContentCell.txt +28 -28
  95. package/mcp-docs/web/components/ControlGroup.txt +27 -27
  96. package/mcp-docs/web/components/Divider.txt +27 -27
  97. package/mcp-docs/web/components/Fallback.txt +28 -28
  98. package/mcp-docs/web/components/FloatingAssetCard.txt +27 -27
  99. package/mcp-docs/web/components/Grid.txt +28 -28
  100. package/mcp-docs/web/components/GridColumn.txt +27 -27
  101. package/mcp-docs/web/components/HStack.txt +28 -28
  102. package/mcp-docs/web/components/Icon.txt +27 -27
  103. package/mcp-docs/web/components/IconButton.txt +28 -28
  104. package/mcp-docs/web/components/InputChip.txt +27 -27
  105. package/mcp-docs/web/components/Interactable.txt +28 -28
  106. package/mcp-docs/web/components/LineChart.txt +1598 -1116
  107. package/mcp-docs/web/components/Link.txt +28 -28
  108. package/mcp-docs/web/components/ListCell.txt +48 -30
  109. package/mcp-docs/web/components/Lottie.txt +27 -27
  110. package/mcp-docs/web/components/MediaChip.txt +27 -27
  111. package/mcp-docs/web/components/Modal.txt +27 -27
  112. package/mcp-docs/web/components/ModalBody.txt +27 -27
  113. package/mcp-docs/web/components/ModalFooter.txt +27 -27
  114. package/mcp-docs/web/components/ModalHeader.txt +27 -27
  115. package/mcp-docs/web/components/MultiContentModule.txt +28 -28
  116. package/mcp-docs/web/components/NavigationBar.txt +5 -5
  117. package/mcp-docs/web/components/NudgeCard.txt +27 -27
  118. package/mcp-docs/web/components/Overlay.txt +27 -27
  119. package/mcp-docs/web/components/PageFooter.txt +26 -26
  120. package/mcp-docs/web/components/PageHeader.txt +26 -26
  121. package/mcp-docs/web/components/Pagination.txt +27 -27
  122. package/mcp-docs/web/components/PeriodSelector.txt +49 -49
  123. package/mcp-docs/web/components/Point.txt +228 -79
  124. package/mcp-docs/web/components/Pressable.txt +28 -28
  125. package/mcp-docs/web/components/RadioCell.txt +25 -25
  126. package/mcp-docs/web/components/ReferenceLine.txt +208 -60
  127. package/mcp-docs/web/components/RemoteImage.txt +26 -26
  128. package/mcp-docs/web/components/RollingNumber.txt +28 -28
  129. package/mcp-docs/web/components/Scrubber.txt +463 -68
  130. package/mcp-docs/web/components/SectionHeader.txt +27 -27
  131. package/mcp-docs/web/components/SegmentedTabs.txt +27 -27
  132. package/mcp-docs/web/components/SelectChip.txt +27 -27
  133. package/mcp-docs/web/components/SelectOption.txt +27 -27
  134. package/mcp-docs/web/components/Sidebar.txt +27 -27
  135. package/mcp-docs/web/components/SidebarItem.txt +27 -27
  136. package/mcp-docs/web/components/Spacer.txt +34 -34
  137. package/mcp-docs/web/components/SparklineInteractive.txt +1 -1
  138. package/mcp-docs/web/components/Spinner.txt +27 -27
  139. package/mcp-docs/web/components/Stepper.txt +27 -27
  140. package/mcp-docs/web/components/TabLabel.txt +27 -27
  141. package/mcp-docs/web/components/TabNavigation.txt +26 -26
  142. package/mcp-docs/web/components/TabbedChips.txt +26 -26
  143. package/mcp-docs/web/components/TabbedChipsAlpha.txt +1 -1
  144. package/mcp-docs/web/components/Tabs.txt +27 -27
  145. package/mcp-docs/web/components/Tag.txt +27 -27
  146. package/mcp-docs/web/components/Text.txt +28 -28
  147. package/mcp-docs/web/components/TileButton.txt +28 -28
  148. package/mcp-docs/web/components/Toast.txt +27 -27
  149. package/mcp-docs/web/components/Tooltip.txt +17 -1
  150. package/mcp-docs/web/components/VStack.txt +28 -28
  151. package/mcp-docs/web/components/XAxis.txt +86 -22
  152. package/mcp-docs/web/components/YAxis.txt +133 -89
  153. package/package.json +1 -1
@@ -10,311 +10,325 @@ import { LineChart } from '@coinbase/cds-mobile-visualization'
10
10
 
11
11
  ## Examples
12
12
 
13
- ### Basic Example
13
+ LineChart is a wrapper for [CartesianChart](/components/graphs/CartesianChart) that makes it easy to create standard line charts, supporting a single x/y axis pair. Charts are built using `@shopify/react-native-skia`.
14
14
 
15
- ```jsx
16
- function BasicExample() {
17
- const [scrubIndex, setScrubIndex] = useState(undefined);
18
- const data = [10, 22, 29, 45, 98, 45, 22, 52, 21, 4, 68, 20, 21, 58];
15
+ ### Setup
19
16
 
20
- const accessibilityLabel = useMemo(() => {
21
- if (scrubIndex === undefined) return undefined;
22
- return `Value: ${data[scrubIndex]} at index ${scrubIndex}`;
23
- }, [scrubIndex, data]);
17
+ Before using LineChart, you need to wrap your app with `ChartBridgeProvider`. This enables charts to access CDS theming and other React contexts within the Skia renderer. See [CartesianChart](/components/graphs/CartesianChart/#setup) for details.
24
18
 
25
- return (
26
- <LineChart
27
- enableScrubbing
28
- onScrubberPositionChange={setScrubIndex}
29
- height={150}
30
- series={[
31
- {
32
- id: 'prices',
33
- data: data,
34
- },
35
- ]}
36
- curve="monotone"
37
- showYAxis
38
- showArea
39
- yAxis={{
40
- showGrid: true,
41
- }}
42
- accessibilityLabel={accessibilityLabel}
43
- >
44
- <Scrubber />
45
- </LineChart>
46
- );
47
- }
48
- ```
19
+ ### Basics
49
20
 
50
- ### Simple
21
+ The only prop required is `series`, which takes an array of series objects. Each series object needs an `id` and a `data` array of numbers.
51
22
 
52
23
  ```jsx
53
24
  <LineChart
54
- height={150}
25
+ showArea
26
+ height={200}
55
27
  series={[
56
28
  {
57
29
  id: 'prices',
58
30
  data: [10, 22, 29, 45, 98, 45, 22, 52, 21, 4, 68, 20, 21, 58],
59
31
  },
60
32
  ]}
61
- curve="monotone"
62
33
  />
63
34
  ```
64
35
 
65
- ### Compact
66
-
67
- You can specify the dimensions of the chart to make it more compact.
36
+ LineChart also supports multiple lines, interaction, and axes.
37
+ Other props, such as `areaType` can be applied to the chart as a whole or per series.
68
38
 
69
39
  ```jsx
70
- function CompactLineChart() {
71
- const theme = useTheme();
72
- const dimensions = { width: 62, height: 18 };
40
+ <LineChart
41
+ enableScrubbing
42
+ showArea
43
+ series={[
44
+ {
45
+ id: 'pageViews',
46
+ data: [2400, 1398, 9800, 3908, 4800, 3800, 4300],
47
+ color: theme.color.accentBoldGreen,
48
+ // Label will render next to scrubber beacon
49
+ label: 'Page Views',
50
+ },
51
+ {
52
+ id: 'uniqueVisitors',
53
+ data: [4000, 3000, 2000, 2780, 1890, 2390, 3490],
54
+ color: theme.color.accentBoldPurple,
55
+ label: 'Unique Visitors',
56
+ // Default area is gradient
57
+ areaType: 'dotted',
58
+ },
59
+ ]}
60
+ xAxis={{
61
+ // Used on the x-axis to provide context for each index from the series data array
62
+ data: ['Page A', 'Page B', 'Page C', 'Page D', 'Page E', 'Page F', 'Page G'],
63
+ }}
64
+ >
65
+ <Scrubber />
66
+ </LineChart>
67
+ ```
73
68
 
74
- const sparklineData = prices
75
- .map((price) => parseFloat(price))
76
- .filter((price, index) => index % 10 === 0);
77
- const positiveFloor = Math.min(...sparklineData) - 10;
69
+ ### Data
78
70
 
79
- const negativeData = sparklineData.map((price) => -1 * price).reverse();
80
- const negativeCeiling = Math.max(...negativeData) + 10;
71
+ The data array for each series defines the y values for that series. You can adjust the y values for a series of data by setting the `data` prop on the xAxis.
81
72
 
82
- const formatPrice = useCallback((price: number) => {
83
- return `$${price.toLocaleString('en-US', {
84
- minimumFractionDigits: 2,
85
- maximumFractionDigits: 2,
86
- })}`;
87
- }, []);
73
+ ```jsx
74
+ const yData = [2, 5.5, 2, 8.5, 1.5, 5];
75
+ const xData = [1, 2, 3, 5, 8, 10];
76
+
77
+ return (
78
+ <LineChart
79
+ enableScrubbing
80
+ showArea
81
+ series={[
82
+ {
83
+ id: 'line',
84
+ data: yData,
85
+ },
86
+ ]}
87
+ xAxis={{ data: xData, showLine: true, showTickMarks: true, showGrid: true }}
88
+ yAxis={{
89
+ domain: { min: 0 },
90
+ position: 'left',
91
+ showLine: true,
92
+ showTickMarks: true,
93
+ showGrid: true,
94
+ }}
95
+ >
96
+ <Scrubber />
97
+ </LineChart>
98
+ );
99
+ ```
88
100
 
89
- const CompactChart = memo(({ data, showArea, color, referenceY }) => (
90
- <Box style={{ padding: 1 }}>
91
- <LineChart
92
- {...dimensions}
93
- enableScrubbing={false}
94
- overflow="visible"
95
- inset={0}
96
- showArea={showArea}
97
- series={[
98
- {
99
- id: 'btc',
100
- data,
101
- color,
102
- },
103
- ]}
104
- >
105
- <ReferenceLine dataY={referenceY} />
106
- </LineChart>
107
- </Box>
108
- ));
101
+ #### Live Updates
109
102
 
110
- const ChartCell = memo(({ data, showArea, color, referenceY, subdetail, variant }) => {
111
- return (
112
- <ListCell
113
- detail={formatPrice(parseFloat(prices[0]))}
114
- intermediary={
115
- <CompactChart data={data} showArea={showArea} color={color} referenceY={referenceY} />
116
- }
117
- media={<CellMedia source={assets.btc.imageUrl} title="BTC" type="image" />}
118
- onClick={() => console.log('clicked')}
119
- subdetail={subdetail}
120
- title={isPhone ? undefined : assets.btc.name}
121
- variant={variant}
122
- style={{ padding: 0 }}
123
- />
124
- );
125
- });
103
+ You can change the data passed in via `series` prop to update the chart.
126
104
 
127
- return (
128
- <VStack gap={2}>
129
- <ChartCell
130
- data={sparklineData}
131
- color={assets.btc.color}
132
- referenceY={parseFloat(prices[Math.floor(prices.length / 4)])}
133
- subdetail="-4.55%"
134
- variant="negative"
135
- />
136
- <ChartCell
137
- data={sparklineData}
138
- showArea
139
- color={assets.btc.color}
140
- referenceY={parseFloat(prices[Math.floor(prices.length / 4)])}
141
- subdetail="-4.55%"
142
- variant="negative"
143
- />
144
- <ChartCell
145
- data={sparklineData}
146
- showArea
147
- color={theme.color.fgPositive}
148
- referenceY={positiveFloor}
149
- subdetail="+0.25%"
150
- variant="positive"
151
- />
152
- <ChartCell
153
- data={negativeData}
154
- showArea
155
- color={theme.color.fgNegative}
156
- referenceY={negativeCeiling}
157
- subdetail="-4.55%"
158
- variant="negative"
159
- />
160
- </VStack>
161
- );
162
- };
163
- ```
105
+ You can also use the `useRef` hook to reference the scrubber and pulse it on each update.
164
106
 
165
- ### Gain/Loss
107
+ ```jsx
108
+ function LiveUpdates() {
109
+ const scrubberRef = useRef < ScrubberRef > null;
166
110
 
167
- You can use the y-axis scale and a [linearGradient](https://developer.mozilla.org/en-US/docs/Web/SVG/Reference/Element/linearGradient) to create a gain/loss chart.
111
+ const initialData = useMemo(() => {
112
+ return sparklineInteractiveData.hour.map((d) => d.value);
113
+ }, []);
168
114
 
169
- ```jsx
170
- function GainLossChart() {
171
- const theme = useTheme();
172
- const gradientId = useId();
115
+ const [priceData, setPriceData] = useState(initialData);
173
116
 
174
- const data = [-40, -28, -21, -5, 48, -5, -28, 2, -29, -46, 16, -30, -29, 8];
117
+ const lastDataPointTimeRef = useRef(Date.now());
118
+ const updateCountRef = useRef(0);
175
119
 
176
- const priceFormatter = useCallback(
177
- (value) =>
178
- new Intl.NumberFormat('en-US', {
179
- style: 'currency',
180
- currency: 'USD',
181
- maximumFractionDigits: 0,
182
- }).format(value),
183
- [],
184
- );
120
+ const intervalSeconds = 3600 / initialData.length;
121
+
122
+ const maxPercentChange = Math.abs(initialData[initialData.length - 1] - initialData[0]) * 0.05;
185
123
 
186
- const ChartDefs = ({ threshold = 0 }) => {
187
- const { getYScale } = useCartesianChartContext();
188
- // get the default y-axis scale
189
- const yScale = getYScale();
124
+ useEffect(() => {
125
+ const priceUpdateInterval = setInterval(
126
+ () => {
127
+ setPriceData((currentData) => {
128
+ const newData = [...currentData];
129
+ const lastPrice = newData[newData.length - 1];
190
130
 
191
- if (yScale) {
192
- const domain = yScale.domain();
193
- const range = yScale.range();
131
+ const priceChange = (Math.random() - 0.5) * maxPercentChange;
132
+ const newPrice = Math.round((lastPrice + priceChange) * 100) / 100;
194
133
 
195
- const baselinePercentage = ((threshold - domain[0]) / (domain[1] - domain[0])) * 100;
134
+ // Check if we should roll over to a new data point
135
+ const currentTime = Date.now();
136
+ const timeSinceLastPoint = (currentTime - lastDataPointTimeRef.current) / 1000;
196
137
 
197
- const negativeColor = `rgb(${theme.color.gray15})`;
198
- const positiveColor = theme.color.fgPositive;
138
+ if (timeSinceLastPoint >= intervalSeconds) {
139
+ // Time for a new data point - remove first, add new at end
140
+ lastDataPointTimeRef.current = currentTime;
141
+ newData.shift(); // Remove oldest data point
142
+ newData.push(newPrice); // Add new data point
143
+ updateCountRef.current = 0;
144
+ } else {
145
+ // Just update the last data point
146
+ newData[newData.length - 1] = newPrice;
147
+ updateCountRef.current++;
148
+ }
199
149
 
200
- return (
201
- <Defs>
202
- <LinearGradient
203
- gradientUnits="userSpaceOnUse"
204
- id={`${gradientId}-solid`}
205
- x1="0%"
206
- x2="0%"
207
- y1={range[0]}
208
- y2={range[1]}
209
- >
210
- <Stop offset="0%" stopColor={negativeColor} />
211
- <Stop offset={`${baselinePercentage}%`} stopColor={negativeColor} />
212
- <Stop offset={`${baselinePercentage}%`} stopColor={positiveColor} />
213
- <Stop offset="100%" stopColor={positiveColor} />
214
- </LinearGradient>
215
- <LinearGradient
216
- gradientUnits="userSpaceOnUse"
217
- id={`${gradientId}-gradient`}
218
- x1="0%"
219
- x2="0%"
220
- y1={range[0]}
221
- y2={range[1]}
222
- >
223
- <Stop offset="0%" stopColor={negativeColor} stopOpacity={0.3} />
224
- <Stop offset={`${baselinePercentage}%`} stopColor={negativeColor} stopOpacity={0} />
225
- <Stop offset={`${baselinePercentage}%`} stopColor={positiveColor} stopOpacity={0} />
226
- <Stop offset="100%" stopColor={positiveColor} stopOpacity={0.3} />
227
- </LinearGradient>
228
- </Defs>
229
- );
230
- }
150
+ return newData;
151
+ });
231
152
 
232
- return null;
233
- };
153
+ // Pulse the scrubber on each update
154
+ scrubberRef.current?.pulse();
155
+ },
156
+ 2000 + Math.random() * 1000,
157
+ );
234
158
 
235
- const solidColor = `url(#${gradientId}-solid)`;
159
+ return () => clearInterval(priceUpdateInterval);
160
+ }, [intervalSeconds, maxPercentChange]);
236
161
 
237
162
  return (
238
- <CartesianChart
163
+ <LineChart
239
164
  enableScrubbing
240
- height={150}
165
+ showArea
166
+ height={200}
167
+ inset={{ right: 64 }}
241
168
  series={[
242
169
  {
243
- id: 'prices',
244
- data: data,
245
- color: solidColor,
170
+ id: 'btc',
171
+ data: priceData,
172
+ color: assets.btc.color,
246
173
  },
247
174
  ]}
248
- padding={{ top: 1.5, bottom: 1.5, left: 2, right: 0 }}
249
175
  >
250
- <ChartDefs />
251
- <YAxis requestedTickCount={2} showGrid tickLabelFormatter={priceFormatter} />
252
- <Area seriesId="prices" curve="monotone" fill={`url(#${gradientId}-gradient)`} />
253
- <Line strokeWidth={3} curve="monotone" seriesId="prices" stroke={solidColor} />
254
- <Scrubber hideOverlay />
255
- </CartesianChart>
176
+ <Scrubber ref={scrubberRef} labelElevated />
177
+ </LineChart>
256
178
  );
257
179
  }
258
180
  ```
259
181
 
260
- ### Multiple Series
182
+ #### Missing Data
261
183
 
262
- You can add multiple series to a line chart.
184
+ By default, null values in data create gaps in a line. Use `connectNulls` to skip null values and draw a continuous line.
185
+ Note that scrubber beacons and points are still only shown at non-null data values.
263
186
 
264
187
  ```jsx
265
- function MultipleSeriesChart() {
188
+ function MissingData() {
266
189
  const theme = useTheme();
267
- const [scrubIndex, setScrubIndex] = useState(undefined);
190
+ const pages = ['Page A', 'Page B', 'Page C', 'Page D', 'Page E', 'Page F', 'Page G'];
191
+ const pageViews = [2400, 1398, null, 3908, 4800, 3800, 4300];
192
+ const uniqueVisitors = [4000, 3000, null, 2780, 1890, 2390, 3490];
268
193
 
269
- const prices = [10, 22, 29, 45, 98, 45, 22, 52, 21, 4, 68, 20, 21, 58];
270
- const volume = [4, 8, 11, 15, 16, 14, 16, 10, 12, 14, 16, 14, 16, 10];
194
+ const numberFormatter = useCallback(
195
+ (value: number) => new Intl.NumberFormat('en-US', { maximumFractionDigits: 0 }).format(value),
196
+ [],
197
+ );
271
198
 
272
199
  return (
273
200
  <LineChart
274
201
  enableScrubbing
275
- height={150}
202
+ showArea
203
+ showXAxis
204
+ showYAxis
205
+ height={200}
206
+ // You can render points at every valid data point by always returning true
207
+ points
276
208
  series={[
277
209
  {
278
- id: 'prices',
279
- data: [10, 22, 29, 45, 98, 45, 22, 52, 21, 4, 68, 20, 21, 58],
280
- label: 'Prices',
281
- color: theme.color.accentBoldBlue,
210
+ id: 'pageViews',
211
+ data: pageViews,
212
+ color: theme.color.accentBoldGreen,
213
+ // Label will render next to scrubber beacon
214
+ label: 'Page Views',
215
+ connectNulls: true,
282
216
  },
283
217
  {
284
- id: 'volume',
285
- data: [4, 8, 11, 15, 16, 14, 16, 10, 12, 14, 16, 14, 16, 10],
286
- label: 'Volume',
287
- color: theme.color.accentBoldGreen,
218
+ id: 'uniqueVisitors',
219
+ data: uniqueVisitors,
220
+ color: theme.color.accentBoldPurple,
221
+ label: 'Unique Visitors',
288
222
  },
289
223
  ]}
290
- showYAxis
224
+ xAxis={{
225
+ // Used on the x-axis to provide context for each index from the series data array
226
+ data: pages,
227
+ }}
291
228
  yAxis={{
292
- domain: {
293
- min: 0,
294
- },
295
229
  showGrid: true,
230
+ tickLabelFormatter: numberFormatter,
296
231
  }}
297
- curve="monotone"
298
232
  >
299
- <Scrubber />
233
+ {/* We can offset the overlay to account for the points being drawn on the lines */}
234
+ <Scrubber overlayOffset={6} />
300
235
  </LineChart>
301
236
  );
302
237
  }
303
238
  ```
304
239
 
305
- ### Points
240
+ ##### Empty State
241
+
242
+ ```jsx
243
+ function EmptyState() {
244
+ const theme = useTheme();
245
+ return (
246
+ <LineChart
247
+ height={200}
248
+ series={[
249
+ {
250
+ id: 'line',
251
+ color: `rgb(${theme.spectrum.gray50})`,
252
+ data: [1, 1],
253
+ showArea: true,
254
+ },
255
+ ]}
256
+ yAxis={{ domain: { min: -1, max: 3 } }}
257
+ />
258
+ );
259
+ }
260
+ ```
261
+
262
+ #### Scales
263
+
264
+ LineChart uses `linear` scaling on axes by default, but you can also use other types, such as `log`. See [XAxis](/components/graphs/XAxis) and [YAxis](/components/graphs/YAxis) for more information.
265
+
266
+ ```jsx
267
+ <LineChart
268
+ showArea
269
+ showYAxis
270
+ height={200}
271
+ series={[
272
+ {
273
+ id: 'prices',
274
+ data: [10, 22, 29, 45, 98, 45, 22, 52, 21, 4, 68, 20, 21, 58],
275
+ },
276
+ ]}
277
+ yAxis={{
278
+ scaleType: 'log',
279
+ showGrid: true,
280
+ ticks: [1, 10, 100],
281
+ }}
282
+ />
283
+ ```
284
+
285
+ ### Interaction
286
+
287
+ Charts have built in functionality enabled through scrubbing, which can be used by setting `enableScrubbing` to true. You can listen to value changes through `onScrubberPositionChange`. Adding `Scrubber` to LineChart showcases the current scrubber position.
288
+
289
+ ```jsx
290
+ function Interaction() {
291
+ const [scrubberPosition, setScrubberPosition] = useState<number | undefined>();
292
+
293
+ return (
294
+ <VStack gap={2}>
295
+ <Text font="label1">
296
+ {scrubberPosition !== undefined
297
+ ? `Scrubber position: ${scrubberPosition}`
298
+ : 'Not scrubbing'}
299
+ </Text>
300
+ <LineChart
301
+ enableScrubbing
302
+ showArea
303
+ height={200}
304
+ onScrubberPositionChange={setScrubberPosition}
305
+ series={[
306
+ {
307
+ id: 'prices',
308
+ data: [10, 22, 29, 45, 98, 45, 22, 52, 21, 4, 68, 20, 21, 58],
309
+ },
310
+ ]}
311
+ >
312
+ <Scrubber />
313
+ </LineChart>
314
+ </VStack>
315
+ );
316
+ }
317
+ ```
318
+
319
+ #### Points
306
320
 
307
- You can use the `renderPoints` prop to dynamically show points on a line.
321
+ You can use `points` from LineChart to render instances of [Point](/components/graphs/Point) at specific data locations with custom styling.
308
322
 
309
323
  ```jsx
310
- function PointsChart() {
324
+ function Points() {
311
325
  const theme = useTheme();
312
326
  const keyMarketShiftIndices = [4, 6, 7, 9, 10];
313
327
  const data = [10, 22, 29, 45, 98, 45, 22, 52, 21, 4, 68, 20, 21, 58];
314
328
 
315
329
  return (
316
330
  <CartesianChart
317
- height={150}
331
+ height={200}
318
332
  series={[
319
333
  {
320
334
  id: 'prices',
@@ -322,385 +336,1171 @@ function PointsChart() {
322
336
  },
323
337
  ]}
324
338
  >
325
- <Area seriesId="prices" curve="monotone" fill={`rgb(${theme.color.blue5})`} />
339
+ <Area fill={`rgb(${theme.spectrum.blue5})`} seriesId="prices" />
326
340
  <Line
327
- seriesId="prices"
328
- renderPoints={({ dataX, dataY, ...props }) =>
341
+ points={({ dataX, ...props }) =>
329
342
  keyMarketShiftIndices.includes(dataX)
330
343
  ? {
331
344
  ...props,
332
345
  strokeWidth: 2,
333
346
  stroke: theme.color.bg,
334
347
  radius: 5,
335
- onClick: () =>
336
- alert(
337
- `You have clicked a key market shift at position ${dataX + 1} with value ${dataY}!`,
338
- ),
339
- accessibilityLabel: `Key market shift point at position ${dataX + 1}, value ${dataY}. Click to view details.`,
340
348
  }
341
349
  : false
342
350
  }
343
- curve="monotone"
351
+ seriesId="prices"
344
352
  />
345
353
  </CartesianChart>
346
354
  );
347
355
  }
348
356
  ```
349
357
 
350
- ### Empty State
358
+ #### Performance
351
359
 
352
- This example shows how to use an empty state for a line chart.
360
+ Renders are done on JS thread, other code is in UI
353
361
 
354
362
  ```jsx
355
- function EmptyStateChart() {
356
- const theme = useTheme();
357
- return (
358
- <LineChart
359
- series={[
360
- {
361
- id: 'line',
362
- color: `rgb(${theme.color.gray50})`,
363
- data: [1, 1],
364
- showArea: true,
365
- },
366
- ]}
367
- yAxis={{ domain: { min: -1, max: 3 } }}
368
- height={150}
369
- />
370
- );
371
- }
372
- ```
373
-
374
- ### Line Styles
363
+
364
+ function Performance() {
365
+ const tabs = useMemo(
366
+ () => [
367
+ { id: 'hour', label: '1H' },
368
+ { id: 'day', label: '1D' },
369
+ { id: 'week', label: '1W' },
370
+ { id: 'month', label: '1M' },
371
+ { id: 'year', label: '1Y' },
372
+ { id: 'all', label: 'All' },
373
+ ],
374
+ [],
375
+ );
376
+ const [timePeriod, setTimePeriod] = useState<TabValue>(tabs[0]);
377
+ const [scrubberPosition, setScrubberPosition] = useState<number | undefined>();
378
+
379
+ const sparklineTimePeriodData = useMemo(() => {
380
+ return sparklineInteractiveData[timePeriod.id as keyof typeof sparklineInteractiveData];
381
+ }, [timePeriod]);
382
+
383
+ const sparklineTimePeriodDataValues = useMemo(() => {
384
+ return sparklineTimePeriodData.map((d) => d.value);
385
+ }, [sparklineTimePeriodData]);
386
+
387
+ const onPeriodChange = useCallback(
388
+ (period: TabValue | null) => {
389
+ setTimePeriod(period || tabs[0]);
390
+ },
391
+ [tabs],
392
+ );
393
+
394
+ return (
395
+ <VStack gap={2} style={{ marginLeft: -8, marginRight: -8 }}>
396
+ <PerformanceHeader
397
+ scrubberPosition={scrubberPosition}
398
+ sparklineTimePeriodDataValues={sparklineTimePeriodDataValues}
399
+ />
400
+ <PerformanceChart onScrubberPositionChange={setScrubberPosition} timePeriod={timePeriod} />
401
+ <PeriodSelector activeTab={timePeriod} onChange={onPeriodChange} tabs={tabs} />
402
+ </VStack>
403
+ );
404
+ }
405
+
406
+ const PerformanceHeader = memo(
407
+ ({
408
+ scrubberPosition,
409
+ sparklineTimePeriodDataValues,
410
+ }: {
411
+ scrubberPosition: number | undefined;
412
+ sparklineTimePeriodDataValues: number[];
413
+ }) => {
414
+ const theme = useTheme();
415
+
416
+ const formatPriceThousands = useCallback((price: number) => {
417
+ return `${new Intl.NumberFormat('en-US', {
418
+ style: 'currency',
419
+ currency: 'USD',
420
+ minimumFractionDigits: 0,
421
+ maximumFractionDigits: 0,
422
+ }).format(price / 1000)}k`;
423
+ }, []);
424
+
425
+ const shownPosition =
426
+ scrubberPosition !== undefined ? scrubberPosition : sparklineTimePeriodDataValues.length - 1;
427
+
428
+ return (
429
+ <HStack gap={1} paddingX={1}>
430
+ <LegendItem
431
+ color={theme.color.fgPositive}
432
+ label="High Price"
433
+ value={formatPriceThousands(sparklineTimePeriodDataValues[shownPosition] * 1.2)}
434
+ />
435
+ <LegendItem
436
+ color={assets.btc.color}
437
+ label="Actual Price"
438
+ value={formatPriceThousands(sparklineTimePeriodDataValues[shownPosition])}
439
+ />
440
+ <LegendItem
441
+ color={theme.color.fgNegative}
442
+ label="Low Price"
443
+ value={formatPriceThousands(sparklineTimePeriodDataValues[shownPosition] * 0.8)}
444
+ />
445
+ </HStack>
446
+ );
447
+ },
448
+ );
449
+
450
+ const PerformanceChart = memo(
451
+ ({
452
+ timePeriod,
453
+ onScrubberPositionChange,
454
+ }: {
455
+ timePeriod: TabValue;
456
+ onScrubberPositionChange: (position: number | undefined) => void;
457
+ }) => {
458
+ const theme = useTheme();
459
+
460
+ const sparklineTimePeriodData = useMemo(() => {
461
+ return sparklineInteractiveData[timePeriod.id as keyof typeof sparklineInteractiveData];
462
+ }, [timePeriod]);
463
+
464
+ const sparklineTimePeriodDataValues = useMemo(() => {
465
+ return sparklineTimePeriodData.map((d) => d.value);
466
+ }, [sparklineTimePeriodData]);
467
+
468
+ const sparklineTimePeriodDataTimestamps = useMemo(() => {
469
+ return sparklineTimePeriodData.map((d) => d.date);
470
+ }, [sparklineTimePeriodData]);
471
+
472
+ const formatPriceThousands = useCallback((price: number) => {
473
+ return `${new Intl.NumberFormat('en-US', {
474
+ style: 'currency',
475
+ currency: 'USD',
476
+ minimumFractionDigits: 0,
477
+ maximumFractionDigits: 0,
478
+ }).format(price / 1000)}k`;
479
+ }, []);
480
+
481
+ const formatDate = useCallback((date: Date) => {
482
+ const dayOfWeek = date.toLocaleDateString('en-US', { weekday: 'short' });
483
+
484
+ const monthDay = date.toLocaleDateString('en-US', {
485
+ month: 'short',
486
+ day: 'numeric',
487
+ });
488
+
489
+ const time = date.toLocaleTimeString('en-US', {
490
+ hour: 'numeric',
491
+ minute: '2-digit',
492
+ hour12: true,
493
+ });
494
+
495
+ return `${dayOfWeek}, ${monthDay}, ${time}`;
496
+ }, []);
497
+
498
+ const getScrubberLabel = useCallback(
499
+ (d: number) => formatDate(sparklineTimePeriodDataTimestamps[d]),
500
+ [formatDate, sparklineTimePeriodDataTimestamps],
501
+ );
502
+
503
+ return (
504
+ <LineChart
505
+ enableScrubbing
506
+ showArea
507
+ showYAxis
508
+ areaType="dotted"
509
+ height={300}
510
+ inset={{ top: 52, left: 0, right: 0 }}
511
+ onScrubberPositionChange={onScrubberPositionChange}
512
+ series={[
513
+ {
514
+ id: 'high',
515
+ data: sparklineTimePeriodDataValues.map((d) => d * 1.2),
516
+ color: theme.color.fgPositive,
517
+ label: 'High Price',
518
+ },
519
+ {
520
+ id: 'btc',
521
+ data: sparklineTimePeriodDataValues,
522
+ color: assets.btc.color,
523
+ label: 'Actual Price',
524
+ },
525
+ {
526
+ id: 'low',
527
+ data: sparklineTimePeriodDataValues.map((d) => d * 0.8),
528
+ color: theme.color.fgNegative,
529
+ label: 'Low Price',
530
+ },
531
+ ]}
532
+ xAxis={{ range: ({ min, max }) => ({ min, max: max - 16 }) }}
533
+ yAxis={{ showGrid: true, tickLabelFormatter: formatPriceThousands }}
534
+ >
535
+ <Scrubber idlePulse label={getScrubberLabel} />
536
+ </LineChart>
537
+ );
538
+ },
539
+ );
540
+ ```
541
+
542
+ #### Gestures
543
+
544
+ By default, charts will not track gestures that go outside of the chart bounds. You can allow overflow gestures by setting `allowOverflowGestures` to `true`.
375
545
 
376
546
  ```jsx
377
547
  <LineChart
378
- height={150}
379
- series={[
380
- {
381
- id: 'top',
382
- data: [15, 28, 32, 44, 46, 36, 40, 45, 48, 38],
383
- },
384
- {
385
- id: 'upperMiddle',
386
- data: [12, 23, 21, 29, 34, 28, 31, 38, 42, 35],
387
- color: '#ef4444',
388
- type: 'dotted',
389
- },
390
- {
391
- id: 'lowerMiddle',
392
- data: [8, 15, 14, 25, 20, 18, 22, 28, 24, 30],
393
- color: '#f59e0b',
394
- curve: 'natural',
395
- LineComponent: (props) => (
396
- <GradientLine {...props} endColor="#F7931A" startColor="#E3D74D" strokeWidth={4} />
397
- ),
398
- },
399
- {
400
- id: 'bottom',
401
- data: [4, 8, 11, 15, 16, 14, 16, 10, 12, 14],
402
- color: '#800080',
403
- curve: 'step',
404
- AreaComponent: DottedArea,
405
- showArea: true,
406
- },
407
- ]}
408
- />
548
+ enableScrubbing
549
+ allowOverflowGestures
550
+ ...
551
+ >
552
+ ...
553
+ </LineChart>
409
554
  ```
410
555
 
411
- ### Live Data
556
+ ### Animations
557
+
558
+ You can configure chart transitions using `transition` on LineChart and `beaconTransitions` on [Scrubber](/components/graphs/Scrubber). You can also disable animations by setting the `animate` on LineChart to `false`.
412
559
 
413
560
  ```jsx
414
- function LiveAssetPrice() {
415
- const scrubberRef = useRef(null);
416
- const [scrubIndex, setScrubIndex] = useState(undefined);
561
+ function Transitions() {
562
+ const theme = useTheme();
563
+ const dataCount = 20;
564
+ const maxDataOffset = 15000;
565
+ const minStepOffset = 2500;
566
+ const maxStepOffset = 10000;
567
+ const domainLimit = 20000;
568
+ const updateInterval = 500;
569
+
570
+ const myTransitionConfig: Transition = { type: 'spring', stiffness: 700, damping: 20 };
571
+ const negativeColor = `rgb(${theme.spectrum.gray15})`;
572
+ const positiveColor = theme.color.fgPositive;
573
+
574
+ function generateNextValue(previousValue: number) {
575
+ const range = maxStepOffset - minStepOffset;
576
+ const offset = Math.random() * range + minStepOffset;
577
+
578
+ let direction;
579
+ if (previousValue >= maxDataOffset) {
580
+ direction = -1;
581
+ } else if (previousValue <= -maxDataOffset) {
582
+ direction = 1;
583
+ } else {
584
+ direction = Math.random() < 0.5 ? -1 : 1;
585
+ }
417
586
 
418
- const initialData = useMemo(() => {
419
- return sparklineInteractiveData.hour.map((d) => d.value);
420
- }, []);
587
+ let newValue = previousValue + offset * direction;
588
+ newValue = Math.max(-maxDataOffset, Math.min(maxDataOffset, newValue));
589
+ return newValue;
590
+ }
421
591
 
422
- const [priceData, setPriceData] = useState(initialData);
592
+ function generateInitialData() {
593
+ const data = [];
423
594
 
424
- const lastDataPointTimeRef = useRef(Date.now());
425
- const updateCountRef = useRef(0);
595
+ let previousValue = Math.random() * 2 * maxDataOffset - maxDataOffset;
596
+ data.push(previousValue);
426
597
 
427
- const intervalSeconds = 3600 / initialData.length;
598
+ for (let i = 1; i < dataCount; i++) {
599
+ const newValue = generateNextValue(previousValue);
600
+ data.push(newValue);
601
+ previousValue = newValue;
602
+ }
428
603
 
429
- const maxPercentChange = Math.abs(initialData[initialData.length - 1] - initialData[0]) * 0.05;
604
+ return data;
605
+ }
430
606
 
431
- useEffect(() => {
432
- const priceUpdateInterval = setInterval(
433
- () => {
434
- setPriceData((currentData) => {
435
- const newData = [...currentData];
436
- const lastPrice = newData[newData.length - 1];
607
+ const MyGradient = memo((props: DottedAreaProps) => {
608
+ const areaGradient = {
609
+ stops: ({ min, max }: AxisBounds) => [
610
+ { offset: min, color: negativeColor, opacity: 1 },
611
+ { offset: 0, color: negativeColor, opacity: 0 },
612
+ { offset: 0, color: positiveColor, opacity: 0 },
613
+ { offset: max, color: positiveColor, opacity: 1 },
614
+ ],
615
+ };
437
616
 
438
- const priceChange = (Math.random() - 0.5) * maxPercentChange;
439
- const newPrice = Math.round((lastPrice + priceChange) * 100) / 100;
617
+ return <DottedArea {...props} gradient={areaGradient} />;
618
+ });
440
619
 
441
- // Check if we should roll over to a new data point
442
- const currentTime = Date.now();
443
- const timeSinceLastPoint = (currentTime - lastDataPointTimeRef.current) / 1000;
620
+ function CustomTransitionsChart() {
621
+ const [data, setData] = useState(generateInitialData);
444
622
 
445
- if (timeSinceLastPoint >= intervalSeconds) {
446
- // Time for a new data point - remove first, add new at end
447
- lastDataPointTimeRef.current = currentTime;
448
- newData.shift(); // Remove oldest data point
449
- newData.push(newPrice); // Add new data point
450
- updateCountRef.current = 0;
451
- } else {
452
- // Just update the last data point
453
- newData[newData.length - 1] = newPrice;
454
- updateCountRef.current++;
455
- }
623
+ useEffect(() => {
624
+ const intervalId = setInterval(() => {
625
+ setData((currentData) => {
626
+ const lastValue = currentData[currentData.length - 1] ?? 0;
627
+ const newValue = generateNextValue(lastValue);
456
628
 
457
- return newData;
629
+ return [...currentData.slice(1), newValue];
458
630
  });
631
+ }, updateInterval);
459
632
 
460
- // Pulse the scrubber on each update
461
- scrubberRef.current?.pulse();
462
- },
463
- 2000 + Math.random() * 1000,
633
+ return () => clearInterval(intervalId);
634
+ }, []);
635
+
636
+ const tickLabelFormatter = useCallback(
637
+ (value: number) =>
638
+ new Intl.NumberFormat('en-US', {
639
+ style: 'currency',
640
+ currency: 'USD',
641
+ maximumFractionDigits: 0,
642
+ }).format(value),
643
+ [],
464
644
  );
465
645
 
466
- return () => clearInterval(priceUpdateInterval);
467
- }, [intervalSeconds, maxPercentChange]);
646
+ const valueAtIndexFormatter = useCallback(
647
+ (dataIndex: number) =>
648
+ new Intl.NumberFormat('en-US', {
649
+ style: 'currency',
650
+ currency: 'USD',
651
+ }).format(data[dataIndex]),
652
+ [data],
653
+ );
654
+
655
+ const lineGradient = {
656
+ stops: [
657
+ { offset: 0, color: negativeColor },
658
+ { offset: 0, color: positiveColor },
659
+ ],
660
+ };
661
+
662
+ return (
663
+ <CartesianChart
664
+ enableScrubbing
665
+ height={200}
666
+ inset={{ top: 32, bottom: 32, left: 16, right: 16 }}
667
+ series={[
668
+ {
669
+ id: 'prices',
670
+ data: data,
671
+ gradient: lineGradient,
672
+ },
673
+ ]}
674
+ yAxis={{ domain: { min: -domainLimit, max: domainLimit } }}
675
+ >
676
+ <YAxis showGrid requestedTickCount={2} tickLabelFormatter={tickLabelFormatter} />
677
+ <Line
678
+ showArea
679
+ AreaComponent={MyGradient}
680
+ seriesId="prices"
681
+ strokeWidth={3}
682
+ transition={myTransitionConfig}
683
+ />
684
+ <Scrubber
685
+ hideOverlay
686
+ beaconTransitions={{ update: myTransitionConfig }}
687
+ label={valueAtIndexFormatter}
688
+ />
689
+ </CartesianChart>
690
+ );
691
+ }
692
+
693
+ return <CustomTransitionsChart />;
694
+ }
695
+ ```
696
+
697
+ ### Accessibility
698
+
699
+ You can use `accessibilityLabel` on the chart to provide a descriptive label.
700
+
701
+ ```jsx
702
+ function BasicAccessible() {
703
+ const [scrubberPosition, setScrubberPosition] = useState<number | undefined>();
704
+ const data = useMemo(() => [10, 22, 29, 45, 98, 45, 22, 52, 21, 4, 68, 20, 21, 58], []);
705
+
706
+ // Chart-level accessibility label provides overview
707
+ const chartAccessibilityLabel = useMemo(() => {
708
+ const currentPrice = data[data.length - 1];
709
+ return `Price chart showing trend over ${data.length} data points. Current value: ${currentPrice}. Use arrow keys to adjust view`;
710
+ }, [data]);
711
+
712
+ // Scrubber-level accessibility label provides specific position info
713
+ const scrubberAccessibilityLabel = useCallback(
714
+ (index: number) => {
715
+ return `Price at position ${index + 1} of ${data.length}: ${data[index]}`;
716
+ },
717
+ [data],
718
+ );
468
719
 
469
720
  const accessibilityLabel = useMemo(() => {
470
- if (scrubIndex === undefined)
471
- return `Bitcoin Price: $${priceData[priceData.length - 1].toFixed(2)}`;
472
- const price = priceData[scrubIndex];
473
- return `Bitcoin Price: $${price.toFixed(2)} at position ${scrubIndex + 1}`;
474
- }, [scrubIndex, priceData]);
721
+ if (scrubberPosition !== undefined) {
722
+ return scrubberAccessibilityLabel(scrubberPosition);
723
+ }
724
+ return chartAccessibilityLabel;
725
+ }, [scrubberPosition, chartAccessibilityLabel, scrubberAccessibilityLabel]);
475
726
 
476
727
  return (
477
728
  <LineChart
478
729
  enableScrubbing
479
- onScrubberPositionChange={setScrubIndex}
480
730
  showArea
481
- height={150}
731
+ showYAxis
732
+ accessibilityLabel={accessibilityLabel}
733
+ height={200}
734
+ onScrubberPositionChange={setScrubberPosition}
482
735
  series={[
483
736
  {
484
- id: 'btc',
485
- data: priceData,
486
- color: assets.btc.color,
737
+ id: 'prices',
738
+ data: data,
487
739
  },
488
740
  ]}
489
- inset={{ right: 64 }}
490
- accessibilityLabel={accessibilityLabel}
741
+ yAxis={{
742
+ showGrid: true,
743
+ }}
491
744
  >
492
- <Scrubber ref={scrubberRef} labelProps={{ elevation: 1 }} />
745
+ <Scrubber />
493
746
  </LineChart>
494
747
  );
495
748
  }
496
749
  ```
497
750
 
498
- ### Data Format
751
+ ### Styling
752
+
753
+ #### Axes
499
754
 
500
- You can adjust the y values for a series of data by setting the `data` prop on the xAxis.
755
+ Using `showXAxis` and `showYAxis` allows you to display the axes. For more information, such as adjusting domain and range, see [XAxis](/components/graphs/XAxis) and [YAxis](/components/graphs/YAxis).
501
756
 
502
757
  ```jsx
503
- function DataFormatChart() {
504
- const [scrubIndex, setScrubIndex] = useState(undefined);
758
+ <LineChart
759
+ showArea
760
+ showXAxis
761
+ showYAxis
762
+ height={200}
763
+ series={[
764
+ {
765
+ id: 'prices',
766
+ data: [10, 22, 29, 45, 98, 45, 22, 52, 21, 4, 68, 20, 21, 58],
767
+ },
768
+ ]}
769
+ xAxis={{
770
+ showGrid: true,
771
+ showLine: true,
772
+ showTickMarks: true,
773
+ tickLabelFormatter: (dataX: number) => `Day ${dataX}`,
774
+ }}
775
+ yAxis={{
776
+ showGrid: true,
777
+ showLine: true,
778
+ showTickMarks: true,
779
+ }}
780
+ />
781
+ ```
505
782
 
506
- const yData = [2, 5.5, 2, 8.5, 1.5, 5];
507
- const xData = [1, 2, 3, 5, 8, 10];
783
+ #### Fonts
508
784
 
509
- const accessibilityLabel = useMemo(() => {
510
- if (scrubIndex === undefined) return undefined;
511
- return `X: ${xData[scrubIndex]}, Y: ${yData[scrubIndex]} at point ${scrubIndex + 1}`;
512
- }, [scrubIndex, xData, yData]);
785
+ By default, charts will use the default font of the system. You can use `fontFamily` at the chart level to customize this. For more, see [Skia's documentation on fonts](https://shopify.github.io/react-native-skia/docs/text/paragraph/#fonts).
786
+
787
+ ```jsx
788
+ <LineChart
789
+ fontFamilies={["Coinbase Sans"]}
790
+ ...
791
+ >
792
+ ...
793
+ </LineChart>
794
+ ```
795
+
796
+ You can also use `fontProvider` along with `useFonts` from Skia if you need to load a custom font.
797
+
798
+ ```jsx
799
+ const fontProvider = useFonts({
800
+ MyCustomFontFamily: [
801
+ require("./MyCustomFont-Regular.ttf"),
802
+ require("./MyCustomFont-Bold.ttf"),
803
+ ],
804
+ });
805
+
806
+ return (
807
+ <LineChart
808
+ fontFamilies={["MyCustomFontFamily"]}
809
+ fontProvider={fontProvider}
810
+ ...
811
+ >
812
+ ...
813
+ </LineChart>
814
+ );
815
+ ```
816
+
817
+ #### Gradients
818
+
819
+ Gradients can be applied to the y-axis (default) or x-axis. Each stop requires an `offset`, which is based on the data within the x/y scale and `color`, with an optional `opacity` (defaults to 1).
820
+
821
+ Values in between stops will be interpolated smoothly using [srgb color space](https://www.w3.org/TR/SVG11/painting.html#ColorInterpolationProperty).
822
+
823
+ ```jsx
824
+ function Gradients() {
825
+ const theme = useTheme();
826
+ const spectrumColors: ThemeVars.SpectrumHue[] = [
827
+ 'blue',
828
+ 'green',
829
+ 'orange',
830
+ 'yellow',
831
+ 'gray',
832
+ 'indigo',
833
+ 'pink',
834
+ 'purple',
835
+ 'red',
836
+ 'teal',
837
+ 'chartreuse',
838
+ ];
839
+ const data = [10, 22, 29, 45, 98, 45, 22, 52, 21, 4, 68, 20, 21, 58];
840
+
841
+ const [currentSpectrumColor, setCurrentSpectrumColor] = useState<ThemeVars.SpectrumHue>('pink');
513
842
 
514
843
  return (
515
- <LineChart
844
+ <VStack gap={2}>
845
+ <HStack flexWrap="wrap" gap={1} justifyContent="flex-end">
846
+ {spectrumColors.map((color) => (
847
+ <Pressable
848
+ key={color}
849
+ accessibilityLabel={`Select ${color}`}
850
+ height={16}
851
+ onPress={() => setCurrentSpectrumColor(color)}
852
+ style={{
853
+ backgroundColor: `rgb(${theme.spectrum[`${color}20`]})`,
854
+ borderColor: `rgb(${theme.spectrum[`${color}50`]})`,
855
+ borderWidth: 2,
856
+ }}
857
+ width={16}
858
+ />
859
+ ))}
860
+ </HStack>
861
+ <LineChart
862
+ showYAxis
863
+ height={200}
864
+ points
865
+ series={[
866
+ {
867
+ id: 'continuousGradient',
868
+ data: data,
869
+ gradient: {
870
+ stops: [
871
+ { offset: 0, color: `rgb(${theme.spectrum[`${currentSpectrumColor}80`]})` },
872
+ {
873
+ offset: Math.max(...data),
874
+ color: `rgb(${theme.spectrum[`${currentSpectrumColor}20`]})`,
875
+ },
876
+ ],
877
+ },
878
+ },
879
+ {
880
+ id: 'discreteGradient',
881
+ data: data.map((d) => d + 50),
882
+ // You can create a "discrete" gradient by having multiple stops at the same offset
883
+ gradient: {
884
+ stops: ({ min, max }) => [
885
+ // Allows a function which accepts min/max or direct array
886
+ { offset: min, color: `rgb(${theme.spectrum[`${currentSpectrumColor}80`]})` },
887
+ {
888
+ offset: min + (max - min) / 3,
889
+ color: `rgb(${theme.spectrum[`${currentSpectrumColor}80`]})`,
890
+ },
891
+ {
892
+ offset: min + (max - min) / 3,
893
+ color: `rgb(${theme.spectrum[`${currentSpectrumColor}50`]})`,
894
+ },
895
+ {
896
+ offset: min + ((max - min) / 3) * 2,
897
+ color: `rgb(${theme.spectrum[`${currentSpectrumColor}50`]})`,
898
+ },
899
+ {
900
+ offset: min + ((max - min) / 3) * 2,
901
+ color: `rgb(${theme.spectrum[`${currentSpectrumColor}20`]})`,
902
+ },
903
+ { offset: max, color: `rgb(${theme.spectrum[`${currentSpectrumColor}20`]})` },
904
+ ],
905
+ },
906
+ },
907
+ {
908
+ id: 'xAxisGradient',
909
+ data: data.map((d) => d + 100),
910
+ gradient: {
911
+ // You can also configure by the x-axis.
912
+ axis: 'x',
913
+ stops: ({ min, max }) => [
914
+ {
915
+ offset: min,
916
+ color: `rgb(${theme.spectrum[`${currentSpectrumColor}80`]})`,
917
+ opacity: 0,
918
+ },
919
+ {
920
+ offset: max,
921
+ color: `rgb(${theme.spectrum[`${currentSpectrumColor}20`]})`,
922
+ opacity: 1,
923
+ },
924
+ ],
925
+ },
926
+ },
927
+ ]}
928
+ strokeWidth={4}
929
+ yAxis={{
930
+ showGrid: true,
931
+ }}
932
+ />
933
+ </VStack>
934
+ );
935
+ }
936
+ ```
937
+
938
+ You can even pass in a separate gradient for your `Line` and `Area` components.
939
+
940
+ ```jsx
941
+ function GainLossChart() {
942
+ const theme = useTheme();
943
+ const data = useMemo(() => [-40, -28, -21, -5, 48, -5, -28, 2, -29, -46, 16, -30, -29, 8], []);
944
+ const negativeColor = `rgb(${theme.spectrum.gray15})`;
945
+ const positiveColor = theme.color.fgPositive;
946
+
947
+ const tickLabelFormatter = useCallback(
948
+ (value: number) =>
949
+ new Intl.NumberFormat('en-US', {
950
+ style: 'currency',
951
+ currency: 'USD',
952
+ maximumFractionDigits: 0,
953
+ }).format(value),
954
+ [],
955
+ );
956
+
957
+ // Line gradient: hard color change at 0 (full opacity for line)
958
+ const lineGradient = {
959
+ stops: [
960
+ { offset: 0, color: negativeColor },
961
+ { offset: 0, color: positiveColor },
962
+ ],
963
+ };
964
+
965
+ const GradientDottedArea = memo((props: DottedAreaProps) => (
966
+ <DottedArea
967
+ {...props}
968
+ gradient={{
969
+ stops: ({ min, max }) => [
970
+ { offset: min, color: negativeColor, opacity: 0.4 },
971
+ { offset: 0, color: negativeColor, opacity: 0 },
972
+ { offset: 0, color: positiveColor, opacity: 0 },
973
+ { offset: max, color: positiveColor, opacity: 0.4 },
974
+ ],
975
+ }}
976
+ />
977
+ ));
978
+
979
+ return (
980
+ <CartesianChart
516
981
  enableScrubbing
517
- onScrubberPositionChange={setScrubIndex}
982
+ height={200}
518
983
  series={[
519
984
  {
520
- id: 'line',
521
- data: yData,
985
+ id: 'prices',
986
+ data: data,
987
+ gradient: lineGradient,
522
988
  },
523
989
  ]}
524
- height={150}
525
- showArea
526
- renderPoints={() => true}
527
- curve="natural"
528
- showXAxis
529
- xAxis={{ data: xData, showLine: true, showTickMarks: true, showGrid: true }}
530
- showYAxis
531
- yAxis={{
532
- domain: { min: 0 },
533
- position: 'left',
534
- showLine: true,
535
- showTickMarks: true,
536
- showGrid: true,
990
+ xAxis={{
991
+ range: ({ min, max }) => ({ min, max: max - 16 }),
537
992
  }}
538
- inset={{ top: 16, right: 16, bottom: 0, left: 0 }}
539
- accessibilityLabel={accessibilityLabel}
540
993
  >
994
+ <YAxis showGrid requestedTickCount={2} tickLabelFormatter={tickLabelFormatter} />
995
+ <Line showArea AreaComponent={GradientDottedArea} seriesId="prices" strokeWidth={3} />
541
996
  <Scrubber hideOverlay />
542
- </LineChart>
997
+ </CartesianChart>
543
998
  );
544
999
  }
545
1000
  ```
546
1001
 
547
- ### Accessibility
1002
+ #### Lines
1003
+
1004
+ You can customize lines by placing props in `LineChart` or at each individual series. Lines can have a `type` of `solid` or `dotted`. They can optionally show an area underneath them (using `showArea`).
1005
+
1006
+ ```jsx
1007
+ <LineChart
1008
+ height={200}
1009
+ series={[
1010
+ {
1011
+ id: 'top',
1012
+ data: [15, 28, 32, 44, 46, 36, 40, 45, 48, 38],
1013
+ },
1014
+ {
1015
+ id: 'upperMiddle',
1016
+ data: [12, 23, 21, 29, 34, 28, 31, 38, 42, 35],
1017
+ color: '#ef4444',
1018
+ type: 'dotted',
1019
+ },
1020
+ {
1021
+ id: 'lowerMiddle',
1022
+ data: [8, 15, 14, 25, 20, 18, 22, 28, 24, 30],
1023
+ color: '#f59e0b',
1024
+ curve: 'natural',
1025
+ gradient: {
1026
+ axis: 'x',
1027
+ stops: [
1028
+ { offset: 0, color: '#E3D74D' },
1029
+ { offset: 9, color: '#F7931A' },
1030
+ ],
1031
+ },
1032
+ strokeWidth: 6,
1033
+ },
1034
+ {
1035
+ id: 'bottom',
1036
+ data: [4, 8, 11, 15, 16, 14, 16, 10, 12, 14],
1037
+ color: '#800080',
1038
+ curve: 'step',
1039
+ AreaComponent: DottedArea,
1040
+ showArea: true,
1041
+ },
1042
+ ]}
1043
+ />
1044
+ ```
1045
+
1046
+ You can also add instances of [ReferenceLine](/components/graphs/ReferenceLine) to your LineChart to highlight a specific x or y value.
1047
+
1048
+ ```jsx
1049
+ <LineChart
1050
+ enableScrubbing
1051
+ showArea
1052
+ height={200}
1053
+ series={[
1054
+ {
1055
+ id: 'prices',
1056
+ data: [10, 22, 29, 45, 98, 45, 22, 52, 21, 4, 68, 20, 21, 58],
1057
+ color: theme.color.fgPositive,
1058
+ },
1059
+ ]}
1060
+ xAxis={{
1061
+ // Give space before the end of the chart for the scrubber
1062
+ range: ({ min, max }) => ({ min, max: max - 24 }),
1063
+ }}
1064
+ >
1065
+ <ReferenceLine
1066
+ LineComponent={(props) => <DottedLine {...props} dashIntervals={[0, 16]} strokeWidth={3} />}
1067
+ dataY={10}
1068
+ stroke={theme.color.fg}
1069
+ />
1070
+ <Scrubber />
1071
+ </LineChart>
1072
+ ```
548
1073
 
549
- You can use `accessibilityLabel` to provide a descriptive label for the chart. This is especially important for charts with scrubbing enabled, where the label should update dynamically to reflect the current data point.
1074
+ #### Points
1075
+
1076
+ You can also add instances of [Point](/components/graphs/Point) directly inside of a LineChart.
550
1077
 
551
1078
  ```jsx
552
- function AccessibleBasicChart() {
553
- const [scrubIndex, setScrubIndex] = useState(undefined);
1079
+ function HighLowPrice() {
554
1080
  const data = [10, 22, 29, 45, 98, 45, 22, 52, 21, 4, 68, 20, 21, 58];
1081
+ const minPrice = Math.min(...data);
1082
+ const maxPrice = Math.max(...data);
555
1083
 
556
- const accessibilityLabel = useMemo(() => {
557
- if (scrubIndex === undefined) {
558
- return `Current price: ${data[data.length - 1]}`;
559
- }
560
- return `Price at position ${scrubIndex + 1}: ${data[scrubIndex]}`;
561
- }, [scrubIndex, data]);
1084
+ const minPriceIndex = data.indexOf(minPrice);
1085
+ const maxPriceIndex = data.indexOf(maxPrice);
1086
+
1087
+ const formatPrice = useCallback((price: number) => {
1088
+ return `$${price.toLocaleString('en-US', {
1089
+ minimumFractionDigits: 2,
1090
+ maximumFractionDigits: 2,
1091
+ })}`;
1092
+ }, []);
562
1093
 
563
1094
  return (
564
1095
  <LineChart
565
- enableScrubbing
566
- onScrubberPositionChange={setScrubIndex}
567
- height={150}
1096
+ showArea
1097
+ height={200}
568
1098
  series={[
569
1099
  {
570
1100
  id: 'prices',
571
1101
  data: data,
572
1102
  },
573
1103
  ]}
574
- curve="monotone"
575
- showYAxis
1104
+ >
1105
+ <Point
1106
+ dataX={minPriceIndex}
1107
+ dataY={minPrice}
1108
+ label={formatPrice(minPrice)}
1109
+ labelPosition="bottom"
1110
+ />
1111
+ <Point
1112
+ dataX={maxPriceIndex}
1113
+ dataY={maxPrice}
1114
+ label={formatPrice(maxPrice)}
1115
+ labelPosition="top"
1116
+ />
1117
+ </LineChart>
1118
+ );
1119
+ }
1120
+ ```
1121
+
1122
+ #### Scrubber
1123
+
1124
+ When using [Scrubber](/components/graphs/Scrubber) with series that have labels, labels will automatically render to the side of the scrubber beacon.
1125
+
1126
+ You can customize the line used for and which series will render a scrubber beacon.
1127
+
1128
+ You can have scrubber beacon's pulse by either adding `idlePulse` to Scrubber or use Scrubber's ref to dynamically pulse.
1129
+
1130
+ ```jsx
1131
+ function StylingScrubber() {
1132
+ const theme = useTheme();
1133
+ const pages = ['Page A', 'Page B', 'Page C', 'Page D', 'Page E', 'Page F', 'Page G'];
1134
+ const pageViews = [2400, 1398, 9800, 3908, 4800, 3800, 4300];
1135
+ const uniqueVisitors = [4000, 3000, 2000, 2780, 1890, 2390, 3490];
1136
+
1137
+ const numberFormatter = useCallback(
1138
+ (value: number) => new Intl.NumberFormat('en-US', { maximumFractionDigits: 0 }).format(value),
1139
+ [],
1140
+ );
1141
+
1142
+ return (
1143
+ <LineChart
1144
+ enableScrubbing
576
1145
  showArea
1146
+ showXAxis
1147
+ showYAxis
1148
+ height={200}
1149
+ series={[
1150
+ {
1151
+ id: 'pageViews',
1152
+ data: pageViews,
1153
+ color: theme.color.accentBoldGreen,
1154
+ // Label will render next to scrubber beacon
1155
+ label: 'Page Views',
1156
+ },
1157
+ {
1158
+ id: 'uniqueVisitors',
1159
+ data: uniqueVisitors,
1160
+ color: theme.color.accentBoldPurple,
1161
+ label: 'Unique Visitors',
1162
+ // Default area is gradient
1163
+ areaType: 'dotted',
1164
+ },
1165
+ ]}
1166
+ xAxis={{
1167
+ // Used on the x-axis to provide context for each index from the series data array
1168
+ data: pages,
1169
+ }}
577
1170
  yAxis={{
578
1171
  showGrid: true,
1172
+ tickLabelFormatter: numberFormatter,
579
1173
  }}
580
- accessibilityLabel={accessibilityLabel}
581
1174
  >
582
- <Scrubber />
1175
+ <Scrubber idlePulse LineComponent={SolidLine} seriesIds={['pageViews']} />
583
1176
  </LineChart>
584
1177
  );
585
1178
  }
586
1179
  ```
587
1180
 
588
- When a chart has a visible header or title, you can use `aria-labelledby` to reference it.
1181
+ #### Sizing
1182
+
1183
+ Charts by default take up `100%` of the `width` and `height` available, but can be customized as any other component.
1184
+
1185
+ ##### Compact
1186
+
1187
+ You can also have charts in a compact form.
589
1188
 
590
1189
  ```jsx
591
- function AccessibleChartWithHeader() {
592
- const headerId = useId();
593
- const [scrubIndex, setScrubIndex] = useState(undefined);
594
- const data = [10, 22, 29, 45, 98, 45, 22, 52, 21, 4, 68, 20, 21, 58];
1190
+ function Compact() {
1191
+ const theme = useTheme();
1192
+ const dimensions = { width: 62, height: 18 };
595
1193
 
596
- const accessibilityLabel = useMemo(() => {
597
- if (scrubIndex === undefined) {
598
- return `Line chart showing price trend. Current value: ${data[data.length - 1]}`;
599
- }
600
- return `Value: ${data[scrubIndex]} at position ${scrubIndex + 1}`;
601
- }, [scrubIndex, data]);
1194
+ const sparklineData = prices
1195
+ .map((price) => parseFloat(price))
1196
+ .filter((price, index) => index % 10 === 0);
1197
+ const positiveFloor = Math.min(...sparklineData) - 10;
602
1198
 
603
- return (
604
- <VStack gap={2}>
605
- <Text id={headerId} font="label1">
606
- {accessibilityLabel}
607
- </Text>
1199
+ const negativeData = sparklineData.map((price) => -1 * price).reverse();
1200
+ const negativeCeiling = Math.max(...negativeData) + 10;
1201
+
1202
+ const formatPrice = useCallback((price: number) => {
1203
+ return `$${price.toLocaleString('en-US', {
1204
+ minimumFractionDigits: 2,
1205
+ maximumFractionDigits: 2,
1206
+ })}`;
1207
+ }, []);
1208
+
1209
+ type CompactChartProps = {
1210
+ data: number[];
1211
+ showArea?: boolean;
1212
+ color?: string;
1213
+ referenceY: number;
1214
+ };
1215
+
1216
+ const CompactChart = memo(({ data, showArea, color, referenceY }: CompactChartProps) => (
1217
+ <Box style={{ padding: 1 }}>
608
1218
  <LineChart
609
- enableScrubbing
610
- onScrubberPositionChange={setScrubIndex}
611
- height={150}
1219
+ {...dimensions}
1220
+ enableScrubbing={false}
1221
+ inset={0}
612
1222
  series={[
613
1223
  {
614
- id: 'revenue',
615
- data: data,
1224
+ id: 'btc',
1225
+ data,
1226
+ color,
616
1227
  },
617
1228
  ]}
618
- curve="monotone"
619
- showYAxis
620
- showArea
621
- yAxis={{
622
- showGrid: true,
623
- }}
624
- aria-labelledby={headerId}
1229
+ showArea={showArea}
625
1230
  >
626
- <Scrubber />
1231
+ <ReferenceLine dataY={referenceY} />
627
1232
  </LineChart>
1233
+ </Box>
1234
+ ));
1235
+
1236
+ const ChartCell = memo(
1237
+ ({
1238
+ data,
1239
+ showArea,
1240
+ color,
1241
+ referenceY,
1242
+ subdetail,
1243
+ }: CompactChartProps & { subdetail: string }) => {
1244
+ return (
1245
+ <ListCell
1246
+ detail={formatPrice(parseFloat(prices[0]))}
1247
+ intermediary={
1248
+ <CompactChart color={color} data={data} referenceY={referenceY} showArea={showArea} />
1249
+ }
1250
+ media={<Avatar src={assets.btc.imageUrl} />}
1251
+ onPress={() => console.log('clicked')}
1252
+ spacingVariant="condensed"
1253
+ style={{ padding: 0 }}
1254
+ subdetail={subdetail}
1255
+ />
1256
+ );
1257
+ },
1258
+ );
1259
+
1260
+ return (
1261
+ <VStack>
1262
+ <ChartCell
1263
+ color={assets.btc.color}
1264
+ data={sparklineData}
1265
+ referenceY={parseFloat(prices[Math.floor(prices.length / 4)])}
1266
+ subdetail="-4.55%"
1267
+ />
1268
+ <ChartCell
1269
+ showArea
1270
+ color={assets.btc.color}
1271
+ data={sparklineData}
1272
+ referenceY={parseFloat(prices[Math.floor(prices.length / 4)])}
1273
+ subdetail="-4.55%"
1274
+ />
1275
+ <ChartCell
1276
+ showArea
1277
+ color={theme.color.fgPositive}
1278
+ data={sparklineData}
1279
+ referenceY={positiveFloor}
1280
+ subdetail="+0.25%"
1281
+ />
1282
+ <ChartCell
1283
+ showArea
1284
+ color={theme.color.fgNegative}
1285
+ data={negativeData}
1286
+ referenceY={negativeCeiling}
1287
+ subdetail="-4.55%"
1288
+ />
628
1289
  </VStack>
629
1290
  );
630
1291
  }
631
1292
  ```
632
1293
 
633
- ### Customization
1294
+ ### Composed Examples
634
1295
 
635
1296
  #### Asset Price with Dotted Area
636
1297
 
1298
+ You can use [PeriodSelector](/components/graphs/PeriodSelector) to have a chart where the user can select a time period and the chart automatically animates.
1299
+
637
1300
  ```jsx
638
1301
  function AssetPriceWithDottedArea() {
1302
+ const fontMgr = useMemo(() => {
1303
+ const fontProvider = Skia.TypefaceFontProvider.Make();
1304
+ // Register system fonts if available, otherwise Skia will use defaults
1305
+ return fontProvider;
1306
+ }, []);
1307
+
639
1308
  const BTCTab: TabComponent = memo(
640
- forwardRef(
641
- ({ label, ...props }: SegmentedTabProps, ref: React.ForwardedRef<HTMLButtonElement>) => {
642
- const { activeTab } = useTabsContext();
643
- const isActive = activeTab?.id === props.id;
644
-
645
- return (
646
- <SegmentedTab
647
- ref={ref}
648
- label={
649
- <TextLabel1
650
- style={{
651
- transition: 'color 0.2s ease',
652
- color: isActive ? assets.btc.color : undefined,
653
- }}
654
- >
655
- {label}
656
- </TextLabel1>
657
- }
658
- {...props}
659
- />
660
- );
661
- },
662
- ),
663
- );
1309
+ forwardRef(({ label, ...props }: SegmentedTabProps, ref: React.ForwardedRef<View>) => {
1310
+ const { activeTab } = useTabsContext();
1311
+ const isActive = activeTab?.id === props.id;
664
1312
 
665
- const BTCActiveIndicator = memo(({ style, ...props }: TabsActiveIndicatorProps) => (
1313
+ return (
1314
+ <SegmentedTab
1315
+ ref={ref}
1316
+ label={
1317
+ <TextLabel1
1318
+ style={{
1319
+ color: isActive ? assets.btc.color : undefined,
1320
+ }}
1321
+ >
1322
+ {label}
1323
+ </TextLabel1>
1324
+ }
1325
+ {...props}
1326
+ />
1327
+ );
1328
+ }),
1329
+ );
1330
+ const BTCActiveIndicator = memo(({ style, ...props }: TabsActiveIndicatorProps) => (
666
1331
  <PeriodSelectorActiveIndicator
667
1332
  {...props}
668
- style={{ ...style, backgroundColor: `${assets.btc.color}1A` }}
1333
+ style={[style, { backgroundColor: `${assets.btc.color}1A` }]}
669
1334
  />
670
1335
  ));
671
1336
 
672
- const AssetPriceDotted = memo(() => {
673
- const [scrubIndex, setScrubIndex] = useState<number | undefined>(undefined);
674
- const currentPrice =
675
- sparklineInteractiveData.hour[sparklineInteractiveData.hour.length - 1].value;
676
- const tabs = [
677
- { id: 'hour', label: '1H' },
678
- { id: 'day', label: '1D' },
679
- { id: 'week', label: '1W' },
680
- { id: 'month', label: '1M' },
681
- { id: 'year', label: '1Y' },
682
- { id: 'all', label: 'All' },
683
- ];
684
- const [timePeriod, setTimePeriod] = useState<TabValue>(tabs[0]);
1337
+ const AssetPriceDotted = memo(() => {
1338
+ const theme = useTheme();
1339
+ const currentPrice =
1340
+ sparklineInteractiveData.hour[sparklineInteractiveData.hour.length - 1].value;
1341
+ const tabs = useMemo(
1342
+ () => [
1343
+ { id: 'hour', label: '1H' },
1344
+ { id: 'day', label: '1D' },
1345
+ { id: 'week', label: '1W' },
1346
+ { id: 'month', label: '1M' },
1347
+ { id: 'year', label: '1Y' },
1348
+ { id: 'all', label: 'All' },
1349
+ ],
1350
+ [],
1351
+ );
1352
+ const [timePeriod, setTimePeriod] = useState<TabValue>(tabs[0]);
1353
+
1354
+ const sparklineTimePeriodData = useMemo(() => {
1355
+ return sparklineInteractiveData[timePeriod.id as keyof typeof sparklineInteractiveData];
1356
+ }, [timePeriod]);
1357
+
1358
+ const sparklineTimePeriodDataValues = useMemo(() => {
1359
+ return sparklineTimePeriodData.map((d) => d.value);
1360
+ }, [sparklineTimePeriodData]);
1361
+
1362
+ const sparklineTimePeriodDataTimestamps = useMemo(() => {
1363
+ return sparklineTimePeriodData.map((d) => d.date);
1364
+ }, [sparklineTimePeriodData]);
1365
+
1366
+ const onPeriodChange = useCallback(
1367
+ (period: TabValue | null) => {
1368
+ setTimePeriod(period || tabs[0]);
1369
+ },
1370
+ [tabs, setTimePeriod],
1371
+ );
1372
+
1373
+ const priceFormatter = useMemo(
1374
+ () =>
1375
+ new Intl.NumberFormat('en-US', {
1376
+ style: 'currency',
1377
+ currency: 'USD',
1378
+ }),
1379
+ [],
1380
+ );
1381
+
1382
+ const formatPrice = useCallback(
1383
+ (price: number) => {
1384
+ return priceFormatter.format(price);
1385
+ },
1386
+ [priceFormatter],
1387
+ );
1388
+
1389
+ const formatDate = useCallback((date: Date) => {
1390
+ const dayOfWeek = date.toLocaleDateString('en-US', { weekday: 'short' });
1391
+
1392
+ const monthDay = date.toLocaleDateString('en-US', {
1393
+ month: 'short',
1394
+ day: 'numeric',
1395
+ });
1396
+
1397
+ const time = date.toLocaleTimeString('en-US', {
1398
+ hour: 'numeric',
1399
+ minute: '2-digit',
1400
+ hour12: true,
1401
+ });
1402
+
1403
+ return `${dayOfWeek}, ${monthDay}, ${time}`;
1404
+ }, []);
1405
+
1406
+ return (
1407
+ <VStack gap={2}>
1408
+ <SectionHeader
1409
+ balance={<Text font="title2">{formatPrice(currentPrice)}</Text>}
1410
+ end={
1411
+ <VStack justifyContent="center">
1412
+ <RemoteImage shape="circle" size="xl" source={assets.btc.imageUrl} />
1413
+ </VStack>
1414
+ }
1415
+ title={<Text font="title1">Bitcoin</Text>}
1416
+ />
1417
+ <LineChart
1418
+ enableScrubbing
1419
+ showArea
1420
+ areaType="dotted"
1421
+ height={200}
1422
+ inset={{ top: 52 }}
1423
+ series={[
1424
+ {
1425
+ id: 'btc',
1426
+ data: sparklineTimePeriodDataValues,
1427
+ color: assets.btc.color,
1428
+ },
1429
+ ]}
1430
+ >
1431
+ <Scrubber
1432
+ idlePulse
1433
+ label={(d: number) => {
1434
+ const date = formatDate(sparklineTimePeriodDataTimestamps[d]);
1435
+ const price = formatPrice(sparklineTimePeriodDataValues[d]);
1436
+
1437
+ const regularStyle: SkTextStyle = {
1438
+ fontFamilies: ['Inter'],
1439
+ fontSize: 14,
1440
+ fontStyle: {
1441
+ weight: FontWeight.Normal,
1442
+ },
1443
+ color: Skia.Color(theme.color.fgMuted),
1444
+ };
1445
+
1446
+ const boldStyle: SkTextStyle = {
1447
+ fontFamilies: ['Inter'],
1448
+ ...regularStyle,
1449
+ fontStyle: {
1450
+ weight: FontWeight.Bold,
1451
+ },
1452
+ };
1453
+
1454
+ // 3. Use the ParagraphBuilder
1455
+ const builder = Skia.ParagraphBuilder.Make(
1456
+ {
1457
+ textAlign: TextAlign.Left,
1458
+ },
1459
+ fontMgr,
1460
+ );
1461
+
1462
+ builder.pushStyle(boldStyle);
1463
+ builder.addText(price);
1464
+
1465
+ builder.pushStyle(regularStyle);
1466
+ builder.addText(` ${date}`);
1467
+
1468
+ const para = builder.build();
1469
+ para.layout(512);
1470
+ return para;
1471
+ }}
1472
+ labelElevated
1473
+ />
1474
+ </LineChart>
1475
+ <PeriodSelector
1476
+ TabComponent={BTCTab}
1477
+ TabsActiveIndicatorComponent={BTCActiveIndicator}
1478
+ activeTab={timePeriod}
1479
+ onChange={onPeriodChange}
1480
+ tabs={tabs}
1481
+ />
1482
+ </VStack>
1483
+ );
1484
+ });
1485
+
1486
+ return <AssetPriceDotted />;
1487
+ }
1488
+ ```
685
1489
 
686
- const sparklineTimePeriodData = useMemo(() => {
687
- return sparklineInteractiveData[timePeriod.id as keyof typeof sparklineInteractiveData];
688
- }, [timePeriod]);
1490
+ #### Monotone Asset Price
689
1491
 
690
- const sparklineTimePeriodDataValues = useMemo(() => {
691
- return sparklineTimePeriodData.map((d) => d.value);
692
- }, [sparklineTimePeriodData]);
1492
+ You can adjust [YAxis](/components/graphs/YAxis) and [Scrubber](/components/graphs/Scrubber) to have a chart where the y-axis is overlaid and the beacon is inverted in style.
693
1493
 
694
- const sparklineTimePeriodDataTimestamps = useMemo(() => {
695
- return sparklineTimePeriodData.map((d) => d.date);
696
- }, [sparklineTimePeriodData]);
1494
+ ```jsx
1495
+ function MonotoneAssetPrice() {
1496
+ const theme = useTheme();
1497
+ const prices = sparklineInteractiveData.hour;
697
1498
 
698
- const onPeriodChange = useCallback(
699
- (period: TabValue | null) => {
700
- setTimePeriod(period || tabs[0]);
701
- },
702
- [tabs, setTimePeriod],
703
- );
1499
+ const fontMgr = useMemo(() => {
1500
+ const fontProvider = Skia.TypefaceFontProvider.Make();
1501
+ // Register system fonts if available, otherwise Skia will use defaults
1502
+ return fontProvider;
1503
+ }, []);
704
1504
 
705
1505
  const priceFormatter = useMemo(
706
1506
  () =>
@@ -720,9 +1520,12 @@ const BTCActiveIndicator = memo(({ style, ...props }: TabsActiveIndicatorProps)
720
1520
  [],
721
1521
  );
722
1522
 
723
- const formatPrice = useCallback((price: number) => {
724
- return priceFormatter.format(price);
725
- }, [priceFormatter]);
1523
+ const formatPrice = useCallback(
1524
+ (price: number) => {
1525
+ return priceFormatter.format(price);
1526
+ },
1527
+ [priceFormatter],
1528
+ );
726
1529
 
727
1530
  const formatDate = useCallback((date: Date) => {
728
1531
  const dayOfWeek = date.toLocaleDateString('en-US', { weekday: 'short' });
@@ -743,351 +1546,198 @@ const BTCActiveIndicator = memo(({ style, ...props }: TabsActiveIndicatorProps)
743
1546
 
744
1547
  const scrubberLabel = useCallback(
745
1548
  (index: number) => {
746
- const price = scrubberPriceFormatter.format(sparklineTimePeriodDataValues[index]);
747
- const date = formatDate(sparklineTimePeriodDataTimestamps[index]);
748
- return (
749
- <>
750
- <tspan style={{ fontWeight: 'bold' }}>{price} USD</tspan> {date}
751
- </>
1549
+ const price = scrubberPriceFormatter.format(prices[index].value);
1550
+ const date = formatDate(prices[index].date);
1551
+
1552
+ const regularStyle: SkTextStyle = {
1553
+ fontFamilies: ['Inter'],
1554
+ fontSize: 14,
1555
+ fontStyle: {
1556
+ weight: FontWeight.Normal,
1557
+ },
1558
+ color: Skia.Color(theme.color.fgMuted),
1559
+ };
1560
+
1561
+ const boldStyle: SkTextStyle = {
1562
+ fontFamilies: ['Inter'],
1563
+ ...regularStyle,
1564
+ fontStyle: {
1565
+ weight: FontWeight.Bold,
1566
+ },
1567
+ };
1568
+
1569
+ const builder = Skia.ParagraphBuilder.Make(
1570
+ {
1571
+ textAlign: TextAlign.Left,
1572
+ },
1573
+ fontMgr,
752
1574
  );
1575
+
1576
+ builder.pushStyle(boldStyle);
1577
+ builder.addText(`${price} USD`);
1578
+
1579
+ builder.pushStyle(regularStyle);
1580
+ builder.addText(` ${date}`);
1581
+
1582
+ const para = builder.build();
1583
+ para.layout(512);
1584
+ return para;
753
1585
  },
754
- [scrubberPriceFormatter, sparklineTimePeriodDataValues, sparklineTimePeriodDataTimestamps, formatDate],
1586
+ [scrubberPriceFormatter, prices, formatDate, theme.color.fgMuted, fontMgr],
755
1587
  );
756
1588
 
757
- const accessibilityLabel = useCallback(
758
- (index: number) => {
759
- const price = scrubberPriceFormatter.format(sparklineTimePeriodDataValues[index]);
760
- const date = formatDate(sparklineTimePeriodDataTimestamps[index]);
761
- return `${price} USD ${date}`;
1589
+ const formatAxisLabelPrice = useCallback(
1590
+ (price: number) => {
1591
+ return formatPrice(price);
762
1592
  },
763
- [scrubberPriceFormatter, sparklineTimePeriodDataValues, sparklineTimePeriodDataTimestamps, formatDate],
1593
+ [formatPrice],
764
1594
  );
765
1595
 
766
- return (
767
- <VStack gap={2}>
768
- <SectionHeader
769
- style={{ padding: 0 }}
770
- title={<Text font="title1">Bitcoin</Text>}
771
- balance={<Text font="title2">{formatPrice(currentPrice)}</Text>}
772
- end={
773
- <VStack justifyContent="center">
774
- <RemoteImage source={assets.btc.imageUrl} size="xl" shape="circle" />
775
- </VStack>
776
- }
777
- />
778
- <LineChart
779
- overflow="visible"
780
- enableScrubbing
781
- onScrubberPositionChange={setScrubIndex}
782
- series={[
783
- {
784
- id: 'btc',
785
- data: sparklineTimePeriodDataValues,
786
- color: assets.btc.color,
787
- },
788
- ]}
789
- showArea
790
- areaType="dotted"
791
- height={150}
792
- style={{ outlineColor: assets.btc.color }}
793
- accessibilityLabel={scrubberLabel}
794
- padding={{ left: 2, right: 2 }}
795
- >
796
- <Scrubber label={scrubberLabel} labelProps={{ elevation: 1 }} idlePulse />
797
- </LineChart>
798
- <PeriodSelector
799
- TabComponent={BTCTab}
800
- TabsActiveIndicatorComponent={BTCActiveIndicator}
801
- tabs={tabs}
802
- activeTab={timePeriod}
803
- onChange={onPeriodChange}
804
- />
805
- </VStack>
806
- )});
807
-
808
- return <AssetPriceDotted />;
809
- };
810
- ```
811
-
812
- #### Forecast Asset Price
813
-
814
- ```jsx
815
- function ForecastAssetPrice() {
816
- const ForecastAreaComponent = memo(
817
- (props: AreaComponentProps) => (
818
- <DottedArea {...props} peakOpacity={0.4} baselineOpacity={0.4} />
819
- ),
1596
+ // Custom tick label component with offset positioning
1597
+ const CustomYAxisTickLabel = useCallback(
1598
+ (props: any) => <DefaultAxisTickLabel {...props} dx={4} dy={-12} horizontalAlignment="left" />,
1599
+ [],
820
1600
  );
821
1601
 
822
- const ForecastChart = memo(() => {
823
- const [scrubIndex, setScrubIndex] = useState<number | undefined>(undefined);
824
-
825
- const getDataFromSparkline = useCallback((startDate: Date) => {
826
- const allData = sparklineInteractiveData.all;
827
- if (!allData || allData.length === 0) return [];
828
-
829
- const timelineData = allData.filter((point) => point.date >= startDate);
830
-
831
- return timelineData.map((point) => ({
832
- date: point.date,
833
- value: point.value,
834
- }));
835
- }, []);
836
-
837
- const historicalData = useMemo(() => getDataFromSparkline(new Date('2019-01-01')), [getDataFromSparkline]);
838
-
839
- const annualGrowthRate = 10;
840
-
841
- const generateForecastData = useCallback(
842
- (lastDate: Date, lastPrice: number, growthRate: number) => {
843
- const dailyGrowthRate = Math.pow(1 + growthRate / 100, 1 / 365) - 1;
844
- const forecastData = [];
845
- const fiveYearsFromNow = new Date(lastDate);
846
- fiveYearsFromNow.setFullYear(fiveYearsFromNow.getFullYear() + 5);
847
-
848
- // Generate daily forecast points for 5 years
849
- const currentDate = new Date(lastDate);
850
- let currentPrice = lastPrice;
851
-
852
- while (currentDate <= fiveYearsFromNow) {
853
- currentPrice = currentPrice * (1 + dailyGrowthRate * 10);
854
- forecastData.push({
855
- date: new Date(currentDate),
856
- value: Math.round(currentPrice),
857
- });
858
- currentDate.setDate(currentDate.getDate() + 10);
859
- }
860
-
861
- return forecastData;
862
- },
863
- [],
864
- );
865
-
866
- const priceFormatter = useMemo(
867
- () =>
868
- new Intl.NumberFormat('en-US', {
869
- minimumFractionDigits: 2,
870
- maximumFractionDigits: 2,
871
- }),
872
- [],
873
- );
874
-
875
- const formatDate = useCallback((date: Date) => {
876
- const dayOfWeek = date.toLocaleDateString('en-US', { weekday: 'short' });
877
-
878
- const monthDay = date.toLocaleDateString('en-US', {
879
- month: 'short',
880
- day: 'numeric',
881
- });
882
-
883
- const time = date.toLocaleTimeString('en-US', {
884
- hour: 'numeric',
885
- minute: '2-digit',
886
- hour12: true,
887
- });
888
-
889
- return `${dayOfWeek}, ${monthDay}, ${time}`;
890
- }, []);
891
-
892
- const forecastData = useMemo(() => {
893
- if (historicalData.length === 0) return [];
894
- const lastPoint = historicalData[historicalData.length - 1];
895
- return generateForecastData(lastPoint.date, lastPoint.value, annualGrowthRate);
896
- }, [generateForecastData, historicalData, annualGrowthRate]);
1602
+ const CustomScrubberBeacon = memo(
1603
+ ({ dataX, dataY, seriesId, isIdle, animate = true }: ScrubberBeaconProps) => {
1604
+ const { getSeries, getXSerializableScale, getYSerializableScale } =
1605
+ useCartesianChartContext();
897
1606
 
898
- // Combine all data points with dates converted to timestamps for x-axis
899
- const allDataPoints = useMemo(
900
- () => [...historicalData, ...forecastData],
901
- [historicalData, forecastData],
902
- );
1607
+ const targetSeries = useMemo(() => getSeries(seriesId), [getSeries, seriesId]);
1608
+ const xScale = useMemo(() => getXSerializableScale(), [getXSerializableScale]);
1609
+ const yScale = useMemo(
1610
+ () => getYSerializableScale(targetSeries?.yAxisId),
1611
+ [getYSerializableScale, targetSeries?.yAxisId],
1612
+ );
903
1613
 
904
- const historicalDataValues = useMemo(
905
- () => historicalData.map((d) => d.value),
906
- [historicalData],
907
- );
1614
+ const animatedX = useSharedValue(0);
1615
+ const animatedY = useSharedValue(0);
1616
+
1617
+ // Calculate the target point position - project data to pixels
1618
+ const targetPoint = useDerivedValue(() => {
1619
+ if (!xScale || !yScale) return { x: 0, y: 0 };
1620
+ return projectPointWithSerializableScale({
1621
+ x: unwrapAnimatedValue(dataX),
1622
+ y: unwrapAnimatedValue(dataY),
1623
+ xScale,
1624
+ yScale,
1625
+ });
1626
+ }, [dataX, dataY, xScale, yScale]);
908
1627
 
909
- const forecastDataValues = useMemo(
910
- () => [...historicalData.map((d) => null), ...forecastData.map((d) => d.value)],
911
- [historicalData, forecastData],
912
- );
1628
+ useAnimatedReaction(
1629
+ () => {
1630
+ return { point: targetPoint.value, isIdle: unwrapAnimatedValue(isIdle) };
1631
+ },
1632
+ (current, previous) => {
1633
+ // When animation is disabled, on initial render, or when we are starting,
1634
+ // continuing, or finishing scrubbing we should immediately transition
1635
+ if (!animate || previous === null || !previous.isIdle || !current.isIdle) {
1636
+ animatedX.value = current.point.x;
1637
+ animatedY.value = current.point.y;
1638
+ return;
1639
+ }
913
1640
 
914
- const xAxisData = useMemo(
915
- () => allDataPoints.map((d) => d.date.getTime()),
916
- [allDataPoints],
917
- );
1641
+ animatedX.value = buildTransition(current.point.x, defaultTransition);
1642
+ animatedY.value = buildTransition(current.point.y, defaultTransition);
1643
+ },
1644
+ [animate],
1645
+ );
918
1646
 
919
- const scrubberLabel = useCallback(
920
- (index: number) => {
921
- const price = priceFormatter.format(allDataPoints[index].value);
922
- const date = formatDate(allDataPoints[index].date);
923
- return (
924
- <>
925
- <tspan style={{ fontWeight: 'bold' }}>{price} USD</tspan> {date}
926
- </>
927
- );
928
- },
929
- [priceFormatter, allDataPoints, formatDate],
930
- );
1647
+ // Create animated point using the animated values
1648
+ const animatedPoint = useDerivedValue(() => {
1649
+ return { x: animatedX.value, y: animatedY.value };
1650
+ }, [animatedX, animatedY]);
931
1651
 
932
- const accessibilityLabel = useCallback(
933
- (index: number) => {
934
- const price = priceFormatter.format(allDataPoints[index].value);
935
- const date = formatDate(allDataPoints[index].date);
936
- return `${price} USD ${date}`;
937
- },
938
- [priceFormatter, allDataPoints, formatDate],
939
- );
1652
+ return (
1653
+ <>
1654
+ <Circle c={animatedPoint} color={theme.color.bg} r={5} />
1655
+ <Circle c={animatedPoint} color={theme.color.fg} r={5} strokeWidth={3} style="stroke" />
1656
+ </>
1657
+ );
1658
+ },
1659
+ );
940
1660
 
941
- return (
942
- <LineChart
943
- overflow="visible"
944
- animate={false}
945
- enableScrubbing
946
- showArea
947
- showXAxis
948
- AreaComponent={ForecastAreaComponent}
949
- height={150}
950
- padding={{
951
- top: 4,
952
- left: 2,
953
- right: 2,
954
- bottom: 0,
955
- }}
956
- series={[
957
- {
958
- id: 'historical',
959
- data: historicalDataValues,
960
- color: assets.btc.color,
961
- },
962
- {
963
- id: 'forecast',
964
- data: forecastDataValues,
965
- color: assets.btc.color,
966
- type: 'dotted',
967
- },
968
- ]}
969
- xAxis={{
970
- data: xAxisData,
971
- tickLabelFormatter: (value: number) => {
972
- return new Date(value).toLocaleDateString('en-US', {
973
- month: 'numeric',
974
- year: 'numeric',
975
- });
1661
+ return (
1662
+ <LineChart
1663
+ enableScrubbing
1664
+ showYAxis
1665
+ height={200}
1666
+ inset={{ top: 64 }}
1667
+ series={[
1668
+ {
1669
+ id: 'btc',
1670
+ data: prices.map((price) => price.value),
1671
+ color: theme.color.fg,
1672
+ gradient: {
1673
+ axis: 'x',
1674
+ stops: ({ min }) => [
1675
+ { offset: min, color: theme.color.fg, opacity: 0 },
1676
+ { offset: 32, color: theme.color.fg, opacity: 1 },
1677
+ ],
976
1678
  },
977
- tickInterval: 2,
978
- }}
979
- accessibilityLabel={accessibilityLabel}
980
- style={{ outlineColor: assets.btc.color }}
981
- >
982
- <Scrubber label={scrubberLabel} labelProps={{ elevation: 1 }} />
983
- </LineChart>
984
- );
985
- });
986
-
987
- return <ForecastChart />;
988
- };
1679
+ },
1680
+ ]}
1681
+ xAxis={{
1682
+ range: ({ max }) => ({ min: 96, max }),
1683
+ }}
1684
+ yAxis={{
1685
+ position: 'left',
1686
+ width: 0,
1687
+ showGrid: true,
1688
+ tickLabelFormatter: formatAxisLabelPrice,
1689
+ TickLabelComponent: CustomYAxisTickLabel,
1690
+ }}
1691
+ >
1692
+ <Scrubber
1693
+ labelElevated
1694
+ hideOverlay
1695
+ BeaconComponent={CustomScrubberBeacon}
1696
+ LineComponent={SolidLine}
1697
+ label={scrubberLabel}
1698
+ />
1699
+ </LineChart>
1700
+ );
1701
+ }
989
1702
  ```
990
1703
 
991
- #### Availability
1704
+ #### Service Availability
1705
+
1706
+ You can have irregular data points by passing in `data` to `xAxis`.
992
1707
 
993
1708
  ```jsx
994
- function AvailabilityChart() {
1709
+ function ServiceAvailability() {
995
1710
  const theme = useTheme();
996
- const [scrubIndex, setScrubIndex] = useState(undefined);
997
-
998
- const availabilityEvents = [
999
- {
1000
- date: new Date('2022-01-01'),
1001
- availability: 79,
1002
- },
1003
- {
1004
- date: new Date('2022-01-03'),
1005
- availability: 81,
1006
- },
1007
- {
1008
- date: new Date('2022-01-04'),
1009
- availability: 82,
1010
- },
1011
- {
1012
- date: new Date('2022-01-06'),
1013
- availability: 91,
1014
- },
1015
- {
1016
- date: new Date('2022-01-07'),
1017
- availability: 92,
1018
- },
1019
- {
1020
- date: new Date('2022-01-10'),
1021
- availability: 86,
1022
- },
1023
- ];
1024
-
1025
- const accessibilityLabel = useMemo(() => {
1026
- if (scrubIndex === undefined) return undefined;
1027
- const event = availabilityEvents[scrubIndex];
1028
- const formattedDate = event.date.toLocaleDateString('en-US', {
1029
- weekday: 'short',
1030
- month: 'short',
1031
- day: 'numeric',
1032
- year: 'numeric',
1033
- });
1034
- const status =
1035
- event.availability >= 90 ? 'Good' : event.availability >= 85 ? 'Warning' : 'Critical';
1036
- return `${formattedDate}: Availability ${event.availability}% - Status: ${status}`;
1037
- }, [scrubIndex, availabilityEvents]);
1038
-
1039
- const ChartDefs = memo(({ yellowThresholdPercentage = 85, greenThresholdPercentage = 90 }) => {
1040
- const { drawingArea, height, series, getYScale, getYAxis } = useCartesianChartContext();
1041
- const yScale = getYScale();
1042
- const yAxis = getYAxis();
1043
-
1044
- if (!series || !drawingArea || !yScale) return null;
1045
-
1046
- const rangeBounds = yAxis?.domain;
1047
- const rangeMin = rangeBounds?.min ?? 0;
1048
- const rangeMax = rangeBounds?.max ?? 100;
1049
-
1050
- // Calculate the Y positions in the chart coordinate system
1051
- const yellowThresholdY = yScale(yellowThresholdPercentage) ?? 0;
1052
- const greenThresholdY = yScale(greenThresholdPercentage) ?? 0;
1053
- const minY = yScale(rangeMax) ?? 0; // Top of chart (max value)
1054
- const maxY = yScale(rangeMin) ?? drawingArea.height; // Bottom of chart (min value)
1055
-
1056
- // Calculate percentages based on actual chart positions
1057
- const yellowThreshold = ((yellowThresholdY - minY) / (maxY - minY)) * 100;
1058
- const greenThreshold = ((greenThresholdY - minY) / (maxY - minY)) * 100;
1059
-
1060
- return (
1061
- <Defs>
1062
- <LinearGradient
1063
- gradientUnits="userSpaceOnUse"
1064
- id="availabilityGradient"
1065
- x1="0%"
1066
- x2="0%"
1067
- y1={minY}
1068
- y2={maxY}
1069
- >
1070
- <stop offset="0%" stopColor={theme.color.fgPositive} />
1071
- <stop offset={`${greenThreshold}%`} stopColor={theme.color.fgPositive} />
1072
- <stop offset={`${greenThreshold}%`} stopColor={theme.color.fgWarning} />
1073
- <stop offset={`${yellowThreshold}%`} stopColor={theme.color.fgWarning} />
1074
- <stop offset={`${yellowThreshold}%`} stopColor={theme.color.fgNegative} />
1075
- <stop offset="100%" stopColor={theme.color.fgNegative} />
1076
- </LinearGradient>
1077
- </Defs>
1078
- );
1079
- });
1711
+ const availabilityEvents = useMemo(
1712
+ () => [
1713
+ { date: new Date('2022-01-01'), availability: 79 },
1714
+ { date: new Date('2022-01-03'), availability: 81 },
1715
+ { date: new Date('2022-01-04'), availability: 82 },
1716
+ { date: new Date('2022-01-06'), availability: 91 },
1717
+ { date: new Date('2022-01-07'), availability: 92 },
1718
+ { date: new Date('2022-01-10'), availability: 86 },
1719
+ ],
1720
+ [],
1721
+ );
1080
1722
 
1081
1723
  return (
1082
1724
  <CartesianChart
1083
1725
  enableScrubbing
1084
- onScrubberPositionChange={setScrubIndex}
1085
- height={150}
1726
+ height={200}
1086
1727
  series={[
1087
1728
  {
1088
1729
  id: 'availability',
1089
1730
  data: availabilityEvents.map((event) => event.availability),
1090
- color: 'url(#availabilityGradient)',
1731
+ gradient: {
1732
+ stops: ({ min, max }) => [
1733
+ { offset: min, color: theme.color.fgNegative },
1734
+ { offset: 85, color: theme.color.fgNegative },
1735
+ { offset: 85, color: theme.color.fgWarning },
1736
+ { offset: 90, color: theme.color.fgWarning },
1737
+ { offset: 90, color: theme.color.fgPositive },
1738
+ { offset: max, color: theme.color.fgPositive },
1739
+ ],
1740
+ },
1091
1741
  },
1092
1742
  ]}
1093
1743
  xAxis={{
@@ -1096,10 +1746,7 @@ function AvailabilityChart() {
1096
1746
  yAxis={{
1097
1747
  domain: ({ min, max }) => ({ min: Math.max(min - 2, 0), max: Math.min(max + 2, 100) }),
1098
1748
  }}
1099
- padding={{ left: 2, right: 2 }}
1100
- accessibilityLabel={accessibilityLabel}
1101
1749
  >
1102
- <ChartDefs />
1103
1750
  <XAxis
1104
1751
  showGrid
1105
1752
  showLine
@@ -1115,10 +1762,10 @@ function AvailabilityChart() {
1115
1762
  />
1116
1763
  <Line
1117
1764
  curve="stepAfter"
1118
- renderPoints={() => ({
1765
+ points={(props) => ({
1766
+ ...props,
1119
1767
  fill: theme.color.bg,
1120
- stroke: 'url(#availabilityGradient)',
1121
- strokeWidth: 2,
1768
+ stroke: props.fill,
1122
1769
  })}
1123
1770
  seriesId="availability"
1124
1771
  />
@@ -1128,81 +1775,139 @@ function AvailabilityChart() {
1128
1775
  }
1129
1776
  ```
1130
1777
 
1131
- #### Asset Price Widget
1778
+ #### Forecast Asset Price
1132
1779
 
1133
- You can coordinate LineChart with custom styles to create a custom card that shows the latest price and percent change.
1780
+ You can combine multiple lines within a series to change styles dynamically.
1134
1781
 
1135
1782
  ```jsx
1136
- function BitcoinChartWithScrubberBeacon() {
1137
- const theme = useTheme();
1138
- const prices = [...btcCandles].reverse().map((candle) => parseFloat(candle.close));
1139
- const latestPrice = prices[prices.length - 1];
1140
-
1141
- const formatPrice = (price: number) => {
1142
- return new Intl.NumberFormat('en-US', {
1143
- style: 'currency',
1144
- currency: 'USD',
1145
- }).format(price);
1146
- };
1783
+ function ForecastAssetPrice() {
1784
+ const startYear = 2020;
1785
+ const data = [50, 45, 47, 46, 54, 54, 60, 61, 63, 66, 70];
1786
+ const currentIndex = 6;
1147
1787
 
1148
- const formatPercentChange = (price: number) => {
1149
- return new Intl.NumberFormat('en-US', {
1150
- style: 'percent',
1151
- minimumFractionDigits: 2,
1152
- maximumFractionDigits: 2,
1153
- }).format(price);
1154
- };
1788
+ const strokeWidth = 3;
1789
+ // To prevent cutting off the edge of our lines
1790
+ const clipOffset = strokeWidth;
1791
+
1792
+ const axisFormatter = useCallback(
1793
+ (dataIndex: number) => {
1794
+ return `${startYear + dataIndex}`;
1795
+ },
1796
+ [startYear],
1797
+ );
1798
+
1799
+ const HistoricalLineComponent = memo((props: SolidLineProps) => {
1800
+ const { drawingArea, getXScale } = useCartesianChartContext();
1801
+ const xScale = getXScale();
1802
+
1803
+ const historicalClipPath = useMemo(() => {
1804
+ if (!xScale || !drawingArea) return null;
1805
+
1806
+ const currentX = xScale(currentIndex);
1807
+ if (currentX === undefined) return null;
1808
+
1809
+ // Create clip path for historical data (left side)
1810
+ const clip = Skia.Path.Make();
1811
+ clip.addRect({
1812
+ x: drawingArea.x - clipOffset,
1813
+ y: drawingArea.y - clipOffset,
1814
+ width: currentX + clipOffset - drawingArea.x,
1815
+ height: drawingArea.height + clipOffset * 2,
1816
+ });
1817
+ return clip;
1818
+ }, [xScale, drawingArea]);
1819
+
1820
+ if (!historicalClipPath) return null;
1821
+
1822
+ return (
1823
+ <Group clip={historicalClipPath}>
1824
+ <SolidLine strokeWidth={strokeWidth} {...props} />
1825
+ </Group>
1826
+ );
1827
+ });
1828
+
1829
+ // Since the solid and dotted line have different curves,
1830
+ // we need two separate line components. Otherwise we could
1831
+ // have one line component with SolidLine and DottedLine inside
1832
+ // of it and two clipPaths.
1833
+ const ForecastLineComponent = memo((props: DottedLineProps) => {
1834
+ const { drawingArea, getXScale } = useCartesianChartContext();
1835
+ const xScale = getXScale();
1836
+
1837
+ const forecastClipPath = useMemo(() => {
1838
+ if (!xScale || !drawingArea) return null;
1839
+
1840
+ const currentX = xScale(currentIndex);
1841
+ if (currentX === undefined) return null;
1842
+
1843
+ // Create clip path for forecast data (right side)
1844
+ const clip = Skia.Path.Make();
1845
+ clip.addRect({
1846
+ x: currentX,
1847
+ y: drawingArea.y - clipOffset,
1848
+ width: drawingArea.x + drawingArea.width - currentX + clipOffset * 2,
1849
+ height: drawingArea.height + clipOffset * 2,
1850
+ });
1851
+ return clip;
1852
+ }, [xScale, drawingArea]);
1155
1853
 
1156
- const percentChange = (latestPrice - prices[0]) / prices[0];
1854
+ if (!forecastClipPath) return null;
1855
+
1856
+ return (
1857
+ <Group clip={forecastClipPath}>
1858
+ <DottedLine dashIntervals={[0, strokeWidth * 2]} strokeWidth={strokeWidth} {...props} />
1859
+ </Group>
1860
+ );
1861
+ });
1862
+ const CustomScrubber = memo(() => {
1863
+ const { scrubberPosition } = useScrubberContext();
1864
+
1865
+ const idleScrubberOpacity = useDerivedValue(
1866
+ () => (scrubberPosition.value === undefined ? 1 : 0),
1867
+ [scrubberPosition],
1868
+ );
1869
+ const scrubberOpacity = useDerivedValue(
1870
+ () => (scrubberPosition.value !== undefined ? 1 : 0),
1871
+ [scrubberPosition],
1872
+ );
1873
+
1874
+ // Fade in animation for the Scrubber
1875
+ const fadeInOpacity = useSharedValue(0);
1876
+
1877
+ useEffect(() => {
1878
+ fadeInOpacity.value = withDelay(350, withTiming(1, { duration: 150 }));
1879
+ }, [fadeInOpacity]);
1880
+
1881
+ return (
1882
+ <Group opacity={fadeInOpacity}>
1883
+ <Group opacity={scrubberOpacity}>
1884
+ <Scrubber hideOverlay />
1885
+ </Group>
1886
+ <Group opacity={idleScrubberOpacity}>
1887
+ <DefaultScrubberBeacon
1888
+ isIdle
1889
+ dataX={currentIndex}
1890
+ dataY={data[currentIndex]}
1891
+ seriesId="price"
1892
+ />
1893
+ </Group>
1894
+ </Group>
1895
+ );
1896
+ });
1157
1897
 
1158
1898
  return (
1159
- <VStack
1160
- style={{
1161
- background:
1162
- 'linear-gradient(0deg, rgba(0, 0, 0, 0.80) 0%, rgba(0, 0, 0, 0.80) 100%), #ED702F',
1163
- }}
1164
- borderRadius={300}
1165
- gap={2}
1166
- padding={2}
1167
- paddingBottom={0}
1168
- overflow="hidden"
1899
+ <CartesianChart
1900
+ enableScrubbing
1901
+ height={200}
1902
+ series={[{ id: 'price', data, color: assets.btc.color }]}
1169
1903
  >
1170
- <HStack gap={2} alignItems="center">
1171
- <RemoteImage source={assets.btc.imageUrl} size="xxl" shape="circle" aria-hidden />
1172
- <VStack gap={0.25} alignItems="flex-end" flexGrow={1}>
1173
- <Text font="title1" style={{ color: 'white' }}>
1174
- {formatPrice(latestPrice)}
1175
- </Text>
1176
- <Text font="label1" color="fgPositive" accessibilityLabel={`Up ${formatPercentChange(percentChange)}`}>
1177
- +{formatPercentChange(percentChange)}
1178
- </Text>
1179
- </VStack>
1180
- </HStack>
1181
- <div
1182
- style={{
1183
- marginLeft: `calc(-1 * ${theme.space[2]})`,
1184
- marginRight: `calc(-1 * ${theme.space[2]})`,
1185
- }}
1186
- >
1187
- <LineChart
1188
- inset={{ left: 0, right: 18, bottom: 0, top: 0 }}
1189
- series={[
1190
- {
1191
- id: 'btcPrice',
1192
- data: prices,
1193
- color: assets.btc.color,
1194
- },
1195
- ]}
1196
- showArea
1197
- width="100%"
1198
- height={92}
1199
- >
1200
- <Scrubber idlePulse styles={{ beacon: { stroke: 'white' } }} />
1201
- </LineChart>
1202
- </div>
1203
- </VStack>
1904
+ <Line LineComponent={HistoricalLineComponent} curve="linear" seriesId="price" />
1905
+ <Line LineComponent={ForecastLineComponent} curve="monotone" seriesId="price" type="dotted" />
1906
+ <XAxis position="bottom" requestedTickCount={3} tickLabelFormatter={axisFormatter} />
1907
+ <CustomScrubber />
1908
+ </CartesianChart>
1204
1909
  );
1205
- };
1910
+ }
1206
1911
  ```
1207
1912
 
1208
1913
  ## Props
@@ -1211,7 +1916,7 @@ function BitcoinChartWithScrubberBeacon() {
1211
1916
  | --- | --- | --- | --- | --- |
1212
1917
  | `AreaComponent` | `AreaComponent` | No | `-` | Custom component to render line area fill. |
1213
1918
  | `LineComponent` | `LineComponent` | No | `-` | Component to render the line. Takes precedence over the type prop if provided. |
1214
- | `alignContent` | `flex-start \| flex-end \| center \| stretch \| space-between \| space-around \| space-evenly` | No | `-` | - |
1919
+ | `alignContent` | `flex-start \| flex-end \| center \| space-between \| space-around \| space-evenly \| stretch` | No | `-` | - |
1215
1920
  | `alignItems` | `flex-start \| flex-end \| center \| stretch \| baseline` | No | `-` | - |
1216
1921
  | `alignSelf` | `auto \| FlexAlignType` | No | `-` | - |
1217
1922
  | `allowOverflowGestures` | `boolean` | No | `-` | Allows continuous gestures on the chart to continue outside the bounds of the chart element. |
@@ -1240,10 +1945,11 @@ function BitcoinChartWithScrubberBeacon() {
1240
1945
  | `borderedVertical` | `boolean` | No | `-` | Add a border to the top and bottom sides of the box. |
1241
1946
  | `bottom` | `string \| number` | No | `-` | - |
1242
1947
  | `color` | `currentColor \| fg \| fgMuted \| fgInverse \| fgPrimary \| fgWarning \| fgPositive \| fgNegative \| bg \| bgAlternate \| bgInverse \| bgOverlay \| bgElevation1 \| bgElevation2 \| bgPrimary \| bgPrimaryWash \| bgSecondary \| bgTertiary \| bgSecondaryWash \| bgNegative \| bgNegativeWash \| bgPositive \| bgPositiveWash \| bgWarning \| bgWarningWash \| bgLine \| bgLineHeavy \| bgLineInverse \| bgLinePrimary \| bgLinePrimarySubtle \| accentSubtleRed \| accentBoldRed \| accentSubtleGreen \| accentBoldGreen \| accentSubtleBlue \| accentBoldBlue \| accentSubtlePurple \| accentBoldPurple \| accentSubtleYellow \| accentBoldYellow \| accentSubtleGray \| accentBoldGray \| transparent` | No | `-` | - |
1243
- | `columnGap` | `0 \| 1 \| 2 \| 3 \| 4 \| 5 \| 6 \| 7 \| 8 \| 9 \| 10 \| 0.25 \| 0.5 \| 0.75 \| 1.5` | No | `-` | - |
1244
- | `curve` | `bump \| catmullRom \| linear \| linearClosed \| monotone \| natural \| step \| stepBefore \| stepAfter` | No | `'linear'` | The curve interpolation method to use for the line. |
1948
+ | `columnGap` | `0 \| 1 \| 2 \| 0.25 \| 0.5 \| 0.75 \| 1.5 \| 3 \| 4 \| 5 \| 6 \| 7 \| 8 \| 9 \| 10` | No | `-` | - |
1949
+ | `connectNulls` | `boolean` | No | `-` | When true, the area is connected across null values. |
1950
+ | `curve` | `bump \| catmullRom \| linear \| linearClosed \| monotone \| natural \| step \| stepBefore \| stepAfter` | No | `'bump'` | The curve interpolation method to use for the line. |
1245
1951
  | `dangerouslySetBackground` | `string` | No | `-` | - |
1246
- | `display` | `flex \| none` | No | `-` | - |
1952
+ | `display` | `none \| flex` | No | `-` | - |
1247
1953
  | `elevation` | `0 \| 1 \| 2` | No | `-` | Determines box shadow styles. Parent should have overflow set to visible to ensure styles are not clipped. |
1248
1954
  | `enableScrubbing` | `boolean` | No | `-` | Enables scrubbing interactions. When true, allows scrubbing and makes scrubber components interactive. |
1249
1955
  | `flexBasis` | `string \| number` | No | `-` | - |
@@ -1252,23 +1958,24 @@ function BitcoinChartWithScrubberBeacon() {
1252
1958
  | `flexShrink` | `number` | No | `-` | - |
1253
1959
  | `flexWrap` | `wrap \| nowrap \| wrap-reverse` | No | `-` | - |
1254
1960
  | `font` | `inherit \| FontFamily` | No | `-` | - |
1255
- | `fontFamily` | `inherit \| FontFamily` | No | `-` | - |
1256
- | `fontSize` | `inherit \| FontSize` | No | `-` | - |
1961
+ | `fontFamilies` | `string[]` | No | `-` | Default font families to use within ChartText. If not provided, will be the default for the system. |
1962
+ | `fontProvider` | `SkTypefaceFontProvider` | No | `-` | Skia font provider to allow for custom fonts. If not provided, the only available fonts will be those defined by the system. |
1963
+ | `fontSize` | `FontSize \| inherit` | No | `-` | - |
1257
1964
  | `fontWeight` | `inherit \| FontWeight` | No | `-` | - |
1258
- | `gap` | `0 \| 1 \| 2 \| 3 \| 4 \| 5 \| 6 \| 7 \| 8 \| 9 \| 10 \| 0.25 \| 0.5 \| 0.75 \| 1.5` | No | `-` | - |
1259
- | `height` | `string \| number` | No | `-` | Chart height. If not provided, will use the containers measured height. |
1965
+ | `gap` | `0 \| 1 \| 2 \| 0.25 \| 0.5 \| 0.75 \| 1.5 \| 3 \| 4 \| 5 \| 6 \| 7 \| 8 \| 9 \| 10` | No | `-` | - |
1966
+ | `height` | `string \| number` | No | `-` | - |
1260
1967
  | `inset` | `number \| Partial<ChartInset>` | No | `-` | Inset around the entire chart (outside the axes). |
1261
1968
  | `justifyContent` | `flex-start \| flex-end \| center \| space-between \| space-around \| space-evenly` | No | `-` | - |
1262
1969
  | `key` | `Key \| null` | No | `-` | - |
1263
1970
  | `left` | `string \| number` | No | `-` | - |
1264
1971
  | `lineHeight` | `inherit \| LineHeight` | No | `-` | - |
1265
- | `margin` | `0 \| -1 \| -2 \| -3 \| -4 \| -5 \| -6 \| -7 \| -8 \| -9 \| -10 \| -0.25 \| -0.5 \| -0.75 \| -1.5` | No | `-` | - |
1266
- | `marginBottom` | `0 \| -1 \| -2 \| -3 \| -4 \| -5 \| -6 \| -7 \| -8 \| -9 \| -10 \| -0.25 \| -0.5 \| -0.75 \| -1.5` | No | `-` | - |
1267
- | `marginEnd` | `0 \| -1 \| -2 \| -3 \| -4 \| -5 \| -6 \| -7 \| -8 \| -9 \| -10 \| -0.25 \| -0.5 \| -0.75 \| -1.5` | No | `-` | - |
1268
- | `marginStart` | `0 \| -1 \| -2 \| -3 \| -4 \| -5 \| -6 \| -7 \| -8 \| -9 \| -10 \| -0.25 \| -0.5 \| -0.75 \| -1.5` | No | `-` | - |
1269
- | `marginTop` | `0 \| -1 \| -2 \| -3 \| -4 \| -5 \| -6 \| -7 \| -8 \| -9 \| -10 \| -0.25 \| -0.5 \| -0.75 \| -1.5` | No | `-` | - |
1270
- | `marginX` | `0 \| -1 \| -2 \| -3 \| -4 \| -5 \| -6 \| -7 \| -8 \| -9 \| -10 \| -0.25 \| -0.5 \| -0.75 \| -1.5` | No | `-` | - |
1271
- | `marginY` | `0 \| -1 \| -2 \| -3 \| -4 \| -5 \| -6 \| -7 \| -8 \| -9 \| -10 \| -0.25 \| -0.5 \| -0.75 \| -1.5` | No | `-` | - |
1972
+ | `margin` | `0 \| -1 \| -2 \| -0.25 \| -0.5 \| -0.75 \| -1.5 \| -3 \| -4 \| -5 \| -6 \| -7 \| -8 \| -9 \| -10` | No | `-` | - |
1973
+ | `marginBottom` | `0 \| -1 \| -2 \| -0.25 \| -0.5 \| -0.75 \| -1.5 \| -3 \| -4 \| -5 \| -6 \| -7 \| -8 \| -9 \| -10` | No | `-` | - |
1974
+ | `marginEnd` | `0 \| -1 \| -2 \| -0.25 \| -0.5 \| -0.75 \| -1.5 \| -3 \| -4 \| -5 \| -6 \| -7 \| -8 \| -9 \| -10` | No | `-` | - |
1975
+ | `marginStart` | `0 \| -1 \| -2 \| -0.25 \| -0.5 \| -0.75 \| -1.5 \| -3 \| -4 \| -5 \| -6 \| -7 \| -8 \| -9 \| -10` | No | `-` | - |
1976
+ | `marginTop` | `0 \| -1 \| -2 \| -0.25 \| -0.5 \| -0.75 \| -1.5 \| -3 \| -4 \| -5 \| -6 \| -7 \| -8 \| -9 \| -10` | No | `-` | - |
1977
+ | `marginX` | `0 \| -1 \| -2 \| -0.25 \| -0.5 \| -0.75 \| -1.5 \| -3 \| -4 \| -5 \| -6 \| -7 \| -8 \| -9 \| -10` | No | `-` | - |
1978
+ | `marginY` | `0 \| -1 \| -2 \| -0.25 \| -0.5 \| -0.75 \| -1.5 \| -3 \| -4 \| -5 \| -6 \| -7 \| -8 \| -9 \| -10` | No | `-` | - |
1272
1979
  | `maxHeight` | `string \| number` | No | `-` | - |
1273
1980
  | `maxWidth` | `string \| number` | No | `-` | - |
1274
1981
  | `minHeight` | `string \| number` | No | `-` | - |
@@ -1286,39 +1993,42 @@ function BitcoinChartWithScrubberBeacon() {
1286
1993
  | `onPointerUp` | `((event: PointerEvent) => void)` | No | `-` | - |
1287
1994
  | `onPointerUpCapture` | `((event: PointerEvent) => void)` | No | `-` | - |
1288
1995
  | `onScrubberPositionChange` | `((index: number) => void) \| undefined` | No | `-` | Callback fired when the scrubber position changes. Receives the dataIndex of the scrubber or undefined when not scrubbing. |
1289
- | `opacity` | `number \| AnimatedNode` | No | `-` | - |
1996
+ | `opacity` | `number \| AnimatedNode & number` | No | `1` | Opacity of the lines stroke. Will also be applied to points and area fill. |
1290
1997
  | `overflow` | `visible \| hidden \| scroll` | No | `-` | - |
1291
- | `padding` | `0 \| 1 \| 2 \| 3 \| 4 \| 5 \| 6 \| 7 \| 8 \| 9 \| 10 \| 0.25 \| 0.5 \| 0.75 \| 1.5` | No | `-` | - |
1292
- | `paddingBottom` | `0 \| 1 \| 2 \| 3 \| 4 \| 5 \| 6 \| 7 \| 8 \| 9 \| 10 \| 0.25 \| 0.5 \| 0.75 \| 1.5` | No | `-` | - |
1293
- | `paddingEnd` | `0 \| 1 \| 2 \| 3 \| 4 \| 5 \| 6 \| 7 \| 8 \| 9 \| 10 \| 0.25 \| 0.5 \| 0.75 \| 1.5` | No | `-` | - |
1294
- | `paddingStart` | `0 \| 1 \| 2 \| 3 \| 4 \| 5 \| 6 \| 7 \| 8 \| 9 \| 10 \| 0.25 \| 0.5 \| 0.75 \| 1.5` | No | `-` | - |
1295
- | `paddingTop` | `0 \| 1 \| 2 \| 3 \| 4 \| 5 \| 6 \| 7 \| 8 \| 9 \| 10 \| 0.25 \| 0.5 \| 0.75 \| 1.5` | No | `-` | - |
1296
- | `paddingX` | `0 \| 1 \| 2 \| 3 \| 4 \| 5 \| 6 \| 7 \| 8 \| 9 \| 10 \| 0.25 \| 0.5 \| 0.75 \| 1.5` | No | `-` | - |
1297
- | `paddingY` | `0 \| 1 \| 2 \| 3 \| 4 \| 5 \| 6 \| 7 \| 8 \| 9 \| 10 \| 0.25 \| 0.5 \| 0.75 \| 1.5` | No | `-` | - |
1998
+ | `padding` | `0 \| 1 \| 2 \| 0.25 \| 0.5 \| 0.75 \| 1.5 \| 3 \| 4 \| 5 \| 6 \| 7 \| 8 \| 9 \| 10` | No | `-` | - |
1999
+ | `paddingBottom` | `0 \| 1 \| 2 \| 0.25 \| 0.5 \| 0.75 \| 1.5 \| 3 \| 4 \| 5 \| 6 \| 7 \| 8 \| 9 \| 10` | No | `-` | - |
2000
+ | `paddingEnd` | `0 \| 1 \| 2 \| 0.25 \| 0.5 \| 0.75 \| 1.5 \| 3 \| 4 \| 5 \| 6 \| 7 \| 8 \| 9 \| 10` | No | `-` | - |
2001
+ | `paddingStart` | `0 \| 1 \| 2 \| 0.25 \| 0.5 \| 0.75 \| 1.5 \| 3 \| 4 \| 5 \| 6 \| 7 \| 8 \| 9 \| 10` | No | `-` | - |
2002
+ | `paddingTop` | `0 \| 1 \| 2 \| 0.25 \| 0.5 \| 0.75 \| 1.5 \| 3 \| 4 \| 5 \| 6 \| 7 \| 8 \| 9 \| 10` | No | `-` | - |
2003
+ | `paddingX` | `0 \| 1 \| 2 \| 0.25 \| 0.5 \| 0.75 \| 1.5 \| 3 \| 4 \| 5 \| 6 \| 7 \| 8 \| 9 \| 10` | No | `-` | - |
2004
+ | `paddingY` | `0 \| 1 \| 2 \| 0.25 \| 0.5 \| 0.75 \| 1.5 \| 3 \| 4 \| 5 \| 6 \| 7 \| 8 \| 9 \| 10` | No | `-` | - |
1298
2005
  | `pin` | `top \| bottom \| left \| right \| all` | No | `-` | Direction in which to absolutely pin the box. |
1299
- | `position` | `absolute \| relative \| static \| fixed \| sticky` | No | `-` | - |
2006
+ | `points` | `boolean \| ((defaults: PointBaseProps) => boolean \| Partial<PointProps> \| null) \| undefined` | No | `-` | Controls whether and how to render points at each data point in the series. - true: Show all points with default styling - false or undefined: Hide all points - Function: Called for every entry in the data array to customize individual points |
2007
+ | `position` | `static \| relative \| fixed \| absolute \| sticky` | No | `-` | - |
1300
2008
  | `ref` | `((instance: View \| null) => void) \| RefObject<View> \| null` | No | `-` | - |
1301
- | `renderPoints` | `((params: RenderPointsParams) => boolean \| PointConfig \| null) \| undefined` | No | `-` | Callback function to determine how to render points at each data point in the series. Called for every entry in the data array. |
1302
2009
  | `right` | `string \| number` | No | `-` | - |
1303
- | `rowGap` | `0 \| 1 \| 2 \| 3 \| 4 \| 5 \| 6 \| 7 \| 8 \| 9 \| 10 \| 0.25 \| 0.5 \| 0.75 \| 1.5` | No | `-` | - |
2010
+ | `rowGap` | `0 \| 1 \| 2 \| 0.25 \| 0.5 \| 0.75 \| 1.5 \| 3 \| 4 \| 5 \| 6 \| 7 \| 8 \| 9 \| 10` | No | `-` | - |
1304
2011
  | `series` | `LineSeries[]` | No | `-` | Configuration objects that define how to visualize the data. Each series supports Line component props for individual customization. |
1305
- | `showArea` | `boolean` | No | `-` | Show area fill under the line. |
2012
+ | `showArea` | `boolean` | No | `-` | Whether to show area fill under the line. |
1306
2013
  | `showXAxis` | `boolean` | No | `-` | Whether to show the X axis. |
1307
2014
  | `showYAxis` | `boolean` | No | `-` | Whether to show the Y axis. |
1308
- | `strokeWidth` | `number` | No | `-` | - |
1309
- | `style` | `false \| RegisteredStyle<ViewStyle> \| Value \| AnimatedInterpolation<string \| number> \| WithAnimatedObject<ViewStyle> \| WithAnimatedArray<Falsy \| ViewStyle \| RegisteredStyle<ViewStyle> \| RecursiveArray<Falsy \| ViewStyle \| RegisteredStyle<ViewStyle>> \| readonly (Falsy \| ViewStyle \| RegisteredStyle<ViewStyle>)[]> \| null` | No | `-` | - |
2015
+ | `strokeOpacity` | `number` | No | `1` | Opacity of the line |
2016
+ | `strokeWidth` | `number` | No | `2` | Width of the line |
2017
+ | `style` | `((false \| RegisteredStyle<ViewStyle> \| WithAnimatedObject<ViewStyle> \| Value \| AnimatedInterpolation<string \| number> \| WithAnimatedArray<ViewStyle \| Falsy \| RegisteredStyle<ViewStyle> \| RecursiveArray<ViewStyle \| Falsy \| RegisteredStyle<ViewStyle>> \| readonly (ViewStyle \| Falsy \| RegisteredStyle<ViewStyle>)[]>) & ((false \| RegisteredStyle<ViewStyle> \| WithAnimatedObject<ViewStyle> \| Value \| AnimatedInterpolation<string \| number> \| WithAnimatedArray<ViewStyle \| Falsy \| RegisteredStyle<ViewStyle> \| RecursiveArray<ViewStyle \| Falsy \| RegisteredStyle<ViewStyle>> \| readonly (ViewStyle \| Falsy \| RegisteredStyle<ViewStyle>)[]>) & (false \| ViewStyle \| RegisteredStyle<ViewStyle> \| RecursiveArray<ViewStyle \| Falsy \| RegisteredStyle<ViewStyle>>))) \| null` | No | `-` | Custom styles for the root element. |
2018
+ | `styles` | `{ root?: StyleProp<ViewStyle>; chart?: StyleProp<ViewStyle>; }` | No | `-` | Custom styles for the component. |
1310
2019
  | `testID` | `string` | No | `-` | Used to locate this element in unit and end-to-end tests. Used to locate this view in end-to-end tests. |
1311
- | `textAlign` | `auto \| left \| right \| center \| justify` | No | `-` | - |
2020
+ | `textAlign` | `left \| right \| auto \| center \| justify` | No | `-` | - |
1312
2021
  | `textDecorationLine` | `none \| underline \| line-through \| underline line-through` | No | `-` | - |
1313
- | `textDecorationStyle` | `solid \| dotted \| dashed \| double` | No | `-` | - |
2022
+ | `textDecorationStyle` | `solid \| double \| dotted \| dashed` | No | `-` | - |
1314
2023
  | `textTransform` | `none \| capitalize \| uppercase \| lowercase` | No | `-` | - |
1315
2024
  | `top` | `string \| number` | No | `-` | - |
1316
- | `transform` | `string \| (({ scaleX: AnimatableNumericValue; } & { scaleY?: undefined; translateX?: undefined; translateY?: undefined; perspective?: undefined; rotate?: undefined; rotateX?: undefined; rotateY?: undefined; rotateZ?: undefined; scale?: undefined; skewX?: undefined; skewY?: undefined; matrix?: undefined; }) \| ({ scaleY: AnimatableNumericValue; } & { scaleX?: undefined; translateX?: undefined; translateY?: undefined; perspective?: undefined; rotate?: undefined; rotateX?: undefined; rotateY?: undefined; rotateZ?: undefined; scale?: undefined; skewX?: undefined; skewY?: undefined; matrix?: undefined; }) \| ({ translateX: AnimatableNumericValue \| ${number}%; } & { scaleX?: undefined; scaleY?: undefined; translateY?: undefined; perspective?: undefined; rotate?: undefined; rotateX?: undefined; rotateY?: undefined; rotateZ?: undefined; scale?: undefined; skewX?: undefined; skewY?: undefined; matrix?: undefined; }) \| ({ translateY: AnimatableNumericValue \| ${number}%; } & { scaleX?: undefined; scaleY?: undefined; translateX?: undefined; perspective?: undefined; rotate?: undefined; rotateX?: undefined; rotateY?: undefined; rotateZ?: undefined; scale?: undefined; skewX?: undefined; skewY?: undefined; matrix?: undefined; }) \| ({ perspective: AnimatableNumericValue; } & { scaleX?: undefined; scaleY?: undefined; translateX?: undefined; translateY?: undefined; rotate?: undefined; rotateX?: undefined; rotateY?: undefined; rotateZ?: undefined; scale?: undefined; skewX?: undefined; skewY?: undefined; matrix?: undefined; }) \| ({ rotate: AnimatableStringValue; } & { scaleX?: undefined; scaleY?: undefined; translateX?: undefined; translateY?: undefined; perspective?: undefined; rotateX?: undefined; rotateY?: undefined; rotateZ?: undefined; scale?: undefined; skewX?: undefined; skewY?: undefined; matrix?: undefined; }) \| ({ rotateX: AnimatableStringValue; } & { scaleX?: undefined; scaleY?: undefined; translateX?: undefined; translateY?: undefined; perspective?: undefined; rotate?: undefined; rotateY?: undefined; rotateZ?: undefined; scale?: undefined; skewX?: undefined; skewY?: undefined; matrix?: undefined; }) \| ({ rotateY: AnimatableStringValue; } & { scaleX?: undefined; scaleY?: undefined; translateX?: undefined; translateY?: undefined; perspective?: undefined; rotate?: undefined; rotateX?: undefined; rotateZ?: undefined; scale?: undefined; skewX?: undefined; skewY?: undefined; matrix?: undefined; }) \| ({ rotateZ: AnimatableStringValue; } & { scaleX?: undefined; scaleY?: undefined; translateX?: undefined; translateY?: undefined; perspective?: undefined; rotate?: undefined; rotateX?: undefined; rotateY?: undefined; scale?: undefined; skewX?: undefined; skewY?: undefined; matrix?: undefined; }) \| ({ scale: AnimatableNumericValue; } & { scaleX?: undefined; scaleY?: undefined; translateX?: undefined; translateY?: undefined; perspective?: undefined; rotate?: undefined; rotateX?: undefined; rotateY?: undefined; rotateZ?: undefined; skewX?: undefined; skewY?: undefined; matrix?: undefined; }) \| ({ skewX: AnimatableStringValue; } & { scaleX?: undefined; scaleY?: undefined; translateX?: undefined; translateY?: undefined; perspective?: undefined; rotate?: undefined; rotateX?: undefined; rotateY?: undefined; rotateZ?: undefined; scale?: undefined; skewY?: undefined; matrix?: undefined; }) \| ({ skewY: AnimatableStringValue; } & { scaleX?: undefined; scaleY?: undefined; translateX?: undefined; translateY?: undefined; perspective?: undefined; rotate?: undefined; rotateX?: undefined; rotateY?: undefined; rotateZ?: undefined; scale?: undefined; skewX?: undefined; matrix?: undefined; }) \| ({ matrix: AnimatableNumericValue[]; } & { scaleX?: undefined; scaleY?: undefined; translateX?: undefined; translateY?: undefined; perspective?: undefined; rotate?: undefined; rotateX?: undefined; rotateY?: undefined; rotateZ?: undefined; scale?: undefined; skewX?: undefined; skewY?: undefined; }))[]` | No | `-` | - |
1317
- | `type` | `solid \| dotted \| gradient` | No | `'solid'` | The type of line to render. |
1318
- | `userSelect` | `auto \| none \| text \| contain \| all` | No | `-` | - |
1319
- | `width` | `string \| number` | No | `-` | Chart width. If not provided, will use the containers measured width. |
1320
- | `xAxis` | `(Partial<AxisConfigProps> & AxisBaseProps & { className?: string; classNames?: { root?: string \| undefined; tickLabel?: string \| undefined; gridLine?: string \| undefined; line?: string \| undefined; tickMark?: string \| undefined; } \| undefined; style?: CSSProperties \| undefined; styles?: { root?: CSSProperties \| undefined; tickLabel?: CSSProperties \| undefined; gridLine?: CSSProperties \| undefined; line?: CSSProperties \| undefined; tickMark?: CSSProperties \| undefined; } \| undefined; } & { position?: top \| bottom \| undefined; height?: number \| undefined; }) \| undefined` | No | `-` | - |
1321
- | `yAxis` | `(Partial<AxisConfigProps> & AxisBaseProps & { className?: string; classNames?: { root?: string \| undefined; tickLabel?: string \| undefined; gridLine?: string \| undefined; line?: string \| undefined; tickMark?: string \| undefined; } \| undefined; style?: CSSProperties \| undefined; styles?: { root?: CSSProperties \| undefined; tickLabel?: CSSProperties \| undefined; gridLine?: CSSProperties \| undefined; line?: CSSProperties \| undefined; tickMark?: CSSProperties \| undefined; } \| undefined; } & { axisId?: string \| undefined; position?: left \| right \| undefined; width?: number \| undefined; }) \| undefined` | No | `-` | - |
2025
+ | `transform` | `string \| (({ perspective: AnimatableNumericValue; } & { rotate?: undefined; rotateX?: undefined; rotateY?: undefined; rotateZ?: undefined; scale?: undefined; scaleX?: undefined; scaleY?: undefined; translateX?: undefined; translateY?: undefined; skewX?: undefined; skewY?: undefined; matrix?: undefined; }) \| ({ rotate: AnimatableStringValue; } & { perspective?: undefined; rotateX?: undefined; rotateY?: undefined; rotateZ?: undefined; scale?: undefined; scaleX?: undefined; scaleY?: undefined; translateX?: undefined; translateY?: undefined; skewX?: undefined; skewY?: undefined; matrix?: undefined; }) \| ({ rotateX: AnimatableStringValue; } & { perspective?: undefined; rotate?: undefined; rotateY?: undefined; rotateZ?: undefined; scale?: undefined; scaleX?: undefined; scaleY?: undefined; translateX?: undefined; translateY?: undefined; skewX?: undefined; skewY?: undefined; matrix?: undefined; }) \| ({ rotateY: AnimatableStringValue; } & { perspective?: undefined; rotate?: undefined; rotateX?: undefined; rotateZ?: undefined; scale?: undefined; scaleX?: undefined; scaleY?: undefined; translateX?: undefined; translateY?: undefined; skewX?: undefined; skewY?: undefined; matrix?: undefined; }) \| ({ rotateZ: AnimatableStringValue; } & { perspective?: undefined; rotate?: undefined; rotateX?: undefined; rotateY?: undefined; scale?: undefined; scaleX?: undefined; scaleY?: undefined; translateX?: undefined; translateY?: undefined; skewX?: undefined; skewY?: undefined; matrix?: undefined; }) \| ({ scale: AnimatableNumericValue; } & { perspective?: undefined; rotate?: undefined; rotateX?: undefined; rotateY?: undefined; rotateZ?: undefined; scaleX?: undefined; scaleY?: undefined; translateX?: undefined; translateY?: undefined; skewX?: undefined; skewY?: undefined; matrix?: undefined; }) \| ({ scaleX: AnimatableNumericValue; } & { perspective?: undefined; rotate?: undefined; rotateX?: undefined; rotateY?: undefined; rotateZ?: undefined; scale?: undefined; scaleY?: undefined; translateX?: undefined; translateY?: undefined; skewX?: undefined; skewY?: undefined; matrix?: undefined; }) \| ({ scaleY: AnimatableNumericValue; } & { perspective?: undefined; rotate?: undefined; rotateX?: undefined; rotateY?: undefined; rotateZ?: undefined; scale?: undefined; scaleX?: undefined; translateX?: undefined; translateY?: undefined; skewX?: undefined; skewY?: undefined; matrix?: undefined; }) \| ({ translateX: AnimatableNumericValue \| ${number}%; } & { perspective?: undefined; rotate?: undefined; rotateX?: undefined; rotateY?: undefined; rotateZ?: undefined; scale?: undefined; scaleX?: undefined; scaleY?: undefined; translateY?: undefined; skewX?: undefined; skewY?: undefined; matrix?: undefined; }) \| ({ translateY: AnimatableNumericValue \| ${number}%; } & { perspective?: undefined; rotate?: undefined; rotateX?: undefined; rotateY?: undefined; rotateZ?: undefined; scale?: undefined; scaleX?: undefined; scaleY?: undefined; translateX?: undefined; skewX?: undefined; skewY?: undefined; matrix?: undefined; }) \| ({ skewX: AnimatableStringValue; } & { perspective?: undefined; rotate?: undefined; rotateX?: undefined; rotateY?: undefined; rotateZ?: undefined; scale?: undefined; scaleX?: undefined; scaleY?: undefined; translateX?: undefined; translateY?: undefined; skewY?: undefined; matrix?: undefined; }) \| ({ skewY: AnimatableStringValue; } & { perspective?: undefined; rotate?: undefined; rotateX?: undefined; rotateY?: undefined; rotateZ?: undefined; scale?: undefined; scaleX?: undefined; scaleY?: undefined; translateX?: undefined; translateY?: undefined; skewX?: undefined; matrix?: undefined; }) \| ({ matrix: AnimatableNumericValue[]; } & { perspective?: undefined; rotate?: undefined; rotateX?: undefined; rotateY?: undefined; rotateZ?: undefined; scale?: undefined; scaleX?: undefined; scaleY?: undefined; translateX?: undefined; translateY?: undefined; skewX?: undefined; skewY?: undefined; }))[]` | No | `-` | - |
2026
+ | `transition` | `{ type: timing; } & TimingConfig \| { type: spring; } & { stiffness?: number \| undefined; overshootClamping?: boolean \| undefined; restDisplacementThreshold?: number \| undefined; restSpeedThreshold?: number \| undefined; velocity?: number \| undefined; reduceMotion?: ReduceMotion \| undefined; } & { mass?: number \| undefined; damping?: number \| undefined; duration?: undefined; dampingRatio?: undefined; clamp?: undefined; } \| { type: spring; } & { stiffness?: number \| undefined; overshootClamping?: boolean \| undefined; restDisplacementThreshold?: number \| undefined; restSpeedThreshold?: number \| undefined; velocity?: number \| undefined; reduceMotion?: ReduceMotion \| undefined; } & { mass?: undefined; damping?: undefined; duration?: number \| undefined; dampingRatio?: number \| undefined; clamp?: { min?: number \| undefined; max?: number \| undefined; } \| undefined; }` | No | `-` | Transition configuration for line animations. |
2027
+ | `type` | `solid \| dotted` | No | `'solid'` | The type of line to render. |
2028
+ | `userSelect` | `none \| auto \| text \| contain \| all` | No | `-` | - |
2029
+ | `width` | `string \| number` | No | `-` | - |
2030
+ | `xAxis` | `(Partial<AxisConfigProps> & AxisBaseProps & { GridLineComponent?: LineComponent; LineComponent?: LineComponent \| undefined; TickMarkLineComponent?: LineComponent \| undefined; tickLabelFormatter?: ((value: number) => ChartTextChildren) \| undefined; TickLabelComponent?: AxisTickLabelComponent \| undefined; } & { position?: top \| bottom \| undefined; height?: number \| undefined; }) \| undefined` | No | `-` | Configuration for x-axis. Accepts axis config and axis props. To show the axis, set showXAxis to true. |
2031
+ | `yAxis` | `(Partial<AxisConfigProps> & AxisBaseProps & { GridLineComponent?: LineComponent; LineComponent?: LineComponent \| undefined; TickMarkLineComponent?: LineComponent \| undefined; tickLabelFormatter?: ((value: number) => ChartTextChildren) \| undefined; TickLabelComponent?: AxisTickLabelComponent \| undefined; } & { axisId?: string \| undefined; position?: left \| right \| undefined; width?: number \| undefined; }) \| undefined` | No | `-` | Configuration for y-axis. Accepts axis config and axis props. To show the axis, set showYAxis to true. |
1322
2032
  | `zIndex` | `number` | No | `-` | - |
1323
2033
 
1324
2034