我正在使用react.js v15.3.0中的d3 v4.4.4构建堆积条形图。以下是我用来构建堆积条形图的数据:
[
{ timestamp: "2006", source1: "20", source2: "20", source3: "20", source4: '20'},
{ timestamp: "2007", source1: "70", source2: "50", source3: "10", source4: '70'},
{ timestamp: "2008", source1: "80", source2: "50", source3: "60", source4: '40'},
{ timestamp: "2009", source1: "30", source2: "20", source3: "40", source4: '50'},
{ timestamp: "2010", source1: "70", source2: "20", source3: "90", source4: '20'}
]
我已经显示堆积条形图显示seen here,但它没有显示数据数组中的第一个对象。相反,堆积条形图复制数据数组中的最后一个对象,并用于图表中第一个和最后一个堆叠条。
const stack = d3.stack().keys(keys)
const layers = stack(chartDataWorkingCopy)
this was the result of logging layers to the console
从上面的屏幕截图中可以看出,第一个和最后一个数组是相同的,第一个对象(即{timestamp:" 2006",source1:" 20",source2 :" 20",source3:" 20",source4:' 20'})我用来构建图表的原始数据不存在。
有人可以向我解释为什么d3会跳过数组中的第一个对象并在构建堆积条形图时复制最后一个对象吗?
以下是堆积条形图组件的源代码:
import React, { PropTypes } from 'react'
import * as d3 from 'd3'
import _ from 'lodash'
class StackedBarChart extends React.Component {
static propTypes = {
chartData: PropTypes.array,
barWidth: PropTypes.number,
barOffset: PropTypes.number,
chartHeight: PropTypes.number,
chartWidth: PropTypes.number
}
static defaultProps = {
chartData: [
{ timestamp: "2006", source1: "20", source2: "20", source3: "20", source4: '20'},
{ timestamp: "2007", source1: "70", source2: "50", source3: "10", source4: '70'},
{ timestamp: "2008", source1: "80", source2: "50", source3: "60", source4: '40'},
{ timestamp: "2009", source1: "30", source2: "20", source3: "40", source4: '50'},
{ timestamp: "2010", source1: "70", source2: "20", source3: "90", source4: '20'}
],
barWidth: 10,
barOffset: 5,
chartHeight: 200,
chartWidth: 300
}
componentDidMount() {
this.renderChart()
}
componentDidUpdate(prevProps){
if(!_.isEqual(this.props, prevProps)){
d3.select('svg').remove()
this.renderChart()
}
}
splitTheDifference(data) {
let str = String(data)
const points = str.split(',')
return (points[1] - points[0])
}
renderChart() {
const { chartData, barWidth, barOffset, chartHeight, chartWidth} = this.props
let chartDataWorkingCopy = [...chartData]
// keep the bars from going off the page
const maxBars = (chartWidth / (barWidth + 2))
if (chartDataWorkingCopy.length > maxBars) {
chartDataWorkingCopy = chartDataWorkingCopy.slice(-Math.floor(maxBars))
}
const keys = _.remove(_.keys(_.extend.apply({}, chartDataWorkingCopy)), (d) => {
return d !== 'timestamp'
})
const xScale = d3.scaleBand().range([0, chartWidth]).padding(0.1)
const yScale = d3.scaleLinear().range([chartHeight, 0])
const color = ['hsl(8, 82%, 50%)', 'hsl(76, 96%, 50%)', 'hsl(178, 99%, 50%)', 'hsl(302, 100%, 50%)', 'hsl(58, 98%, 50%)', 'hsl(144, 100%, 50%)']
const xAxis = d3.axisBottom(xScale).tickFormat(d3.timeFormat("%b"))
const yAxis = d3.axisLeft(yScale)
const width = chartWidth
const height = chartHeight
let svg = d3.select('.barChartContainer').append("svg")
.attr('width', width)
.attr('height', height)
.append('g')
const stack = d3.stack().keys(keys)
const layers = stack(chartDataWorkingCopy)
xScale.domain(chartDataWorkingCopy.map(function(d) { return d.timestamp }))
yScale.domain([0, d3.max(layers[layers.length - 1], function(d) {
return d[0] + d[1]
}) ]).nice()
const layer = svg.selectAll('.layer')
.data(layers)
.enter().append('g')
.attr('class', 'layer')
.style('fill', function(d, i) { return color[i] })
layer.selectAll("rect")
.data(function(d) {
return d
})
.enter().append("rect")
.attr("x", function(d, i) {
return i * 12
})
.attr("y", function(d) { return yScale(d[1]) })
.attr("height", function(d) { return yScale(d[0]) - yScale(d[1]) })
.attr("width", 10)
.on('mouseover', (d) => {
d3.select('#tooltip')
.classed('hidden', false)
.style('position', 'absolute')
.style('background', '#333333')
.style('color', '#fff')
.style('padding', 10)
.style('left', `${d3.event.pageX}px`)
.style('top', `${d3.event.pageY - 80}px`)
.select('#value')
.text(this.splitTheDifference(d))
})
.on('mouseout', (d) => {
d3.select('#tooltip')
.classed('hidden', true)
})
}
render(){
return(
<div className={ 'barChartContainer' }>
<div id='tooltip' className='hidden'>
<p id='value'></p>
</div>
</div>
)
}
}
export default StackedBarChart
答案 0 :(得分:0)
好吧,d3.stack()
没有跳过数据数组中的第一个对象。
问题是这段代码......
const keys = _.remove(_.keys(_.extend.apply({}, chartDataWorkingCopy)), (d) => {
return d !== 'timestamp'
})
...不仅可以获取密钥,还可以修改chartDataWorkingCopy
数组,仍然指向到您的chartData
数组。
现在,您可能认为您正在克隆原始数组:
let chartDataWorkingCopy = [...chartData];
但不幸的是,你不是。您无法使用spread运算符克隆对象数组。 documentation说:
通常,ES2015中的扩展运算符在复制数组时会变深一级。因此,它们不适合复制多维数组。与Object.assign()和Object spread运算符的情况相同。请查看下面的示例以便更好地理解。
因此,在您的代码中,对chartDataWorkingCopy
的任何更改也会更改chartData
。
解决方案01
你可以在不修改数组的情况下获取密钥,使用vanilla JS或D3,你不需要lodash。例如,这是一种非常简单的方法,只需使用D3获取所需的键,而无需修改源数组:
const keys = d3.keys(chartDataWorkingCopy[0]).filter(d => d != "timestamp");
解决方案02
如果你想保留你的lodash函数,请使用一个真正复制深度数组的方法,如下所示:
let chartDataWorkingCopy = JSON.parse(JSON.stringify(chartData));
在这个演示中,我将保留您的代码(使用lodash),只需更改生成原始数组副本的代码即可。现在价值是正确的:
var chartData = [
{ timestamp: "2006", source1: "20", source2: "20", source3: "20", source4: '20'},
{ timestamp: "2007", source1: "70", source2: "50", source3: "10", source4: '70'},
{ timestamp: "2008", source1: "80", source2: "50", source3: "60", source4: '40'},
{ timestamp: "2009", source1: "30", source2: "20", source3: "40", source4: '50'},
{ timestamp: "2010", source1: "70", source2: "20", source3: "90", source4: '20'}
];
let chartDataWorkingCopy = JSON.parse(JSON.stringify(chartData));
const keys = _.remove(_.keys(_.extend.apply({}, chartDataWorkingCopy)), (d) => {
return d !== 'timestamp'
})
const stack = d3.stack().keys(keys)
const layers = stack(chartData);
console.log(layers);
<script src="https://d3js.org/d3.v4.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.4/lodash.min.js"></script>
PS:只有在该数组中没有实际的日期对象时,才能使用JSON.parse(JSON.stringify(array))
复制数组。现在,“2006”,“2007”,“2008”等只是字符串,所以这种方法有效。