How to add a new chart type to dc.js
This is a step by step description of how I extended dc.js with a new chart type. It is inspired by a wiki page by Thomas Robert.
A Bullet chart
Mike Bostock already implemented a bullet chart in D3.js. The vertical version from Jason Davies uses the official code which is available as a d3-plugin. (I have submitted a proposal for a bug fix and will use it instead.)
So how can I make it available in dc.js?
Below you can see what I did and here what is still in the todo list.
The result is not too bad.
Scaffolding
Starting from dc.js
directory
- add a file named
src\bullet-chart.js
- copy
bullet.js
(with the fix proposed here) from the d3-plugin to insrc\d3.bullet.js
- add the two files above in the
module.exports.jsFiles
array in `Gruntfile.js
.exports.jsFiles = [
module...
'src/d3.bullet.js',
'src/bullet-chart.js',
'src/footer.js' // NOTE: keep this last
; ]
- add the example file
examples\web\bullet.html
, a copy oford.html
for example, and updateexamples\web\index.html
accordingly
Stub your chart
bullet-chart.js
can initially be something like
/**
## Bullet Chart
Includes: [Margin Mixin](#margin-mixin), [Color Mixin](#color-mixin),
[Base Mixin](#base-mixin)
Bullet chart implementation.
#### dc.bulletChart(parent[, chartGroup])
Create a bullet chart instance and attach it to the given parent element.
#### Example of usage
```javascript
var chart = dc.bulletCloudChart()
.dimension(department)
.group(salesPerDepartment)
.colors(d3.scale.quantize().range(["#E2F2FF", "#C4E4FF", "#9ED2FF",
"#81C5FF", "#6BBAFF", "#51AEFF", "#36A2FF", "#1E96FF", "#0089FF", "#0061B5"]))
.colorDomain([0, 200])
.label(function (d) { return labels[d.key]; })
.title(function (d) { return d.value+" $"; })
```
Parameters:
* parent : string | node | selection - any valid
[d3 single selector](https://github.com/mbostock/d3/wiki/Selections#selecting-elements)
specifying a dom block element such as a div; or a dom element or d3 selection.
* chartGroup : string (optional) - name of the chart group this chart instance should
be placed in.
Interaction with a chart will only trigger events and redraws within the chart's group.
Returns:
A newly created bullet chart instance
```javascript
// create a bullet chart under #chart-container1 element using the default global chart group
var chart1 = dc.bulletChart('#chart-container1');
// create a bullet chart under #chart-container2 element using chart group A
var chart2 = dc.bulletChart('#chart-container2', 'chartGroupA');
```
**/
.bulletChart = function (parent, chartGroup) {
dc
var _chart = dc.marginMixin(dc.colorMixin(dc.baseMixin({}))));
//--- specifics ---
//-----------------
return _chart.anchor(parent, chartGroup);
; }
The rationale for the mixin used is:
- Base: you cannot really get away from it
- Margin: useful to get proper spacing around
- Color: allows for selecting the bullet colors
The rendering for the new chart is to be coded in the //--- specifics ---
part.
The source code (yes! It should be better documented, hence this post) of Base Mixin specifies that _doRender
and _doRedraw
are the functions to be implemented in the concrete charts.
Rendering the chart
Luckily for bullet charts there are examples to get inspired from both for the horizontal and the vertical layout.
The _doRender
function is mimicking what done by Mike Bostock in his horizontal layout with the parametrization of the title transform
.
var _bulletMargin = {top: 5, right: 40, bottom: 20, left:120},
= 960 - _bulletMargin.left - _bulletMargin.right,
_bulletWidth = 50 - _bulletMargin.top - _bulletMargin.bottom,
_bulletHeight = "left",
_bulletOrient = titleTranslate(_bulletOrient);
_titleTranslate
._doRender = function () {
_chartvar _bullet = d3.bullet()
.width(_bulletWidth)
.height(_bulletHeight)
.orient(_bulletOrient);
var svg = _chart.root().selectAll("svg")
.data(_chart.data())
.enter().append("svg")
.attr("class", "bullet")
.attr("width", _bulletWidth + _bulletMargin.left + _bulletMargin.right)
.attr("height", _bulletHeight + _bulletMargin.top + _bulletMargin.bottom)
.append("g")
.attr("transform", "translate(" + _bulletMargin.left + "," + _bulletMargin.top + ")")
.call(_bullet);
var title = svg.append("g")
.style("text-anchor", "end")
.attr("transform", "translate(" + _titleTranslate[0] + "," + _titleTranslate[1] + ")");
.append("text")
title.attr("class", "title")
.text(function(d) {
return d.title;
})
.append("text")
title.attr("class", "subtitle")
.attr("dy", "1em")
.text(function(d) {
return d.subtitle;
})
return _chart;
; }
The proper positioning of the title is taken care by the titleTranslate
function. Titles will either be on the left of the bullet bar in the horizontal layout or at the bottom in the vertical one.
Chart options
The bullet chart can be customized in order to produce the desired graph via the getter/setter methods described in the following sections.
These are quite low level customizations, see the To Do section for a better approach.
.bulletWidth
/**
#### .bulletWidth([value])
Set or get the bullet width.
**/
.bulletWidth = function (_) {
_chartif (!arguments.length) {
return _bulletWidth;
}= +_;
_bulletWidth return _chart;
; }
.bulletHeight
/**
#### .bulletHeight([value])
Set or get the bullet height.
**/
.bulletHeight = function (_) {
_chartif (!arguments.length) {
return _bulletHeight;
}= +_;
_bulletHeight return _chart;
; }
.bulletMargin
/**
#### .bulletMargin([value])
Set or get the bullet margin, i.e. `{top: 5, right: 40, bottom: 50, left:120}`.
**/
.bulletMargin = function (_) {
_chartif (!arguments.length) {
return _bulletMargin;
}= _;
_bulletMargin return _chart;
; }
.orient
This method defines the starting point for the bullet.
Note that it influences where the title/subtitle will be positioned: the current implementation of bullet.js
allows for title to either be on the left or at the bottom in the horizontal and vertical layout respectively.
The internal function titleTranslate
sets sensible values for the title position.
/**
#### .orient([value])
Set or get the bullet orientation (one of `"left"`, `"right"`, `"top"` or `"bottom"`).
**/
.orient = function (_) {
_chartif (!arguments.length) {
return _bulletOrient;
}= _;
_bulletOrient = titleTranslate(_bulletOrient);
_titleTranslate return _chart;
; }
titleTranslate (internal)
This internal function sets the right parameters for the positioning of the title/subtitle for the vertical and horizontal layout.
function titleTranslate(orient) {
if (!arguments.length) {
return _titleTranslate;
}
if (_bulletOrient == "left" || _bulletOrient == "right") {
return [-6, _bulletHeight / 2];
}else if (_bulletOrient == "bottom" || _bulletOrient == "top") {
return [_bulletWidth, _bulletHeight + 20]
}
return [-6, _bulletHeight / 2];
}
Example
The bullet.html
file is structured pretty much the same as the other examples:
- the head part,
- the libraries,
- the style part
- the
<div>
’s for the charts - the code for the chart instantiation and rendering
<!DOCTYPE html>
<html lang="en">
<head>
<title>dc.js - Bullet Chart Example</title>
<meta charset="UTF-8">
<link rel="stylesheet" type="text/css" href="../css/dc.css"/>
</head>
<script type="text/javascript" src="../js/d3.js"></script>
<script type="text/javascript" src="../js/crossfilter.js"></script>
<script type="text/javascript" src="../js/dc.js"></script>
<style>
/* see "A Matter of Style" section */
</style>
<div id="test-horizontal"></div>
<div id="test-vertical"></div>
<script type="text/javascript">
// see "The Two Layouts" section
</script>
</html>
The Two Layouts
The example mimics charts in the gists from Mike Bostock and Jason Davies.
There is the usual binding to the <div>
’s, the crossfilter bits and the chart definition and rendering.
Note the trick about how statusGroup
has been defined in order to comply with dc.js
way to pass the data to the underlying d3.js
: this is based on the knowledge that the default implementation of .data()
is returning group.all()
.
var chart1 = dc.bulletChart("#test-horizontal");
var chart2 = dc.bulletChart("#test-vertical");
var data = [
"title":"Revenue","subtitle":"US$, in thousands","ranges":[150,225,300],"measures":[220,270],"markers":[250]},
{"title":"Profit","subtitle":"%","ranges":[20,25,30],"measures":[21,23],"markers":[26]},
{"title":"Order Size","subtitle":"US$, average","ranges":[350,500,600],"measures":[100,320],"markers":[550]},
{"title":"New Customers","subtitle":"count","ranges":[1400,2000,2500],"measures":[1000,1650],"markers":[2100]},
{"title":"Satisfaction","subtitle":"out of 5","ranges":[3.5,4.25,5],"measures":[3.2,4.7],"markers":[4.4]}
{;
]
var ndx = crossfilter(data),
= ndx.dimension(function(d) {return d.title;}),
titleDimension = {
statusGroup all: function(){
return data;
;
}}
// dims from Mike Bostock's bl.ock, https://bl.ocks.org/mbostock/4061961
chart1.width(960)
.height(450)
.bulletMargin({top: 5, right: 40, bottom: 20, left: 120})
.bulletWidth(960 - 120 - 40)
.bulletHeight(50 - 5 - 20)
.orient("left")
.dimension(titleDimension)
.group(statusGroup);
.render();
chart1
// dims from Jason Davies's bl.ock, https://bl.ocks.org/jasondavies/5452290
chart2.width(185)
.height(450)
.bulletMargin({top: 5, right: 40, bottom: 50, left: 120})
.bulletWidth(185 - 120 - 40)
.bulletHeight(450 - 5 - 50)
.orient("top")
.dimension(titleDimension)
.group(statusGroup);
.render(); chart2
A Matter of Style
In the best tradition of the web, everything in the chart can be customized via CSS
body {font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
margin: auto;
padding-top: 40px;
position: relative;
width: 960px;
}
button {position: absolute;
right: 10px;
top: 10px;
}
.bullet { font: 10px sans-serif; }
.bullet .marker { stroke: #000; stroke-width: 2px; }
.bullet .tick line { stroke: #666; stroke-width: .5px;}
.bullet .range.s0 { fill: #eee; }
.bullet .range.s1 { fill: #ddd; }
.bullet .range.s2 { fill: #ccc; }
.bullet .measure.s0 { fill: lightsteelblue; }
.bullet .measure.s1 { fill: steelblue; }
.bullet .title { font-size: 14px; font-weight: bold; }
.bullet .subtitle { fill: #999; }
.bullet .axis line, .bullet .axis path { opacity: 0.5; }
> </style
To Do
The current implementation lacks few feature and refinements.
Tests
I am studying the existing ones and will add them!
Automatic Layout
From a user perspective I would like to just say:
chart.width(960)
.height(450)
.orient("left")
.dimension(titleDimension)
.group(statusGroup);
and have sensible width/height/margins being calculated by the internals of the implementation, with the above methods available for fine-tuning.
Colors selection
Even if I made bulletChart
include the Color Mixin, I haven’t really tackled colors customization.
With code like the following
chart.width(960)
.height(450)
.orient("left")
.dimension(titleDimension)
.group(statusGroup)
.colors(d3.scale.ordinal().range(['red','green','blue']));
you would be able to define the three colors for the bad
, satisfactory
and good
ranges.
Chart Margins
Even if included, the Margin Mixin hasn’t been handled.
These will be the margins for the whole chart, not the ones for the bullets as described above.