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
ref(ui): svg-based bar charts #8802
Changes from 9 commits
90ec931
3330874
8ada768
e04e8d7
3edc908
dae1ae7
4778688
c8cdbf5
b3fdab6
51bb905
af04721
b8ab38d
a9bbeaa
1cb7422
9d29796
62c7d92
2bca6db
da4aeb5
a9135dd
703c508
2ffb198
ddeb43e
ac7ec91
60d673e
1219f8f
cb8ece8
de6be6b
4c0aa82
593a722
8bc282c
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 |
---|---|---|
@@ -1,17 +1,15 @@ | ||
import React from 'react'; | ||
import createReactClass from 'create-react-class'; | ||
import PropTypes from 'prop-types'; | ||
import moment from 'moment-timezone'; | ||
import _ from 'lodash'; | ||
import styled from 'react-emotion'; | ||
|
||
import Tooltip from 'app/components/tooltip'; | ||
import Count from 'app/components/count'; | ||
import ConfigStore from 'app/stores/configStore'; | ||
|
||
const StackedBarChart = createReactClass({ | ||
displayName: 'StackedBarChart', | ||
|
||
propTypes: { | ||
class StackedBarChart extends React.Component { | ||
static propTypes = { | ||
// TODO(dcramer): DEPRECATED, use series instead | ||
points: PropTypes.arrayOf( | ||
PropTypes.shape({ | ||
|
@@ -42,73 +40,39 @@ const StackedBarChart = createReactClass({ | |
), | ||
tooltip: PropTypes.func, | ||
barClasses: PropTypes.array, | ||
}, | ||
|
||
statics: { | ||
getInterval(series) { | ||
// TODO(dcramer): not guaranteed correct | ||
return series.length && series[0].data.length > 1 | ||
? series[0].data[1].x - series[0].data[0].x | ||
: null; | ||
}, | ||
|
||
pointsToSeries(points) { | ||
let series = []; | ||
points.forEach((p, pIdx) => { | ||
p.y.forEach((y, yIdx) => { | ||
if (!series[yIdx]) { | ||
series[yIdx] = {data: []}; | ||
} | ||
series[yIdx].data.push({x: p.x, y}); | ||
}); | ||
}); | ||
return series; | ||
}, | ||
|
||
pointIndex(series) { | ||
let points = {}; | ||
series.forEach(s => { | ||
s.data.forEach(p => { | ||
if (!points[p.x]) { | ||
points[p.x] = {y: [], x: p.x}; | ||
} | ||
points[p.x].y.push(p.y); | ||
}); | ||
}); | ||
return points; | ||
}, | ||
}, | ||
|
||
getDefaultProps() { | ||
return { | ||
className: 'sparkline', | ||
height: null, | ||
label: '', | ||
points: [], | ||
series: [], | ||
markers: [], | ||
width: null, | ||
barClasses: ['chart-bar'], | ||
}; | ||
}, | ||
}; | ||
|
||
static defaultProps = { | ||
className: 'sparkline', | ||
height: null, | ||
label: '', | ||
points: [], | ||
series: [], | ||
markers: [], | ||
width: null, | ||
barClasses: ['chart-bar'], | ||
}; | ||
|
||
constructor(props) { | ||
super(props); | ||
|
||
getInitialState() { | ||
// massage points | ||
let series = this.props.series; | ||
|
||
if (this.props.points.length) { | ||
if (series.length) { | ||
throw new Error('Only one of [points|series] should be specified.'); | ||
} | ||
|
||
series = StackedBarChart.pointsToSeries(this.props.points); | ||
series = this.pointsToSeries(this.props.points); | ||
} | ||
|
||
return { | ||
this.state = { | ||
series, | ||
pointIndex: StackedBarChart.pointIndex(series), | ||
interval: StackedBarChart.getInterval(series), | ||
pointIndex: this.pointIndex(series), | ||
interval: this.getInterval(series), | ||
}; | ||
}, | ||
} | ||
|
||
componentWillReceiveProps(nextProps) { | ||
if (nextProps.points || nextProps.series) { | ||
|
@@ -118,31 +82,64 @@ const StackedBarChart = createReactClass({ | |
throw new Error('Only one of [points|series] should be specified.'); | ||
} | ||
|
||
series = StackedBarChart.pointsToSeries(nextProps.points); | ||
series = this.pointsToSeries(nextProps.points); | ||
} | ||
|
||
this.setState({ | ||
series, | ||
pointIndex: StackedBarChart.pointIndex(series), | ||
interval: StackedBarChart.getInterval(series), | ||
pointIndex: this.pointIndex(series), | ||
interval: this.getInterval(series), | ||
}); | ||
} | ||
}, | ||
} | ||
|
||
shouldComponentUpdate(nextProps, nextState) { | ||
return !_.isEqual(this.props, nextProps); | ||
}, | ||
} | ||
|
||
getInterval = series => { | ||
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. none of these changed, i just moved them when I removed |
||
// TODO(dcramer): not guaranteed correct | ||
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. 🧐 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. RIGHT?! |
||
return series.length && series[0].data.length > 1 | ||
? series[0].data[1].x - series[0].data[0].x | ||
: null; | ||
}; | ||
|
||
pointsToSeries = points => { | ||
let series = []; | ||
points.forEach((p, pIdx) => { | ||
p.y.forEach((y, yIdx) => { | ||
if (!series[yIdx]) { | ||
series[yIdx] = {data: []}; | ||
} | ||
series[yIdx].data.push({x: p.x, y}); | ||
}); | ||
}); | ||
return series; | ||
}; | ||
|
||
pointIndex = series => { | ||
let points = {}; | ||
series.forEach(s => { | ||
s.data.forEach(p => { | ||
if (!points[p.x]) { | ||
points[p.x] = {y: [], x: p.x}; | ||
} | ||
points[p.x].y.push(p.y); | ||
}); | ||
}); | ||
return points; | ||
}; | ||
|
||
use24Hours() { | ||
let user = ConfigStore.get('user'); | ||
let options = user ? user.options : {}; | ||
return options.clock24Hours; | ||
}, | ||
} | ||
|
||
floatFormat(number, places) { | ||
let multi = Math.pow(10, places); | ||
return parseInt(number * multi, 10) / multi; | ||
}, | ||
} | ||
|
||
timeLabelAsHour(point) { | ||
let timeMoment = moment(point.x * 1000); | ||
|
@@ -158,13 +155,13 @@ const StackedBarChart = createReactClass({ | |
nextMoment.format(format) + | ||
'</span>' | ||
); | ||
}, | ||
} | ||
|
||
timeLabelAsDay(point) { | ||
let timeMoment = moment(point.x * 1000); | ||
|
||
return `<span>${timeMoment.format('LL')}</span>`; | ||
}, | ||
} | ||
|
||
timeLabelAsRange(interval, point) { | ||
let timeMoment = moment(point.x * 1000); | ||
|
@@ -179,12 +176,12 @@ const StackedBarChart = createReactClass({ | |
nextMoment.format(format) + | ||
'</span>' | ||
); | ||
}, | ||
} | ||
|
||
timeLabelAsFull(point) { | ||
let timeMoment = moment(point.x * 1000); | ||
return timeMoment.format('lll'); | ||
}, | ||
} | ||
|
||
getTimeLabel(point) { | ||
switch (this.state.interval) { | ||
|
@@ -197,7 +194,7 @@ const StackedBarChart = createReactClass({ | |
default: | ||
return this.timeLabelAsRange(this.state.interval, point); | ||
} | ||
}, | ||
} | ||
|
||
maxPointValue() { | ||
return Math.max( | ||
|
@@ -206,7 +203,7 @@ const StackedBarChart = createReactClass({ | |
.map(s => Math.max(...s.data.map(p => p.y))) | ||
.reduce((a, b) => a + b, 0) | ||
); | ||
}, | ||
} | ||
|
||
renderMarker(marker) { | ||
let timeLabel = moment(marker.x * 1000).format('lll'); | ||
|
@@ -219,14 +216,14 @@ const StackedBarChart = createReactClass({ | |
|
||
return ( | ||
<Tooltip title={title} key={key} tooltipOptions={{html: true, placement: 'bottom'}}> | ||
<a className={className} style={{height: '100%'}}> | ||
<a data-test-id="chart-column" className={className} style={{height: '100%'}}> | ||
<span>{marker.label}</span> | ||
</a> | ||
</Tooltip> | ||
); | ||
}, | ||
} | ||
|
||
renderTooltip(point, pointIdx) { | ||
renderTooltip = (point, pointIdx) => { | ||
let timeLabel = this.getTimeLabel(point); | ||
let totalY = point.y.reduce((a, b) => a + b); | ||
let title = | ||
|
@@ -245,26 +242,30 @@ const StackedBarChart = createReactClass({ | |
} | ||
}); | ||
return title; | ||
}, | ||
}; | ||
|
||
renderChartColumn(point, maxval, pointWidth) { | ||
renderChartColumn(point, maxval, pointWidth, index, totalPoints) { | ||
let totalY = point.y.reduce((a, b) => a + b); | ||
let totalPct = totalY / maxval; | ||
let prevPct = 0; | ||
let space = 36 / totalPoints; | ||
let pts = point.y.map((y, i) => { | ||
let pct = totalY && this.floatFormat(y / totalY * totalPct * 99, 2); | ||
let pct = Math.max( | ||
totalY && this.floatFormat(y / totalY * totalPct * 99, 2), | ||
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. what does 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. (takes a long, deep breath...) As best I can tell the "99" is used to reserve a little extra space for the However none of the charts have overflow: hidden so if the bars exceed 100% height it doesn't matter (so long as visually it doesn't look weird) I kept the 99 in there to keep things similar to what they were before. Truthfully, the charts have never been that accurate lol. We could do 100 and nobody would notice. 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. Also good for this to be in commentz |
||
i == 0 ? 1.5 : 0 | ||
); | ||
let pt = ( | ||
<span | ||
<rect | ||
key={i} | ||
x={index * pointWidth} | ||
y={100 - pct - prevPct} | ||
width={pointWidth - space} | ||
height={pct} | ||
fill={this.state.series[i].color} | ||
className={this.props.barClasses[i]} | ||
style={{ | ||
height: pct + '%', | ||
bottom: prevPct + '%', | ||
backgroundColor: this.state.series[i].color || null, | ||
}} | ||
> | ||
{y} | ||
</span> | ||
</rect> | ||
); | ||
prevPct += pct; | ||
return pt; | ||
|
@@ -277,19 +278,17 @@ const StackedBarChart = createReactClass({ | |
<Tooltip | ||
title={tooltipFunc(this.state.pointIndex[pointIdx], pointIdx, this)} | ||
key={point.x} | ||
tooltipOptions={{html: true, placement: 'bottom'}} | ||
tooltipOptions={{html: true, placement: 'bottom', container: 'body'}} | ||
> | ||
<a className="chart-column" style={{width: pointWidth, height: '100%'}}> | ||
{pts} | ||
</a> | ||
<g data-test-id="chart-column">{pts}</g> | ||
</Tooltip> | ||
); | ||
}, | ||
} | ||
|
||
renderChart() { | ||
let {pointIndex, series} = this.state; | ||
let totalPoints = Math.max(...series.map(s => s.data.length)); | ||
let pointWidth = this.floatFormat(100.0 / totalPoints, 2) + '%'; | ||
let pointWidth = this.floatFormat(101.0 / totalPoints, 2); | ||
|
||
let maxval = this.maxPointValue(); | ||
let markers = this.props.markers.slice(); | ||
|
@@ -309,12 +308,14 @@ const StackedBarChart = createReactClass({ | |
}); | ||
|
||
let children = []; | ||
points.forEach(point => { | ||
points.forEach((point, index) => { | ||
while (markers.length && markers[0].x <= point.x) { | ||
children.push(this.renderMarker(markers.shift())); | ||
} | ||
|
||
children.push(this.renderChartColumn(point, maxval, pointWidth)); | ||
children.push( | ||
this.renderChartColumn(point, maxval, pointWidth, index, totalPoints) | ||
); | ||
}); | ||
|
||
// in bizarre case where markers never got rendered, render them last | ||
|
@@ -324,7 +325,7 @@ const StackedBarChart = createReactClass({ | |
}); | ||
|
||
return children; | ||
}, | ||
} | ||
|
||
render() { | ||
let {className, style, height, width} = this.props; | ||
|
@@ -337,10 +338,21 @@ const StackedBarChart = createReactClass({ | |
<Count value={maxval} /> | ||
</span> | ||
<span className="min-y">0</span> | ||
<span>{this.renderChart()}</span> | ||
<StyledSvg | ||
shapeRendering="geometricPrecision" | ||
viewBox={'0 0 99.9 99.9'} | ||
preserveAspectRatio="none" | ||
> | ||
{this.renderChart()} | ||
</StyledSvg> | ||
</figure> | ||
); | ||
}, | ||
}); | ||
} | ||
} | ||
|
||
const StyledSvg = styled('svg')` | ||
width: 100%; | ||
height: 100%; | ||
`; | ||
|
||
export default StackedBarChart; |
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.
is this really necessary? seems like a) this would be close to the default behavior and b) it would be a confusing surprise to not re-render on state change.
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.
I'm genuinely asking tho (hence why I left it in)
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.
Difference is shallow vs deep compare, agreed with (b).
Since we're not updating on state changes... the values that are currently in state, should not be in state. I would leave this in here though and open a followup for cleanups.