mirror of
https://github.com/apache/superset.git
synced 2024-09-17 11:09:47 -04:00
[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
173
caravel/assets/javascripts/components/VictoryTheme.js
Normal file
173
caravel/assets/javascripts/components/VictoryTheme.js
Normal 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;
|
@ -1,11 +1,67 @@
|
|||||||
import React from 'react';
|
import React, { PropTypes } from 'react';
|
||||||
import { Panel } from 'react-bootstrap';
|
import { Panel } from 'react-bootstrap';
|
||||||
|
import TimeSeriesLineChart from './charts/TimeSeriesLineChart';
|
||||||
|
import moment from 'moment';
|
||||||
|
|
||||||
const ChartContainer = function () {
|
const propTypes = {
|
||||||
return (
|
viz: PropTypes.shape({
|
||||||
<Panel header="Chart title">
|
data: PropTypes.object.isRequired,
|
||||||
chart goes here
|
form_data: PropTypes.shape({
|
||||||
</Panel>
|
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 ChartContainer from './ChartContainer';
|
||||||
import ControlPanelsContainer from './ControlPanelsContainer';
|
import ControlPanelsContainer from './ControlPanelsContainer';
|
||||||
import QueryAndSaveButtons from './QueryAndSaveButtons';
|
import QueryAndSaveButtons from './QueryAndSaveButtons';
|
||||||
|
|
||||||
const ExploreViewContainer = function () {
|
const propTypes = {
|
||||||
|
data: PropTypes.shape({
|
||||||
|
viz: PropTypes.object.isRequired,
|
||||||
|
}).isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
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 (
|
return (
|
||||||
<div className="container-fluid">
|
<div
|
||||||
<div className="row">
|
className="container-fluid"
|
||||||
<div className="col-sm-3">
|
style={{
|
||||||
|
height: this.state.height,
|
||||||
|
overflow: 'hidden',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="row table-body">
|
||||||
|
<div className="table-cell col-sm-4">
|
||||||
<QueryAndSaveButtons
|
<QueryAndSaveButtons
|
||||||
canAdd="True"
|
canAdd="True"
|
||||||
onQuery={() => { console.log('clicked query'); }}
|
onQuery={() => {}}
|
||||||
/>
|
/>
|
||||||
<br /><br />
|
<br /><br />
|
||||||
<ControlPanelsContainer />
|
<ControlPanelsContainer />
|
||||||
</div>
|
</div>
|
||||||
<div className="col-sm-9">
|
<div className="table-cell col-sm-8">
|
||||||
<ChartContainer />
|
<ChartContainer
|
||||||
|
viz={this.props.data.viz}
|
||||||
|
height={this.state.height}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export default ExploreViewContainer;
|
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 React from 'react';
|
||||||
import ReactDOM from 'react-dom';
|
import ReactDOM from 'react-dom';
|
||||||
import ExploreViewContainer from './components/ExploreViewContainer';
|
import ExploreViewContainer from './components/ExploreViewContainer';
|
||||||
|
|
||||||
import { createStore, applyMiddleware, compose } from 'redux';
|
import { createStore, applyMiddleware, compose } from 'redux';
|
||||||
import { Provider } from 'react-redux';
|
import { Provider } from 'react-redux';
|
||||||
import thunk from 'redux-thunk';
|
import thunk from 'redux-thunk';
|
||||||
|
@ -48,6 +48,7 @@
|
|||||||
"d3": "^3.5.14",
|
"d3": "^3.5.14",
|
||||||
"d3-cloud": "^1.2.1",
|
"d3-cloud": "^1.2.1",
|
||||||
"d3-sankey": "^0.2.1",
|
"d3-sankey": "^0.2.1",
|
||||||
|
"d3-scale": "^1.0.3",
|
||||||
"d3-tip": "^0.6.7",
|
"d3-tip": "^0.6.7",
|
||||||
"datamaps": "^0.4.4",
|
"datamaps": "^0.4.4",
|
||||||
"datatables-bootstrap3-plugin": "^0.4.0",
|
"datatables-bootstrap3-plugin": "^0.4.0",
|
||||||
@ -84,6 +85,7 @@
|
|||||||
"style-loader": "^0.13.0",
|
"style-loader": "^0.13.0",
|
||||||
"supercluster": "https://github.com/georgeke/supercluster/tarball/ac3492737e7ce98e07af679623aad452373bbc40",
|
"supercluster": "https://github.com/georgeke/supercluster/tarball/ac3492737e7ce98e07af679623aad452373bbc40",
|
||||||
"topojson": "^1.6.22",
|
"topojson": "^1.6.22",
|
||||||
|
"victory": "^0.12.1",
|
||||||
"viewport-mercator-project": "^2.1.0"
|
"viewport-mercator-project": "^2.1.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
8
caravel/assets/stylesheets/exploreV2/exploreV2.css
Normal file
8
caravel/assets/stylesheets/exploreV2/exploreV2.css
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
.table-body {
|
||||||
|
display: table;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-cell {
|
||||||
|
float: none;
|
||||||
|
display: table-cell;
|
||||||
|
}
|
@ -1,4 +1,5 @@
|
|||||||
{% extends "caravel/basic.html" %}
|
{% extends "caravel/basic.html" %}
|
||||||
|
<link rel="stylesheet" type="text/css" href="/static/assets/stylesheets/exploreV2/exploreV2.css" />
|
||||||
|
|
||||||
{% block body %}
|
{% block body %}
|
||||||
<div
|
<div
|
||||||
|
Loading…
Reference in New Issue
Block a user