[explorev2] chart and controls (#1251)

* create structure for new forked explore view (#1099)

* create structure for new forked explore view

* update component name

* add bootstrap data pattern

* remove console.log

* Created store and reducers (#1108)

* Created store and reducers

* Added spec

* Modifications based on comments

* do use bootstrap data for now

* don't deal with bootstrap data for now

* use victory as a base

* import fake line data, add fake panels, make chart fixed

* add fetch support

* get slice data from json endpoint

* render chart with slicejson

* update chart and label demo

* remove fetch config

* remove dummy control panels

* should be a func

* make TimeSeriesLineChart

* add a comment

* inner height for height

* don't need fetch yet

* trailing comma breaks in package json

* pass in viz data from props

* add style sheet

* set height on explore container

* add legend

* make chart responsive to window resize

* can't use head_css in template bc overrides head_css in basic

* fix linting

* break labelItem into own SFC, make legend SFC

* add propTypes and fix linter
This commit is contained in:
Alanna Scott 2016-10-05 19:41:16 -07:00 committed by GitHub
parent 66b498de25
commit f837733d85
11 changed files with 436 additions and 29 deletions

View File

@ -0,0 +1,173 @@
const { assign } = Object;
const A11Y_BABU = '#00A699';
const AXIS_LINE_GRAY = '#484848';
// Colors
const colors = [
'#ffffff',
'#f0f0f0',
'#d9d9d9',
'#bdbdbd',
'#969696',
'#737373',
'#525252',
'#252525',
'#000000',
];
const charcoal = '#484848';
// Typography
const sansSerif = '"Roboto", sans-serif';
const letterSpacing = 'normal';
const fontSize = 8;
// Layout
const baseProps = {
width: 450,
height: 300,
padding: 50,
colorScale: colors,
};
// Labels
const baseLabelStyles = {
fontFamily: sansSerif,
fontSize,
letterSpacing,
padding: 10,
fill: charcoal,
stroke: 'transparent',
};
// Strokes
const strokeLinecap = 'round';
const strokeLinejoin = 'round';
// Create the theme
const theme = {
area: assign({
style: {
data: {
fill: charcoal,
},
labels: baseLabelStyles,
},
}, baseProps),
axis: assign({
style: {
axis: {
fill: 'none',
stroke: AXIS_LINE_GRAY,
strokeWidth: 1,
strokeLinecap,
strokeLinejoin,
},
axisLabel: assign({}, baseLabelStyles, {
padding: 25,
}),
grid: {
fill: 'none',
stroke: 'transparent',
},
ticks: {
fill: 'none',
padding: 10,
size: 1,
stroke: 'transparent',
},
tickLabels: baseLabelStyles,
},
}, baseProps),
bar: assign({
style: {
data: {
fill: A11Y_BABU,
padding: 10,
stroke: 'transparent',
strokeWidth: 0,
width: 8,
},
labels: baseLabelStyles,
},
}, baseProps),
candlestick: assign({
style: {
data: {
stroke: A11Y_BABU,
strokeWidth: 1,
},
labels: assign({}, baseLabelStyles, {
padding: 25,
textAnchor: 'end',
}),
},
candleColors: {
positive: '#ffffff',
negative: charcoal,
},
}, baseProps),
chart: baseProps,
errorbar: assign({
style: {
data: {
fill: 'none',
stroke: charcoal,
strokeWidth: 2,
},
labels: assign({}, baseLabelStyles, {
textAnchor: 'start',
}),
},
}, baseProps),
group: assign({
colorScale: colors,
}, baseProps),
line: assign({
style: {
data: {
fill: 'none',
stroke: A11Y_BABU,
strokeWidth: 2,
},
labels: assign({}, baseLabelStyles, {
textAnchor: 'start',
}),
},
}, baseProps),
pie: {
style: {
data: {
padding: 10,
stroke: 'none',
strokeWidth: 1,
},
labels: assign({}, baseLabelStyles, {
padding: 200,
textAnchor: 'middle',
}),
},
colorScale: colors,
width: 400,
height: 400,
padding: 50,
},
scatter: assign({
style: {
data: {
fill: charcoal,
stroke: 'transparent',
strokeWidth: 0,
},
labels: assign({}, baseLabelStyles, {
textAnchor: 'middle',
}),
},
}, baseProps),
stack: assign({
colorScale: colors,
}, baseProps),
};
export default theme;

View File

@ -1,11 +1,67 @@
import React from 'react';
import React, { PropTypes } from 'react';
import { Panel } from 'react-bootstrap';
import TimeSeriesLineChart from './charts/TimeSeriesLineChart';
import moment from 'moment';
const ChartContainer = function () {
return (
<Panel header="Chart title">
chart goes here
</Panel>
);
const propTypes = {
viz: PropTypes.shape({
data: PropTypes.object.isRequired,
form_data: PropTypes.shape({
slice_name: PropTypes.object.isRequired,
}).isRequired,
}).isRequired,
height: PropTypes.number.isRequired,
};
export default ChartContainer;
export default class ChartContainer extends React.Component {
constructor(props) {
super(props);
this.state = {
params: this.getParamsFromUrl(),
data: props.viz.data,
label1: 'Label 1',
};
}
getParamsFromUrl() {
const hash = window.location.search;
const params = hash.split('?')[1].split('&');
const newParams = {};
params.forEach((p) => {
const value = p.split('=')[1].replace(/\+/g, ' ');
const key = p.split('=')[0];
newParams[key] = value;
});
return newParams;
}
formatDates(values) {
const newValues = values.map(function (val) {
return {
x: moment(new Date(val.x)).format('MMM D'),
y: val.y,
};
});
return newValues;
}
render() {
return (
<div className="chart-container">
<Panel
style={{ height: this.props.height }}
header={
<div className="panel-title">{this.props.viz.form_data.slice_name}</div>
}
>
<TimeSeriesLineChart
data={this.state.data}
label1="Label 1"
/>
</Panel>
</div>
);
}
}
ChartContainer.propTypes = propTypes;

View File

@ -1,26 +1,55 @@
import React from 'react';
import React, { PropTypes } from 'react';
import ChartContainer from './ChartContainer';
import ControlPanelsContainer from './ControlPanelsContainer';
import QueryAndSaveButtons from './QueryAndSaveButtons';
const ExploreViewContainer = function () {
return (
<div className="container-fluid">
<div className="row">
<div className="col-sm-3">
<QueryAndSaveButtons
canAdd="True"
onQuery={() => { console.log('clicked query'); }}
/>
<br /><br />
<ControlPanelsContainer />
</div>
<div className="col-sm-9">
<ChartContainer />
</div>
</div>
</div>
);
const propTypes = {
data: PropTypes.shape({
viz: PropTypes.object.isRequired,
}).isRequired,
};
export default ExploreViewContainer;
export default class ExploreViewContainer extends React.Component {
constructor(props) {
super(props);
this.state = {
height: this.getHeight(),
};
}
getHeight() {
const navHeight = 90;
return `${window.innerHeight - navHeight}px`;
}
render() {
return (
<div
className="container-fluid"
style={{
height: this.state.height,
overflow: 'hidden',
}}
>
<div className="row table-body">
<div className="table-cell col-sm-4">
<QueryAndSaveButtons
canAdd="True"
onQuery={() => {}}
/>
<br /><br />
<ControlPanelsContainer />
</div>
<div className="table-cell col-sm-8">
<ChartContainer
viz={this.props.data.viz}
height={this.state.height}
/>
</div>
</div>
</div>
);
}
}
ExploreViewContainer.propTypes = propTypes;

View File

@ -0,0 +1,31 @@
import React, { PropTypes } from 'react';
import classnames from 'classnames';
const propTypes = {
canAdd: PropTypes.string.isRequired,
onQuery: PropTypes.func.isRequired,
};
export default function QueryAndSaveBtns({ canAdd, onQuery }) {
const saveClasses = classnames('btn btn-default btn-sm', {
'disabled disabledButton': canAdd !== 'True',
});
return (
<div className="btn-group query-and-save">
<button type="button" className="btn btn-primary btn-sm" onClick={onQuery}>
<i className="fa fa-bolt"></i> Query
</button>
<button
type="button"
className={saveClasses}
data-target="#save_modal"
data-toggle="modal"
>
<i className="fa fa-plus-circle"></i> Save as
</button>
</div>
);
}
QueryAndSaveBtns.propTypes = propTypes;

View File

@ -0,0 +1,21 @@
import React, { PropTypes } from 'react';
import LegendItem from './LegendItem';
const propTypes = {
data: PropTypes.array.isRequired,
keysToColorsMap: PropTypes.object.isRequired,
};
export default function Legend({ data, keysToColorsMap }) {
const legendEls = data.map((d) => {
const color = keysToColorsMap[d.key] ? keysToColorsMap[d.key] : '#000';
return <LegendItem label={d.key} color={color} key={d.key} />;
});
return (
<ul className="list-unstyled list-inline">
{legendEls}
</ul>
);
}
Legend.propTypes = propTypes;

View File

@ -0,0 +1,17 @@
import React, { PropTypes } from 'react';
const propTypes = {
label: PropTypes.string.isRequired,
color: PropTypes.string.isRequired,
};
export default function LegendItem({ label, color }) {
return (
<li style={{ float: 'left' }} key={label}>
<i className="fa fa-circle" style={{ color }} /> &nbsp;&nbsp;
<span>{label}</span>
</li>
);
}
LegendItem.propTypes = propTypes;

View File

@ -0,0 +1,70 @@
import React, { PropTypes } from 'react';
import * as V from 'victory';
import theme from '../../../components/VictoryTheme';
import moment from 'moment';
import { schemeCategory20c } from 'd3-scale';
import Legend from './Legend';
const propTypes = {
data: PropTypes.array.isRequired,
label1: PropTypes.string.isRequired,
};
export default class TimeSeriesLineChart extends React.Component {
constructor(props) {
super(props);
this.keysToColorsMap = this.mapKeysToColors(props.data);
}
mapKeysToColors(data) {
// todo: what if there are more lines than colors in schemeCategory20c?
const keysToColorsMap = {};
data.forEach((d, i) => {
keysToColorsMap[d.key] = schemeCategory20c[i];
});
return keysToColorsMap;
}
renderLines() {
return this.props.data.map(function (d) {
return (
<V.VictoryLine
key={d.key}
data={d.values}
interpolation="cardinal"
style={{ data: { stroke: this.keysToColorsMap[d.key] } }}
/>
);
});
}
render() {
return (
<div style={{ height: '80%', width: '100%' }}>
<V.VictoryChart
theme={theme}
>
{this.renderLines()}
<V.VictoryAxis
label={this.props.label1}
orientation="left"
/>
<V.VictoryAxis
dependentAxis
label={'label needed'}
orientation="bottom"
tickValues={this.props.data[0].values.map((d) => d.x)}
tickFormat={(x) => moment(new Date(x)).format('YYYY')}
fixLabelOverlap
/>
</V.VictoryChart>
<Legend
data={this.props.data}
keysToColorsMap={this.keysToColorsMap}
/>
</div>
);
}
}
TimeSeriesLineChart.propTypes = propTypes;

View File

@ -1,7 +1,6 @@
import React from 'react';
import ReactDOM from 'react-dom';
import ExploreViewContainer from './components/ExploreViewContainer';
import { createStore, applyMiddleware, compose } from 'redux';
import { Provider } from 'react-redux';
import thunk from 'redux-thunk';

View File

@ -48,6 +48,7 @@
"d3": "^3.5.14",
"d3-cloud": "^1.2.1",
"d3-sankey": "^0.2.1",
"d3-scale": "^1.0.3",
"d3-tip": "^0.6.7",
"datamaps": "^0.4.4",
"datatables-bootstrap3-plugin": "^0.4.0",
@ -84,6 +85,7 @@
"style-loader": "^0.13.0",
"supercluster": "https://github.com/georgeke/supercluster/tarball/ac3492737e7ce98e07af679623aad452373bbc40",
"topojson": "^1.6.22",
"victory": "^0.12.1",
"viewport-mercator-project": "^2.1.0"
},
"devDependencies": {

View File

@ -0,0 +1,8 @@
.table-body {
display: table;
}
.table-cell {
float: none;
display: table-cell;
}

View File

@ -1,4 +1,5 @@
{% extends "caravel/basic.html" %}
<link rel="stylesheet" type="text/css" href="/static/assets/stylesheets/exploreV2/exploreV2.css" />
{% block body %}
<div