-
Notifications
You must be signed in to change notification settings - Fork 14.4k
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
Changes from all commits
3901804
442dc25
ce9287c
2537a82
5098402
86d6e62
a6c4ce4
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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); | ||
} |
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; |
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, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. any way to write this without bitwise operators for readability? like There was a problem hiding this comment. Choose a reason for hiding this commentThe 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; |
This file was deleted.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
💯