From 0fb32f3afc3df679a14d4ce145906e082f35d136 Mon Sep 17 00:00:00 2001 From: Thomas Rueckstiess Date: Tue, 26 May 2015 16:45:33 -0400 Subject: [PATCH 1/2] WIP: add new percentFormat() function it returns 3 values, top percentage, halfway percentage, 0%. --- scout-ui/src/minicharts/d3fns/few.js | 2 +- scout-ui/src/minicharts/d3fns/many.js | 10 +++++++++- scout-ui/src/minicharts/d3fns/shared.js | 13 ++++++++++++- scout-ui/test/minicharts.test.js | 19 +++++++++++++++++++ 4 files changed, 41 insertions(+), 3 deletions(-) create mode 100644 scout-ui/test/minicharts.test.js diff --git a/scout-ui/src/minicharts/d3fns/few.js b/scout-ui/src/minicharts/d3fns/few.js index ae9368335bb..e552d4a6c68 100644 --- a/scout-ui/src/minicharts/d3fns/few.js +++ b/scout-ui/src/minicharts/d3fns/few.js @@ -26,7 +26,7 @@ module.exports = function(data, g, width, height, options) { } return d.tooltip || tooltipHtml({ label: d.label, - value: shared.percentFormat(d.value / sumValues) + value: shared.percentFormat(d.value / sumValues)[2] }); }) .direction('n') diff --git a/scout-ui/src/minicharts/d3fns/many.js b/scout-ui/src/minicharts/d3fns/many.js index d5ea952f236..a3d96300dca 100644 --- a/scout-ui/src/minicharts/d3fns/many.js +++ b/scout-ui/src/minicharts/d3fns/many.js @@ -35,7 +35,7 @@ module.exports = function(data, g, width, height, options) { } return d.tooltip || tooltipHtml({ label: d.label, - value: shared.percentFormat(d.value / sumValues) + value: shared.percentFormat(d.value / sumValues)[2] }); }) .direction('n') @@ -47,9 +47,17 @@ module.exports = function(data, g, width, height, options) { if (options.scale) { var maxVal = d3.max(y.domain()); + var scaleLabels = percentFormat(maxVal); // @todo use a scale and wrap both text and line in g element var legend = g.append('g') + .attr('class', 'legend') + .data(scaleLabes) + .enter() + .append('text'); + + + g.append('g') .attr('class', 'legend'); legend.append('text') diff --git a/scout-ui/src/minicharts/d3fns/shared.js b/scout-ui/src/minicharts/d3fns/shared.js index a247f24b3e7..4c1f314dd43 100644 --- a/scout-ui/src/minicharts/d3fns/shared.js +++ b/scout-ui/src/minicharts/d3fns/shared.js @@ -9,6 +9,17 @@ module.exports = { left: 40 }, - percentFormat: d3.format('%.1f') + percentFormat: function(v) { + // round max value to 1 digit precision + var prec1Format = d3.format('.1r'); + var intFormat = d3.format('.0f'); + // multiply by 100 for percentages + v *= 100; + + var top = v > 1 ? intFormat(v) : prec1Format(v); + var mid = parseFloat(top) / 2; + + return ['0%', mid + '%', top + '%']; + } }; diff --git a/scout-ui/test/minicharts.test.js b/scout-ui/test/minicharts.test.js new file mode 100644 index 00000000000..4618340b459 --- /dev/null +++ b/scout-ui/test/minicharts.test.js @@ -0,0 +1,19 @@ +var shared = require('../src/minicharts/d3fns/shared'); +var assert = require('assert'); + +describe('shared components', function() { + it('should return percentages for bottom, middle and top scale correctly', function() { + assert.deepEqual(shared.percentFormat(2.1), ['0%', '105%', '210%']); + assert.deepEqual(shared.percentFormat(2.0), ['0%', '100%', '200%']); + assert.deepEqual(shared.percentFormat(1.0), ['0%', '50%', '100%']); + assert.deepEqual(shared.percentFormat(0.995), ['0%', '50%', '100%']); + assert.deepEqual(shared.percentFormat(0.99), ['0%', '49.5%', '99%']); + assert.deepEqual(shared.percentFormat(0.9900001), ['0%', '49.5%', '99%']); + assert.deepEqual(shared.percentFormat(0.49999), ['0%', '25%', '50%']); + assert.deepEqual(shared.percentFormat(0.011), ['0%', '0.5%', '1%']); + assert.deepEqual(shared.percentFormat(0.009), ['0%', '0.45%', '0.9%']); + assert.deepEqual(shared.percentFormat(0.004), ['0%', '0.2%', '0.4%']); + assert.deepEqual(shared.percentFormat(0.0), ['0%', '0%', '0%']); + assert.deepEqual(shared.percentFormat(-0.015), ['0%', '-1%', '-2%']); + }); +}); From d7ac688b4503abfec4ce59e0f762bb0f6899ed52 Mon Sep 17 00:00:00 2001 From: Thomas Rueckstiess Date: Tue, 26 May 2015 23:36:29 -0400 Subject: [PATCH 2/2] improved rounding & percentage handling complex rounding rules, see minicharts/d3nfs/shared.js feels natural now though. --- scout-ui/src/minicharts/d3fns/few.js | 5 +- scout-ui/src/minicharts/d3fns/many.js | 79 +++++++++---------------- scout-ui/src/minicharts/d3fns/number.js | 1 - scout-ui/src/minicharts/d3fns/shared.js | 30 +++++++--- scout-ui/test/minicharts.test.js | 58 ++++++++++++++---- 5 files changed, 98 insertions(+), 75 deletions(-) diff --git a/scout-ui/src/minicharts/d3fns/few.js b/scout-ui/src/minicharts/d3fns/few.js index e552d4a6c68..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)[2] + 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 a3d96300dca..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)[2] + value: percentFormat(d.value / sumValues * 100, false) }); }) .direction('n') @@ -46,70 +47,48 @@ module.exports = function(data, g, width, height, options) { g.call(tip); if (options.scale) { - var maxVal = d3.max(y.domain()); - var scaleLabels = percentFormat(maxVal); - - // @todo use a scale and wrap both text and line in g element - var legend = g.append('g') - .attr('class', 'legend') - .data(scaleLabes) - .enter() - .append('text'); + 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]); - g.append('g') + // @todo use a scale and wrap both text and line in g element + var legend = g.selectAll('.legend') + .data(scaleLabels) + .enter().append('g') .attr('class', 'legend'); - legend.append('text') - .attr('class', 'legend') + legend + .append('text') .attr('x', 0) .attr('dx', '-1em') - .attr('y', 0) + .attr('y', function(d) { + return labelScale(d); + }) .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') - .attr('x', 0) - .attr('dx', '-1em') - .attr('y', height) - .attr('dy', '0.3em') - .attr('text-anchor', 'end') - .text('0%'); - - legend.append('line') - .attr('class', 'bg legend') - .attr('x1', -5) - .attr('x2', width) - .attr('y1', 0) - .attr('y2', 0); + .text(function(d) { + return d; + }); 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 4c1f314dd43..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,17 +16,22 @@ module.exports = { left: 40 }, - percentFormat: function(v) { - // round max value to 1 digit precision + 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); - // multiply by 100 for percentages - v *= 100; - - var top = v > 1 ? intFormat(v) : prec1Format(v); - var mid = parseFloat(top) / 2; - - return ['0%', mid + '%', top + '%']; + 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 index 4618340b459..b24323869c8 100644 --- a/scout-ui/test/minicharts.test.js +++ b/scout-ui/test/minicharts.test.js @@ -1,19 +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 bottom, middle and top scale correctly', function() { - assert.deepEqual(shared.percentFormat(2.1), ['0%', '105%', '210%']); - assert.deepEqual(shared.percentFormat(2.0), ['0%', '100%', '200%']); - assert.deepEqual(shared.percentFormat(1.0), ['0%', '50%', '100%']); - assert.deepEqual(shared.percentFormat(0.995), ['0%', '50%', '100%']); - assert.deepEqual(shared.percentFormat(0.99), ['0%', '49.5%', '99%']); - assert.deepEqual(shared.percentFormat(0.9900001), ['0%', '49.5%', '99%']); - assert.deepEqual(shared.percentFormat(0.49999), ['0%', '25%', '50%']); - assert.deepEqual(shared.percentFormat(0.011), ['0%', '0.5%', '1%']); - assert.deepEqual(shared.percentFormat(0.009), ['0%', '0.45%', '0.9%']); - assert.deepEqual(shared.percentFormat(0.004), ['0%', '0.2%', '0.4%']); - assert.deepEqual(shared.percentFormat(0.0), ['0%', '0%', '0%']); - assert.deepEqual(shared.percentFormat(-0.015), ['0%', '-1%', '-2%']); + 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%']); }); });