mirror of https://github.com/apache/superset.git
[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:
parent
66b498de25
commit
f837733d85
|
@ -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;
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
|
@ -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;
|
|
@ -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 }} />
|
||||
<span>{label}</span>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
LegendItem.propTypes = propTypes;
|
|
@ -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;
|
|
@ -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';
|
||||
|
|
|
@ -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": {
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
.table-body {
|
||||
display: table;
|
||||
}
|
||||
|
||||
.table-cell {
|
||||
float: none;
|
||||
display: table-cell;
|
||||
}
|
|
@ -1,4 +1,5 @@
|
|||
{% extends "caravel/basic.html" %}
|
||||
<link rel="stylesheet" type="text/css" href="/static/assets/stylesheets/exploreV2/exploreV2.css" />
|
||||
|
||||
{% block body %}
|
||||
<div
|
||||
|
|
Loading…
Reference in New Issue