Over the past week I have been building a focus + context bar chart in JavaScript. I searched several alternatives on the Internet that would achieve the desired functionality. A focus + context bar chart (also called master + detail or “brushing” chart) allows a smaller bar chart to select the range for the bigger one. In other words, one bar chart selects the X-domain range and the other shows data. As simple as this may seem, I came across only a few good examples:
- D3’s Focus + context via brushing (mbostock)
- Protovis’s Focus + context (superseded by D3.js from the same author)
- Highchart’s Master + detail
- Nick Qi Zhu’s Dimensional charting website
Most charting libraries would attempt to plot all the data without filtering. This is not a problem for small datasets, but degrades performance when dealing with large datasets. Libraries such as crossfilter (created by Square) enable fast access to large datasets, but it does not provide rendering capabilities. Dimensional Charting takes the best of both worlds: it build upon D3.js and crossfilter. Its website provides an excellent example of its capabilities, which you should check out.
Since I only wanted to reuse one of the charts, I extracted the focus and context with brushing example.
Here is the source code:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
|
<!DOCTYPE html>
<meta http-equiv="content-type" content="text/html; charset=UTF8">
<html>
<head>
<title></title>
<link href="http://www.localhost/js/dc.js/v1.4.0/dc.css" rel='stylesheet' type="text/css">
</head>
<body>
<div id="monthly-move-chart"></div>
<div id="monthly-volume-chart"></div>
</body>
<script type="text/javascript" src="http://www.localhost/js/d3/v3.2.8/d3.min.js"></script>
<script type="text/javascript" src="http://www.localhost/js/crossfilter/v1.2.0/crossfilter.min.js"></script>
<script type="text/javascript" src="http://www.localhost/js/dc.js/v1.4.0/dc.min.js"></script>
<script type="text/javascript">
var moveChart = dc.compositeChart("#monthly-move-chart");
var volumeChart = dc.barChart("#monthly-volume-chart");
// load data from a csv file: download http://nickqizhu.github.io/dc.js/ndx.csv
d3.csv("ndx.csv", function (data) {
// since its a csv file we need to format the data a bit
var dateFormat = d3.time.format("%m/%d/%Y");
var numberFormat = d3.format(".2f");
data.forEach(function (e) {
e.dd = dateFormat.parse(e.date);
e.month = d3.time.month(e.dd); // pre-calculate month for better performance
});
// feed it through crossfilter
var ndx = crossfilter(data);
// monthly index avg fluctuation in percentage
var moveMonths = ndx.dimension(function (d) {
return d.month;
});
var monthlyMoveGroup = moveMonths.group().reduceSum(function (d) {
return Math.abs(+d.close - +d.open);
});
var indexAvgByMonthGroup = moveMonths.group().reduce(
function (p, v) {
++p.days;
p.total += (+v.open + +v.close) / 2;
p.avg = Math.round(p.total / p.days);
return p;
},
function (p, v) {
--p.days;
p.total -= (+v.open + +v.close) / 2;
p.avg = p.days == 0 ? 0 : Math.round(p.total / p.days);
return p;
},
function () {
return {days: 0, total: 0, avg: 0};
}
);
var indexCloseByMonthGroup = moveMonths.group().reduce(
function (p, v) {
++p.days;
p.total += +v.close;
p.avg = Math.round(p.total / p.days);
return p;
},
function (p, v) {
--p.days;
p.total -= +v.close;
p.avg = p.days == 0 ? 0 : Math.round(p.total / p.days);
return p;
},
function () {
return {days: 0, total: 0, avg: 0};
}
);
translate = 3;
moveChart.width(990)
.height(180)
.transitionDuration(1000)
.margins({top: 10, right: 50, bottom: 25, left: 40})
.dimension(moveMonths)
.group(indexAvgByMonthGroup)
.valueAccessor(function (d) {
return d.value.avg;
})
.mouseZoomable(false)
.x(d3.time.scale().domain([new Date(1985, 0, 1), new Date(2012, 11, 31)]))
.round(d3.time.month.round)
.xUnits(d3.time.months)
.elasticY(true)
.renderHorizontalGridLines(true)
.brushOn(false)
.rangeChart(volumeChart)
.compose([
dc.barChart(moveChart).group(indexAvgByMonthGroup)
.valueAccessor(function (d) {
return d.value.avg;
})
.gap(5)
.title(function (d) {
var value = d.value.avg ? d.value.avg : d.value;
if (isNaN(value)) value = 0;
return dateFormat(d.key) + "\n" + numberFormat(value);
}),
dc.barChart(moveChart).group(indexCloseByMonthGroup)
.valueAccessor(function (d) {
return d.value.avg;
})
.gap(5)
.title(function (d) {
var value = d.value.avg ? d.value.avg : d.value;
if (isNaN(value)) value = 0;
return dateFormat(d.key) + "\n" + numberFormat(value);
})
])
.renderlet(function (chart) {
chart.selectAll("g._1").attr("transform", "translate(" + translate + ", 0)");
});
volumeChart.width(990)
.height(40)
.margins({top: 0, right: 50, bottom: 20, left: 40})
.dimension(moveMonths)
.group(monthlyMoveGroup)
.centerBar(true)
.gap(1)
.x(d3.time.scale().domain([new Date(1985, 0, 1), new Date(2012, 11, 31)]))
.round(d3.time.month.round)
.xUnits(d3.time.months)
.brushOn(true);
dc.renderAll();
}
);
</script>
</html>
|
UPDATED January 28, 2014: Added a link to a GitHub Gist.