Permalink
Browse files

Here we render our first d3 graph using ART!

It's a big commit with a lot going on, so let's dive in.

# weather/WeatherGraph.js
This is the component that actually renders our D3 path using ART.

This is mostly using ART.Surface, ART.Group, and ART.Shape
to render any arbitrary SVG path value in the ART.Shape's d property.

To power that value we have to actually get a proper path value.

# weather/graph-utils.js

This file is what uses d3 to create the SVG path value.

Given a width, height, and data, we can then use that information
to create d3 scale functions and then use those with a d3 shape
generator to create an SVG line path.

This is returned to WeatherGraph and used in the ART.Shape d property.
  • Loading branch information...
hswolff committed Jul 9, 2016
1 parent 11ebd86 commit a05e3ae87929f94e5564732cf2c352b0bee576a7
Showing with 204 additions and 0 deletions.
  1. +107 −0 js/weather/WeatherGraph.js
  2. +16 −0 js/weather/WeatherPage.js
  3. +78 −0 js/weather/graph-utils.js
  4. +3 −0 package.json
View
@@ -0,0 +1,107 @@
+import React, {
+ Component,
+ PropTypes,
+} from 'react';
+import {
+ ART,
+ Dimensions,
+ StyleSheet,
+ View,
+} from 'react-native';
+
+const {
+ Group,
+ Shape,
+ Surface,
+} = ART;
+
+import * as graphUtils from './graph-utils';
+
+import Color from '../services/color';
+
+const PaddingSize = 20;
+
+const dimensionWindow = Dimensions.get('window');
+
+export default class WeatherGraph extends Component {
+ static propTypes = {
+ data: PropTypes.array.isRequired,
+ width: PropTypes.number.isRequired,
+ height: PropTypes.number.isRequired,
+ xAccessor: PropTypes.func.isRequired,
+ yAccessor: PropTypes.func.isRequired,
+ }
+
+ static defaultProps = {
+ width: Math.round(dimensionWindow.width * 0.9),
+ height: Math.round(dimensionWindow.height * 0.5),
+ };
+
+ state = {
+ graphWidth: 0,
+ graphHeight: 0,
+ linePath: '',
+ };
+
+ componentWillMount() {
+ this.computeNextState(this.props);
+ }
+
+ componentWillReceiveProps(nextProps) {
+ this.computeNextState(nextProps);
+ }
+
+ computeNextState(nextProps) {
+ const {
+ data,
+ width,
+ height,
+ xAccessor,
+ yAccessor,
+ } = nextProps;
+
+ const graphWidth = width - PaddingSize * 2;
+ const graphHeight = height - PaddingSize * 2;
+
+ const lineGraph = graphUtils.createLineGraph({
+ data,
+ xAccessor,
+ yAccessor,
+ width: graphWidth,
+ height: graphHeight,
+ });
+
+ this.setState({
+ graphWidth,
+ graphHeight,
+ linePath: lineGraph.path,
+ });
+ }
+
+ render() {
+ const {
+ graphWidth,
+ graphHeight,
+ linePath,
+ } = this.state;
+
+ return (
+ <View style={styles.container}>
+ <Surface width={graphWidth} height={graphHeight}>
+ <Group x={0} y={0}>
+ <Shape
+ d={linePath}
+ stroke={Color.Orange}
+ strokeWidth={1}
+ />
+ </Group>
+ </Surface>
+ </View>
+ );
+ }
+}
+
+const styles = StyleSheet.create({
+ container: {
+ },
+});
View
@@ -12,6 +12,7 @@ import {
import Color from '../services/color';
import Header from '../components/Header';
+import WeatherGraph from './WeatherGraph';
// eslint-disable-next-line react/prefer-stateless-function
export default class WeatherPage extends Component {
@@ -25,8 +26,14 @@ export default class WeatherPage extends Component {
const {
name,
changeAddress,
+ data: graphData,
} = this.props;
+ const graphProps = {};
+ graphProps.data = graphData.daily.data;
+ graphProps.xAccessor = (d) => new Date(d.time * 1000);
+ graphProps.yAccessor = (d) => d.temperatureMax;
+
return (
<View style={styles.container}>
<Header>
@@ -39,6 +46,9 @@ export default class WeatherPage extends Component {
</Text>
</TouchableOpacity>
</Header>
+ <View style={styles.content}>
+ <WeatherGraph {...graphProps} />
+ </View>
</View>
);
}
@@ -54,4 +64,10 @@ const styles = StyleSheet.create({
borderBottomWidth: 1,
borderBottomColor: Color.BlueDark,
},
+ content: {
+ flex: 1,
+ flexDirection: 'column',
+ justifyContent: 'space-around',
+ alignItems: 'center',
+ },
});
View
@@ -0,0 +1,78 @@
+import * as scale from 'd3-scale';
+import * as shape from 'd3-shape';
+import * as d3Array from 'd3-array';
+const d3 = {
+ scale,
+ shape,
+};
+
+/**
+ * Create an x-scale.
+ * @param {number} start Start time in seconds.
+ * @param {number} end End time in seconds.
+ * @param {number} width Width to create the scale with.
+ * @return {Function} D3 scale instance.
+ */
+function createScaleX(start, end, width) {
+ return d3.scale.scaleTime()
+ .domain([new Date(start * 1000), new Date(end * 1000)])
+ .range([0, width]);
+}
+
+/**
+ * Create a y-scale.
+ * @param {number} minY Minimum y value to use in our domain.
+ * @param {number} maxY Maximum y value to use in our domain.
+ * @param {number} height Height for our scale's range.
+ * @return {Function} D3 scale instance.
+ */
+function createScaleY(minY, maxY, height) {
+ return d3.scale.scaleLinear()
+ .domain([minY, maxY]).nice()
+ // We invert our range so it outputs using the axis that React uses.
+ .range([height, 0]);
+}
+
+/**
+ * Creates a line graph SVG path that we can then use to render in our
+ * React Native application with ART.
+ * @param {Array.<Object>} options.data Array of data we'll use to create
+ * our graphs from.
+ * @param {function} xAccessor Function to access the x value from our data.
+ * @param {function} yAccessor Function to access the y value from our data.
+ * @param {number} width Width our graph will render to.
+ * @param {number} height Height our graph will render to.
+ * @return {Object} Object with data needed to render.
+ */
+export function createLineGraph({
+ data,
+ xAccessor,
+ yAccessor,
+ width,
+ height,
+}) {
+ const lastDatum = data[data.length - 1];
+
+ const scaleX = createScaleX(
+ data[0].time,
+ lastDatum.time,
+ width
+ );
+
+ // Collect all y values.
+ const allYValues = data.reduce((all, datum) => {
+ all.push(yAccessor(datum));
+ return all;
+ }, []);
+ // Get the min and max y value.
+ const extentY = d3Array.extent(allYValues);
+ const scaleY = createScaleY(extentY[0], extentY[1], height);
+
+ const lineShape = d3.shape.line()
+ .x((d) => scaleX(xAccessor(d)))
+ .y((d) => scaleY(yAccessor(d)));
+
+ return {
+ path: lineShape(data),
+ };
+}
View
@@ -7,6 +7,9 @@
"lint": "eslint ."
},
"dependencies": {
+ "d3-array": "^1.0.0",
+ "d3-scale": "^1.0.0",
+ "d3-shape": "^1.0.0",
"lodash": "^4.13.1",
"query-string": "^4.2.2",
"react": "15.2.0",

0 comments on commit a05e3ae

Please sign in to comment.