@chainplatform/charts 0.0.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.
package/.gitattributes ADDED
@@ -0,0 +1,2 @@
1
+ # Auto detect text files and perform LF normalization
2
+ * text=auto
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Chain Platform
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,173 @@
1
+ # @chainplatform/charts
2
+
3
+ Reusable chart components for React Native / React Native Web.
4
+
5
+ <p align="center">
6
+ <a href="https://github.com/ChainPlatform/react-native-charts/blob/HEAD/LICENSE">
7
+ <img src="https://img.shields.io/badge/license-MIT-blue.svg" />
8
+ </a>
9
+ <a href="https://www.npmjs.com/package/@chainplatform/react-native-charts">
10
+ <img src="https://img.shields.io/npm/v/@chainplatform/react-native-charts?color=brightgreen&label=npm%20package" alt="Current npm package version." />
11
+ </a>
12
+ <a href="https://www.npmjs.com/package/@chainplatform/react-native-charts">
13
+ <img src="https://img.shields.io/npm/dt/@chainplatform/react-native-charts.svg"></img>
14
+ </a>
15
+ <a href="https://www.npmjs.com/package/@chainplatform/react-native-charts">
16
+ <img src="https://img.shields.io/badge/platform-android%20%7C%20ios%20%7C%20web-blue"></img>
17
+ </a>
18
+ <a href="https://github.com/ChainPlatform/react-native-charts/pulls">
19
+ <img src="https://img.shields.io/badge/PRs-welcome-brightgreen.svg" alt="PRs welcome!" />
20
+ </a>
21
+ <a href="https://twitter.com/intent/follow?screen_name=doansan">
22
+ <img src="https://img.shields.io/twitter/follow/doansan.svg?label=Follow%20@doansan" alt="Follow @doansan" />
23
+ </a>
24
+ </p>
25
+
26
+
27
+ [![GitHub stars](https://img.shields.io/github/stars/ChainPlatform/react-native-charts?style=social)](https://github.com/ChainPlatform/react-native-charts)
28
+ [![GitHub forks](https://img.shields.io/github/forks/ChainPlatform/react-native-charts?style=social)](https://github.com/ChainPlatform/react-native-charts)
29
+
30
+
31
+ ## Install
32
+
33
+ ```bash
34
+ npm install @chainplatform/charts react-native-svg prop-types @chainplatform/layout --save
35
+ ```
36
+
37
+ or:
38
+
39
+ ```bash
40
+ yarn add @chainplatform/charts react-native-svg prop-types @chainplatform/layout
41
+ ```
42
+
43
+ ## Usage
44
+
45
+ ```jsx
46
+ import React from 'react';
47
+ import { View } from 'react-native';
48
+ import { BarChart, DonutChart, LineChart } from '@chainplatform/charts';
49
+
50
+ export default function Example() {
51
+ return (
52
+ <View>
53
+ <BarChart
54
+ data={[12, 18, 9, 24]}
55
+ categories={['T1', 'T2', 'T3', 'T4']}
56
+ enableTooltip
57
+ />
58
+
59
+ <DonutChart
60
+ data={[
61
+ { label: 'Pass', value: 70, total: 70, color: '#10B981' },
62
+ { label: 'Fail', value: 30, total: 30, color: '#EF4444' },
63
+ ]}
64
+ centerContent="100"
65
+ enableTooltip
66
+ />
67
+
68
+ <LineChart
69
+ data={[10, 20, 14, 30, 25]}
70
+ labels={['A', 'B', 'C', 'D', 'E']}
71
+ />
72
+ </View>
73
+ );
74
+ }
75
+ ```
76
+
77
+ ## Components
78
+
79
+ ### BarChart
80
+
81
+ ```jsx
82
+ <BarChart data={[10, 20, 30]} categories={['A', 'B', 'C']} enableTooltip />
83
+ ```
84
+
85
+ Main props:
86
+
87
+ | Prop | Type | Default |
88
+ | --- | --- | --- |
89
+ | `data` | `number[]` | required |
90
+ | `categories` | `string[]` | `[]` |
91
+ | `chartHeight` | `number` | `setSize(220)` |
92
+ | `showGrid` | `boolean` | `true` |
93
+ | `gridColor` | `string` | `#ddd` |
94
+ | `axisLabelColor` | `string` | `#666` |
95
+ | `barSpacing` | `number` | `setSize(12)` |
96
+ | `barWidth` | `number` | `setSize(25)` |
97
+ | `colors` | `string[]` | default palette |
98
+ | `barLabels` | `string[]` | `[]` |
99
+ | `theme` | `object` | `null` |
100
+ | `enableTooltip` | `boolean` | `true` |
101
+
102
+ ### DonutChart
103
+
104
+ ```jsx
105
+ <DonutChart
106
+ data={[{ label: 'Done', value: 60, total: 60, color: '#10B981' }]}
107
+ centerContent="60"
108
+ />
109
+ ```
110
+
111
+ Main props:
112
+
113
+ | Prop | Type | Default |
114
+ | --- | --- | --- |
115
+ | `data` | `{ label, value, color, total? }[]` | required |
116
+ | `size` | `number` | `setSize(320)` |
117
+ | `strokeWidth` | `number` | `setSize(70)` |
118
+ | `startAngle` | `number` | `-90` |
119
+ | `segmentGap` | `number` | `2` |
120
+ | `backgroundColor` | `string` | `#ECECEC` |
121
+ | `showPercent` | `boolean` | `true` |
122
+ | `showInnerLabels` | `boolean` | `true` |
123
+ | `centerContent` | `string` | `''` |
124
+ | `enableTooltip` | `boolean` | `true` |
125
+
126
+ ### LineChart
127
+
128
+ ```jsx
129
+ <LineChart data={[8, 12, 16, 10]} labels={['Q1', 'Q2', 'Q3', 'Q4']} />
130
+ ```
131
+
132
+ Main props:
133
+
134
+ | Prop | Type | Default |
135
+ | --- | --- | --- |
136
+ | `data` | `number[]` | required |
137
+ | `labels` | `string[]` | `[]` |
138
+ | `width` | `number` | `setSize(160)` |
139
+ | `height` | `number` | `setSize(60)` |
140
+ | `strokeColor` | `string` | `#5B7CFA` |
141
+ | `strokeWidth` | `number` | `setSize(2)` |
142
+ | `showDots` | `boolean` | `true` |
143
+ | `dotRadius` | `number` | `setSize(3)` |
144
+ | `touchRadius` | `number` | `setSize(10)` |
145
+ | `backgroundColor` | `string` | `transparent` |
146
+ | `enableTooltip` | `boolean` | `true` |
147
+
148
+ ---
149
+
150
+ ## 🪪 License
151
+ MIT © 2026 [Chain Platform](https://chainplatform.net)
152
+
153
+ ---
154
+
155
+ ## 💖 Support & Donate
156
+
157
+ If you find this package helpful, consider supporting the development:
158
+
159
+ | Currency | Address |
160
+ |----------------|----------|
161
+ | **MB Bank** | `MB Bank` |
162
+ ![alt text](imgs/qr.png)
163
+ | **Bitcoin (BTC)** | `17grbSNSEcEybS1nHh4TGYVodBwT16cWtc` |
164
+ ![alt text](imgs/image-1.png)
165
+ | **Ethereum (ETH)** | `0xa2fd119a619908d53928e5848b49bf1cc15689d4` |
166
+ ![alt text](imgs/image-2.png)
167
+ | **Tron (TRX)** | `TYL8p2PLCLDfq3CgGBp58WdUvvg9zsJ8pd` |
168
+ ![alt text](imgs/image.png)
169
+ | **DOGE (DOGE)** | `DDfKN2ys4frNaUkvPKcAdfL6SiVss5Bm19` |
170
+ | **USDT (SOLANA)** | `cPUZsb7T9tMfiZFqXbWbRvrUktxgZQXQ2Ni1HiVXgFm` |
171
+
172
+
173
+ Your contribution helps maintain open-source development under the Chain Platform ecosystem 🚀
Binary file
Binary file
package/imgs/image.png ADDED
Binary file
package/imgs/qr.png ADDED
Binary file
package/package.json ADDED
@@ -0,0 +1,30 @@
1
+ {
2
+ "name": "@chainplatform/charts",
3
+ "version": "0.0.2",
4
+ "description": "Reusable React Native charts for Chain Platform.",
5
+ "main": "src/index.js",
6
+ "devDependencies": {
7
+ "react": "*",
8
+ "react-native": "*",
9
+ "@chainplatform/layout": "*",
10
+ "prop-types": "*",
11
+ "react-native-svg": ">=12.0.0"
12
+ },
13
+ "author": "Chain Platform <santran686@gmail.com> (https://chainplatform.net/)",
14
+ "license": "MIT",
15
+ "keywords": [
16
+ "react-native",
17
+ "charts",
18
+ "bar-chart",
19
+ "line-chart",
20
+ "donut-chart",
21
+ "chainplatform",
22
+ "react-native-web"
23
+ ],
24
+ "homepage": "https://github.com/ChainPlatform/react-native-charts",
25
+ "bugs": "https://github.com/ChainPlatform/react-native-charts/issues",
26
+ "repository": {
27
+ "type": "git",
28
+ "url": "git://github.com/ChainPlatform/react-native-charts.git"
29
+ }
30
+ }
@@ -0,0 +1,361 @@
1
+ import React, { PureComponent } from 'react';
2
+ import { View, Platform, ScrollView } from 'react-native';
3
+ import Svg, { Rect, Text as SvgText, Line } from 'react-native-svg';
4
+ import PropTypes from 'prop-types';
5
+ import { setSize } from '@chainplatform/layout';
6
+ import ChartTooltip from './ChartTooltip';
7
+ import { chartStyles, dynamicChartStyles } from './styles/chartStyles';
8
+ import { px } from './utils';
9
+
10
+ class BarChart extends PureComponent {
11
+ chartColors = [
12
+ '#93C5FD', '#3B82F6', '#1E40AF', '#0EA5E9', '#0369A1',
13
+ '#C4B5FD', '#8B5CF6', '#7C3AED', '#A855F7', '#6D28D9',
14
+ '#6EE7B7', '#10B981', '#059669', '#34D399', '#047857',
15
+ '#F87171', '#EF4444', '#DC2626', '#FB923C', '#FACC15',
16
+ ];
17
+
18
+ constructor(props) {
19
+ super(props);
20
+
21
+ this.state = {
22
+ tooltip: null,
23
+ containerWidth: 0,
24
+ };
25
+ }
26
+
27
+ showTooltip = (e, dataIndex, value, color) => {
28
+ const { locationX, locationY } = e.nativeEvent;
29
+
30
+ const label =
31
+ this.props.barLabels?.[dataIndex] ||
32
+ this.props.categories?.[dataIndex] ||
33
+ `Bar ${dataIndex}`;
34
+
35
+ this.setState({
36
+ tooltip: {
37
+ x: locationX,
38
+ y: locationY,
39
+ dataIndex,
40
+ value,
41
+ label,
42
+ color,
43
+ },
44
+ });
45
+ };
46
+
47
+ hideTooltip = () => { this.setState({ tooltip: null }); };
48
+
49
+ renderSingleBar = (
50
+ dataIndex,
51
+ value,
52
+ chartHeight,
53
+ maxValue,
54
+ x,
55
+ barWidth,
56
+ color,
57
+ enableTooltip
58
+ ) => {
59
+ if (!value || value <= 0) return null;
60
+
61
+ const barHeight =
62
+ (value / maxValue) *
63
+ (chartHeight - setSize(25));
64
+
65
+ const y = chartHeight - barHeight;
66
+
67
+ return (
68
+ <Rect
69
+ key={`bar-${dataIndex}`}
70
+ x={px(x)}
71
+ y={px(y)}
72
+ width={barWidth}
73
+ height={barHeight}
74
+ fill={color}
75
+ rx={setSize(4)}
76
+ ry={setSize(4)}
77
+ onMouseEnter={
78
+ Platform.OS === 'web'
79
+ ? e =>
80
+ this.showTooltip(
81
+ e,
82
+ dataIndex,
83
+ value,
84
+ color
85
+ )
86
+ : undefined
87
+ }
88
+ onMouseLeave={
89
+ Platform.OS === 'web'
90
+ ? this.hideTooltip
91
+ : undefined
92
+ }
93
+ onPress={
94
+ Platform.OS !== 'web' && enableTooltip
95
+ ? e =>
96
+ this.showTooltip(
97
+ e,
98
+ dataIndex,
99
+ value,
100
+ color
101
+ )
102
+ : undefined
103
+ }
104
+ onPressOut={
105
+ Platform.OS !== 'web' && enableTooltip
106
+ ? this.hideTooltip
107
+ : undefined
108
+ }
109
+ />
110
+ );
111
+ };
112
+
113
+ render() {
114
+ const {
115
+ data,
116
+ categories,
117
+ chartHeight,
118
+ showGrid,
119
+ gridColor,
120
+ axisLabelColor,
121
+ axisLabelSizeMax,
122
+ axisLabelSizeMin,
123
+ tooltipFontSize,
124
+ axisLabelPadding,
125
+ barSpacing,
126
+ colors: customColors,
127
+ barWidth,
128
+ theme,
129
+ enableTooltip = false
130
+ } = this.props;
131
+
132
+ const {
133
+ tooltip,
134
+ containerWidth,
135
+ } = this.state;
136
+
137
+ if (!data?.length) return null;
138
+
139
+ const maxValue = Math.max(...data);
140
+
141
+ if (maxValue <= 0) return null;
142
+
143
+ const colors = data.map((_, index) => (
144
+ customColors?.[index] ||
145
+ this.chartColors[index % this.chartColors.length]
146
+ ));
147
+
148
+ const leftPadding = setSize(20);
149
+ const rightPadding = setSize(20);
150
+
151
+ const fixedBarsWidth = data.length * barWidth;
152
+
153
+ const minimumRequiredWidth =
154
+ fixedBarsWidth +
155
+ (data.length - 1) * barSpacing +
156
+ leftPadding +
157
+ rightPadding;
158
+
159
+ const isExpanded = containerWidth > minimumRequiredWidth;
160
+
161
+ const svgWidth = isExpanded ? containerWidth : minimumRequiredWidth;
162
+
163
+ const dynamicSpacing =
164
+ data.length > 1
165
+ ? isExpanded
166
+ ? (
167
+ svgWidth -
168
+ fixedBarsWidth -
169
+ leftPadding -
170
+ rightPadding
171
+ ) / (data.length - 1)
172
+ : barSpacing
173
+ : 0;
174
+
175
+ const totalChartHeight = chartHeight - setSize(30);
176
+
177
+ const gridSteps = 5;
178
+ const stepHeight = totalChartHeight / gridSteps;
179
+
180
+ return (<View style={chartStyles.relativeFullWidth} onLayout={e => { this.setState({ containerWidth: e.nativeEvent.layout.width }); }}>
181
+ <ScrollView horizontal showsHorizontalScrollIndicator={false} contentContainerStyle={dynamicChartStyles.minWidth(svgWidth)} >
182
+ <Svg width={svgWidth} height={chartHeight} >
183
+ <Line
184
+ x1={px(0)}
185
+ y1={px(0)}
186
+ x2={px(0)}
187
+ y2={px(totalChartHeight)}
188
+ stroke={theme?.colors?.border || '#888'}
189
+ strokeWidth={setSize(1)}
190
+ />
191
+
192
+ <Line
193
+ x1={px(0)}
194
+ y1={px(totalChartHeight)}
195
+ x2={px(svgWidth)}
196
+ y2={px(totalChartHeight)}
197
+ stroke={theme?.colors?.border || '#888'}
198
+ strokeWidth={setSize(1)}
199
+ />
200
+
201
+ {showGrid &&
202
+ Array.from({
203
+ length: gridSteps + 1,
204
+ }).map((_, i) => {
205
+ const y =
206
+ totalChartHeight -
207
+ i * stepHeight;
208
+
209
+ return (
210
+ <Line
211
+ key={`grid-${i}`}
212
+ x1={px(0)}
213
+ y1={px(y)}
214
+ x2={px(svgWidth)}
215
+ y2={px(y)}
216
+ stroke={gridColor}
217
+ strokeWidth={setSize(1)}
218
+ />
219
+ );
220
+ })}
221
+
222
+ {data.map((value, dataIndex) => {
223
+ const x =
224
+ leftPadding +
225
+ dataIndex *
226
+ (
227
+ barWidth +
228
+ dynamicSpacing
229
+ );
230
+
231
+ const barHeight =
232
+ (value / maxValue) *
233
+ (
234
+ totalChartHeight -
235
+ setSize(25)
236
+ );
237
+
238
+ return (
239
+ <React.Fragment
240
+ key={`bar-group-${dataIndex}`}
241
+ >
242
+ {this.renderSingleBar(
243
+ dataIndex,
244
+ value,
245
+ totalChartHeight,
246
+ maxValue,
247
+ x,
248
+ barWidth,
249
+ colors[dataIndex],
250
+ enableTooltip
251
+ )}
252
+
253
+ {value > 0 && (
254
+ <SvgText
255
+ x={x + barWidth / 2}
256
+ y={
257
+ totalChartHeight -
258
+ barHeight -
259
+ setSize(8)
260
+ }
261
+ fontSize={tooltipFontSize}
262
+ fill={
263
+ theme?.colors?.text ||
264
+ '#000'
265
+ }
266
+ fontWeight="600"
267
+ textAnchor="middle"
268
+ >
269
+ {value}
270
+ </SvgText>
271
+ )}
272
+ </React.Fragment>
273
+ );
274
+ })}
275
+
276
+ {categories?.map((label, idx) => {
277
+ const x =
278
+ leftPadding +
279
+ idx *
280
+ (
281
+ barWidth +
282
+ dynamicSpacing
283
+ ) +
284
+ barWidth / 2;
285
+
286
+ return (
287
+ <SvgText
288
+ key={`label-${idx}`}
289
+ x={x}
290
+ y={
291
+ totalChartHeight +
292
+ axisLabelPadding
293
+ }
294
+ fontSize={
295
+ label.length >= 6
296
+ ? axisLabelSizeMin
297
+ : axisLabelSizeMax
298
+ }
299
+ fill={axisLabelColor}
300
+ textAnchor="middle"
301
+ >
302
+ {label}
303
+ </SvgText>
304
+ );
305
+ })}
306
+ </Svg>
307
+ </ScrollView>
308
+ {enableTooltip ? <ChartTooltip tooltip={tooltip} valueText={tooltip ? String(tooltip.value) : ''} /> : null}
309
+ </View>);
310
+ }
311
+ }
312
+
313
+ BarChart.propTypes = {
314
+ data: PropTypes.arrayOf(
315
+ PropTypes.number
316
+ ).isRequired,
317
+ categories: PropTypes.arrayOf(
318
+ PropTypes.string
319
+ ),
320
+ chartHeight: PropTypes.number,
321
+ showGrid: PropTypes.bool,
322
+ gridColor: PropTypes.string,
323
+ axisLabelColor: PropTypes.string,
324
+ axisLabelSizeMin: PropTypes.number,
325
+ axisLabelSizeMax: PropTypes.number,
326
+ tooltipFontSize: PropTypes.number,
327
+ axisLabelPadding: PropTypes.number,
328
+ barSpacing: PropTypes.number,
329
+ colors: PropTypes.arrayOf(
330
+ PropTypes.string
331
+ ),
332
+ barLabels: PropTypes.arrayOf(
333
+ PropTypes.string
334
+ ),
335
+ barWidth: PropTypes.number,
336
+ theme: PropTypes.object,
337
+ };
338
+
339
+ BarChart.defaultProps = {
340
+ categories: [],
341
+ chartHeight: setSize(220),
342
+ showGrid: true,
343
+ gridColor: '#ddd',
344
+ axisLabelColor: '#666',
345
+ axisLabelSizeMax: setSize(13),
346
+ axisLabelSizeMin: setSize(7),
347
+ tooltipFontSize: setSize(13),
348
+ axisLabelPadding: setSize(18),
349
+ barSpacing: setSize(12),
350
+ barWidth: setSize(25),
351
+ colors: [
352
+ '#3B82F6',
353
+ '#8B5CF6',
354
+ '#10B981',
355
+ '#EF4444',
356
+ ],
357
+ barLabels: [],
358
+ theme: null,
359
+ };
360
+
361
+ export default BarChart;
@@ -0,0 +1,61 @@
1
+ import React from 'react';
2
+ import { View, Text } from 'react-native';
3
+ import PropTypes from 'prop-types';
4
+ import { setSize } from '@chainplatform/layout';
5
+ import { chartStyles, dynamicChartStyles } from './styles/chartStyles';
6
+
7
+ const ChartTooltip = ({
8
+ tooltip,
9
+ valueText,
10
+ offsetX,
11
+ offsetY,
12
+ }) => {
13
+ if (!tooltip) return null;
14
+
15
+ return (
16
+ <View
17
+ pointerEvents="none"
18
+ style={[
19
+ chartStyles.tooltipContainer,
20
+ dynamicChartStyles.tooltipPosition(tooltip, offsetX, offsetY),
21
+ ]}
22
+ >
23
+ <View style={chartStyles.tooltipHeader}>
24
+ <View style={[chartStyles.tooltipDot, dynamicChartStyles.tooltipDotColor(tooltip.color)]} />
25
+ <Text numberOfLines={1} style={chartStyles.tooltipLabel}>
26
+ {tooltip.label}
27
+ </Text>
28
+ </View>
29
+
30
+ <Text style={chartStyles.tooltipValue}>
31
+ {valueText || tooltip.value}
32
+ </Text>
33
+ <View style={chartStyles.tooltipArrow} />
34
+ </View>
35
+ );
36
+ };
37
+
38
+ ChartTooltip.propTypes = {
39
+ tooltip: PropTypes.shape({
40
+ x: PropTypes.number,
41
+ y: PropTypes.number,
42
+ label: PropTypes.string,
43
+ value: PropTypes.oneOfType([
44
+ PropTypes.string,
45
+ PropTypes.number,
46
+ ]),
47
+ color: PropTypes.string,
48
+ }),
49
+ valueText: PropTypes.string,
50
+ offsetX: PropTypes.number,
51
+ offsetY: PropTypes.number,
52
+ };
53
+
54
+ ChartTooltip.defaultProps = {
55
+ tooltip: null,
56
+ valueText: '',
57
+ offsetX: setSize(35),
58
+ offsetY: setSize(78),
59
+ };
60
+
61
+ export default ChartTooltip;
@@ -0,0 +1,281 @@
1
+ import React, { PureComponent } from 'react';
2
+ import { View, Platform } from 'react-native';
3
+ import Svg, { Circle, Path, Text as SvgText } from 'react-native-svg';
4
+ import PropTypes from 'prop-types';
5
+ import ChartTooltip from './ChartTooltip';
6
+ import { setSize } from '@chainplatform/layout';
7
+ import { chartStyles } from './styles/chartStyles';
8
+
9
+ class DonutChart extends PureComponent {
10
+ static propTypes = {
11
+ data: PropTypes.arrayOf(
12
+ PropTypes.shape({
13
+ label: PropTypes.string,
14
+ value: PropTypes.number,
15
+ color: PropTypes.string,
16
+ })
17
+ ).isRequired,
18
+ size: PropTypes.number,
19
+ strokeWidth: PropTypes.number,
20
+ startAngle: PropTypes.number,
21
+ segmentGap: PropTypes.number,
22
+ backgroundColor: PropTypes.string,
23
+ showPercent: PropTypes.bool,
24
+ showInnerLabels: PropTypes.bool,
25
+ percentFontSize: PropTypes.number,
26
+ labelFontSize: PropTypes.number,
27
+ contentFontSize: PropTypes.number,
28
+ percentColor: PropTypes.string,
29
+ centerContent: PropTypes.string,
30
+ innerOffset: PropTypes.number,
31
+ enableTooltip: PropTypes.bool,
32
+ };
33
+
34
+ static defaultProps = {
35
+ size: setSize(320),
36
+ strokeWidth: setSize(70),
37
+ startAngle: -90,
38
+ segmentGap: 2,
39
+ backgroundColor: '#ECECEC',
40
+ showPercent: true,
41
+ showInnerLabels: true,
42
+ percentFontSize: setSize(18),
43
+ labelFontSize: setSize(15),
44
+ contentFontSize: setSize(18),
45
+ percentColor: '#FFFFFF',
46
+ centerContent: '',
47
+ innerOffset: setSize(24),
48
+ enableTooltip: true,
49
+ };
50
+
51
+ constructor(props) {
52
+ super(props);
53
+
54
+ this.state = {
55
+ tooltip: null,
56
+ };
57
+ }
58
+
59
+ polarToCartesian = (cx, cy, radius, angle) => {
60
+ const rad = ((angle - 90) * Math.PI) / 180;
61
+
62
+ return {
63
+ x: cx + radius * Math.cos(rad),
64
+ y: cy + radius * Math.sin(rad),
65
+ };
66
+ };
67
+
68
+ describeArc = (cx, cy, radius, startAngle, endAngle) => {
69
+ const start = this.polarToCartesian(cx, cy, radius, endAngle);
70
+ const end = this.polarToCartesian(cx, cy, radius, startAngle);
71
+
72
+ const largeArcFlag = endAngle - startAngle <= 180 ? '0' : '1';
73
+
74
+ return [
75
+ 'M',
76
+ start.x,
77
+ start.y,
78
+ 'A',
79
+ radius,
80
+ radius,
81
+ 0,
82
+ largeArcFlag,
83
+ 0,
84
+ end.x,
85
+ end.y,
86
+ ].join(' ');
87
+ };
88
+
89
+ showTooltip = (item, percent, point) => {
90
+ this.setState({
91
+ tooltip: {
92
+ x: point.x,
93
+ y: point.y,
94
+ label: item.label,
95
+ value: item.total,
96
+ percent: item.value,
97
+ color: item.color,
98
+ },
99
+ });
100
+ };
101
+
102
+ hideTooltip = () => { this.setState({ tooltip: null }); };
103
+
104
+ render() {
105
+ const {
106
+ data,
107
+ size,
108
+ strokeWidth,
109
+ startAngle,
110
+ segmentGap,
111
+ backgroundColor,
112
+ showPercent,
113
+ showInnerLabels,
114
+ percentFontSize,
115
+ labelFontSize,
116
+ contentFontSize,
117
+ percentColor,
118
+ centerContent,
119
+ innerOffset,
120
+ enableTooltip,
121
+ } = this.props;
122
+
123
+ const { tooltip } = this.state;
124
+
125
+ if (!data?.length) return null;
126
+
127
+ const validData = data.filter(
128
+ item => item?.value && item.value > 0
129
+ );
130
+
131
+ if (!validData.length) return null;
132
+
133
+ const total = validData.reduce(
134
+ (sum, item) => sum + item.value,
135
+ 0
136
+ );
137
+
138
+ if (total <= 0) return null;
139
+
140
+ const radius = (size - strokeWidth) / 2;
141
+ const center = size / 2;
142
+
143
+ let currentAngle = startAngle;
144
+
145
+ return (
146
+ <View style={chartStyles.centerRelative}>
147
+ <Svg width={size} height={size}>
148
+ <Circle cx={center} cy={center} r={radius} stroke={backgroundColor} strokeWidth={strokeWidth} fill="transparent" />
149
+
150
+ {validData.map((item, index) => {
151
+ const percent = item.value / total;
152
+ const sweepAngle = percent * 360;
153
+
154
+ const segmentStartAngle = currentAngle + segmentGap / 2;
155
+
156
+ const segmentEndAngle = currentAngle + sweepAngle - segmentGap / 2;
157
+
158
+ const middleAngle = currentAngle + sweepAngle / 2;
159
+
160
+ currentAngle += sweepAngle;
161
+
162
+ if (segmentEndAngle <= segmentStartAngle) {
163
+ return null;
164
+ }
165
+
166
+ const arcPath = this.describeArc(
167
+ center,
168
+ center,
169
+ radius,
170
+ segmentStartAngle,
171
+ segmentEndAngle
172
+ );
173
+
174
+ const percentPoint = this.polarToCartesian(
175
+ center,
176
+ center,
177
+ radius,
178
+ middleAngle
179
+ );
180
+
181
+ const innerPoint = this.polarToCartesian(
182
+ center,
183
+ center,
184
+ radius - strokeWidth / 2 - innerOffset,
185
+ middleAngle
186
+ );
187
+
188
+ const roundedPercent = Math.round(percent * 100);
189
+
190
+ return (
191
+ <React.Fragment key={`segment-${index}`}>
192
+ <Path
193
+ d={arcPath}
194
+ stroke={item.color}
195
+ strokeWidth={strokeWidth}
196
+ fill="transparent"
197
+ strokeLinecap="butt"
198
+ onPress={
199
+ Platform.OS !== 'web' && enableTooltip
200
+ ? () =>
201
+ this.showTooltip(
202
+ item,
203
+ roundedPercent,
204
+ percentPoint
205
+ )
206
+ : undefined
207
+ }
208
+ onPressOut={
209
+ Platform.OS !== 'web'
210
+ ? this.hideTooltip
211
+ : undefined
212
+ }
213
+ onMouseEnter={
214
+ Platform.OS === 'web' && enableTooltip
215
+ ? () =>
216
+ this.showTooltip(
217
+ item,
218
+ roundedPercent,
219
+ percentPoint
220
+ )
221
+ : undefined
222
+ }
223
+ onMouseLeave={
224
+ Platform.OS === 'web'
225
+ ? this.hideTooltip
226
+ : undefined
227
+ }
228
+ />
229
+
230
+ {showPercent && (
231
+ <SvgText
232
+ x={percentPoint.x}
233
+ y={percentPoint.y}
234
+ fill={percentColor}
235
+ fontSize={percentFontSize}
236
+ fontWeight="600"
237
+ textAnchor="middle"
238
+ alignmentBaseline="middle"
239
+ >
240
+ {`${roundedPercent}%`}
241
+ </SvgText>
242
+ )}
243
+
244
+ {showInnerLabels && (
245
+ <SvgText
246
+ x={innerPoint.x}
247
+ y={innerPoint.y}
248
+ fill={item.color}
249
+ fontSize={labelFontSize}
250
+ textAnchor="middle"
251
+ alignmentBaseline="middle"
252
+ >
253
+ {item.label}
254
+ </SvgText>
255
+ )}
256
+ </React.Fragment>
257
+ );
258
+ })}
259
+
260
+ {!!centerContent && (
261
+ <SvgText
262
+ x={center}
263
+ y={center}
264
+ fontSize={contentFontSize}
265
+ fontWeight="600"
266
+ fill="#333"
267
+ textAnchor="middle"
268
+ alignmentBaseline="middle"
269
+ >
270
+ {centerContent}
271
+ </SvgText>
272
+ )}
273
+ </Svg>
274
+
275
+ {enableTooltip ? <ChartTooltip tooltip={tooltip} valueText={tooltip ? `${tooltip.value} (${tooltip.percent}%)` : ''} /> : null}
276
+ </View>
277
+ );
278
+ }
279
+ }
280
+
281
+ export default DonutChart;
@@ -0,0 +1,121 @@
1
+ import React, { PureComponent } from 'react';
2
+ import { View, Platform } from 'react-native';
3
+ import Svg, { Path, Circle, Defs, LinearGradient, Stop } from 'react-native-svg';
4
+ import PropTypes from 'prop-types';
5
+ import { setSize } from '@chainplatform/layout';
6
+ import ChartTooltip from './ChartTooltip';
7
+ import { dynamicChartStyles } from './styles/chartStyles';
8
+ import { px } from './utils';
9
+
10
+ class LineChart extends PureComponent {
11
+ state = { tooltip: null };
12
+
13
+ showTooltip = (e, point, index) => {
14
+ const { locationX, locationY } = e.nativeEvent;
15
+ const label = this.props.labels?.[index] || this.props.data?.[index] || `P ${index + 1}`;
16
+ this.setState({ tooltip: { x: locationX, y: locationY, value: point.value, label, color: this.props.strokeColor } });
17
+ };
18
+
19
+ hideTooltip = () => this.setState({ tooltip: null });
20
+
21
+ getSmoothPath = points => {
22
+ if (!points.length) return '';
23
+ let d = `M ${px(points[0].x)} ${px(points[0].y)}`;
24
+ for (let i = 0; i < points.length - 1; i++) {
25
+ const p0 = points[i];
26
+ const p1 = points[i + 1];
27
+ const midX = (p0.x + p1.x) / 2;
28
+ d += ` C ${px(midX)} ${px(p0.y)}, ${px(midX)} ${px(p1.y)}, ${px(p1.x)} ${px(p1.y)}`;
29
+ }
30
+ return d;
31
+ };
32
+
33
+ render() {
34
+ const { enableTooltip, data, width, height, strokeColor, strokeWidth, showDots, dotRadius, touchRadius, paddingHorizontal, paddingVertical, backgroundColor } = this.props;
35
+ const { tooltip } = this.state;
36
+
37
+ if (!data?.length) return null;
38
+
39
+ const maxValue = Math.max(...data);
40
+ const minValue = Math.min(...data);
41
+ const chartWidth = width - paddingHorizontal * 2;
42
+ const chartHeight = height - paddingVertical * 2;
43
+ const range = maxValue - minValue || 1;
44
+
45
+ const points = data.map((value, index) => ({
46
+ x: paddingHorizontal + (data.length === 1 ? chartWidth / 2 : index * (chartWidth / (data.length - 1))),
47
+ y: paddingVertical + chartHeight - ((value - minValue) / range) * chartHeight,
48
+ value,
49
+ }));
50
+
51
+ const linePath = this.getSmoothPath(points);
52
+ const areaPath = `${linePath} L ${px(points[points.length - 1].x)} ${px(height - paddingVertical)} L ${px(points[0].x)} ${px(height - paddingVertical)} Z`;
53
+
54
+ return (
55
+ <View style={dynamicChartStyles.lineChartRoot(width, height, backgroundColor)}>
56
+ <Svg width={width} height={height}>
57
+ <Defs>
58
+ <LinearGradient id="lineGradient" x1="0" y1="0" x2="0" y2="1">
59
+ <Stop offset="0" stopColor={strokeColor} stopOpacity="0.25" />
60
+ <Stop offset="1" stopColor={strokeColor} stopOpacity="0" />
61
+ </LinearGradient>
62
+ </Defs>
63
+
64
+ <Path d={areaPath} fill="url(#lineGradient)" />
65
+ <Path d={linePath} fill="none" stroke={strokeColor} strokeWidth={strokeWidth} strokeLinecap="round" strokeLinejoin="round" />
66
+
67
+ {points.map((point, index) => (
68
+ <React.Fragment key={`point-${index}`}>
69
+ {showDots && <Circle cx={point.x} cy={point.y} r={dotRadius} fill="#fff" stroke={strokeColor} strokeWidth={setSize(2)} />}
70
+ <Circle
71
+ cx={point.x}
72
+ cy={point.y}
73
+ r={touchRadius}
74
+ fill="transparent"
75
+ onMouseEnter={Platform.OS === 'web' && enableTooltip ? e => this.showTooltip(e, point, index) : undefined}
76
+ onMouseLeave={Platform.OS === 'web' ? this.hideTooltip : undefined}
77
+ onPress={Platform.OS !== 'web' && enableTooltip ? e => this.showTooltip(e, point, index) : undefined}
78
+ onPressOut={Platform.OS !== 'web' ? this.hideTooltip : undefined}
79
+ />
80
+ </React.Fragment>
81
+ ))}
82
+ </Svg>
83
+
84
+ {enableTooltip ? <ChartTooltip tooltip={tooltip} valueText={tooltip ? String(tooltip.value) : ''} /> : null}
85
+ </View>
86
+ );
87
+ }
88
+ }
89
+
90
+ LineChart.propTypes = {
91
+ data: PropTypes.arrayOf(PropTypes.number).isRequired,
92
+ labels: PropTypes.arrayOf(PropTypes.string),
93
+ width: PropTypes.number,
94
+ height: PropTypes.number,
95
+ strokeColor: PropTypes.string,
96
+ strokeWidth: PropTypes.number,
97
+ showDots: PropTypes.bool,
98
+ dotRadius: PropTypes.number,
99
+ touchRadius: PropTypes.number,
100
+ paddingHorizontal: PropTypes.number,
101
+ paddingVertical: PropTypes.number,
102
+ backgroundColor: PropTypes.string,
103
+ enableTooltip: PropTypes.bool,
104
+ };
105
+
106
+ LineChart.defaultProps = {
107
+ labels: [],
108
+ width: setSize(160),
109
+ height: setSize(60),
110
+ strokeColor: '#5B7CFA',
111
+ strokeWidth: setSize(2),
112
+ showDots: true,
113
+ dotRadius: setSize(3),
114
+ touchRadius: setSize(10),
115
+ paddingHorizontal: setSize(10),
116
+ paddingVertical: setSize(8),
117
+ backgroundColor: 'transparent',
118
+ enableTooltip: true,
119
+ };
120
+
121
+ export default LineChart;
package/src/index.js ADDED
@@ -0,0 +1,7 @@
1
+ import BarChart from './BarChart';
2
+ import DonutChart from './DonutChart';
3
+ import LineChart from './LineChart';
4
+ import ChartTooltip from './ChartTooltip';
5
+
6
+ export { BarChart, DonutChart, LineChart, ChartTooltip };
7
+ export default { BarChart, DonutChart, LineChart, ChartTooltip };
@@ -0,0 +1,80 @@
1
+ import { StyleSheet } from 'react-native';
2
+ import { setSize } from '@chainplatform/layout';
3
+
4
+ export const chartStyles = StyleSheet.create({
5
+ relativeFullWidth: {
6
+ width: '100%',
7
+ position: 'relative',
8
+ },
9
+ centerRelative: {
10
+ alignItems: 'center',
11
+ justifyContent: 'center',
12
+ position: 'relative',
13
+ },
14
+ tooltipContainer: {
15
+ position: 'absolute',
16
+ backgroundColor: '#111827',
17
+ borderRadius: setSize(12),
18
+ paddingHorizontal: setSize(12),
19
+ paddingVertical: setSize(10),
20
+ minWidth: setSize(95),
21
+ shadowColor: '#000',
22
+ shadowOffset: { width: 0, height: setSize(6) },
23
+ shadowOpacity: 0.25,
24
+ shadowRadius: setSize(12),
25
+ elevation: 8,
26
+ borderWidth: setSize(1),
27
+ borderColor: 'rgba(255,255,255,0.08)',
28
+ zIndex: 999,
29
+ },
30
+ tooltipHeader: {
31
+ flexDirection: 'row',
32
+ alignItems: 'center',
33
+ marginBottom: setSize(5),
34
+ },
35
+ tooltipDot: {
36
+ width: setSize(10),
37
+ height: setSize(10),
38
+ borderRadius: 999,
39
+ marginRight: setSize(5),
40
+ },
41
+ tooltipLabel: {
42
+ color: '#D1D5DB',
43
+ fontSize: setSize(12),
44
+ fontWeight: '500',
45
+ maxWidth: setSize(120),
46
+ },
47
+ tooltipValue: {
48
+ color: '#FFF',
49
+ fontSize: setSize(14),
50
+ fontWeight: '700',
51
+ },
52
+ tooltipArrow: {
53
+ position: 'absolute',
54
+ bottom: -setSize(6),
55
+ left: '50%',
56
+ marginLeft: -setSize(6),
57
+ width: setSize(12),
58
+ height: setSize(12),
59
+ backgroundColor: '#111827',
60
+ transform: [{ rotate: '45deg' }],
61
+ borderRightWidth: setSize(1),
62
+ borderBottomWidth: setSize(1),
63
+ borderColor: 'rgba(255,255,255,0.08)',
64
+ },
65
+ });
66
+
67
+ export const dynamicChartStyles = {
68
+ minWidth: minWidth => ({ minWidth }),
69
+ tooltipPosition: (tooltip, offsetX, offsetY) => ({
70
+ left: Math.max(tooltip.x - offsetX, 10),
71
+ top: Math.max(tooltip.y - offsetY, 10),
72
+ }),
73
+ tooltipDotColor: color => ({ backgroundColor: color }),
74
+ lineChartRoot: (width, height, backgroundColor) => ({
75
+ width,
76
+ height,
77
+ backgroundColor,
78
+ position: 'relative',
79
+ }),
80
+ };
package/src/utils.js ADDED
@@ -0,0 +1,3 @@
1
+ import { Platform } from 'react-native';
2
+
3
+ export const px = v => (Platform.OS === 'web' ? Math.round(v) + 0.5 : v);