dc.js 动态瀑布图
dc.js Dynamic Waterfall Chart
Vistas 在 github 上有一个很好的示例,其中包含如何在 dc.js 中制作瀑布的设置。它使用第二个数据集来实际创建堆叠条形图的底部。但是,如果您在第一个数据集中进行过滤,它将无法正常工作,因为堆叠图表的底部值是固定的。
因此我的问题是是否可以根据此公式计算 d.value,因此不需要第二个数据集 (dummy_data):
Dummy value of current column = previous dummy value + previous real data value
第一列和最后一列的值设置为 0
代码
<!DOCTYPE html>
<html lang='en'>
<head>
<meta charset='utf-8'>
<meta content='width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0' name='viewport'>
<title>Waterfall Chart with DC.js</title>
<script src='js/d3.js' type='text/javascript'></script>
<script src='js/crossfilter.js' type='text/javascript'></script>
<script src='js/reductio.js' type='text/javascript'></script>
<script src='js/dc.js' type='text/javascript'></script>
<link href='css/dc.css' rel='stylesheet' type='text/css'>
</head>
<body>
<div class='pie-graph span6' id='dc-waterfall-chart'></div>
<script>
var waterfallChart = dc.barChart("#dc-waterfall-chart");
var original_data = [];
var dummy_data = [];
//creating example data - could easily be any data reading process from sources like CSV or JSON
original_data.push({item: "x0", value: 10});
original_data.push({item: "x1", value: 2});
original_data.push({item: "x2", value: -1});
original_data.push({item: "x3", value: -3});
original_data.push({item: "x4", value: 8});
//creating the dummy data, the invisible columns supporting the waterfall chart.
//This is going to be the first stack whereas the real data (original_data) is the
//second stack
dummy_data.push({item: "x0", value: 0});
dummy_data.push({item: "x1", value: 10});
dummy_data.push({item: "x2", value: 12});
dummy_data.push({item: "x3", value: 11});
dummy_data.push({item: "x4", value: 0});
//creating crossfilter based off of the real data. Again, you can have your own crossfilter creation process here.
var ndx = crossfilter(original_data);
var itemDimension = ndx.dimension(function (d) { return d.item; });
var reducerValue = reductio().count(true).sum(function(d) { return d.value; }).avg(true);
var itemGroup = itemDimension.group();
var grp = reducerValue(itemGroup);
// we should also have a similar cross filter on the dummy data
var ndx_dummy = crossfilter(dummy_data);
var itemDimension_dummy = ndx_dummy.dimension(function (d) { return d.item; });
var reducerValue_dummy = reductio().count(true).sum(function(d) { return d.value; }).avg(true);
var itemGroup_dummy = itemDimension_dummy.group();
var dummy_grp = reducerValue_dummy(itemGroup_dummy);
waterfallChart.width(600)
.height(400)
.margins({top: 5, right: 40, bottom: 80, left: 40})
.dimension(itemDimension)
.group(dummy_grp)
.valueAccessor(function (d) { // specific to reductio
return d.value.sum;
})
.title(function(d){
return (d.key + " (" + d.value.sum+ ")" );
})
.transitionDuration(1000)
.centerBar(false)
.gap(7)
.x(d3.scaleBand())
.xUnits(dc.units.ordinal)
.controlsUseVisibility(true)
.addFilterHandler(function(filters, filter) {return [filter];})
.elasticY(true)
.xAxis().tickFormat(function(v) {return v;});
waterfallChart.stack(grp,"x")
waterfallChart.on("pretransition",function (chart) {
//coloring the bars
chart.selectAll("rect.bar").style("fill", function(d){return "white";});
chart.selectAll("rect.bar").style("stroke", "#ccc");//change the color to white if you want a clean waterfall without dashed boundaries
chart.selectAll("rect.bar").style("stroke-dasharray", "1,0,2,0,1");
// stack._1 is your real data, whereas stack._0 is the dummy data. You want to treat the styling of these stacks differently
chart.selectAll("svg g g.chart-body g.stack._1 rect.bar").style("fill", function(d){console.log(d.data.value.sum);if (d.data.value.sum >0) return '#ff7c19'; else return '#7c7c7c';});
chart.selectAll("svg g g.chart-body g.stack._1 rect.bar").style("stroke", "white");
chart.selectAll("svg g g.chart-body g.stack._1 rect.bar").style("stroke-dasharray", "1");
// chose the color of deselected bars, but only for the real data.
chart.selectAll("svg g g.chart-body g.stack._1 rect.deselected").style("fill", function (d) {return '#ccc';});
chart.selectAll('g.x text').style('fill', '#8e8e8e');
chart.selectAll('g.y text').style('fill', '#777777');
chart.selectAll('g.x text').style('font-size', '10.5px');
chart.selectAll('g.y.axis g.tick line').style("stroke", "#f46542");
chart.selectAll('.domain').style("stroke","#c6c6c6");
chart.selectAll('rect.bar').on("contextmenu",function(d){d3.event.preventDefault();});
});
dc.renderAll();
</script>
</body>
</html>
我们可以使用 fake group 以基线和最终值所需的方式累积值:
function waterfall_group(group, endkey, acc) {
acc = acc || (x => x);
return {
all: () => {
let cumulate = 0;
let all = group.all().map(({key,value}) => {
value = acc(value)
const kv = {
key,
value: {
baseline: cumulate,
data: value
}
};
cumulate += value;
return kv;
});
return all.concat([{key: endkey, value: {baseline: 0, data: cumulate}}]);
}
};
}
此函数采用最终 "sum total" 条的键和访问函数,此处需要,因为 reductio 将值包装在一个额外的对象中。
它 returns 一组值为 {baseline,data}
,其中 baseline
是不可见堆栈所需的虚拟值,data
是条形图的值。
像
一样构建假群
var waterfall_group = waterfall_group(grp, 'x5', x => x.sum);
并将其传递给 .group()
和 .stack()
使用访问器来获取子值:
waterfallChart
.group(waterfall_group, 'baseline', kv => kv.value.baseline)
.stack(waterfall_group, 'data', kv => kv.value.data)
我还更改了着色代码以获取新的数据格式:
chart.selectAll("svg g g.chart-body g.stack._1 rect.bar")
.style("fill", function(d){if (d.data.value.data >0) return '#ff7c19'; else return '#7c7c7c';});
为了测试它,我添加了另一个 "category" 字段和一个饼图。请注意,瀑布图可能会进入一些具有负值和零值的奇怪状态(例如单击 "C"),但它们看起来是正确的。
请注意,由于最后一项(此处为 x5)是纯合成的,与任何基础数据无关,因此通过单击该项目进行过滤将导致其他图表空白。我不确定如何禁用对某一特定项目的点击。
Vistas 在 github 上有一个很好的示例,其中包含如何在 dc.js 中制作瀑布的设置。它使用第二个数据集来实际创建堆叠条形图的底部。但是,如果您在第一个数据集中进行过滤,它将无法正常工作,因为堆叠图表的底部值是固定的。
因此我的问题是是否可以根据此公式计算 d.value,因此不需要第二个数据集 (dummy_data):
Dummy value of current column = previous dummy value + previous real data value
第一列和最后一列的值设置为 0
代码
<!DOCTYPE html>
<html lang='en'>
<head>
<meta charset='utf-8'>
<meta content='width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0' name='viewport'>
<title>Waterfall Chart with DC.js</title>
<script src='js/d3.js' type='text/javascript'></script>
<script src='js/crossfilter.js' type='text/javascript'></script>
<script src='js/reductio.js' type='text/javascript'></script>
<script src='js/dc.js' type='text/javascript'></script>
<link href='css/dc.css' rel='stylesheet' type='text/css'>
</head>
<body>
<div class='pie-graph span6' id='dc-waterfall-chart'></div>
<script>
var waterfallChart = dc.barChart("#dc-waterfall-chart");
var original_data = [];
var dummy_data = [];
//creating example data - could easily be any data reading process from sources like CSV or JSON
original_data.push({item: "x0", value: 10});
original_data.push({item: "x1", value: 2});
original_data.push({item: "x2", value: -1});
original_data.push({item: "x3", value: -3});
original_data.push({item: "x4", value: 8});
//creating the dummy data, the invisible columns supporting the waterfall chart.
//This is going to be the first stack whereas the real data (original_data) is the
//second stack
dummy_data.push({item: "x0", value: 0});
dummy_data.push({item: "x1", value: 10});
dummy_data.push({item: "x2", value: 12});
dummy_data.push({item: "x3", value: 11});
dummy_data.push({item: "x4", value: 0});
//creating crossfilter based off of the real data. Again, you can have your own crossfilter creation process here.
var ndx = crossfilter(original_data);
var itemDimension = ndx.dimension(function (d) { return d.item; });
var reducerValue = reductio().count(true).sum(function(d) { return d.value; }).avg(true);
var itemGroup = itemDimension.group();
var grp = reducerValue(itemGroup);
// we should also have a similar cross filter on the dummy data
var ndx_dummy = crossfilter(dummy_data);
var itemDimension_dummy = ndx_dummy.dimension(function (d) { return d.item; });
var reducerValue_dummy = reductio().count(true).sum(function(d) { return d.value; }).avg(true);
var itemGroup_dummy = itemDimension_dummy.group();
var dummy_grp = reducerValue_dummy(itemGroup_dummy);
waterfallChart.width(600)
.height(400)
.margins({top: 5, right: 40, bottom: 80, left: 40})
.dimension(itemDimension)
.group(dummy_grp)
.valueAccessor(function (d) { // specific to reductio
return d.value.sum;
})
.title(function(d){
return (d.key + " (" + d.value.sum+ ")" );
})
.transitionDuration(1000)
.centerBar(false)
.gap(7)
.x(d3.scaleBand())
.xUnits(dc.units.ordinal)
.controlsUseVisibility(true)
.addFilterHandler(function(filters, filter) {return [filter];})
.elasticY(true)
.xAxis().tickFormat(function(v) {return v;});
waterfallChart.stack(grp,"x")
waterfallChart.on("pretransition",function (chart) {
//coloring the bars
chart.selectAll("rect.bar").style("fill", function(d){return "white";});
chart.selectAll("rect.bar").style("stroke", "#ccc");//change the color to white if you want a clean waterfall without dashed boundaries
chart.selectAll("rect.bar").style("stroke-dasharray", "1,0,2,0,1");
// stack._1 is your real data, whereas stack._0 is the dummy data. You want to treat the styling of these stacks differently
chart.selectAll("svg g g.chart-body g.stack._1 rect.bar").style("fill", function(d){console.log(d.data.value.sum);if (d.data.value.sum >0) return '#ff7c19'; else return '#7c7c7c';});
chart.selectAll("svg g g.chart-body g.stack._1 rect.bar").style("stroke", "white");
chart.selectAll("svg g g.chart-body g.stack._1 rect.bar").style("stroke-dasharray", "1");
// chose the color of deselected bars, but only for the real data.
chart.selectAll("svg g g.chart-body g.stack._1 rect.deselected").style("fill", function (d) {return '#ccc';});
chart.selectAll('g.x text').style('fill', '#8e8e8e');
chart.selectAll('g.y text').style('fill', '#777777');
chart.selectAll('g.x text').style('font-size', '10.5px');
chart.selectAll('g.y.axis g.tick line').style("stroke", "#f46542");
chart.selectAll('.domain').style("stroke","#c6c6c6");
chart.selectAll('rect.bar').on("contextmenu",function(d){d3.event.preventDefault();});
});
dc.renderAll();
</script>
</body>
</html>
我们可以使用 fake group 以基线和最终值所需的方式累积值:
function waterfall_group(group, endkey, acc) {
acc = acc || (x => x);
return {
all: () => {
let cumulate = 0;
let all = group.all().map(({key,value}) => {
value = acc(value)
const kv = {
key,
value: {
baseline: cumulate,
data: value
}
};
cumulate += value;
return kv;
});
return all.concat([{key: endkey, value: {baseline: 0, data: cumulate}}]);
}
};
}
此函数采用最终 "sum total" 条的键和访问函数,此处需要,因为 reductio 将值包装在一个额外的对象中。
它 returns 一组值为 {baseline,data}
,其中 baseline
是不可见堆栈所需的虚拟值,data
是条形图的值。
像
一样构建假群var waterfall_group = waterfall_group(grp, 'x5', x => x.sum);
并将其传递给 .group()
和 .stack()
使用访问器来获取子值:
waterfallChart
.group(waterfall_group, 'baseline', kv => kv.value.baseline)
.stack(waterfall_group, 'data', kv => kv.value.data)
我还更改了着色代码以获取新的数据格式:
chart.selectAll("svg g g.chart-body g.stack._1 rect.bar")
.style("fill", function(d){if (d.data.value.data >0) return '#ff7c19'; else return '#7c7c7c';});
为了测试它,我添加了另一个 "category" 字段和一个饼图。请注意,瀑布图可能会进入一些具有负值和零值的奇怪状态(例如单击 "C"),但它们看起来是正确的。
请注意,由于最后一项(此处为 x5)是纯合成的,与任何基础数据无关,因此通过单击该项目进行过滤将导致其他图表空白。我不确定如何禁用对某一特定项目的点击。