Skip to content

Commit

Permalink
[SIP-5] Refactor and improve histogram (#5758)
Browse files Browse the repository at this point in the history
* Extract slice and formData

* indent

* update data proptype

* enable theme

* remove legacy code

* rename file

* Add legend

* Implement WithLegend

* align legend items to the right for bottom position

* add line at end of file

* fix linting issues
  • Loading branch information
kristw authored and williaster committed Sep 5, 2018
1 parent 2811498 commit b461287
Show file tree
Hide file tree
Showing 10 changed files with 1,305 additions and 832 deletions.
4 changes: 4 additions & 0 deletions superset/assets/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,9 +45,13 @@
"homepage": "http://superset.apache.org/",
"dependencies": {
"@data-ui/event-flow": "^0.0.54",
"@data-ui/histogram": "^0.0.64",
"@data-ui/sparkline": "^0.0.54",
"@data-ui/theme": "^0.0.62",
"@data-ui/xy-chart": "^0.0.61",
"@vx/legend": "^0.0.170",
"@vx/responsive": "0.0.172",
"@vx/scale": "^0.0.165",
"babel-register": "^6.24.1",
"bootstrap": "^3.3.6",
"bootstrap-slider": "^10.0.0",
Expand Down
137 changes: 137 additions & 0 deletions superset/assets/src/visualizations/Histogram.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
import PropTypes from 'prop-types';
import React from 'react';
import ReactDOM from 'react-dom';
import { Histogram, BarSeries, XAxis, YAxis } from '@data-ui/histogram';
import { chartTheme } from '@data-ui/theme';
import { LegendOrdinal } from '@vx/legend';
import { scaleOrdinal } from '@vx/scale';
import WithLegend from './WithLegend';
import { getColorFromScheme } from '../modules/colors';

const propTypes = {
className: PropTypes.string,
data: PropTypes.arrayOf(PropTypes.shape({
key: PropTypes.string,
values: PropTypes.arrayOf(PropTypes.number),
})).isRequired,
width: PropTypes.number.isRequired,
height: PropTypes.number.isRequired,
colorScheme: PropTypes.string,
normalized: PropTypes.bool,
binCount: PropTypes.number,
opacity: PropTypes.number,
xAxisLabel: PropTypes.string,
yAxisLabel: PropTypes.string,
};
const defaultProps = {
className: '',
colorScheme: '',
normalized: false,
binCount: 15,
opacity: 1,
xAxisLabel: '',
yAxisLabel: '',
};

class CustomHistogram extends React.PureComponent {
render() {
const {
className,
data,
width,
height,
binCount,
colorScheme,
normalized,
opacity,
xAxisLabel,
yAxisLabel,
} = this.props;

const keys = data.map(d => d.key);
const colorScale = scaleOrdinal({
domain: keys,
range: keys.map(key => getColorFromScheme(key, colorScheme)),
});

return (
<WithLegend
className={`histogram-chart ${className}`}
width={width}
height={height}
position="top"
renderLegend={({ direction }) => (
<LegendOrdinal
scale={colorScale}
direction={direction}
shape="rect"
labelMargin="0 15px 0 0"
/>
)}
renderChart={parent => (
<Histogram
width={parent.width}
height={parent.height}
ariaLabel="Histogram"
normalized={normalized}
binCount={binCount}
binType="numeric"
renderTooltip={({ datum, color }) => (
<div>
<strong style={{ color }}>{datum.bin0} to {datum.bin1}</strong>
<div><strong>count </strong>{datum.count}</div>
<div><strong>cumulative </strong>{datum.cumulative}</div>
</div>
)}
valueAccessor={datum => datum}
theme={chartTheme}
>
{data.map(series => (
<BarSeries
key={series.key}
animated
rawData={series.values}
fill={colorScale(series.key)}
fillOpacity={opacity}
/>
))}
<XAxis label={xAxisLabel} />
<YAxis label={yAxisLabel} />
</Histogram>
)}
/>
);
}
}

CustomHistogram.propTypes = propTypes;
CustomHistogram.defaultProps = defaultProps;

function adaptor(slice, payload) {
const { selector, formData } = slice;
const {
color_scheme: colorScheme,
link_length: binCount,
normalized,
global_opacity: opacity,
x_axis_label: xAxisLabel,
y_axis_label: yAxisLabel,
} = formData;

ReactDOM.render(
<CustomHistogram
data={payload.data}
width={slice.width()}
height={slice.height()}
binCount={parseInt(binCount, 10)}
colorScheme={colorScheme}
normalized={normalized}
opacity={opacity}
xAxisLabel={xAxisLabel}
yAxisLabel={yAxisLabel}
/>,
document.querySelector(selector),
);
}

export default adaptor;
4 changes: 4 additions & 0 deletions superset/assets/src/visualizations/WithLegend.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
.with-legend .legend-container {
padding-top: 5px;
font-size: 0.9em;
}
123 changes: 123 additions & 0 deletions superset/assets/src/visualizations/WithLegend.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
import React from 'react';
import PropTypes from 'prop-types';
import { ParentSize } from '@vx/responsive';
import './WithLegend.css';

const propTypes = {
className: PropTypes.string,
width: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
height: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
renderChart: PropTypes.func.isRequired,
renderLegend: PropTypes.func.isRequired,
position: PropTypes.oneOf(['top', 'left', 'bottom', 'right']),
legendJustifyContent: PropTypes.oneOf(['center', 'flex-start', 'flex-end']),
};
const defaultProps = {
className: '',
width: 'auto',
height: 'auto',
position: 'top',
legendJustifyContent: undefined,
};

const LEGEND_STYLE_BASE = {
display: 'flex',
flexGrow: 0,
flexShrink: 0,
order: -1,
};

const CHART_STYLE_BASE = {
flexGrow: 1,
flexShrink: 1,
flexBasis: 'auto',
position: 'relative',
};

class WithLegend extends React.Component {
getContainerDirection() {
const { position } = this.props;
switch (position) {
case 'left': return 'row';
case 'right': return 'row-reverse';
case 'bottom': return 'column-reverse';
default:
case 'top': return 'column';
}
}

getLegendJustifyContent() {
const { legendJustifyContent, position } = this.props;
if (legendJustifyContent) {
return legendJustifyContent;
}
switch (position) {
case 'left': return 'flex-start';
case 'right': return 'flex-start';
case 'bottom': return 'flex-end';
default:
case 'top': return 'flex-end';
}
}

render() {
const {
className,
width,
height,
position,
renderChart,
renderLegend,
} = this.props;

const isHorizontal = position === 'left' || position === 'right';

const style = {
display: 'flex',
flexDirection: this.getContainerDirection(),
};
if (width) {
style.width = width;
}
if (height) {
style.height = height;
}

const chartStyle = { ...CHART_STYLE_BASE };
if (isHorizontal) {
chartStyle.width = 0;
} else {
chartStyle.height = 0;
}

const legendDirection = isHorizontal ? 'column' : 'row';
const legendStyle = {
...LEGEND_STYLE_BASE,
flexDirection: legendDirection,
justifyContent: this.getLegendJustifyContent(),
};

return (
<div className={`with-legend ${className}`} style={style}>
<div className="legend-container" style={legendStyle}>
{renderLegend({
// Pass flexDirection for @vx/legend to arrange legend items
direction: legendDirection,
})}
</div>
<div className="main-container" style={chartStyle}>
<ParentSize>{parent => (parent.width > 0 && parent.height > 0)
// Only render when necessary
? renderChart(parent)
: null}
</ParentSize>
</div>
</div>
);
}
}

WithLegend.propTypes = propTypes;
WithLegend.defaultProps = defaultProps;

export default WithLegend;
Original file line number Diff line number Diff line change
Expand Up @@ -67,17 +67,6 @@ export default class CategoricalDeckGLContainer extends React.PureComponent {
componentWillReceiveProps(nextProps) {
this.setState(CategoricalDeckGLContainer.getDerivedStateFromProps(nextProps, this.state));
}
addColor(data, fd) {
const c = fd.color_picker || { r: 0, g: 0, b: 0, a: 1 };
return data.map((d) => {
let color;
if (fd.dimension) {
color = hexToRGB(getColorFromScheme(d.cat_color, fd.color_scheme), c.a * 255);
return { ...d, color };
}
return d;
});
}
getLayers(values) {
const { getLayer, payload, slice } = this.props;
const fd = slice.formData;
Expand Down Expand Up @@ -107,6 +96,17 @@ export default class CategoricalDeckGLContainer extends React.PureComponent {
payload.data.features = data;
return [getLayer(fd, payload, slice)];
}
addColor(data, fd) {
const c = fd.color_picker || { r: 0, g: 0, b: 0, a: 1 };
return data.map((d) => {
let color;
if (fd.dimension) {
color = hexToRGB(getColorFromScheme(d.cat_color, fd.color_scheme), c.a * 255);
return { ...d, color };
}
return d;
});
}
toggleCategory(category) {
const categoryState = this.state.categories[category];
categoryState.enabled = !categoryState.enabled;
Expand Down
16 changes: 0 additions & 16 deletions superset/assets/src/visualizations/histogram.css

This file was deleted.

Loading

0 comments on commit b461287

Please sign in to comment.