Permalink
Browse files

Here we add animations to our graph!

We can now toggle between showing the temperature Max or Min values.
And when we switch values the graph and numbers animate!

# package.json

To tween the SVG path value we're going to directly use the ART library.
This is only needed for tweening the SVG path.

# weather/ControlButton.js
Generic component to let the user click to change what value to show.

# weather/WeatherPage.js
Hooks up the ControlButton and uses it to change the yAccessor
that is passed to the WeatherGraph.

# weather/WeatherGraph.js
This is where most of the work is done. Most of new changes are commented
inline and will be better understood in the context of where the
changes were applied.
  • Loading branch information...
hswolff committed Jul 9, 2016
1 parent 07a5e42 commit 6a51bce269c58f14f59e6f00dd362ce281a2b19f
Showing with 164 additions and 2 deletions.
  1. +54 −0 js/weather/ControlButton.js
  2. +79 −0 js/weather/WeatherGraph.js
  3. +30 −2 js/weather/WeatherPage.js
  4. +1 −0 package.json
@@ -0,0 +1,54 @@
+import React, {
+ PropTypes,
+} from 'react';
+import {
+ StyleSheet,
+ Text,
+ TouchableOpacity,
+} from 'react-native';
+
+import Color from '../services/color';
+
+export default function ControlButton(props) {
+ const {
+ active,
+ onPress,
+ children,
+ } = props;
+ return (
+ <TouchableOpacity
+ style={[styles.container, active ? styles.active : null]}
+ onPress={onPress}
+ >
+ <Text
+ style={[styles.text, active ? styles.activeText : null]}
+ >
+ {children}
+ </Text>
+ </TouchableOpacity>
+ );
+}
+ControlButton.propTypes = {
+ active: PropTypes.bool.isRequired,
+ onPress: PropTypes.func.isRequired,
+ children: PropTypes.string.isRequired,
+};
+
+const styles = StyleSheet.create({
+ container: {
+ borderWidth: 1,
+ borderRadius: 4,
+ padding: 30,
+ paddingTop: 10,
+ paddingBottom: 10,
+ },
+ text: {
+ fontSize: 28,
+ },
+ active: {
+ backgroundColor: Color.BlueDark,
+ },
+ activeText: {
+ color: Color.White,
+ },
+});
View
@@ -5,6 +5,7 @@ import React, {
import {
ART,
Dimensions,
+ LayoutAnimation,
StyleSheet,
Text,
View,
@@ -16,12 +17,15 @@ const {
Surface,
} = ART;
+import Morph from 'art/morph/path';
+
import * as graphUtils from './graph-utils';
import Color from '../services/color';
const PaddingSize = 20;
const TickWidth = PaddingSize * 2;
+const AnimationDurationMs = 500;
const dimensionWindow = Dimensions.get('window');
@@ -80,6 +84,81 @@ export default class WeatherGraph extends Component {
ticks: lineGraph.ticks,
scale: lineGraph.scale,
});
+
+ // The first time this function is hit we need to set the initial
+ // this.previousGraph value.
+ if (!this.previousGraph) {
+ this.previousGraph = lineGraph;
+ }
+
+ // Only animate if our properties change. Typically this is when our
+ // yAccessor function changes.
+ if (this.props !== nextProps) {
+ const pathFrom = this.previousGraph.path;
+ const pathTo = lineGraph.path;
+
+ cancelAnimationFrame(this.animating);
+ this.animating = null;
+
+ // Opt-into layout animations so our y tickLabel's animate.
+ // If we wanted more discrete control over their animation behavior
+ // we could use the Animated component from React Native, however this
+ // was a nice shortcut to get the same effect.
+ LayoutAnimation.configureNext(
+ LayoutAnimation.create(
+ AnimationDurationMs,
+ LayoutAnimation.Types.easeInEaseOut,
+ LayoutAnimation.Properties.opacity
+ )
+ );
+
+ this.setState({
+ // Create the ART Morph.Tween instance.
+ linePath: Morph.Tween( // eslint-disable-line new-cap
+ pathFrom,
+ pathTo,
+ ),
+ }, () => {
+ // Kick off our animations!
+ this.animate();
+ });
+
+ this.previousGraph = lineGraph;
+ }
+ }
+
+ // This is where we animate our graph's path value.
+ animate(start) {
+ this.animating = requestAnimationFrame((timestamp) => {
+ if (!start) {
+ // eslint-disable-next-line no-param-reassign
+ start = timestamp;
+ }
+
+ // Get the delta on how far long in our animation we are.
+ const delta = (timestamp - start) / AnimationDurationMs;
+
+ // If we're above 1 then our animation should be complete.
+ if (delta > 1) {
+ this.animating = null;
+ // Just to be safe set our final value to the new graph path.
+ this.setState({
+ linePath: this.previousGraph.path,
+ });
+
+ // Stop our animation loop.
+ return;
+ }
+
+ // Tween the SVG path value according to what delta we're currently at.
+ this.state.linePath.tween(delta);
+
+ // Update our state with the new tween value and then jump back into
+ // this loop.
+ this.setState(this.state, () => {
+ this.animate(start);
+ });
+ });
}
render() {
View
@@ -12,27 +12,49 @@ import {
import Color from '../services/color';
import Header from '../components/Header';
+import ControlButton from './ControlButton';
import WeatherGraph from './WeatherGraph';
-// eslint-disable-next-line react/prefer-stateless-function
export default class WeatherPage extends Component {
static propTypes = {
changeAddress: PropTypes.func.isRequired,
name: PropTypes.string.isRequired,
data: PropTypes.object.isRequired,
}
+ state = {
+ showMax: true,
+ };
+
+ onChange = (newVal) => {
+ const showMax = newVal === 'max';
+ if (this.state.showMax !== showMax) {
+ this.setState({ showMax });
+ }
+ };
+
+ setMax = this.onChange.bind(null, 'max');
+ setMin = this.onChange.bind(null, 'min');
+
render() {
const {
name,
changeAddress,
data: graphData,
} = this.props;
+ const {
+ showMax,
+ } = this.state;
+
const graphProps = {};
graphProps.data = graphData.daily.data;
graphProps.xAccessor = (d) => new Date(d.time * 1000);
- graphProps.yAccessor = (d) => d.temperatureMax;
+ if (showMax) {
+ graphProps.yAccessor = (d) => d.temperatureMax;
+ } else {
+ graphProps.yAccessor = (d) => d.temperatureMin;
+ }
return (
<View style={styles.container}>
@@ -47,7 +69,13 @@ export default class WeatherPage extends Component {
</TouchableOpacity>
</Header>
<View style={styles.content}>
+ <ControlButton active={this.state.showMax} onPress={this.setMax}>
+ Max
+ </ControlButton>
<WeatherGraph {...graphProps} />
+ <ControlButton active={!this.state.showMax} onPress={this.setMin}>
+ Min
+ </ControlButton>
</View>
</View>
);
View
@@ -7,6 +7,7 @@
"lint": "eslint ."
},
"dependencies": {
+ "art": "^0.10.1",
"d3-array": "^1.0.0",
"d3-scale": "^1.0.0",
"d3-shape": "^1.0.0",

3 comments on commit 6a51bce

@Obooman

This comment has been minimized.

Show comment
Hide comment
@Obooman

Obooman Sep 2, 2016

You did awesome work!Good job!

You did awesome work!Good job!

@RavishankarR

This comment has been minimized.

Show comment
Hide comment
@RavishankarR

RavishankarR May 26, 2017

I am getting an error that "graphData" property is undefined. Where is this value supplied from?

I am getting an error that "graphData" property is undefined. Where is this value supplied from?

@hswolff

This comment has been minimized.

Show comment
Hide comment
@hswolff

hswolff May 28, 2017

Owner

It's coming from the data prop, and being renamed to a local graphData value in the render function.

This is the prop that is setting the data.

Owner

hswolff replied May 28, 2017

It's coming from the data prop, and being renamed to a local graphData value in the render function.

This is the prop that is setting the data.

Please sign in to comment.