Skip to content

Commit

Permalink
Better number -> string callback for the radial linear scale (#3281)
Browse files Browse the repository at this point in the history
Also create a new Chart.Ticks namespace to host common tick generators and formatters.
  • Loading branch information
etimberg authored and simonbrunel committed Sep 24, 2016
1 parent d407da4 commit d09a17a
Show file tree
Hide file tree
Showing 7 changed files with 213 additions and 110 deletions.
1 change: 1 addition & 0 deletions src/chart.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ require('./core/core.datasetController')(Chart);
require('./core/core.layoutService')(Chart);
require('./core/core.scaleService')(Chart);
require('./core/core.plugin.js')(Chart);
require('./core/core.ticks.js')(Chart);
require('./core/core.scale')(Chart);
require('./core/core.title')(Chart);
require('./core/core.legend')(Chart);
Expand Down
4 changes: 1 addition & 3 deletions src/core/core.scale.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,9 +46,7 @@ module.exports = function(Chart) {
autoSkipPadding: 0,
labelOffset: 0,
// We pass through arrays to be rendered as multiline labels, we convert Others to strings here.
callback: function(value) {
return helpers.isArray(value) ? value : '' + value;
}
callback: Chart.Ticks.formatters.values
}
};

Expand Down
193 changes: 193 additions & 0 deletions src/core/core.ticks.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
'use strict';

module.exports = function(Chart) {

var helpers = Chart.helpers;

/**
* Namespace to hold static tick generation functions
* @namespace Chart.Ticks
*/
Chart.Ticks = {
/**
* Namespace to hold generators for different types of ticks
* @namespace Chart.Ticks.generators
*/
generators: {
/**
* Interface for the options provided to the numeric tick generator
* @interface INumericTickGenerationOptions
*/
/**
* The maximum number of ticks to display
* @name INumericTickGenerationOptions#maxTicks
* @type Number
*/
/**
* The distance between each tick.
* @name INumericTickGenerationOptions#stepSize
* @type Number
* @optional
*/
/**
* Forced minimum for the ticks. If not specified, the minimum of the data range is used to calculate the tick minimum
* @name INumericTickGenerationOptions#min
* @type Number
* @optional
*/
/**
* The maximum value of the ticks. If not specified, the maximum of the data range is used to calculate the tick maximum
* @name INumericTickGenerationOptions#max
* @type Number
* @optional
*/

/**
* Generate a set of linear ticks
* @method Chart.Ticks.generators.linear
* @param generationOptions {INumericTickGenerationOptions} the options used to generate the ticks
* @param dataRange {IRange} the range of the data
* @returns {Array<Number>} array of tick values
*/
linear: function(generationOptions, dataRange) {
var ticks = [];
// To get a "nice" value for the tick spacing, we will use the appropriately named
// "nice number" algorithm. See http://stackoverflow.com/questions/8506881/nice-label-algorithm-for-charts-with-minimum-ticks
// for details.

var spacing;
if (generationOptions.stepSize && generationOptions.stepSize > 0) {
spacing = generationOptions.stepSize;
} else {
var niceRange = helpers.niceNum(dataRange.max - dataRange.min, false);
spacing = helpers.niceNum(niceRange / (generationOptions.maxTicks - 1), true);
}
var niceMin = Math.floor(dataRange.min / spacing) * spacing;
var niceMax = Math.ceil(dataRange.max / spacing) * spacing;
var numSpaces = (niceMax - niceMin) / spacing;

// If very close to our rounded value, use it.
if (helpers.almostEquals(numSpaces, Math.round(numSpaces), spacing / 1000)) {
numSpaces = Math.round(numSpaces);
} else {
numSpaces = Math.ceil(numSpaces);
}

// Put the values into the ticks array
ticks.push(generationOptions.min !== undefined ? generationOptions.min : niceMin);
for (var j = 1; j < numSpaces; ++j) {
ticks.push(niceMin + (j * spacing));
}
ticks.push(generationOptions.max !== undefined ? generationOptions.max : niceMax);

return ticks;
},

/**
* Generate a set of logarithmic ticks
* @method Chart.Ticks.generators.logarithmic
* @param generationOptions {INumericTickGenerationOptions} the options used to generate the ticks
* @param dataRange {IRange} the range of the data
* @returns {Array<Number>} array of tick values
*/
logarithmic: function(generationOptions, dataRange) {
var ticks = [];
var getValueOrDefault = helpers.getValueOrDefault;

// Figure out what the max number of ticks we can support it is based on the size of
// the axis area. For now, we say that the minimum tick spacing in pixels must be 50
// We also limit the maximum number of ticks to 11 which gives a nice 10 squares on
// the graph
var tickVal = getValueOrDefault(generationOptions.min, Math.pow(10, Math.floor(helpers.log10(dataRange.min))));

while (tickVal < dataRange.max) {
ticks.push(tickVal);

var exp;
var significand;

if (tickVal === 0) {
exp = Math.floor(helpers.log10(dataRange.minNotZero));
significand = Math.round(dataRange.minNotZero / Math.pow(10, exp));
} else {
exp = Math.floor(helpers.log10(tickVal));
significand = Math.floor(tickVal / Math.pow(10, exp)) + 1;
}

if (significand === 10) {
significand = 1;
++exp;
}

tickVal = significand * Math.pow(10, exp);
}

var lastTick = getValueOrDefault(generationOptions.max, tickVal);
ticks.push(lastTick);

return ticks;
}
},

/**
* Namespace to hold formatters for different types of ticks
* @namespace Chart.Ticks.formatters
*/
formatters: {
/**
* Formatter for value labels
* @method Chart.Ticks.formatters.values
* @param value the value to display
* @return {String|Array} the label to display
*/
values: function(value) {
return helpers.isArray(value) ? value : '' + value;
},

/**
* Formatter for linear numeric ticks
* @method Chart.Ticks.formatters.linear
* @param tickValue {Number} the value to be formatted
* @param index {Number} the position of the tickValue parameter in the ticks array
* @param ticks {Array<Number>} the list of ticks being converted
* @return {String} string representation of the tickValue parameter
*/
linear: function(tickValue, index, ticks) {
// If we have lots of ticks, don't use the ones
var delta = ticks.length > 3 ? ticks[2] - ticks[1] : ticks[1] - ticks[0];

// If we have a number like 2.5 as the delta, figure out how many decimal places we need
if (Math.abs(delta) > 1) {
if (tickValue !== Math.floor(tickValue)) {
// not an integer
delta = tickValue - Math.floor(tickValue);
}
}

var logDelta = helpers.log10(Math.abs(delta));
var tickString = '';

if (tickValue !== 0) {
var numDecimal = -1 * Math.floor(logDelta);
numDecimal = Math.max(Math.min(numDecimal, 20), 0); // toFixed has a max of 20 decimal places
tickString = tickValue.toFixed(numDecimal);
} else {
tickString = '0'; // never show decimal places for 0
}

return tickString;
},

logarithmic: function(tickValue, index, ticks) {
var remain = tickValue / (Math.pow(10, Math.floor(helpers.log10(tickValue))));

if (tickValue === 0) {
return '0';
} else if (remain === 1 || remain === 2 || remain === 5 || index === 0 || index === ticks.length - 1) {
return tickValue.toExponential();
}
return '';
}
}
};
};
26 changes: 1 addition & 25 deletions src/scales/scale.linear.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,31 +7,7 @@ module.exports = function(Chart) {
var defaultConfig = {
position: 'left',
ticks: {
callback: function(tickValue, index, ticks) {
// If we have lots of ticks, don't use the ones
var delta = ticks.length > 3 ? ticks[2] - ticks[1] : ticks[1] - ticks[0];

// If we have a number like 2.5 as the delta, figure out how many decimal places we need
if (Math.abs(delta) > 1) {
if (tickValue !== Math.floor(tickValue)) {
// not an integer
delta = tickValue - Math.floor(tickValue);
}
}

var logDelta = helpers.log10(Math.abs(delta));
var tickString = '';

if (tickValue !== 0) {
var numDecimal = -1 * Math.floor(logDelta);
numDecimal = Math.max(Math.min(numDecimal, 20), 0); // toFixed has a max of 20 decimal places
tickString = tickValue.toFixed(numDecimal);
} else {
tickString = '0'; // never show decimal places for 0
}

return tickString;
}
callback: Chart.Ticks.formatters.linear
}
};

Expand Down
43 changes: 8 additions & 35 deletions src/scales/scale.linearbase.js
Original file line number Diff line number Diff line change
Expand Up @@ -53,49 +53,22 @@ module.exports = function(Chart) {
buildTicks: function() {
var me = this;
var opts = me.options;
var ticks = me.ticks = [];
var tickOpts = opts.ticks;
var getValueOrDefault = helpers.getValueOrDefault;

// Figure out what the max number of ticks we can support it is based on the size of
// the axis area. For now, we say that the minimum tick spacing in pixels must be 50
// We also limit the maximum number of ticks to 11 which gives a nice 10 squares on
// the graph

// the graph. Make sure we always have at least 2 ticks
var maxTicks = me.getTickLimit();

// Make sure we always have at least 2 ticks
maxTicks = Math.max(2, maxTicks);

// To get a "nice" value for the tick spacing, we will use the appropriately named
// "nice number" algorithm. See http://stackoverflow.com/questions/8506881/nice-label-algorithm-for-charts-with-minimum-ticks
// for details.

var spacing;
var fixedStepSizeSet = (tickOpts.fixedStepSize && tickOpts.fixedStepSize > 0) || (tickOpts.stepSize && tickOpts.stepSize > 0);
if (fixedStepSizeSet) {
spacing = getValueOrDefault(tickOpts.fixedStepSize, tickOpts.stepSize);
} else {
var niceRange = helpers.niceNum(me.max - me.min, false);
spacing = helpers.niceNum(niceRange / (maxTicks - 1), true);
}
var niceMin = Math.floor(me.min / spacing) * spacing;
var niceMax = Math.ceil(me.max / spacing) * spacing;
var numSpaces = (niceMax - niceMin) / spacing;

// If very close to our rounded value, use it.
if (helpers.almostEquals(numSpaces, Math.round(numSpaces), spacing / 1000)) {
numSpaces = Math.round(numSpaces);
} else {
numSpaces = Math.ceil(numSpaces);
}

// Put the values into the ticks array
ticks.push(tickOpts.min !== undefined ? tickOpts.min : niceMin);
for (var j = 1; j < numSpaces; ++j) {
ticks.push(niceMin + (j * spacing));
}
ticks.push(tickOpts.max !== undefined ? tickOpts.max : niceMax);
var numericGeneratorOptions = {
maxTicks: maxTicks,
min: tickOpts.min,
max: tickOpts.max,
stepSize: helpers.getValueOrDefault(tickOpts.fixedStepSize, tickOpts.stepSize)
};
var ticks = me.ticks = Chart.Ticks.generators.linear(numericGeneratorOptions, me);

me.handleDirectionalChanges();

Expand Down
52 changes: 6 additions & 46 deletions src/scales/scale.logarithmic.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,7 @@ module.exports = function(Chart) {

// label settings
ticks: {
callback: function(value, index, arr) {
var remain = value / (Math.pow(10, Math.floor(helpers.log10(value))));

if (value === 0) {
return '0';
} else if (remain === 1 || remain === 2 || remain === 5 || index === 0 || index === arr.length - 1) {
return value.toExponential();
}
return '';
}
callback: Chart.Ticks.formatters.logarithmic
}
};

Expand Down Expand Up @@ -124,43 +115,12 @@ module.exports = function(Chart) {
var me = this;
var opts = me.options;
var tickOpts = opts.ticks;
var getValueOrDefault = helpers.getValueOrDefault;

// Reset the ticks array. Later on, we will draw a grid line at these positions
// The array simply contains the numerical value of the spots where ticks will be
var ticks = me.ticks = [];

// Figure out what the max number of ticks we can support it is based on the size of
// the axis area. For now, we say that the minimum tick spacing in pixels must be 50
// We also limit the maximum number of ticks to 11 which gives a nice 10 squares on
// the graph

var tickVal = getValueOrDefault(tickOpts.min, Math.pow(10, Math.floor(helpers.log10(me.min))));

while (tickVal < me.max) {
ticks.push(tickVal);

var exp;
var significand;

if (tickVal === 0) {
exp = Math.floor(helpers.log10(me.minNotZero));
significand = Math.round(me.minNotZero / Math.pow(10, exp));
} else {
exp = Math.floor(helpers.log10(tickVal));
significand = Math.floor(tickVal / Math.pow(10, exp)) + 1;
}

if (significand === 10) {
significand = 1;
++exp;
}

tickVal = significand * Math.pow(10, exp);
}

var lastTick = getValueOrDefault(tickOpts.max, tickVal);
ticks.push(lastTick);
var generationOptions = {
min: tickOpts.min,
max: tickOpts.max
};
var ticks = me.ticks = Chart.Ticks.generators.logarithmic(generationOptions, me);

if (!me.isHorizontal()) {
// We are in a vertical orientation. The top value is the highest. So reverse the array
Expand Down
4 changes: 3 additions & 1 deletion src/scales/scale.radialLinear.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,9 @@ module.exports = function(Chart) {
backdropPaddingY: 2,

// Number - The backdrop padding to the side of the label in pixels
backdropPaddingX: 2
backdropPaddingX: 2,

callback: Chart.Ticks.formatters.linear
},

pointLabels: {
Expand Down

0 comments on commit d09a17a

Please sign in to comment.