Skip to content

Instantly share code, notes, and snippets.

@MrToph
Created June 21, 2017 21:16
Show Gist options
  • Star 19 You must be signed in to star a gist
  • Fork 3 You must be signed in to fork a gist
  • Save MrToph/49e564044e43a260cd44a35674d57ce7 to your computer and use it in GitHub Desktop.
Save MrToph/49e564044e43a260cd44a35674d57ce7 to your computer and use it in GitHub Desktop.
Charts in React Native with React-Native-SVG and D3.js
import React, { Component, PropTypes } from 'react'
import { G, Line, Path, Rect, Text } from 'react-native-svg'
import * as d3scale from 'd3-scale'
import { dateToShortString } from '../utils'
export default class Axis extends Component {
static propTypes = {
width: PropTypes.number.isRequired,
ticks: PropTypes.number.isRequired,
x: PropTypes.number,
y: PropTypes.number,
startVal: PropTypes.oneOfType([React.PropTypes.number, React.PropTypes.object]),
endVal: PropTypes.oneOfType([React.PropTypes.number, React.PropTypes.object]),
vertical: PropTypes.bool,
scale: PropTypes.func // if scale is specified use that scale
}
render () {
let { width, ticks, x, y, startVal, endVal, vertical } = this.props
const TICKSIZE = width / 35
x = x || 0
y = y || 0
let endX = vertical ? x : x + width
let endY = vertical ? y - width : y
let scale = this.props.scale
if (!scale) {
scale = typeof startVal === 'number' ? d3scale.scaleLinear() : d3scale.scaleTime()
scale.domain(vertical ? [y, endY] : [x, endX]).range([startVal, endVal])
}
let tickPoints = vertical ? this.getTickPoints(vertical, y, endY, ticks)
: this.getTickPoints(vertical, x, endX, ticks)
return (
<G fill='none'>
<Line
stroke='#000'
strokeWidth='3'
x1={x}
x2={endX}
y1={y}
y2={endY} />
{tickPoints.map(
pos => <Line
key={pos}
stroke='#000'
strokeWidth='3'
x1={vertical ? x : pos}
y1={vertical ? pos : y}
x2={vertical ? x - TICKSIZE : pos}
y2={vertical ? pos : y + TICKSIZE} />
)}
{tickPoints.map(
pos => <Text
key={pos}
fill='#000'
stroke='#000'
fontSize='30'
textAnchor='middle'
x={vertical ? x - 2 * TICKSIZE : pos}
y={vertical ? pos : y + 2 * TICKSIZE}>
{typeof startVal === 'number' ? Math.round(scale(pos), 2) : dateToShortString(scale(pos))}
</Text>
)}
</G>
)
}
getTickPoints (vertical, start, end, numTicks) {
let res = []
let ticksEvery = Math.floor(this.props.width / (numTicks - 1))
if (vertical) {
for (let cur = start; cur >= end; cur -= ticksEvery) res.push(cur)
} else {
for (let cur = start; cur <= end; cur += ticksEvery) res.push(cur)
}
return res
}
}
import React, { Component, PropTypes } from 'react'
import { G, Rect, Text } from 'react-native-svg'
export default class Legend extends Component {
static propTypes = {
names: PropTypes.arrayOf(PropTypes.string).isRequired,
colors: PropTypes.arrayOf(PropTypes.string).isRequired,
x: PropTypes.number,
y: PropTypes.number
}
render () {
let {names, colors, x, y} = this.props
return (
<G fill='none'>
{names.map(
(name, i) => <Text
key={name}
fill={colors[i % colors.length]}
stroke={colors[i % colors.length]}
fontSize='30'
x={x}
y={y + i * 30}>
{name}
</Text>
)}
</G>
)
}
}
import React, { Component, PropTypes } from 'react'
import { View } from 'react-native'
import Svg, { Path } from 'react-native-svg'
import * as d3scale from 'd3-scale'
import * as d3shape from 'd3-shape'
import { Text as TEXT, Axis, Legend } from '../components'
import { textColor } from '../styling'
export default class LineChart extends Component {
static propTypes = {
// dynamic shape: exerciseObj
// {
// "Barbell Bench Press": [
// [date: Date, weight],
// [date: Date, weight],
// [date: Date, weight],
// [date: Date, weight],
// [date: Date, weight],
// ],
// "Dumbbell Bench Press": [
// [date: Date, weight],
// [date: Date, weight],
// [date: Date, weight],
// [date: Date, weight],
// [date: Date, weight],
// ],
// ...
// }
exObj: PropTypes.object.isRequired,
colors: PropTypes.arrayOf(PropTypes.string).isRequired,
}
constructor(props) {
super(props)
this.state = { dimensions: undefined }
this.startX = 100
this.viewBoxWidth = 1000
}
render() {
if (this.state.dimensions) {
var { data, minDate, maxDate, minWeight, maxWeight, dimensions, xScale, yScale } = this.state
var { width, height, aspectRatio } = dimensions
var viewBox = { w: this.viewBoxWidth, h: this.viewBoxWidth / aspectRatio }
var { exObj, colors } = this.props
}
return (
<View style={{ flex: 1, alignSelf: 'stretch' }} onLayout={this.onLayout}>
{this.state.dimensions && minDate // minDate is set when there is more than 0 exercises
? <Svg width={width} height={height} viewBox={`0 0 ${viewBox.w} ${viewBox.h}`}>
<Axis
width={viewBox.w - 2 * this.startX}
x={this.startX}
y={viewBox.h - this.startX}
ticks={8}
startVal={minDate}
endVal={maxDate}
scale={xScale}
/>
<Axis
width={viewBox.h - 2 * this.startX}
x={this.startX}
y={viewBox.h - this.startX}
ticks={8}
startVal={minWeight}
endVal={maxWeight}
scale={yScale}
vertical
/>
{data.map(
(pathD, i) => <Path fill="none" stroke={colors[i % colors.length]} strokeWidth="5" d={pathD} key={i} />,
)}
<Legend
x={this.startX + 30}
y={this.startX}
names={Object.keys(exObj)}
colors={colors}
/>
</Svg>
: undefined}
</View>
)
}
onLayout = (event) => {
if (this.state.dimensions) return // layout was already called once
const { width, height } = event.nativeEvent.layout
const aspectRatio = width / height
const graphData = this.createGraphData(this.props.exObj, aspectRatio)
this.setState({ dimensions: { width, height, aspectRatio }, ...graphData })
}
componentWillReceiveProps(nextProps) {
if (!this.state.dimensions) return
const aspectRatio = this.state.dimensions.aspectRatio
this.setState({ ...this.createGraphData(nextProps.exObj, aspectRatio) })
}
createGraphData = (exObj, aspectRatio) => {
const weights = []
let minDate
let maxDate
// find ranges of date and weight by expanding ALL exercises
Object.keys(exObj).forEach((exName) => {
const firstWorkout = exObj[exName][0]
if (firstWorkout) {
minDate = minDate ? new Date(Math.min(minDate.getTime(), firstWorkout[0].getTime()))
: new Date(firstWorkout[0].getTime())
const lastWorkout = exObj[exName][exObj[exName].length - 1]
maxDate = maxDate ? new Date(Math.max(maxDate.getTime(), lastWorkout[0].getTime()))
: new Date(lastWorkout[0].getTime())
}
weights.push(...exObj[exName].map(arr => arr[1]))
})
const minWeight = Math.min(...weights)
const maxWeight = Math.max(...weights)
const { xScale, yScale } = this.createScalesDomain(aspectRatio)
xScale.range([minDate, maxDate])
yScale.range([minWeight, maxWeight])
const data = []
const lineGenerator = d3shape.line()
.x(d => xScale.invert(d[0]))
.y(d => yScale.invert(d[1]))
Object.keys(exObj).forEach((exName) => {
data.push(lineGenerator(exObj[exName]))
})
return { data, minDate, maxDate, minWeight, maxWeight, xScale, yScale }
}
createScalesDomain = (aspectRatio) => {
const viewBoxWidth = this.viewBoxWidth
const viewBoxHeight = viewBoxWidth / aspectRatio
const startX = this.startX
const startY = viewBoxHeight - startX
const xScale = d3scale.scaleTime()
xScale.domain([startX, viewBoxWidth - startX])
const yScale = d3scale.scaleLinear().domain([startY, startX])
return { xScale, yScale }
}
}
const styles = {
text: {
color: textColor,
},
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment