diff --git a/scout-ui/src/minicharts/d3fns/few.js b/scout-ui/src/minicharts/d3fns/few.js index ae9368335bb..936b53e39fe 100644 --- a/scout-ui/src/minicharts/d3fns/few.js +++ b/scout-ui/src/minicharts/d3fns/few.js @@ -11,8 +11,9 @@ module.exports = function(data, g, width, height, options) { var barHeight = 25; var values = _.pluck(data, 'value'); var sumValues = d3.sum(values); + var maxValue = d3.max(values); + var percentFormat = shared.friendlyPercentFormat(maxValue / sumValues * 100); - // data.x is still the label, and data.y the length of the bar var x = d3.scale.linear() .domain([0, sumValues]) .range([0, width]); @@ -26,7 +27,7 @@ module.exports = function(data, g, width, height, options) { } return d.tooltip || tooltipHtml({ label: d.label, - value: shared.percentFormat(d.value / sumValues) + value: percentFormat(d.value / sumValues * 100, false) }); }) .direction('n') diff --git a/scout-ui/src/minicharts/d3fns/many.js b/scout-ui/src/minicharts/d3fns/many.js index d5ea952f236..01604ef5653 100644 --- a/scout-ui/src/minicharts/d3fns/many.js +++ b/scout-ui/src/minicharts/d3fns/many.js @@ -21,6 +21,7 @@ module.exports = function(data, g, width, height, options) { var values = _.pluck(data, 'value'); var maxValue = d3.max(values); var sumValues = d3.sum(values); + var percentFormat = shared.friendlyPercentFormat(maxValue / sumValues * 100); var y = d3.scale.linear() .domain([0, maxValue]) @@ -35,7 +36,7 @@ module.exports = function(data, g, width, height, options) { } return d.tooltip || tooltipHtml({ label: d.label, - value: shared.percentFormat(d.value / sumValues) + value: percentFormat(d.value / sumValues * 100, false) }); }) .direction('n') @@ -46,62 +47,48 @@ module.exports = function(data, g, width, height, options) { g.call(tip); if (options.scale) { - var maxVal = d3.max(y.domain()); + var triples = function(v) { + return [v, v / 2, 0]; + }; + + var scaleLabels = _.map(triples(maxValue / sumValues * 100), function(x) { + return percentFormat(x, true); + }); + var labelScale = d3.scale.ordinal() + .domain(scaleLabels) + .rangePoints([0, height]); // @todo use a scale and wrap both text and line in g element - var legend = g.append('g') + var legend = g.selectAll('.legend') + .data(scaleLabels) + .enter().append('g') .attr('class', 'legend'); - legend.append('text') - .attr('class', 'legend') - .attr('x', 0) - .attr('dx', '-1em') - .attr('y', 0) - .attr('dy', '0.3em') - .attr('text-anchor', 'end') - .text(shared.percentFormat(maxValue / sumValues)); - - legend.append('text') - .attr('class', 'legend') - .attr('x', 0) - .attr('dx', '-1em') - .attr('y', height / 2) - .attr('dy', '0.3em') - .attr('text-anchor', 'end') - .text(shared.percentFormat(maxValue / sumValues / 2)); - - legend.append('text') - .attr('class', 'legend') + legend + .append('text') .attr('x', 0) .attr('dx', '-1em') - .attr('y', height) + .attr('y', function(d) { + return labelScale(d); + }) .attr('dy', '0.3em') .attr('text-anchor', 'end') - .text('0%'); + .text(function(d) { + return d; + }); legend.append('line') .attr('class', 'bg legend') .attr('x1', -5) .attr('x2', width) - .attr('y1', 0) - .attr('y2', 0); - - legend.append('line') - .attr('class', 'bg legend') - .attr('x1', -5) - .attr('x2', width) - .attr('y1', height / 2) - .attr('y2', height / 2); - - legend.append('line') - .attr('class', 'bg legend') - .attr('x1', -5) - .attr('x2', width) - .attr('y1', height) - .attr('y2', height); + .attr('y1', function(d) { + return labelScale(d); + }) + .attr('y2', function(d) { + return labelScale(d); + }); } - var bar = g.selectAll('.bar') .data(data) .enter().append('g') diff --git a/scout-ui/src/minicharts/d3fns/number.js b/scout-ui/src/minicharts/d3fns/number.js index 47c30292cc9..9fd2aad7724 100644 --- a/scout-ui/src/minicharts/d3fns/number.js +++ b/scout-ui/src/minicharts/d3fns/number.js @@ -2,7 +2,6 @@ var d3 = require('d3'); var _ = require('lodash'); var many = require('./many'); var shared = require('./shared'); -var tooltipHtml = require('./tooltip.jade'); var debug = require('debug')('scout-ui:minicharts:number'); module.exports = function(opts) { diff --git a/scout-ui/src/minicharts/d3fns/shared.js b/scout-ui/src/minicharts/d3fns/shared.js index a247f24b3e7..d2a4e88fa33 100644 --- a/scout-ui/src/minicharts/d3fns/shared.js +++ b/scout-ui/src/minicharts/d3fns/shared.js @@ -1,4 +1,11 @@ var d3 = require('d3'); +var debug = require('debug')('scout-ui:minicharts:shared'); + + +// source: http://stackoverflow.com/questions/9539513/is-there-a-reliable-way-in-javascript-to-obtain-the-number-of-decimal-places-of +function decimalPlaces(number) { + return ((+number).toFixed(20)).replace(/^-?\d*\.?|0+$/g, '').length; +} module.exports = { @@ -9,6 +16,22 @@ module.exports = { left: 40 }, - percentFormat: d3.format('%.1f') + friendlyPercentFormat: function(vmax) { + var prec1Format = d3.format('.1r'); + var intFormat = d3.format('.0f'); + var format = (vmax > 1) ? intFormat : prec1Format; + var maxFormatted = format(vmax); + var maxDecimals = decimalPlaces(maxFormatted); + return function(v, incPrec) { + if (v === vmax) { + return maxFormatted + '%'; + } + if (v > 1 && !incPrec) { // v > vmax || maxFormatted % 2 === 0 + return d3.round(v, maxDecimals) + '%'; + } + // adjust for corrections, if increased precision required + return d3.round(v / vmax * maxFormatted, maxDecimals + 1) + '%'; + }; + } }; diff --git a/scout-ui/test/minicharts.test.js b/scout-ui/test/minicharts.test.js new file mode 100644 index 00000000000..b24323869c8 --- /dev/null +++ b/scout-ui/test/minicharts.test.js @@ -0,0 +1,51 @@ +var shared = require('../src/minicharts/d3fns/shared'); +var _ = require('lodash'); +var assert = require('assert'); + +function triples(v) { + return [v, v / 2, 0]; +} + +describe('shared components', function() { + it('should return percentages for top, middle and bottom scale correctly', function() { + assert.deepEqual(_.map(triples(209), function(x) { + return shared.friendlyPercentFormat(209)(x, true); + }), ['209%', '104.5%', '0%']); + assert.deepEqual(_.map(triples(200), function(x) { + return shared.friendlyPercentFormat(200)(x, true); + }), ['200%', '100%', '0%']); + assert.deepEqual(_.map(triples(100), function(x) { + return shared.friendlyPercentFormat(100)(x, true); + }), ['100%', '50%', '0%']); + assert.deepEqual(_.map(triples(99.5), function(x) { + return shared.friendlyPercentFormat(99.5)(x, true); + }), ['100%', '50%', '0%']); + assert.deepEqual(_.map(triples(99.0), function(x) { + return shared.friendlyPercentFormat(99.0)(x, true); + }), ['99%', '49.5%', '0%']); + assert.deepEqual(_.map(triples(99.00001), function(x) { + return shared.friendlyPercentFormat(99.00001)(x, true); + }), ['99%', '49.5%', '0%']); + assert.deepEqual(_.map(triples(49.936), function(x) { + return shared.friendlyPercentFormat(49.936)(x, true); + }), ['50%', '25%', '0%']); + assert.deepEqual(_.map(triples(1.1), function(x) { + return shared.friendlyPercentFormat(1.1)(x, true); + }), ['1%', '0.5%', '0%']); + assert.deepEqual(_.map(triples(0.9), function(x) { + return shared.friendlyPercentFormat(0.9)(x, true); + }), ['0.9%', '0.45%', '0%']); + assert.deepEqual(_.map(triples(0.4), function(x) { + return shared.friendlyPercentFormat(0.4)(x, true); + }), ['0.4%', '0.2%', '0%']); + assert.deepEqual(_.map(triples(0.003), function(x) { + return shared.friendlyPercentFormat(0.003)(x, true); + }), ['0.003%', '0.0015%', '0%']); + assert.deepEqual(_.map(triples(0), function(x) { + return shared.friendlyPercentFormat(0)(x, true); + }), ['0%', '0%', '0%']); + assert.deepEqual(_.map(triples(-1.5), function(x) { + return shared.friendlyPercentFormat(-1.5)(x, true); + }), ['-2%', '-1%', '0%']); + }); +});