Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

added bullet chart #923

Open
wants to merge 1 commit into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion Gruntfile.js
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ module.exports = function (grunt) {
src: ['<%= conf.src %>/**/*.js', 'Gruntfile.js', '<%= conf.web %>/stock.js'],
options: {
jshintrc: '.jshintrc',
ignores: ['<%= conf.src %>/banner.js', '<%= conf.src %>/footer.js']
ignores: ['<%= conf.src %>/banner.js', '<%= conf.src %>/footer.js', '<%= conf.src %>/d3.bullet.js']
}
}
},
Expand Down Expand Up @@ -415,5 +415,7 @@ module.exports.jsFiles = [
'src/heatmap.js',
'src/d3.box.js',
'src/box-plot.js',
'src/d3.bullet.js',
'src/bullet-chart.js',
'src/footer.js' // NOTE: keep this last
];
162 changes: 162 additions & 0 deletions src/bullet-chart.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
/**
## Bullet Chart
Includes: [Color Mixin](#color-mixin), [Margin Mixin](#margin-mixin), [Base Mixin](#base-mixin)

Concrete bullet chart implementation.

Inspired by:

* [Mike Bostock's bullet chart](http://bl.ocks.org/mbostock/4061961)

Examples:

* ToDO

#### dc.bulletChart(parent[, chartGroup])
Create a bullet chart instance and attach it to the given parent element.

Parameters:
* parent : string | node | selection | compositeChart - 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

```js
// 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');
// create a sub-chart under a composite parent chart
var chart3 = dc.bulletChart(compositeChart);
```

**/
dc.bulletChart = function (parent, chartGroup) {
var _chart = dc.marginMixin(dc.baseMixin({}));

// from http://bl.ocks.org/mbostock/4061961
var _bulletMargin = {top: 5, right: 40, bottom: 20, left:120},
_bulletWidth = 960 - _bulletMargin.left - _bulletMargin.right,
_bulletHeight = 50 - _bulletMargin.top - _bulletMargin.bottom,
_bulletOrient = 'left',
_titleTranslate = [-6, _bulletHeight / 2]; // see default from titleTranslate()

// HELPERS
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];
}

_chart._doRender = function () {
var _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] + ')');

title.append('text')
.attr('class', 'title')
.text(function(d) {
return d.title;
});

title.append('text')
.attr('class', 'subtitle')
.attr('dy', '1em')
.text(function(d) {
return d.subtitle;
});

return _chart;
};

_chart._doRedraw = function () {
_chart._doRender();
return _chart;
};


// SPECIFIC API
/**
#### .bulletWidth([value])
Set or get the bullet width.

**/
_chart.bulletWidth = function (_) {
if (!arguments.length) {
return _bulletWidth;
}
_bulletWidth = +_;
return _chart;
};

/**
#### .bulletHeight([value])
Set or get the bullet height.

**/
_chart.bulletHeight = function (_) {
if (!arguments.length) {
return _bulletHeight;
}
_bulletHeight = +_;
return _chart;
};

/**
#### .bulletMargin([value])
Set or get the bullet margin, i.e. `{top: 5, right: 40, bottom: 50, left:120}`.

**/
_chart.bulletMargin = function (_) {
if (!arguments.length) {
return _bulletMargin;
}
_bulletMargin = _;
return _chart;
};

/**
#### .orient([value])
Set or get the bullet orientation (one of `"left"`, `"right"`, `"top"` or `"bottom"`).

**/
_chart.orient = function (_) {
if (!arguments.length) {
return _bulletOrient;
}
_bulletOrient = _;
_titleTranslate = titleTranslate(_bulletOrient);
return _chart;
};

return _chart.anchor(parent, chartGroup);
};
180 changes: 180 additions & 0 deletions src/d3.bullet.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
(function() {

// Chart design based on the recommendations of Stephen Few. Implementation
// based on the work of Clint Ivy, Jamie Love, and Jason Davies.
// http://projects.instantcognition.com/protovis/bulletchart/
d3.bullet = function() {
var orient = "left",
reverse = false,
vertical = false,
ranges = bulletRanges,
markers = bulletMarkers,
measures = bulletMeasures,
width = 380,
height = 30,
xAxis = d3.svg.axis();

// For each small multiple…
function bullet(g) {
g.each(function(d, i) {
var rangez = ranges.call(this, d, i).slice().sort(d3.descending),
markerz = markers.call(this, d, i).slice().sort(d3.descending),
measurez = measures.call(this, d, i).slice().sort(d3.descending),
g = d3.select(this),
extentX,
extentY;

var wrap = g.select("g.wrap");
if (wrap.empty()) wrap = g.append("g").attr("class", "wrap");

if (vertical) {
extentX = height, extentY = width;
wrap.attr("transform", "rotate(90)translate(0," + -width + ")");
} else {
extentX = width, extentY = height;
wrap.attr("transform", "translate(0)");
}

// Compute the new x-scale.
var x1 = d3.scale.linear()
.domain([0, Math.max(rangez[0], markerz[0], measurez[0])])
.range(reverse ? [extentX, 0] : [0, extentX]);

// Retrieve the old x-scale, if this is an update.
var x0 = this.__chart__ || d3.scale.linear()
.domain([0, Infinity])
.range(x1.range());

// Stash the new scale.
this.__chart__ = x1;

// Derive width-scales from the x-scales.
var w0 = bulletWidth(x0),
w1 = bulletWidth(x1);

// Update the range rects.
var range = wrap.selectAll("rect.range")
.data(rangez);

range.enter().append("rect")
.attr("class", function(d, i) { return "range s" + i; })
.attr("width", w0)
.attr("height", extentY)
.attr("x", reverse ? x0 : 0)

d3.transition(range)
.attr("x", reverse ? x1 : 0)
.attr("width", w1)
.attr("height", extentY);

// Update the measure rects.
var measure = wrap.selectAll("rect.measure")
.data(measurez);

measure.enter().append("rect")
.attr("class", function(d, i) { return "measure s" + i; })
.attr("width", w0)
.attr("height", extentY / 3)
.attr("x", reverse ? x0 : 0)
.attr("y", extentY / 3);

d3.transition(measure)
.attr("width", w1)
.attr("height", extentY / 3)
.attr("x", reverse ? x1 : 0)
.attr("y", extentY / 3);

// Update the marker lines.
var marker = wrap.selectAll("line.marker")
.data(markerz);

marker.enter().append("line")
.attr("class", "marker")
.attr("x1", x0)
.attr("x2", x0)
.attr("y1", extentY / 6)
.attr("y2", extentY * 5 / 6);

d3.transition(marker)
.attr("x1", x1)
.attr("x2", x1)
.attr("y1", extentY / 6)
.attr("y2", extentY * 5 / 6);

var axis = g.selectAll("g.axis").data([0]);
axis.enter().append("g").attr("class", "axis");

if (!vertical) {
axis.attr("transform", "translate(0," + height + ")");
}

axis.call(xAxis.scale(x1));
});
d3.timer.flush();
}

// left, right, top, bottom
bullet.orient = function(_) {
if (!arguments.length) return orient;
orient = _ + "";
reverse = orient == "right" || orient == "bottom";
xAxis.orient((vertical = orient == "top" || orient == "bottom") ? "left" : "bottom");
return bullet;
};

// ranges (bad, satisfactory, good)
bullet.ranges = function(_) {
if (!arguments.length) return ranges;
ranges = _;
return bullet;
};

// markers (previous, goal)
bullet.markers = function(_) {
if (!arguments.length) return markers;
markers = _;
return bullet;
};

// measures (actual, forecast)
bullet.measures = function(_) {
if (!arguments.length) return measures;
measures = _;
return bullet;
};

bullet.width = function(_) {
if (!arguments.length) return width;
width = +_;
return bullet;
};

bullet.height = function(_) {
if (!arguments.length) return height;
height = +_;
return bullet;
};

return d3.rebind(bullet, xAxis, "tickFormat");
};

function bulletRanges(d) {
return d.ranges;
}

function bulletMarkers(d) {
return d.markers;
}

function bulletMeasures(d) {
return d.measures;
}

function bulletWidth(x) {
var x0 = x(0);
return function(d) {
return Math.abs(x(d) - x0);
};
}

})();
Loading