UNPKG

react-native-pathjs-charts

Version:

Cross platform React Native charting library based on path-js and react-native-svg

382 lines (329 loc) 12.1 kB
/* Copyright 2016 Capital One Services, LLC Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. SPDX-Copyright: Copyright (c) Capital One Services, LLC SPDX-License-Identifier: Apache-2.0 */ import React, { Component } from 'react'; import { Text as ReactText, View, PanResponder } from 'react-native'; import Svg, { G, Path, Rect, Text, Circle, Line } from 'react-native-svg'; import { Colors, Options, cyclic, fontAdapt } from './util'; import Axis from './Axis'; import GridAxis from './GridAxis'; import _ from 'lodash'; export default class LineChart extends Component { constructor(props, chartType) { super(props); this.chartType = chartType; this.state = { userPressing: false}; } _calcDataPoint(evt) { let posX = evt.nativeEvent.locationX; let posY = evt.nativeEvent.locationY; posX -= this.props.options.margin.left; posY -= this.props.options.margin.top; let chartWidth = this.props.options.width; posX = Math.max(posX, 0); posX = Math.min(posX, chartWidth); // map the datapoint index with the gesture: let curPos = Math.min(posX / chartWidth, 1); // create a 'focus' line let curPosX = posX; this.curPos = curPos; this.setState({curPos}); this.setState({curPosX}); this.setState({chartStartY: 0}); this.setState({chartEndY: this.props.options.height}); } componentWillMount() { this._panResponder = {}; if (!this.props.options.interaction) {return;} this._panResponder = PanResponder.create({ // Ask to be the responder: onStartShouldSetPanResponder: (evt, gestureState) => true, onStartShouldSetPanResponderCapture: (evt, gestureState) => true, onMoveShouldSetPanResponder: (evt, gestureState) => true, onMoveShouldSetPanResponderCapture: (evt, gestureState) => true, onPanResponderGrant: (evt, gestureState) => { this.setState({userPressing: true}); this._calcDataPoint(evt) if (this.props.panHandlerStart) { this.props.panHandlerStart(this.curPos); } }, onPanResponderMove: (evt, gestureState) => { this._calcDataPoint(evt); if (this.props.panHandlerMove) { this.props.panHandlerMove(this.curPos); } }, onPanResponderRelease: (evt, gestureState) => { this._calcDataPoint(evt); if (this.props.panHandlerEnd) { this.props.panHandlerEnd(this.curPos); } this.setState({userPressing: false}); }, onPanResponderTerminationRequest: (evt, gestureState) => true, onPanResponderTerminate: (evt, gestureState) => { this._calcDataPoint(evt); if (this.props.panHandlerEnd) { this.props.panHandlerEnd(this.curPos); } this.setState({userPressing: false}); }, onShouldBlockNativeResponder: (evt, gestureState) => { return true; }, }); } getMaxAndMin(chart, key, scale, chartMin, chartMax) { let maxValue; let minValue; _.each(chart.curves, function(serie) { let values = _.map(serie.item, function(item) { return item[key]; }); let max = _.max(values); if (maxValue === undefined || max > maxValue) maxValue = max; let min = _.min(values); if (minValue === undefined || min < minValue) minValue = min; maxValue = chartMax > maxValue ? chartMax : maxValue; minValue = chartMin < minValue ? chartMin : minValue; }); return { minValue: minValue, maxValue: maxValue, min: scale(minValue), max: scale(maxValue), }; } color(i) { let color = this.props.options.color; if (!_.isString(this.props.options.color)) color = color.color; let pallete = this.props.pallete || Colors.mix(color || '#9ac7f7'); return Colors.string(cyclic(pallete, i)); } render() { const noDataMsg = this.props.noDataMessage || 'No data available'; if (this.props.data === undefined) return <ReactText>{noDataMsg}</ReactText>; let options = new Options(this.props); let accessor = function(key) { return function(x) { return x[key]; }; }; let chart = this.chartType({ data: this.props.data, xaccessor: accessor(this.props.xKey), yaccessor: accessor(this.props.yKey), width: options.chartWidth, height: options.chartHeight, closed: false, min: options.min, max: options.max, }); let chartArea = { x: this.getMaxAndMin(chart, this.props.xKey, chart.xscale), y: this.getMaxAndMin(chart, this.props.yKey, chart.yscale, options.min, options.max), margin: options.margin, }; let showAreas = typeof this.props.options.showAreas !== 'undefined' ? this.props.options.showAreas : true; let strokeWidth = typeof this.props.options.strokeWidth !== 'undefined' ? this.props.options.strokeWidth : '1'; let strokeDasharray = typeof this.props.options.strokeDasharray !== 'undefined' ? this.props.options.strokeDasharray : []; let strokeOpacity = typeof this.props.options.strokeOpacity !== 'undefined' ? this.props.options.strokeOpacity : 1; let lines = _.map( chart.curves, function(c, i) { const strokeWidthForCurve = (typeof strokeWidth === 'function' && strokeWidth(c, i)) || strokeWidth; const strokeDasharrayForCurve = (typeof strokeDasharray === 'function' && strokeDasharray(c, i)) || strokeDasharray; const strokeOpacityForCurve = (typeof strokeOpacity === 'function' && strokeOpacity(c, i)) || strokeOpacity; return ( <Path key={'lines' + i} d={c.line.path.print()} stroke={this.color(i)} strokeWidth={strokeWidthForCurve} strokeOpacity={strokeOpacityForCurve} fill="none" strokeDasharray={strokeDasharrayForCurve} /> ); }.bind(this) ); // gesture line here let gestureLine = null; let color = 'white'; let width = 1; if (this.props.options.cursorLine ) { if (this.props.options.cursorLine.stroke) color = this.props.options.cursorLine.stroke; if (this.props.options.cursorLine.strokeWidth) { width =this.props.options.cursorLine.strokeWidth; } } if (this.state.userPressing && this.props.options.interaction) { gestureLine = <Line x1={this.state.curPosX} y1={this.state.chartStartY} x2={this.state.curPosX} y2={this.state.chartEndY} stroke={color} strokeWidth={width} /> } let areas = null; let showPoints = typeof this.props.options.showPoints !== 'undefined' ? this.props.options.showPoints : false; let points = !showPoints ? [] : _.map( chart.curves, function(c, graphIndex) { return _.map( c.line.path.points(), function(p, pointIndex) { let render = null; if ( (typeof showPoints === 'function' && showPoints(graphIndex, pointIndex)) || (typeof showPoints === 'boolean' && showPoints) ) { return ( <G key={'k' + pointIndex} x={p[0]} y={p[1]}> {typeof this.props.options.renderPoint === 'function' ? this.props.options.renderPoint(graphIndex, pointIndex) : <Circle fill={this.color(graphIndex)} cx={0} cy={0} r={this.props.options.pointRadius || 5} fillOpacity={1} />} </G> ); } }.bind(this) ); }.bind(this) ); if (showAreas) { areas = _.map( chart.curves, function(c, i) { if ( (typeof showAreas === 'function' && showAreas(c, i)) || typeof showAreas === 'boolean' ) return ( <Path key={'areas' + i} d={c.area.path.print()} fillOpacity={0.5} stroke="none" fill={this.color(i)} /> ); return null; }.bind(this) ); } let textStyle = fontAdapt(options.label); let regions; if (this.props.regions != 'undefined') { let styling = typeof this.props.regionStyling != 'undefined' ? this.props.regionStyling : {}; let labelOffsetAllRegions = typeof styling.labelOffset != 'undefined' ? styling.labelOffset : {}; regions = _.map( this.props.regions, function(c, i) { let x, y, height, width, y1, y2, labelX, labelY; let labelOffset = typeof c.labelOffset != 'undefined' ? c.labelOffset : {}; let labelOffsetLeft = typeof labelOffsetAllRegions.left != 'undefined' ? typeof labelOffset.left != 'undefined' ? labelOffset.left : labelOffsetAllRegions.left : 20; let labelOffsetTop = typeof labelOffsetAllRegions.top != 'undefined' ? typeof labelOffset.top != 'undefined' ? labelOffset.top : labelOffsetAllRegions.top : 0; let fillOpacity = typeof styling.fillOpacity != 'undefined' ? typeof c.fillOpacity != 'undefined' ? c.fillOpacity : styling.fillOpacity : 0.5; y1 = chart.yscale(c.from); y2 = chart.yscale(c.to); x = 0; y = y1; height = y2 - y1; width = chartArea.x.max; labelX = labelOffsetLeft; labelY = y2 + labelOffsetTop; let regionLabel = typeof c.label != 'undefined' ? <Text fontFamily={textStyle.fontFamily} fontSize={textStyle.fontSize} fontWeight={textStyle.fontWeight} fontStyle={textStyle.fontStyle} fill={textStyle.fill} textAnchor="middle" x={labelX} y={labelY} > {c.label} </Text> : null; return ( <G key={'region' + i}> <Rect key={'region' + i} x={x} y={y} width={width} height={height} fill={c.fill} fillOpacity={fillOpacity} /> {regionLabel} </G> ); }.bind(this) ); } let returnValue = ( <View width={options.width} height={options.height} {...this._panResponder.panHandlers}> <Svg width={options.width} height={options.height}> <G x={options.margin.left} y={options.margin.top}> <GridAxis key="grid-x" scale={chart.xscale} options={options.axisX} chartArea={chartArea} /> <GridAxis key="grid-y" scale={chart.yscale} options={options.axisY} chartArea={chartArea} /> {regions} {areas} {lines} {points} {gestureLine} <Axis key="axis-x" scale={chart.xscale} options={options.axisX} chartArea={chartArea} /> <Axis key="axis-y" scale={chart.yscale} options={options.axisY} chartArea={chartArea} /> </G> </Svg> </View> ); return returnValue; } }