From 3901804a5602851cad948a53d9b820ab6e55bf6b Mon Sep 17 00:00:00 2001 From: Krist Wongsuphasawat Date: Tue, 21 Aug 2018 12:59:39 -0700 Subject: [PATCH 1/7] Migrate horizon chart to react --- .../assets/src/visualizations/HorizonRow.jsx | 185 +++++++ .../assets/src/visualizations/horizon.css | 25 +- superset/assets/src/visualizations/horizon.js | 522 ++++++++++-------- 3 files changed, 504 insertions(+), 228 deletions(-) create mode 100644 superset/assets/src/visualizations/HorizonRow.jsx diff --git a/superset/assets/src/visualizations/HorizonRow.jsx b/superset/assets/src/visualizations/HorizonRow.jsx new file mode 100644 index 0000000000000..af17bc3699b30 --- /dev/null +++ b/superset/assets/src/visualizations/HorizonRow.jsx @@ -0,0 +1,185 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import d3 from 'd3'; +import './horizon.css'; + +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.array.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, + 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] })) + : rawData; + + const context = this.canvas.getContext('2d'); + context.imageSmoothingEnabled = false; + + // 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]); + + // x = d3.scaleTime().domain[]; + + context.clearRect(0, 0, width, height); + 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))); + + console.log('data', data); + + // skip drawing if there's no data to be drawn + if (startIndex > data.length) { + return; + } + + // we are drawing positive & negative bands separately to avoid mutating canvas state + // http://www.html5rocks.com/en/tutorials/canvas/performance/ + let negative = 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) { + negative = true; + continue; + } + if (value === undefined) { + continue; + } + context.fillRect( + offsetX + i * step, + y(value), + step + 1, + y(0) - y(value), + ); + } + } + + // draw negative bands + if (negative) { + // 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 ( +
+ {title} + { this.canvas = c; }} + /> +
+ ); + } +} + +HorizonRow.propTypes = propTypes; +HorizonRow.defaultProps = defaultProps; + +export default HorizonRow; diff --git a/superset/assets/src/visualizations/horizon.css b/superset/assets/src/visualizations/horizon.css index 013b3e02bd060..4f31ce72a9c1e 100644 --- a/superset/assets/src/visualizations/horizon.css +++ b/superset/assets/src/visualizations/horizon.css @@ -1,17 +1,18 @@ -.horizon .slice_container div.horizon { - border-bottom: solid 1px #444; - border-top: 0px; - padding: 0px; - margin: 0px; +.horizon-chart { + overflow: auto; } -.horizon span { - left: 5; - position: absolute; - color: black; - text-shadow: 1px 1px rgba(255, 255, 255, 0.75); +.horizon-chart .horizon-row { + border-bottom: solid 1px #ddd; + border-top: 0px; + padding: 0px; + margin: 0px; } -.horizon .slice_container { - overflow: auto; +.horizon-row span { + position: absolute; + color: #333; + font-size: 0.8em; + text-shadow: 1px 1px rgba(255, 255, 255, 0.75); } + diff --git a/superset/assets/src/visualizations/horizon.js b/superset/assets/src/visualizations/horizon.js index b676b95446176..127bf1189d976 100644 --- a/superset/assets/src/visualizations/horizon.js +++ b/superset/assets/src/visualizations/horizon.js @@ -1,227 +1,317 @@ /* eslint-disable prefer-rest-params, no-param-reassign */ // Copied and modified from // https://github.com/kmandov/d3-horizon-chart +import React from 'react'; +import ReactDOM from 'react-dom'; +import PropTypes from 'prop-types'; import d3 from 'd3'; -import './horizon.css'; - -const horizonChart = function () { - let colors = [ - '#313695', - '#4575b4', - '#74add1', - '#abd9e9', - '#fee090', - '#fdae61', - '#f46d43', - '#d73027', - ]; - let height = 30; - const y = d3.scale.linear().range([0, height]); - let bands = colors.length >> 1; // number of bands in each direction (positive / negative) - let width = 1000; - let offsetX = 0; - let spacing = 0; - let mode = 'offset'; - let axis; - let title; - let extent; // the extent is derived from the data, unless explicitly set via .extent([min, max]) - let x; - let canvas; - - function my(data) { - const horizon = d3.select(this); - const step = width / data.length; - - horizon.append('span') - .attr('class', 'title') - .text(title); - - horizon.append('span') - .attr('class', 'value'); - - canvas = horizon.append('canvas'); - - canvas - .attr('width', width) - .attr('height', height); - - const context = canvas.node().getContext('2d'); - context.imageSmoothingEnabled = false; - - // update the y scale, based on the data extents - const ext = extent || d3.extent(data, d => d.y); - - const max = Math.max(-ext[0], ext[1]); - y.domain([0, max]); - - // x = d3.scaleTime().domain[]; - axis = d3.svg.axis(x).ticks(5); - - context.clearRect(0, 0, width, height); - // context.translate(0.5, 0.5); - - // 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; - } +import HorizonRow, { DEFAULT_COLORS } from './HorizonRow'; + +// const horizonChart = function () { +// let colors = [ +// '#313695', +// '#4575b4', +// '#74add1', +// '#abd9e9', +// '#fee090', +// '#fdae61', +// '#f46d43', +// '#d73027', +// ]; +// let height = 30; +// const y = d3.scale.linear().range([0, height]); +// let bands = colors.length >> 1; // number of bands in each direction (positive / negative) +// let width = 1000; +// let offsetX = 0; +// let spacing = 0; +// let mode = 'offset'; +// let title; +// let extent; // the extent is derived from the data, unless explicitly set via .extent([min, max]) +// let canvas; + +// function my(data) { +// const horizon = d3.select(this); +// const step = width / data.length; + +// horizon.append('span') +// .attr('class', 'title') +// .text(title); + +// horizon.append('span') +// .attr('class', 'value'); + +// canvas = horizon.append('canvas'); + +// canvas +// .attr('width', width) +// .attr('height', height); + +// const context = canvas.node().getContext('2d'); +// context.imageSmoothingEnabled = false; + +// // update the y scale, based on the data extents +// const ext = extent || d3.extent(data, d => d.y); + +// const max = Math.max(-ext[0], ext[1]); +// y.domain([0, max]); + +// // x = d3.scaleTime().domain[]; + +// context.clearRect(0, 0, width, height); +// context.translate(0.5, 0.5); + +// // 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; +// } + +// // we are drawing positive & negative bands separately to avoid mutating canvas state +// // http://www.html5rocks.com/en/tutorials/canvas/performance/ +// let negative = 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) { negative = true; continue; } +// if (value === undefined) { +// continue; +// } +// context.fillRect(offsetX + i * step, y(value), step + 1, y(0) - y(value)); +// } +// } + +// // draw negative bands +// if (negative) { +// // 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)); +// } +// } +// } +// } + +// my.axis = function (_) { +// if (!arguments.length) { return axis; } +// axis = _; +// return my; +// }; + +// my.title = function (_) { +// if (!arguments.length) { return title; } +// title = _; +// return my; +// }; + +// my.canvas = function (_) { +// if (!arguments.length) { return canvas; } +// canvas = _; +// return my; +// }; + +// // Array of colors representing the number of bands +// my.colors = function (_) { +// if (!arguments.length) { +// return colors; +// } +// colors = _; + +// // update the number of bands +// bands = colors.length >> 1; +// return my; +// }; + +// my.height = function (_) { +// if (!arguments.length) { return height; } +// height = _; +// return my; +// }; + +// my.width = function (_) { +// if (!arguments.length) { return width; } +// width = _; +// return my; +// }; + +// my.spacing = function (_) { +// if (!arguments.length) { return spacing; } +// spacing = _; +// return my; +// }; + +// // mirror or offset +// my.mode = function (_) { +// if (!arguments.length) { return mode; } +// mode = _; +// return my; +// }; + +// my.extent = function (_) { +// if (!arguments.length) { return extent; } +// extent = _; +// return my; +// }; + +// my.offsetX = function (_) { +// if (!arguments.length) { return offsetX; } +// offsetX = _; +// return my; +// }; + +// return my; +// }; + +const propTypes = { + className: PropTypes.string, + width: PropTypes.number, + seriesHeight: PropTypes.number, + data: PropTypes.array.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: DEFAULT_COLORS.length >> 1, + colors: DEFAULT_COLORS, + colorScale: 'series', + mode: 'offset', + offsetX: 0, +}; - // we are drawing positive & negative bands separately to avoid mutating canvas state - // http://www.html5rocks.com/en/tutorials/canvas/performance/ - let negative = 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) { negative = true; continue; } - if (value === undefined) { - continue; - } - context.fillRect(offsetX + i * step, y(value), step + 1, y(0) - y(value)); - } +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); } - // draw negative bands - if (negative) { - // 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)); - } - } - } + return ( +
+ {data.map(row => ( + + ))} +
+ ); } +} - my.axis = function (_) { - if (!arguments.length) { return axis; } - axis = _; - return my; - }; - - my.title = function (_) { - if (!arguments.length) { return title; } - title = _; - return my; - }; - - my.canvas = function (_) { - if (!arguments.length) { return canvas; } - canvas = _; - return my; - }; - - // Array of colors representing the number of bands - my.colors = function (_) { - if (!arguments.length) { - return colors; - } - colors = _; - - // update the number of bands - bands = colors.length >> 1; - return my; - }; - - my.height = function (_) { - if (!arguments.length) { return height; } - height = _; - return my; - }; - - my.width = function (_) { - if (!arguments.length) { return width; } - width = _; - return my; - }; - - my.spacing = function (_) { - if (!arguments.length) { return spacing; } - spacing = _; - return my; - }; - - // mirror or offset - my.mode = function (_) { - if (!arguments.length) { return mode; } - mode = _; - return my; - }; - - my.extent = function (_) { - if (!arguments.length) { return extent; } - extent = _; - return my; - }; - - my.offsetX = function (_) { - if (!arguments.length) { return offsetX; } - offsetX = _; - return my; - }; - - return my; -}; - -function horizonViz(slice, payload) { - const fd = slice.formData; - const div = d3.select(slice.selector); - div.selectAll('*').remove(); - let extent; - if (fd.horizon_color_scale === 'overall') { - let allValues = []; - payload.data.forEach(function (d) { - allValues = allValues.concat(d.values); - }); - extent = d3.extent(allValues, d => d.y); - } else if (fd.horizon_color_scale === 'change') { - payload.data.forEach(function (series) { - const t0y = series.values[0].y; // value at time 0 - series.values = series.values.map(d => - Object.assign({}, d, { y: d.y - t0y }), - ); - }); - } - div.selectAll('.horizon') - .data(payload.data) - .enter() - .append('div') - .attr('class', 'horizon') - .each(function (d, i) { - horizonChart() - .height(fd.series_height) - .width(slice.width()) - .extent(extent) - .title(d.key) - .call(this, d.values, i); - }); +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( + , + element, + ); + + // console.log('payload.data', payload.data); + // return; + + // const div = d3.select(element); + // div.selectAll('*').remove(); + // let extent; + // if (colorScale === 'overall') { + // let allValues = []; + // payload.data.forEach(function (d) { + // allValues = allValues.concat(d.values); + // }); + // extent = d3.extent(allValues, d => d.y); + // } else if (colorScale === 'change') { + // payload.data.forEach(function (series) { + // const t0y = series.values[0].y; // value at time 0 + // series.values = series.values.map(d => + // Object.assign({}, d, { y: d.y - t0y }), + // ); + // }); + // } + // div.selectAll('.horizon') + // .data(payload.data) + // .enter() + // .append('div') + // .attr('class', 'horizon') + // .each(function (d, i) { + // horizonChart() + // .height(seriesHeight) + // .width(slice.width()) + // .extent(extent) + // .title(d.key) + // .call(this, d.values, i); + // }); } -module.exports = horizonViz; +export default adaptor; From 442dc25e2d8914e6630bcff24f03e10d9c3cc450 Mon Sep 17 00:00:00 2001 From: Krist Wongsuphasawat Date: Tue, 21 Aug 2018 13:01:11 -0700 Subject: [PATCH 2/7] remove unused code --- superset/assets/src/visualizations/horizon.js | 218 +----------------- 1 file changed, 1 insertion(+), 217 deletions(-) diff --git a/superset/assets/src/visualizations/horizon.js b/superset/assets/src/visualizations/horizon.js index 127bf1189d976..b17e1ef16a402 100644 --- a/superset/assets/src/visualizations/horizon.js +++ b/superset/assets/src/visualizations/horizon.js @@ -6,189 +6,7 @@ import ReactDOM from 'react-dom'; import PropTypes from 'prop-types'; import d3 from 'd3'; import HorizonRow, { DEFAULT_COLORS } from './HorizonRow'; - -// const horizonChart = function () { -// let colors = [ -// '#313695', -// '#4575b4', -// '#74add1', -// '#abd9e9', -// '#fee090', -// '#fdae61', -// '#f46d43', -// '#d73027', -// ]; -// let height = 30; -// const y = d3.scale.linear().range([0, height]); -// let bands = colors.length >> 1; // number of bands in each direction (positive / negative) -// let width = 1000; -// let offsetX = 0; -// let spacing = 0; -// let mode = 'offset'; -// let title; -// let extent; // the extent is derived from the data, unless explicitly set via .extent([min, max]) -// let canvas; - -// function my(data) { -// const horizon = d3.select(this); -// const step = width / data.length; - -// horizon.append('span') -// .attr('class', 'title') -// .text(title); - -// horizon.append('span') -// .attr('class', 'value'); - -// canvas = horizon.append('canvas'); - -// canvas -// .attr('width', width) -// .attr('height', height); - -// const context = canvas.node().getContext('2d'); -// context.imageSmoothingEnabled = false; - -// // update the y scale, based on the data extents -// const ext = extent || d3.extent(data, d => d.y); - -// const max = Math.max(-ext[0], ext[1]); -// y.domain([0, max]); - -// // x = d3.scaleTime().domain[]; - -// context.clearRect(0, 0, width, height); -// context.translate(0.5, 0.5); - -// // 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; -// } - -// // we are drawing positive & negative bands separately to avoid mutating canvas state -// // http://www.html5rocks.com/en/tutorials/canvas/performance/ -// let negative = 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) { negative = true; continue; } -// if (value === undefined) { -// continue; -// } -// context.fillRect(offsetX + i * step, y(value), step + 1, y(0) - y(value)); -// } -// } - -// // draw negative bands -// if (negative) { -// // 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)); -// } -// } -// } -// } - -// my.axis = function (_) { -// if (!arguments.length) { return axis; } -// axis = _; -// return my; -// }; - -// my.title = function (_) { -// if (!arguments.length) { return title; } -// title = _; -// return my; -// }; - -// my.canvas = function (_) { -// if (!arguments.length) { return canvas; } -// canvas = _; -// return my; -// }; - -// // Array of colors representing the number of bands -// my.colors = function (_) { -// if (!arguments.length) { -// return colors; -// } -// colors = _; - -// // update the number of bands -// bands = colors.length >> 1; -// return my; -// }; - -// my.height = function (_) { -// if (!arguments.length) { return height; } -// height = _; -// return my; -// }; - -// my.width = function (_) { -// if (!arguments.length) { return width; } -// width = _; -// return my; -// }; - -// my.spacing = function (_) { -// if (!arguments.length) { return spacing; } -// spacing = _; -// return my; -// }; - -// // mirror or offset -// my.mode = function (_) { -// if (!arguments.length) { return mode; } -// mode = _; -// return my; -// }; - -// my.extent = function (_) { -// if (!arguments.length) { return extent; } -// extent = _; -// return my; -// }; - -// my.offsetX = function (_) { -// if (!arguments.length) { return offsetX; } -// offsetX = _; -// return my; -// }; - -// return my; -// }; +import './horizon.css'; const propTypes = { className: PropTypes.string, @@ -278,40 +96,6 @@ function adaptor(slice, payload) { />, element, ); - - // console.log('payload.data', payload.data); - // return; - - // const div = d3.select(element); - // div.selectAll('*').remove(); - // let extent; - // if (colorScale === 'overall') { - // let allValues = []; - // payload.data.forEach(function (d) { - // allValues = allValues.concat(d.values); - // }); - // extent = d3.extent(allValues, d => d.y); - // } else if (colorScale === 'change') { - // payload.data.forEach(function (series) { - // const t0y = series.values[0].y; // value at time 0 - // series.values = series.values.map(d => - // Object.assign({}, d, { y: d.y - t0y }), - // ); - // }); - // } - // div.selectAll('.horizon') - // .data(payload.data) - // .enter() - // .append('div') - // .attr('class', 'horizon') - // .each(function (d, i) { - // horizonChart() - // .height(seriesHeight) - // .width(slice.width()) - // .extent(extent) - // .title(d.key) - // .call(this, d.values, i); - // }); } export default adaptor; From ce9287cab95237e8342d30e4b696e41b85d27e2a Mon Sep 17 00:00:00 2001 From: Krist Wongsuphasawat Date: Tue, 21 Aug 2018 13:05:00 -0700 Subject: [PATCH 3/7] rename files --- .../src/visualizations/{horizon.css => HorizonChart.css} | 1 - .../src/visualizations/{horizon.js => HorizonChart.jsx} | 5 +---- superset/assets/src/visualizations/HorizonRow.jsx | 1 - superset/assets/src/visualizations/index.js | 2 +- 4 files changed, 2 insertions(+), 7 deletions(-) rename superset/assets/src/visualizations/{horizon.css => HorizonChart.css} (99%) rename superset/assets/src/visualizations/{horizon.js => HorizonChart.jsx} (93%) diff --git a/superset/assets/src/visualizations/horizon.css b/superset/assets/src/visualizations/HorizonChart.css similarity index 99% rename from superset/assets/src/visualizations/horizon.css rename to superset/assets/src/visualizations/HorizonChart.css index 4f31ce72a9c1e..3b78fdd8ac0cc 100644 --- a/superset/assets/src/visualizations/horizon.css +++ b/superset/assets/src/visualizations/HorizonChart.css @@ -15,4 +15,3 @@ font-size: 0.8em; text-shadow: 1px 1px rgba(255, 255, 255, 0.75); } - diff --git a/superset/assets/src/visualizations/horizon.js b/superset/assets/src/visualizations/HorizonChart.jsx similarity index 93% rename from superset/assets/src/visualizations/horizon.js rename to superset/assets/src/visualizations/HorizonChart.jsx index b17e1ef16a402..17f7c09279c4e 100644 --- a/superset/assets/src/visualizations/horizon.js +++ b/superset/assets/src/visualizations/HorizonChart.jsx @@ -1,12 +1,9 @@ -/* eslint-disable prefer-rest-params, no-param-reassign */ -// Copied and modified from -// https://github.com/kmandov/d3-horizon-chart 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 './horizon.css'; +import './HorizonChart.css'; const propTypes = { className: PropTypes.string, diff --git a/superset/assets/src/visualizations/HorizonRow.jsx b/superset/assets/src/visualizations/HorizonRow.jsx index af17bc3699b30..e67c2143cfa69 100644 --- a/superset/assets/src/visualizations/HorizonRow.jsx +++ b/superset/assets/src/visualizations/HorizonRow.jsx @@ -1,7 +1,6 @@ import React from 'react'; import PropTypes from 'prop-types'; import d3 from 'd3'; -import './horizon.css'; export const DEFAULT_COLORS = [ '#313695', diff --git a/superset/assets/src/visualizations/index.js b/superset/assets/src/visualizations/index.js index 098079ea500dd..df24b67158128 100644 --- a/superset/assets/src/visualizations/index.js +++ b/superset/assets/src/visualizations/index.js @@ -83,7 +83,7 @@ const vizMap = { [VIZ_TYPES.heatmap]: () => loadVis(import(/* webpackChunkName: "heatmap" */ './heatmap.js')), [VIZ_TYPES.histogram]: () => loadVis(import(/* webpackChunkName: "histogram" */ './histogram.js')), - [VIZ_TYPES.horizon]: () => loadVis(import(/* webpackChunkName: "horizon" */ './horizon.js')), + [VIZ_TYPES.horizon]: () => loadVis(import(/* webpackChunkName: "horizon" */ './HorizonChart.jsx')), [VIZ_TYPES.iframe]: () => loadVis(import(/* webpackChunkName: "iframe" */ './iframe.js')), [VIZ_TYPES.line]: loadNvd3, [VIZ_TYPES.line_multi]: () => From 2537a8220e0c59387573cb1fc2ce0bb9982d4517 Mon Sep 17 00:00:00 2001 From: Krist Wongsuphasawat Date: Tue, 21 Aug 2018 13:09:17 -0700 Subject: [PATCH 4/7] update props --- .../src/visualizations/HorizonChart.jsx | 7 ++++++- .../assets/src/visualizations/HorizonRow.jsx | 21 ++++++++----------- 2 files changed, 15 insertions(+), 13 deletions(-) diff --git a/superset/assets/src/visualizations/HorizonChart.jsx b/superset/assets/src/visualizations/HorizonChart.jsx index 17f7c09279c4e..2714c4f721d1a 100644 --- a/superset/assets/src/visualizations/HorizonChart.jsx +++ b/superset/assets/src/visualizations/HorizonChart.jsx @@ -9,7 +9,12 @@ const propTypes = { className: PropTypes.string, width: PropTypes.number, seriesHeight: PropTypes.number, - data: PropTypes.array.isRequired, + 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), diff --git a/superset/assets/src/visualizations/HorizonRow.jsx b/superset/assets/src/visualizations/HorizonRow.jsx index e67c2143cfa69..bff9960a20d97 100644 --- a/superset/assets/src/visualizations/HorizonRow.jsx +++ b/superset/assets/src/visualizations/HorizonRow.jsx @@ -17,7 +17,9 @@ const propTypes = { className: PropTypes.string, width: PropTypes.number, height: PropTypes.number, - data: PropTypes.array.isRequired, + data: PropTypes.arrayOf(PropTypes.shape({ + y: PropTypes.number, + })).isRequired, bands: PropTypes.number, colors: PropTypes.arrayOf(PropTypes.string), colorScale: PropTypes.string, @@ -73,15 +75,6 @@ class HorizonRow extends React.PureComponent { const context = this.canvas.getContext('2d'); context.imageSmoothingEnabled = false; - - // 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]); - - // x = d3.scaleTime().domain[]; - context.clearRect(0, 0, width, height); context.translate(0.5, 0.5); @@ -90,13 +83,17 @@ class HorizonRow extends React.PureComponent { const startIndex = Math.floor(Math.max(0, -(offsetX / step))); const endIndex = Math.floor(Math.min(data.length, startIndex + (width / step))); - console.log('data', data); - // 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 negative = false; From 50984029cb75fc66ded7913544d624bc9a295113 Mon Sep 17 00:00:00 2001 From: Krist Wongsuphasawat Date: Tue, 21 Aug 2018 13:13:02 -0700 Subject: [PATCH 5/7] enable renderTrigger --- superset/assets/src/explore/controls.jsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/superset/assets/src/explore/controls.jsx b/superset/assets/src/explore/controls.jsx index 68fcb05c29010..0d84205e3e261 100644 --- a/superset/assets/src/explore/controls.jsx +++ b/superset/assets/src/explore/controls.jsx @@ -383,6 +383,7 @@ export const controls = { horizon_color_scale: { type: 'SelectControl', + renderTrigger: true, label: t('Horizon Color Scale'), choices: [ ['series', 'series'], @@ -1205,6 +1206,7 @@ export const controls = { series_height: { type: 'SelectControl', + renderTrigger: true, freeForm: true, label: t('Series Height'), default: '25', From 86d6e6282509ee02789be047bff1f69503a4d1d7 Mon Sep 17 00:00:00 2001 From: Krist Wongsuphasawat Date: Tue, 21 Aug 2018 13:32:19 -0700 Subject: [PATCH 6/7] fix canvas transform issue --- superset/assets/src/explore/controls.jsx | 4 +-- .../assets/src/visualizations/HorizonRow.jsx | 25 ++++++++++--------- 2 files changed, 15 insertions(+), 14 deletions(-) diff --git a/superset/assets/src/explore/controls.jsx b/superset/assets/src/explore/controls.jsx index 0d84205e3e261..d2be8b02b3857 100644 --- a/superset/assets/src/explore/controls.jsx +++ b/superset/assets/src/explore/controls.jsx @@ -384,14 +384,14 @@ export const controls = { horizon_color_scale: { type: 'SelectControl', renderTrigger: true, - label: t('Horizon Color Scale'), + 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'), }, canvas_image_rendering: { diff --git a/superset/assets/src/visualizations/HorizonRow.jsx b/superset/assets/src/visualizations/HorizonRow.jsx index bff9960a20d97..fd96ad5f800f2 100644 --- a/superset/assets/src/visualizations/HorizonRow.jsx +++ b/superset/assets/src/visualizations/HorizonRow.jsx @@ -70,12 +70,14 @@ class HorizonRow extends React.PureComponent { } = this.props; const data = colorScale === 'change' - ? rawData.map(d => ({ ...d, y: d.y - rawData[0] })) + ? 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; @@ -96,7 +98,7 @@ class HorizonRow extends React.PureComponent { // we are drawing positive & negative bands separately to avoid mutating canvas state // http://www.html5rocks.com/en/tutorials/canvas/performance/ - let negative = false; + let hasNegative = false; // draw positive bands let value; let bExtents; @@ -111,23 +113,22 @@ class HorizonRow extends React.PureComponent { for (let i = startIndex; i < endIndex; i++) { value = data[i].y; if (value <= 0) { - negative = true; + hasNegative = true; continue; } - if (value === undefined) { - continue; + if (value !== undefined) { + context.fillRect( + offsetX + i * step, + y(value), + step + 1, + y(0) - y(value), + ); } - context.fillRect( - offsetX + i * step, - y(value), - step + 1, - y(0) - y(value), - ); } } // draw negative bands - if (negative) { + if (hasNegative) { // mirror the negative bands, by flipping the canvas if (mode === 'offset') { context.translate(0, height); From a6c4ce4ba7e7cc5fe7b0f3ad38e314941d880448 Mon Sep 17 00:00:00 2001 From: Krist Wongsuphasawat Date: Thu, 23 Aug 2018 16:35:10 -0700 Subject: [PATCH 7/7] Address Chris' comment --- superset/assets/src/visualizations/HorizonChart.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/superset/assets/src/visualizations/HorizonChart.jsx b/superset/assets/src/visualizations/HorizonChart.jsx index 2714c4f721d1a..c17e98266af68 100644 --- a/superset/assets/src/visualizations/HorizonChart.jsx +++ b/superset/assets/src/visualizations/HorizonChart.jsx @@ -26,7 +26,7 @@ const defaultProps = { className: '', width: 800, seriesHeight: 20, - bands: DEFAULT_COLORS.length >> 1, + bands: Math.floor(DEFAULT_COLORS.length / 2), colors: DEFAULT_COLORS, colorScale: 'series', mode: 'offset',