From a621c4679c7c4e17430226cb386819e347603fc5 Mon Sep 17 00:00:00 2001 From: Enrico Spinielli Date: Wed, 22 Apr 2015 15:25:37 +0200 Subject: [PATCH] added bullet chart --- Gruntfile.js | 4 +- src/bullet-chart.js | 162 +++++++++++++++++++++++++++++++++++ src/d3.bullet.js | 180 +++++++++++++++++++++++++++++++++++++++ web/docs/api-latest.md | 50 ++++++++++- web/docs/index.html | 39 +++++++++ web/examples/bullet.html | 92 ++++++++++++++++++++ web/examples/index.html | 9 +- 7 files changed, 530 insertions(+), 6 deletions(-) create mode 100644 src/bullet-chart.js create mode 100644 src/d3.bullet.js create mode 100644 web/examples/bullet.html diff --git a/Gruntfile.js b/Gruntfile.js index 18c8ffefe..256eb29ef 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -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'] } } }, @@ -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 ]; diff --git a/src/bullet-chart.js b/src/bullet-chart.js new file mode 100644 index 000000000..a8d7210e1 --- /dev/null +++ b/src/bullet-chart.js @@ -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); +}; diff --git a/src/d3.bullet.js b/src/d3.bullet.js new file mode 100644 index 000000000..3102a8a50 --- /dev/null +++ b/src/d3.bullet.js @@ -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); + }; +} + +})(); diff --git a/web/docs/api-latest.md b/web/docs/api-latest.md index 0b4edab4d..064bd16a3 100644 --- a/web/docs/api-latest.md +++ b/web/docs/api-latest.md @@ -26,6 +26,7 @@ * [Number Display Widget](#number-display-widget) * [Heat Map](#heat-map) * [Box Plot](#box-plot) + * [Bullet Chart](#bullet-chart) #### Version 2.1.0-dev The entire dc.js library is scoped under the **dc** name space. It does not introduce anything else @@ -2022,4 +2023,51 @@ integer formatting. ```js // format ticks to 2 decimal places chart.tickFormat(d3.format('.2f')); -``` \ No newline at end of file +``` + +## 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); +``` + +#### .bulletWidth([value]) +Set or get the bullet width. + +#### .bulletHeight([value]) +Set or get the bullet height. + +#### .bulletMargin([value]) +Set or get the bullet margin, i.e. `{top: 5, right: 40, bottom: 50, left:120}`. + +#### .orient([value]) +Set or get the bullet orientation (one of `"left"`, `"right"`, `"top"` or `"bottom"`). \ No newline at end of file diff --git a/web/docs/index.html b/web/docs/index.html index 808c5b20e..cb696a260 100644 --- a/web/docs/index.html +++ b/web/docs/index.html @@ -141,6 +141,7 @@

DC API

  • Number Display Widget
  • Heat Map
  • Box Plot
  • +
  • Bullet Chart
  • Version 2.1.0-dev

    The entire dc.js library is scoped under the dc name space. It does not introduce anything else @@ -1746,6 +1747,44 @@

    .tickFormat()

    // format ticks to 2 decimal places
     chart.tickFormat(d3.format('.2f'));
     
    +

    Bullet Chart

    +

    Includes: Color Mixin, Margin Mixin, Base Mixin

    +

    Concrete bullet chart implementation.

    +

    Inspired by:

    + +

    Examples:

    + +

    dc.bulletChart(parent[, chartGroup])

    +

    Create a bullet chart instance and attach it to the given parent element.

    +

    Parameters:

    + +

    Returns: +A newly created bullet chart instance

    +
    // 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);
    +
    +

    .bulletWidth([value])

    +

    Set or get the bullet width.

    +

    .bulletHeight([value])

    +

    Set or get the bullet height.

    +

    .bulletMargin([value])

    +

    Set or get the bullet margin, i.e. {top: 5, right: 40, bottom: 50, left:120}.

    +

    .orient([value])

    +

    Set or get the bullet orientation (one of "left", "right", "top" or "bottom").

    diff --git a/web/examples/bullet.html b/web/examples/bullet.html new file mode 100644 index 000000000..91c88ca10 --- /dev/null +++ b/web/examples/bullet.html @@ -0,0 +1,92 @@ + + + +dc.js - Bullet Chart Example + + + + + + + + + +
    +
    + + + \ No newline at end of file diff --git a/web/examples/index.html b/web/examples/index.html index 1f739e3a7..6cb4ddbfa 100644 --- a/web/examples/index.html +++ b/web/examples/index.html @@ -5,7 +5,7 @@

    Examples of using dc.js

    An attempt to present a simple example of each chart type. Contributions welcome.

    -

    Source +

    Source here.

    @@ -13,23 +13,24 @@

    Examples of using dc.js

    - + + - + - +
    bar.html box-plot-time.html box-plot.htmlcomposite.htmlbullet.html
    composite.html cust.html heat.html heatmap-filtering.html line.htmlmulti-focus.html
    multi-focus.html multi-scatter.html number.html ord.html pie.htmlright-axis.html
    right-axis.html scatter-brushing.html scatter-series.html scatter.html