1 | import React from "react";
|
2 | import { View } from "react-native";
|
3 | import {
|
4 | Svg,
|
5 | Circle,
|
6 | Polygon,
|
7 | Polyline,
|
8 | Path,
|
9 | Rect,
|
10 | G
|
11 | } from "react-native-svg";
|
12 | import AbstractChart from "./abstract-chart";
|
13 |
|
14 | class LineChart extends AbstractChart {
|
15 | getColor = (dataset, opacity) => {
|
16 | return (dataset.color || this.props.chartConfig.color)(opacity);
|
17 | };
|
18 |
|
19 | getStrokeWidth = dataset => {
|
20 | return dataset.strokeWidth || this.props.chartConfig.strokeWidth || 3;
|
21 | };
|
22 |
|
23 | getDatas = data =>
|
24 | data.reduce((acc, item) => (item.data ? [...acc, ...item.data] : acc), []);
|
25 |
|
26 | getPropsForDots = (x, i) => {
|
27 | const { getDotProps, chartConfig = {} } = this.props;
|
28 | if (typeof getDotProps === "function") {
|
29 | return getDotProps(x, i);
|
30 | }
|
31 | const { propsForDots = {} } = chartConfig;
|
32 | return { r: "4", ...propsForDots };
|
33 | };
|
34 | renderDots = config => {
|
35 | const {
|
36 | data,
|
37 | width,
|
38 | height,
|
39 | paddingTop,
|
40 | paddingRight,
|
41 | onDataPointClick
|
42 | } = config;
|
43 | const output = [];
|
44 | const datas = this.getDatas(data);
|
45 | const baseHeight = this.calcBaseHeight(datas, height);
|
46 | const { getDotColor, hidePointsAtIndex = [] } = this.props;
|
47 | data.forEach(dataset => {
|
48 | dataset.data.forEach((x, i) => {
|
49 | if (hidePointsAtIndex.includes(i)) {
|
50 | return;
|
51 | }
|
52 | const cx =
|
53 | paddingRight + (i * (width - paddingRight)) / dataset.data.length;
|
54 | const cy =
|
55 | ((baseHeight - this.calcHeight(x, datas, height)) / 4) * 3 +
|
56 | paddingTop;
|
57 | const onPress = () => {
|
58 | if (!onDataPointClick || hidePointsAtIndex.includes(i)) {
|
59 | return;
|
60 | }
|
61 |
|
62 | onDataPointClick({
|
63 | index: i,
|
64 | value: x,
|
65 | dataset,
|
66 | x: cx,
|
67 | y: cy,
|
68 | getColor: opacity => this.getColor(dataset, opacity)
|
69 | });
|
70 | };
|
71 |
|
72 | output.push(
|
73 | <Circle
|
74 | key={Math.random()}
|
75 | cx={cx}
|
76 | cy={cy}
|
77 | fill={
|
78 | typeof getDotColor === "function"
|
79 | ? getDotColor(x, i)
|
80 | : this.getColor(dataset, 0.9)
|
81 | }
|
82 | onPress={onPress}
|
83 | {...this.getPropsForDots(x, i)}
|
84 | />,
|
85 | <Circle
|
86 | key={Math.random()}
|
87 | cx={cx}
|
88 | cy={cy}
|
89 | r="12"
|
90 | fill="#fff"
|
91 | fillOpacity={0}
|
92 | onPress={onPress}
|
93 | />
|
94 | );
|
95 | });
|
96 | });
|
97 | return output;
|
98 | };
|
99 |
|
100 | renderShadow = config => {
|
101 | if (this.props.bezier) {
|
102 | return this.renderBezierShadow(config);
|
103 | }
|
104 |
|
105 | const { data, width, height, paddingRight, paddingTop } = config;
|
106 | const datas = this.getDatas(data);
|
107 | const baseHeight = this.calcBaseHeight(datas, height);
|
108 | return config.data.map((dataset, index) => {
|
109 | return (
|
110 | <Polygon
|
111 | key={index}
|
112 | points={
|
113 | dataset.data
|
114 | .map((d, i) => {
|
115 | const x =
|
116 | paddingRight +
|
117 | (i * (width - paddingRight)) / dataset.data.length;
|
118 | const y =
|
119 | ((baseHeight - this.calcHeight(d, datas, height)) / 4) * 3 +
|
120 | paddingTop;
|
121 | return `${x},${y}`;
|
122 | })
|
123 | .join(" ") +
|
124 | ` ${paddingRight +
|
125 | ((width - paddingRight) / dataset.data.length) *
|
126 | (dataset.data.length - 1)},${(height / 4) * 3 +
|
127 | paddingTop} ${paddingRight},${(height / 4) * 3 + paddingTop}`
|
128 | }
|
129 | fill="url(#fillShadowGradient)"
|
130 | strokeWidth={0}
|
131 | />
|
132 | );
|
133 | });
|
134 | };
|
135 |
|
136 | renderLine = config => {
|
137 | if (this.props.bezier) {
|
138 | return this.renderBezierLine(config);
|
139 | }
|
140 |
|
141 | const { width, height, paddingRight, paddingTop, data } = config;
|
142 | const output = [];
|
143 | const datas = this.getDatas(data);
|
144 | const baseHeight = this.calcBaseHeight(datas, height);
|
145 | data.forEach((dataset, index) => {
|
146 | const points = dataset.data.map((d, i) => {
|
147 | const x =
|
148 | (i * (width - paddingRight)) / dataset.data.length + paddingRight;
|
149 | const y =
|
150 | ((baseHeight - this.calcHeight(d, datas, height)) / 4) * 3 +
|
151 | paddingTop;
|
152 | return `${x},${y}`;
|
153 | });
|
154 |
|
155 | output.push(
|
156 | <Polyline
|
157 | key={index}
|
158 | points={points.join(" ")}
|
159 | fill="none"
|
160 | stroke={this.getColor(dataset, 0.2)}
|
161 | strokeWidth={this.getStrokeWidth(dataset)}
|
162 | />
|
163 | );
|
164 | });
|
165 |
|
166 | return output;
|
167 | };
|
168 |
|
169 | getBezierLinePoints = (dataset, config) => {
|
170 | const { width, height, paddingRight, paddingTop, data } = config;
|
171 | if (dataset.data.length === 0) {
|
172 | return "M0,0";
|
173 | }
|
174 |
|
175 | const datas = this.getDatas(data);
|
176 | const x = i =>
|
177 | Math.floor(
|
178 | paddingRight + (i * (width - paddingRight)) / dataset.data.length
|
179 | );
|
180 | const baseHeight = this.calcBaseHeight(datas, height);
|
181 | const y = i => {
|
182 | const yHeight = this.calcHeight(dataset.data[i], datas, height);
|
183 | return Math.floor(((baseHeight - yHeight) / 4) * 3 + paddingTop);
|
184 | };
|
185 |
|
186 | return [`M${x(0)},${y(0)}`]
|
187 | .concat(
|
188 | dataset.data.slice(0, -1).map((_, i) => {
|
189 | const x_mid = (x(i) + x(i + 1)) / 2;
|
190 | const y_mid = (y(i) + y(i + 1)) / 2;
|
191 | const cp_x1 = (x_mid + x(i)) / 2;
|
192 | const cp_x2 = (x_mid + x(i + 1)) / 2;
|
193 | return (
|
194 | `Q ${cp_x1}, ${y(i)}, ${x_mid}, ${y_mid}` +
|
195 | ` Q ${cp_x2}, ${y(i + 1)}, ${x(i + 1)}, ${y(i + 1)}`
|
196 | );
|
197 | })
|
198 | )
|
199 | .join(" ");
|
200 | };
|
201 |
|
202 | renderBezierLine = config => {
|
203 | return config.data.map((dataset, index) => {
|
204 | const result = this.getBezierLinePoints(dataset, config);
|
205 | return (
|
206 | <Path
|
207 | key={index}
|
208 | d={result}
|
209 | fill="none"
|
210 | stroke={this.getColor(dataset, 0.2)}
|
211 | strokeWidth={this.getStrokeWidth(dataset)}
|
212 | />
|
213 | );
|
214 | });
|
215 | };
|
216 |
|
217 | renderBezierShadow = config => {
|
218 | const { width, height, paddingRight, paddingTop, data } = config;
|
219 | return data.map((dataset, index) => {
|
220 | const d =
|
221 | this.getBezierLinePoints(dataset, config) +
|
222 | ` L${paddingRight +
|
223 | ((width - paddingRight) / dataset.data.length) *
|
224 | (dataset.data.length - 1)},${(height / 4) * 3 +
|
225 | paddingTop} L${paddingRight},${(height / 4) * 3 + paddingTop} Z`;
|
226 | return (
|
227 | <Path
|
228 | key={index}
|
229 | d={d}
|
230 | fill="url(#fillShadowGradient)"
|
231 | strokeWidth={0}
|
232 | />
|
233 | );
|
234 | });
|
235 | };
|
236 |
|
237 | render() {
|
238 | const {
|
239 | width,
|
240 | height,
|
241 | data,
|
242 | withShadow = true,
|
243 | withDots = true,
|
244 | withInnerLines = true,
|
245 | withOuterLines = true,
|
246 | withHorizontalLabels = true,
|
247 | withVerticalLabels = true,
|
248 | style = {},
|
249 | decorator,
|
250 | onDataPointClick,
|
251 | verticalLabelRotation = 0,
|
252 | horizontalLabelRotation = 0,
|
253 | formatYLabel = yLabel => yLabel,
|
254 | formatXLabel = xLabel => xLabel
|
255 | } = this.props;
|
256 | const { labels = [] } = data;
|
257 | const {
|
258 | borderRadius = 0,
|
259 | paddingTop = 16,
|
260 | paddingRight = 64,
|
261 | margin = 0,
|
262 | marginRight = 0,
|
263 | paddingBottom = 0
|
264 | } = style;
|
265 | const config = {
|
266 | width,
|
267 | height,
|
268 | verticalLabelRotation,
|
269 | horizontalLabelRotation
|
270 | };
|
271 | const datas = this.getDatas(data.datasets);
|
272 | return (
|
273 | <View style={style}>
|
274 | <Svg
|
275 | height={height + paddingBottom}
|
276 | width={width - margin * 2 - marginRight}
|
277 | >
|
278 | <G>
|
279 | {this.renderDefs({
|
280 | ...config,
|
281 | ...this.props.chartConfig
|
282 | })}
|
283 | <Rect
|
284 | width="100%"
|
285 | height={height}
|
286 | rx={borderRadius}
|
287 | ry={borderRadius}
|
288 | fill="url(#backgroundGradient)"
|
289 | />
|
290 | <G>
|
291 | {withInnerLines
|
292 | ? this.renderHorizontalLines({
|
293 | ...config,
|
294 | count: 4,
|
295 | paddingTop,
|
296 | paddingRight
|
297 | })
|
298 | : withOuterLines
|
299 | ? this.renderHorizontalLine({
|
300 | ...config,
|
301 | paddingTop,
|
302 | paddingRight
|
303 | })
|
304 | : null}
|
305 | </G>
|
306 | <G>
|
307 | {withHorizontalLabels
|
308 | ? this.renderHorizontalLabels({
|
309 | ...config,
|
310 | count: Math.min(...datas) === Math.max(...datas) ? 1 : 4,
|
311 | data: datas,
|
312 | paddingTop,
|
313 | paddingRight,
|
314 | formatYLabel
|
315 | })
|
316 | : null}
|
317 | </G>
|
318 | <G>
|
319 | {withInnerLines
|
320 | ? this.renderVerticalLines({
|
321 | ...config,
|
322 | data: data.datasets[0].data,
|
323 | paddingTop,
|
324 | paddingRight
|
325 | })
|
326 | : withOuterLines
|
327 | ? this.renderVerticalLine({
|
328 | ...config,
|
329 | paddingTop,
|
330 | paddingRight
|
331 | })
|
332 | : null}
|
333 | </G>
|
334 | <G>
|
335 | {withVerticalLabels
|
336 | ? this.renderVerticalLabels({
|
337 | ...config,
|
338 | labels,
|
339 | paddingRight,
|
340 | paddingTop,
|
341 | formatXLabel
|
342 | })
|
343 | : null}
|
344 | </G>
|
345 | <G>
|
346 | {this.renderLine({
|
347 | ...config,
|
348 | paddingRight,
|
349 | paddingTop,
|
350 | data: data.datasets
|
351 | })}
|
352 | </G>
|
353 | <G>
|
354 | {withShadow &&
|
355 | this.renderShadow({
|
356 | ...config,
|
357 | data: data.datasets,
|
358 | paddingRight,
|
359 | paddingTop
|
360 | })}
|
361 | </G>
|
362 | <G>
|
363 | {withDots &&
|
364 | this.renderDots({
|
365 | ...config,
|
366 | data: data.datasets,
|
367 | paddingTop,
|
368 | paddingRight,
|
369 | onDataPointClick
|
370 | })}
|
371 | </G>
|
372 | <G>
|
373 | {decorator &&
|
374 | decorator({
|
375 | ...config,
|
376 | data: data.datasets,
|
377 | paddingTop,
|
378 | paddingRight
|
379 | })}
|
380 | </G>
|
381 | </G>
|
382 | </Svg>
|
383 | </View>
|
384 | );
|
385 | }
|
386 | }
|
387 |
|
388 | export default LineChart;
|
389 |
|
\ | No newline at end of file |