Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[SIP-5] Repair and refactor Horizon Chart #5690

Merged
merged 7 commits into from
Aug 27, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions superset/assets/src/explore/controls.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -383,14 +383,15 @@ export const controls = {

horizon_color_scale: {
type: 'SelectControl',
label: t('Horizon Color Scale'),
renderTrigger: true,
label: t('Value Domain'),
choices: [
['series', 'series'],
['overall', 'overall'],
['change', 'change'],
],
default: 'series',
description: t('Defines how the color are attributed.'),
description: t('series: Treat each series independently; overall: All series use the same scale; change: Show changes compared to the first data point in each series'),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💯

},

canvas_image_rendering: {
Expand Down Expand Up @@ -1205,6 +1206,7 @@ export const controls = {

series_height: {
type: 'SelectControl',
renderTrigger: true,
freeForm: true,
label: t('Series Height'),
default: '25',
Expand Down
17 changes: 17 additions & 0 deletions superset/assets/src/visualizations/HorizonChart.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
.horizon-chart {
overflow: auto;
}

.horizon-chart .horizon-row {
border-bottom: solid 1px #ddd;
border-top: 0px;
padding: 0px;
margin: 0px;
}

.horizon-row span {
position: absolute;
color: #333;
font-size: 0.8em;
text-shadow: 1px 1px rgba(255, 255, 255, 0.75);
}
103 changes: 103 additions & 0 deletions superset/assets/src/visualizations/HorizonChart.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import React from 'react';
import ReactDOM from 'react-dom';
import PropTypes from 'prop-types';
import d3 from 'd3';
import HorizonRow, { DEFAULT_COLORS } from './HorizonRow';
import './HorizonChart.css';

const propTypes = {
className: PropTypes.string,
width: PropTypes.number,
seriesHeight: PropTypes.number,
data: PropTypes.arrayOf(PropTypes.shape({
key: PropTypes.arrayOf(PropTypes.string),
values: PropTypes.arrayOf(PropTypes.shape({
y: PropTypes.number,
})),
})).isRequired,
// number of bands in each direction (positive / negative)
bands: PropTypes.number,
colors: PropTypes.arrayOf(PropTypes.string),
colorScale: PropTypes.string,
mode: PropTypes.string,
offsetX: PropTypes.number,
};
const defaultProps = {
className: '',
width: 800,
seriesHeight: 20,
bands: Math.floor(DEFAULT_COLORS.length / 2),
colors: DEFAULT_COLORS,
colorScale: 'series',
mode: 'offset',
offsetX: 0,
};

class HorizonChart extends React.PureComponent {
render() {
const {
className,
width,
data,
seriesHeight,
bands,
colors,
colorScale,
mode,
offsetX,
} = this.props;

let yDomain;
if (colorScale === 'overall') {
const allValues = data.reduce(
(acc, current) => acc.concat(current.values),
[],
);
yDomain = d3.extent(allValues, d => d.y);
}

return (
<div className={`horizon-chart ${className}`}>
{data.map(row => (
<HorizonRow
key={row.key}
width={width}
height={seriesHeight}
title={row.key[0]}
data={row.values}
bands={bands}
colors={colors}
colorScale={colorScale}
mode={mode}
offsetX={offsetX}
yDomain={yDomain}
/>
))}
</div>
);
}
}

HorizonChart.propTypes = propTypes;
HorizonChart.defaultProps = defaultProps;

function adaptor(slice, payload) {
const { selector, formData } = slice;
const element = document.querySelector(selector);
const {
horizon_color_scale: colorScale,
series_height: seriesHeight,
} = formData;

ReactDOM.render(
<HorizonChart
data={payload.data}
width={slice.width()}
seriesHeight={parseInt(seriesHeight, 10)}
colorScale={colorScale}
/>,
element,
);
}

export default adaptor;
182 changes: 182 additions & 0 deletions superset/assets/src/visualizations/HorizonRow.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
import React from 'react';
import PropTypes from 'prop-types';
import d3 from 'd3';

export const DEFAULT_COLORS = [
'#313695',
'#4575b4',
'#74add1',
'#abd9e9',
'#fee090',
'#fdae61',
'#f46d43',
'#d73027',
];

const propTypes = {
className: PropTypes.string,
width: PropTypes.number,
height: PropTypes.number,
data: PropTypes.arrayOf(PropTypes.shape({
y: PropTypes.number,
})).isRequired,
bands: PropTypes.number,
colors: PropTypes.arrayOf(PropTypes.string),
colorScale: PropTypes.string,
mode: PropTypes.string,
offsetX: PropTypes.number,
title: PropTypes.string,
yDomain: PropTypes.arrayOf(PropTypes.number),
};

const defaultProps = {
className: '',
width: 800,
height: 20,
bands: DEFAULT_COLORS.length >> 1,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

any way to write this without bitwise operators for readability? like Math.floor(DEFAULT_COLORS.length / 2) or something?

Copy link
Contributor Author

@kristw kristw Aug 23, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed. 👍

colors: DEFAULT_COLORS,
colorScale: 'series',
mode: 'offset',
offsetX: 0,
title: '',
yDomain: undefined,
};

class HorizonRow extends React.PureComponent {
componentDidMount() {
this.drawChart();
}

componentDidUpdate() {
this.drawChart();
}

componentWillUnmount() {
this.canvas = null;
}

drawChart() {
if (this.canvas) {
const {
data: rawData,
yDomain,
width,
height,
bands,
colors,
colorScale,
offsetX,
mode,
} = this.props;

const data = colorScale === 'change'
? rawData.map(d => ({ ...d, y: d.y - rawData[0].y }))
: rawData;

const context = this.canvas.getContext('2d');
context.imageSmoothingEnabled = false;
context.clearRect(0, 0, width, height);
// Reset transform
context.setTransform(1, 0, 0, 1, 0, 0);
context.translate(0.5, 0.5);

const step = width / data.length;
// the data frame currently being shown:
const startIndex = Math.floor(Math.max(0, -(offsetX / step)));
const endIndex = Math.floor(Math.min(data.length, startIndex + (width / step)));

// skip drawing if there's no data to be drawn
if (startIndex > data.length) {
return;
}

// Create y-scale
const [min, max] = yDomain || d3.extent(data, d => d.y);
const y = d3.scale.linear()
.domain([0, Math.max(-min, max)])
.range([0, height]);

// we are drawing positive & negative bands separately to avoid mutating canvas state
// http://www.html5rocks.com/en/tutorials/canvas/performance/
let hasNegative = false;
// draw positive bands
let value;
let bExtents;
for (let b = 0; b < bands; b += 1) {
context.fillStyle = colors[bands + b];

// Adjust the range based on the current band index.
bExtents = (b + 1 - bands) * height;
y.range([bands * height + bExtents, bExtents]);

// only the current data frame is being drawn i.e. what's visible:
for (let i = startIndex; i < endIndex; i++) {
value = data[i].y;
if (value <= 0) {
hasNegative = true;
continue;
}
if (value !== undefined) {
context.fillRect(
offsetX + i * step,
y(value),
step + 1,
y(0) - y(value),
);
}
}
}

// draw negative bands
if (hasNegative) {
// mirror the negative bands, by flipping the canvas
if (mode === 'offset') {
context.translate(0, height);
context.scale(1, -1);
}

for (let b = 0; b < bands; b++) {
context.fillStyle = colors[bands - b - 1];

// Adjust the range based on the current band index.
bExtents = (b + 1 - bands) * height;
y.range([bands * height + bExtents, bExtents]);

// only the current data frame is being drawn i.e. what's visible:
for (let ii = startIndex; ii < endIndex; ii++) {
value = data[ii].y;
if (value >= 0) {
continue;
}
context.fillRect(
offsetX + ii * step,
y(-value),
step + 1,
y(0) - y(-value),
);
}
}
}

}
}

render() {
const { className, title, width, height } = this.props;
return (
<div className={`horizon-row ${className}`}>
<span className="title">{title}</span>
<canvas
width={width}
height={height}
ref={(c) => { this.canvas = c; }}
/>
</div>
);
}
}

HorizonRow.propTypes = propTypes;
HorizonRow.defaultProps = defaultProps;

export default HorizonRow;
17 changes: 0 additions & 17 deletions superset/assets/src/visualizations/horizon.css

This file was deleted.

Loading