dc.js 按月排序 X 轴
dc.js Ordering X-Axis by Month
我正在尝试使用 dc.js 创建堆积条形图,如下所示:
数据:
<pre id="data">
Status,StatusT,Phase,PhaseT,CompletionDate,CalMonth,Sales,SalesCount
3,Won,Z04,Decide,20130101,201012,2203262.13,1
3,Won,Z04,Decide,20130101,201111,607933.8,1
3,Won,Z05,Deliver,20131001,201202,66.08,1
3,Won,Z04,Decide,20120501,201202,66.08,1
3,Won,Z01,Understand,20120409,201203,65.07,1
3,Won,Z04,Decide,20121231,201204,4645214.7,1
3,Won,Z05,Deliver,20130918,201204,66.52,1
3,Won,Z04,Decide,20130418,201204,2312130.92,1
3,Won,Z05,Deliver,20120504,201205,68.07,1
3,Won,Z05,Deliver,20120504,201205,515994.86,1
3,Won,Z04,Decide,20120504,201205,68.07,1
3,Won,Z04,Decide,20120504,201205,68.07,1
</pre>
Javascript:
var dateFormat = d3.time.format('%Y%m%d');
var monthNameFormat = d3.time.format("%b-%Y");
// Prepare Data
for (var z = 0; z < opportunities.length; z++) {
opportunities[z].DD = dateFormat.parse(opportunities[z].CompletionDate);
}
opportunities.sort(function(a,b){
return b.DD - a.DD;
});
var ndx = crossfilter(opportunities);
var dimMonthYear = ndx.dimension(function(d) {
//return d.CompletionDate
return monthNameFormat(new Date(d.DD));
});
var phaseUnderstand = dimMonthYear.group().reduceSum(function(d) {
if (d.Phase === "Z01") {
return d.Sales;
} else {
return 0;
}
});
var phasePropose = dimMonthYear.group().reduceSum(function(d) {
if (d.Phase === "Z02") {
return d.Sales;
} else {
return 0;
}
});
var phaseNegotiate = dimMonthYear.group().reduceSum(function(d) {
if (d.Phase === "Z03") {
return d.Sales;
} else {
return 0;
}
});
var phaseDecide = dimMonthYear.group().reduceSum(function(d) {
if (d.Phase === "Z04") {
return d.Sales;
} else {
return 0;
}
});
var phaseDeliver = dimMonthYear.group().reduceSum(function(d) {
if (d.Phase === "Z05") {
return d.Sales;
} else {
return 0;
}
});
chart
.width(768)
.height(480)
.margins({
left: 80,
top: 20,
right: 10,
bottom: 80
})
.x(d3.scale.ordinal())
.round(d3.time.month.round)
.alwaysUseRounding(true)
.xUnits(dc.units.ordinal)
.brushOn(false)
.xAxisLabel('Months')
.yAxisLabel("Expected Sales (in thousands)")
.dimension(dimMonthYear)
.renderHorizontalGridLines(true)
.renderVerticalGridLines(true)
// .barPadding(0.1)
//.outerPadding(0.05)
.legend(dc.legend().x(130).y(30).itemHeight(13).gap(5))
.group(phaseUnderstand, "Understand")
.stack(phasePropose, "Propose")
.stack(phaseNegotiate, "Negotiate")
.stack(phaseDecide, "Decide")
.stack(phaseDeliver, "Deliver")
.centerBar(true)
.elasticY(true)
.elasticX(true)
.renderlet(function(chart) {
chart.selectAll("g.x text").attr('dx', '-30').attr(
'dy', '-7').attr('transform', "rotate(-60)");
});
chart.render();
我能够渲染图表,但我无法按月按升序对 X 轴进行排序并使其沿 X 轴具有弹性。如果我只使用日期,那么图表会变得过于详细(尽管 x 轴仍然没有弹性),这对我创建它的目的没有用。这是一个示例 fiddle:https://jsfiddle.net/NewMonk/1hbjwxzy/67/ .
我强烈建议在 X 轴基于时间时使用时间刻度。这只是多了一点工作(以及可怕的空白图表的更多风险),但它更加灵活和准确。
首先,我们需要让 crossfilter 按月分类。 d3.time.month
会将每个日期四舍五入到最接近的月份:
var dimMonthYear = ndx.dimension(function(d) {
return d3.time.month(d.DD)
});
这与您创建的字符串容器具有相同的效果,但它会将键保留为日期。
然后我们可以告诉dc.js使用基于时间的比例和elasticX:
chart
.x(d3.time.scale()).elasticX(true)
.round(d3.time.month.round)
.alwaysUseRounding(true)
.xUnits(d3.time.months)
.xUnits
是使条形宽度正确的原因(以便 dc.js 可以计算可见条形的数量)。
我们可以恢复 monthNameFormat
分时格式并像这样显示每个分时:
chart.xAxis()
.tickFormat(monthNameFormat)
.ticks(d3.time.months,1);
最后,如果使用另一个图表进行过滤,您可能需要 remove empty bins,以便在条柱降为零时删除它们:
function remove_empty_bins(source_group) {
return {
all:function () {
return source_group.all().filter(function(d) {
return d.value != 0;
});
}
};
}
后记
@Kush 在下面评论说,从一组堆叠的基于日期的组中删除空垃圾箱并不容易。原来如此。
这里是 combine_groups 的自定义版本,用于合并基于日期的组:
function combine_date_groups() { // (groups...)
var groups = Array.prototype.slice.call(arguments);
return {
all: function() {
var alls = groups.map(function(g) { return g.all(); });
var gm = {};
alls.forEach(function(a, i) {
a.forEach(function(b) {
var t = b.key.getTime();
if(!gm[t]) {
gm[t] = new Array(groups.length);
for(var j=0; j<groups.length; ++j)
gm[t][j] = 0;
}
gm[t][i] = b.value;
});
});
var ret = [];
for(var k in gm)
ret.push({key: new Date(+k), value: gm[k]});
return ret;
}
};
}
这里令人讨厌的是,我们必须从日期转换为整数才能正确索引对象,然后在构建假组数据时将整数转换回日期。
像这样应用它:
var combinedGroup = combine_date_groups(remove_empty_bins(phaseUnderstand),
remove_empty_bins(phasePropose),
remove_empty_bins(phaseNegotiate),
remove_empty_bins(phaseDecide),
remove_empty_bins(phaseDeliver));
这是一个通过索引拉栈的辅助函数:
function sel_stack(i) {
return function(d) {
return d.value[i];
};
}
现在我们可以像这样指定堆栈:
chart
.group(combinedGroup, "Understand", sel_stack(0))
.stack(combinedGroup, "Propose", sel_stack(1))
.stack(combinedGroup, "Negotiate", sel_stack(2))
.stack(combinedGroup, "Decide", sel_stack(3))
.stack(combinedGroup, "Deliver", sel_stack(4))
更新 fiddle 此处:https://jsfiddle.net/gordonwoodhull/8pc0p1we/17/
(第一次忘了post fiddle,我想是在这次冒险之前的版本8左右。)
我们远远超出了 crossfilter 和 dc.js 最初设计的目的,这有点幽默。数据复杂。
我正在尝试使用 dc.js 创建堆积条形图,如下所示:
数据:
<pre id="data">
Status,StatusT,Phase,PhaseT,CompletionDate,CalMonth,Sales,SalesCount
3,Won,Z04,Decide,20130101,201012,2203262.13,1
3,Won,Z04,Decide,20130101,201111,607933.8,1
3,Won,Z05,Deliver,20131001,201202,66.08,1
3,Won,Z04,Decide,20120501,201202,66.08,1
3,Won,Z01,Understand,20120409,201203,65.07,1
3,Won,Z04,Decide,20121231,201204,4645214.7,1
3,Won,Z05,Deliver,20130918,201204,66.52,1
3,Won,Z04,Decide,20130418,201204,2312130.92,1
3,Won,Z05,Deliver,20120504,201205,68.07,1
3,Won,Z05,Deliver,20120504,201205,515994.86,1
3,Won,Z04,Decide,20120504,201205,68.07,1
3,Won,Z04,Decide,20120504,201205,68.07,1
</pre>
Javascript:
var dateFormat = d3.time.format('%Y%m%d');
var monthNameFormat = d3.time.format("%b-%Y");
// Prepare Data
for (var z = 0; z < opportunities.length; z++) {
opportunities[z].DD = dateFormat.parse(opportunities[z].CompletionDate);
}
opportunities.sort(function(a,b){
return b.DD - a.DD;
});
var ndx = crossfilter(opportunities);
var dimMonthYear = ndx.dimension(function(d) {
//return d.CompletionDate
return monthNameFormat(new Date(d.DD));
});
var phaseUnderstand = dimMonthYear.group().reduceSum(function(d) {
if (d.Phase === "Z01") {
return d.Sales;
} else {
return 0;
}
});
var phasePropose = dimMonthYear.group().reduceSum(function(d) {
if (d.Phase === "Z02") {
return d.Sales;
} else {
return 0;
}
});
var phaseNegotiate = dimMonthYear.group().reduceSum(function(d) {
if (d.Phase === "Z03") {
return d.Sales;
} else {
return 0;
}
});
var phaseDecide = dimMonthYear.group().reduceSum(function(d) {
if (d.Phase === "Z04") {
return d.Sales;
} else {
return 0;
}
});
var phaseDeliver = dimMonthYear.group().reduceSum(function(d) {
if (d.Phase === "Z05") {
return d.Sales;
} else {
return 0;
}
});
chart
.width(768)
.height(480)
.margins({
left: 80,
top: 20,
right: 10,
bottom: 80
})
.x(d3.scale.ordinal())
.round(d3.time.month.round)
.alwaysUseRounding(true)
.xUnits(dc.units.ordinal)
.brushOn(false)
.xAxisLabel('Months')
.yAxisLabel("Expected Sales (in thousands)")
.dimension(dimMonthYear)
.renderHorizontalGridLines(true)
.renderVerticalGridLines(true)
// .barPadding(0.1)
//.outerPadding(0.05)
.legend(dc.legend().x(130).y(30).itemHeight(13).gap(5))
.group(phaseUnderstand, "Understand")
.stack(phasePropose, "Propose")
.stack(phaseNegotiate, "Negotiate")
.stack(phaseDecide, "Decide")
.stack(phaseDeliver, "Deliver")
.centerBar(true)
.elasticY(true)
.elasticX(true)
.renderlet(function(chart) {
chart.selectAll("g.x text").attr('dx', '-30').attr(
'dy', '-7').attr('transform', "rotate(-60)");
});
chart.render();
我能够渲染图表,但我无法按月按升序对 X 轴进行排序并使其沿 X 轴具有弹性。如果我只使用日期,那么图表会变得过于详细(尽管 x 轴仍然没有弹性),这对我创建它的目的没有用。这是一个示例 fiddle:https://jsfiddle.net/NewMonk/1hbjwxzy/67/ .
我强烈建议在 X 轴基于时间时使用时间刻度。这只是多了一点工作(以及可怕的空白图表的更多风险),但它更加灵活和准确。
首先,我们需要让 crossfilter 按月分类。 d3.time.month
会将每个日期四舍五入到最接近的月份:
var dimMonthYear = ndx.dimension(function(d) {
return d3.time.month(d.DD)
});
这与您创建的字符串容器具有相同的效果,但它会将键保留为日期。
然后我们可以告诉dc.js使用基于时间的比例和elasticX:
chart
.x(d3.time.scale()).elasticX(true)
.round(d3.time.month.round)
.alwaysUseRounding(true)
.xUnits(d3.time.months)
.xUnits
是使条形宽度正确的原因(以便 dc.js 可以计算可见条形的数量)。
我们可以恢复 monthNameFormat
分时格式并像这样显示每个分时:
chart.xAxis()
.tickFormat(monthNameFormat)
.ticks(d3.time.months,1);
最后,如果使用另一个图表进行过滤,您可能需要 remove empty bins,以便在条柱降为零时删除它们:
function remove_empty_bins(source_group) {
return {
all:function () {
return source_group.all().filter(function(d) {
return d.value != 0;
});
}
};
}
后记
@Kush 在下面评论说,从一组堆叠的基于日期的组中删除空垃圾箱并不容易。原来如此。
这里是 combine_groups 的自定义版本,用于合并基于日期的组:
function combine_date_groups() { // (groups...)
var groups = Array.prototype.slice.call(arguments);
return {
all: function() {
var alls = groups.map(function(g) { return g.all(); });
var gm = {};
alls.forEach(function(a, i) {
a.forEach(function(b) {
var t = b.key.getTime();
if(!gm[t]) {
gm[t] = new Array(groups.length);
for(var j=0; j<groups.length; ++j)
gm[t][j] = 0;
}
gm[t][i] = b.value;
});
});
var ret = [];
for(var k in gm)
ret.push({key: new Date(+k), value: gm[k]});
return ret;
}
};
}
这里令人讨厌的是,我们必须从日期转换为整数才能正确索引对象,然后在构建假组数据时将整数转换回日期。
像这样应用它:
var combinedGroup = combine_date_groups(remove_empty_bins(phaseUnderstand),
remove_empty_bins(phasePropose),
remove_empty_bins(phaseNegotiate),
remove_empty_bins(phaseDecide),
remove_empty_bins(phaseDeliver));
这是一个通过索引拉栈的辅助函数:
function sel_stack(i) {
return function(d) {
return d.value[i];
};
}
现在我们可以像这样指定堆栈:
chart
.group(combinedGroup, "Understand", sel_stack(0))
.stack(combinedGroup, "Propose", sel_stack(1))
.stack(combinedGroup, "Negotiate", sel_stack(2))
.stack(combinedGroup, "Decide", sel_stack(3))
.stack(combinedGroup, "Deliver", sel_stack(4))
更新 fiddle 此处:https://jsfiddle.net/gordonwoodhull/8pc0p1we/17/
(第一次忘了post fiddle,我想是在这次冒险之前的版本8左右。)
我们远远超出了 crossfilter 和 dc.js 最初设计的目的,这有点幽默。数据复杂。