@tuofeng/react-native-d3multiline-chart
Version:
Android and iOS multipleline/line/scatter chart based on d3.js, change and use for tuofeng
518 lines (500 loc) • 14.8 kB
JavaScript
import React, {Component} from 'react';
import {StyleSheet, View, Animated, TouchableOpacity} from 'react-native';
import Svg, {Circle, Line, G, Path, Text, Rect} from 'react-native-svg';
import * as d3 from 'd3';
import * as scale from 'd3-scale';
import _ from 'lodash';
import createLegend from './utils/createLegend';
import NativePath from './AnimatedSVG';
import NativeCircle from './AnimatedSVGCircle';
import {svgPathProperties} from 'svg-path-properties';
import {
calculateOverallLineChartData,
buildColorArray,
} from './utils/dataCalculation';
import {dummyData, leftAxisData, bottomAxisData} from './dummyData';
var linePathOne,
linePathFill,
linePathSecond,
xCoordinate,
yCoordinate,
pointsOnLine,
circleInFirstLine,
circleTouch,
circleInSecondLine,
legend;
var WIDTH = 380,
HEIGHT = 380,
MARGINS = {
top: 0,
right: 20,
bottom: 20,
left: 30,
};
function createLineProps (path) {
const properties = svgPathProperties (path);
const length = properties.getTotalLength ();
// console.log ('the length', length);
return {
d: path,
strokeDashoffset: new Animated.Value (length),
strokeDasharray: [length, length],
};
}
export default class MulipleLineChart extends Component {
static defaultProps: any = {
data: dummyData,
leftAxisData: leftAxisData,
bottomAxisData: bottomAxisData,
axisColor: '#000',
axisLabelColor: '#000',
axisLineWidth: 1,
chartFontSize: 10,
color: [],
dataPointRadius: 3,
lineWidth: 2,
hideAxis: false,
dataPointsVisible: true,
hideXAxisLabels: false,
hideYAxisLabels: false,
chartHeight: HEIGHT,
chartWidth: WIDTH,
showLegends: true,
tickColorXAxis: '#000',
tickWidthXAxis: '1',
tickColorYAxis: '#000',
tickWidthYAxis: '1',
circleRadiusWidth: '2.5',
circleRadius: 3,
showTicks: true,
lineStrokeOpacity: 1,
lineStrokeDashArray: ['3', '0'],
showDashedLine: false,
GraphWidth: 400,
GraphHeight: 500,
bottomAxisDataToShow: bottomAxisData,
leftAxisDataToShow: leftAxisData,
pointDataToShowOnGraph: 'Y',
legendStyle: {
width: 50,
fillOpacity: 0.5,
height: 20,
strokeWidth: 2,
legendFontSize: 12,
legentTextFill: 'black',
},
circleLegendType: true,
fillArea: false,
yAxisGrid: false,
xAxisGrid: false,
hideXAxis: false,
hideYAxis: false,
inclindTick: true,
animation: true,
delay: 500,
duration: 2000,
staggerLength: 200,
speed: 40,
};
constructor (props) {
super (props);
// this.lineAnimated = new Array (2);
}
animate () {
const {
delay,
duration,
staggerLength,
speed,
circleRadius,
data,
} = this.props;
const animate = [Animated.delay (delay)];
let counter = 0;
this.lineAnimated.forEach ((element, j) => {
let staggerCircle = [];
// console.log ('the counter is', counter);
for (let k = counter; k < data[j].length + counter; k++) {
staggerCircle.push (
Animated.spring (this.AnimatedPoints[k].r, {
toValue: circleRadius,
speed,
})
);
}
this.AnimatedPoints.map ((point, i) => {
if (i < data[j].length + counter && i >= counter) return;
return null;
});
animate.push (
Animated.parallel ([
Animated.timing (element.strokeDashoffset, {
toValue: 0,
duration,
}),
]),
Animated.stagger (staggerLength, staggerCircle)
);
counter = counter + data[j].length;
});
Animated.sequence (animate).start ();
}
treeManipulation () {
const {
data,
leftAxisData,
bottomAxisData,
legendColor,
legendText,
minX,
minY,
maxX,
maxY,
scatterPlotEnable,
dataPointsVisible,
hideAxis,
hideXAxisLabels,
hideYAxisLabels,
showLegends,
axisColor,
axisLabelColor,
axisLineWidth,
chartFontSize,
Color,
chartHeight,
chartWidth,
tickColorXAxis,
tickColorYAxis,
tickWidthXAxis,
tickWidthYAxis,
lineWidth,
circleRadiusWidth,
circleRadius,
showTicks,
legendStyle,
lineStrokeOpacity,
lineStrokeDashArray,
showDashedLine,
leftAxisDataToShow,
bottomAxisDataToShow,
pointDataToShowOnGraph,
circleLegendType,
fillArea,
animation,
GraphWidth,
} = this.props;
const {
yAxisGrid,
xAxisGrid,
hideXAxis,
hideYAxis,
inclindTick,
} = this.props;
this.lineAnimated = new Array (data.length);
const xScale = d3
.scaleLinear ()
.range ([MARGINS.left, chartWidth - MARGINS.right])
.domain ([minX, maxX]),
yScale = d3
.scaleLinear ()
.range ([chartHeight - MARGINS.top, MARGINS.bottom])
.domain ([minY, maxY]),
xAxis = d3.axisBottom (xScale),
yAxis = d3
.axisLeft (yScale)
.ticks (d3.timeDay, 1)
.tickFormat (d3.timeFormat ('%a %d'));
const TICKSIZE = chartWidth / 35;
let x = 0, y = chartHeight - MARGINS.top;
let endX = x + chartWidth;
let endY = y;
xCoordinate = hideAxis
? null
: <G fill="none">
{hideXAxis
? null
: <Line
stroke={axisColor}
strokeWidth={axisLineWidth}
x1={x + 30}
x2={endX + 30}
y1={y}
y2={endY}
/>}
{showTicks
? _.map (bottomAxisData, function (d, i) {
return (
<Line
key={i}
stroke={tickColorXAxis}
strokeWidth={tickWidthXAxis}
x1={xScale (d) + 10}
y1={y}
x2={inclindTick ? xScale (d) - 2 : xScale (d) + 10}
y2={xAxisGrid ? 20 : y + TICKSIZE}
/>
);
})
: null}
{hideXAxisLabels
? null
: _.map (bottomAxisData, function (d, i) {
let x1 = xScale (d) - 2
let x2 = xScale (d) + 10
if (bottomAxisDataToShow.length === 1) {
x1 = x1 + ((GraphWidth - 50) / 2)
x2 = x2 + ((GraphWidth - 50) / 2)
}
return (
<Text
key={i}
fill={axisLabelColor}
fontSize={chartFontSize}
textAnchor="middle"
x={inclindTick ? x1 : x2}
y={chartHeight + 5}
>
{bottomAxisDataToShow[i]}
</Text>
);
})}
</G>;
let xx = 0, yy = chartHeight;
let endXX = xx;
let endYY = yy - chartWidth;
yCoordinate = hideAxis
? null
: <G fill="none">
{hideYAxis
? null
: <Line
stroke={axisColor}
strokeWidth={axisLineWidth}
x1={xx + 40}
x2={endXX + 40}
y1={yy}
y2={endYY}
/>}
{showTicks
? _.map (leftAxisData, function (d, i) {
return (
<Line
key={i}
stroke={tickColorYAxis}
strokeWidth={tickWidthYAxis}
x1={xx + 40}
y1={yScale (d)}
x2={yAxisGrid ? chartWidth - 10 : xx + 30}
y2={inclindTick ? yScale (d) - 5 : yScale (d)}
/>
);
})
: null}
{hideYAxisLabels
? null
: _.map (leftAxisData, function (d, i) {
return (
<Text
key={i}
fill={axisLabelColor}
fontSize={chartFontSize}
textAnchor="middle"
x={inclindTick ? xx + 25 : xx + 20}
y={inclindTick ? yScale (d) - 20 : yScale (d) - 8}
>
{leftAxisDataToShow[i]}
</Text>
);
})}
</G>;
// M40,74.5679012345679L73,20L106,171.11111111111111L139,87.16049382716051L139,360L370,183.70370370370372
var lineGen = d3
.line ()
.x (function (d) {
return xScale (d.x) + 10;
})
.y (function (d) {
return yScale (d.y);
});
let linePointsData = formatLineData (data);
linePathOne = scatterPlotEnable
? null
: animation
? _.map (linePointsData, (data, i) => {
this.lineAnimated[i] = createLineProps (data);
return (
<NativePath
{...this.lineAnimated[i]}
strokeOpacity={lineStrokeOpacity}
key={i}
fill={'none'}
stroke={Color[i] ? Color[i] : '#000'}
strokeWidth={lineWidth}
/>
);
})
: _.map (linePointsData, (data, i) => {
return (
<Path
key={i}
strokeOpacity={lineStrokeOpacity}
strokeDasharray={showDashedLine ? lineStrokeDashArray[i] : ''}
d={data}
fill={fillArea ? 'transparent' : 'none'}
stroke={Color[i] ? Color[i] : '#000'}
strokeWidth={lineWidth}
/>
);
});
linePathFill = scatterPlotEnable
? null
: _.map (linePointsData, (data, i) => {
let fillData = data
const dataArr = data.split('L')
if (dataArr.length > 1) {
const lastPointArr = dataArr[dataArr.length - 1].split(',')
fillData = dataArr.reduce((d, item, index) => {
if (index === 0) {
return `${d}${item.replace('M', 'L')}`
} else {
return `${d}L${item}`
}
}, `M40,${this.props.chartHeight}`) + `L${lastPointArr[0]},${this.props.chartHeight}`
}
return (
<Path
key={i}
strokeOpacity={lineStrokeOpacity}
strokeDasharray={showDashedLine ? lineStrokeDashArray[i] : ''}
d={fillData}
fill={fillArea ? this.props.fillColor : 'none'}
stroke={Color[i] ? 'transparent' : '#000'}
strokeWidth={lineWidth}
/>
);
});
let dataPointsColor = buildColorArray (data, Color);
let pointData = calculateOverallLineChartData (data);
this.AnimatedPoints = new Array (pointData.length);
// console.log ('the anim points are', this.AnimatedPoints[0]);
circleTouch = dataPointsVisible
? _.map (pointData, (d, i) => {
let circleX = xScale (d.x) + 10
if (data.length === dataPointsColor.length) {
circleX = circleX + ((GraphWidth - 50) / 2)
}
return (
<TouchableOpacity
key={'circleTouch_' + i}
style={{
position: 'absolute',
marginLeft: circleX - (this.props.circleTouhRadius / 2),
marginTop: yScale (d.y) - (this.props.circleTouhRadius / 2),
width: this.props.circleTouhRadius,
height: this.props.circleTouhRadius,
backgroundColor: 'transparent' }}
onPress={() => {
this.props.circleOnpress(d, circleX - 10, yScale (d.y))
}}
/>
);
})
: null;
circleInFirstLine = dataPointsVisible
? _.map (pointData, (d, i) => {
let text;
this.AnimatedPoints[i] = createCircleProps (d, dataPointsColor[i]);
let textX = xScale (d.x) + 7
let circleX = xScale (d.x) + 10
if (data.length === dataPointsColor.length) {
textX = textX + ((GraphWidth - 50) / 2)
circleX = circleX + ((GraphWidth - 50) / 2)
}
text = (
<Text
fill={dataPointsColor[i]}
fontSize={chartFontSize}
x={textX}
y={yScale (d.y) - 18}
>
{pointDataToShowOnGraph == 'Y'
? (this.props.showPointDataOnGraph ? d.y : '')
: pointDataToShowOnGraph == 'X' ? d.x : ''}
</Text>
);
// console.log ('the anim points are', this.AnimatedPoints[i]);
return (
<G key={i}>
{animation
? <NativeCircle
key={'circle_' + i}
{...this.AnimatedPoints[i]}
/>
: <Circle
key={'circle_' + i}
strokeWidth={circleRadiusWidth}
stroke={dataPointsColor[i]}
d={d.x}
fill={'white'}
cx={circleX}
cy={yScale (d.y)}
r={circleRadius}
/>
}
{text}
</G>
);
})
: null;
legend = showLegends
? createLegend (
legendColor,
legendText,
chartWidth,
MARGINS,
legendStyle,
circleLegendType
)
: null;
function formatLineData (data) {
let linePointsData = [], lineDataArray = [];
for (let i = 0; i < data.length; i++) {
lineDataArray.push (data[i]);
}
for (var i = 0; i < lineDataArray.length; i++) {
linePointsData.push (lineGen (lineDataArray[i]));
}
return linePointsData;
}
function createCircleProps (d, strokeColor) {
return {
r: new Animated.Value (0),
cx: xScale (d.x) + 10,
cy: yScale (d.y),
stroke: strokeColor,
strokeWidth: circleRadiusWidth,
fill: 'white',
};
}
if (animation) this.animate ();
}
render () {
this.treeManipulation ();
const {GraphWidth, GraphHeight} = this.props;
return (
<View>
<Svg width={GraphWidth} height={GraphHeight}>
<G>
{linePathFill}
{linePathOne}
{yCoordinate}
{xCoordinate}
{circleInFirstLine}
{legend}
</G>
</Svg>
{circleTouch}
</View>
);
}
}