diff --git a/docs/axes/styling.md b/docs/axes/styling.md index 84230f0dbe7..255b779d6e3 100644 --- a/docs/axes/styling.md +++ b/docs/axes/styling.md @@ -35,9 +35,22 @@ The tick configuration is nested under the scale configuration in the `ticks` ke | `fontSize` | `Number` | `12` | Font size for the tick labels. | `fontStyle` | `String` | `'normal'` | Font style for the tick labels, follows CSS font-style options (i.e. normal, italic, oblique, initial, inherit). | `reverse` | `Boolean` | `false` | Reverses order of tick labels. +| `minor` | `object` | `{}` | Minor ticks configuration. Ommited options are inherited from options above. +| `major` | `object` | `{}` | Major ticks configuration. Ommited options are inherited from options above. + +## Minor Tick Configuration +The minorTick configuration is nested under the ticks configuration in the `minor` key. It defines options for the minor tick marks that are generated by the axis. Omitted options are inherited from `ticks` configuration. + +| Name | Type | Default | Description +| -----| ---- | --------| ----------- +| `callback` | `Function` | | Returns the string representation of the tick value as it should be displayed on the chart. See [callback](../axes/labelling.md#creating-custom-tick-formats). +| `fontColor` | Color | `'#666'` | Font color for tick labels. +| `fontFamily` | `String` | `"'Helvetica Neue', 'Helvetica', 'Arial', sans-serif"` | Font family for the tick labels, follows CSS font-family options. +| `fontSize` | `Number` | `12` | Font size for the tick labels. +| `fontStyle` | `String` | `'normal'` | Font style for the tick labels, follows CSS font-style options (i.e. normal, italic, oblique, initial, inherit). ## Major Tick Configuration -The majorTick configuration is nested under the scale configuration in the `majorTicks` key. It defines options for the major tick marks that are generated by the axis. Omitted options are onherited from `ticks` configuration. +The majorTick configuration is nested under the ticks configuration in the `major` key. It defines options for the major tick marks that are generated by the axis. Omitted options are inherited from `ticks` configuration. | Name | Type | Default | Description | -----| ---- | --------| ----------- diff --git a/samples/scales/time/line-point-data.html b/samples/scales/time/line-point-data.html index 537fdf17d1a..95b0fbdd6a4 100644 --- a/samples/scales/time/line-point-data.html +++ b/samples/scales/time/line-point-data.html @@ -73,7 +73,7 @@ x: newDate(5), y: randomScalingFactor() }] - }] + }], }, options: { responsive: true, diff --git a/src/core/core.controller.js b/src/core/core.controller.js index f8c471f3d28..0104b0d8080 100644 --- a/src/core/core.controller.js +++ b/src/core/core.controller.js @@ -265,6 +265,7 @@ module.exports = function(Chart) { }); scales[scale.id] = scale; + scale.mergeTicksOptions(); // TODO(SB): I think we should be able to remove this custom case (options.scale) // and consider it as a regular scale part of the "scales"" map only! This would diff --git a/src/core/core.scale.js b/src/core/core.scale.js index 50aac0e874b..7b6eae48188 100644 --- a/src/core/core.scale.js +++ b/src/core/core.scale.js @@ -48,7 +48,9 @@ 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: Chart.Ticks.formatters.values + callback: Chart.Ticks.formatters.values, + minor: {}, + major: {} } }; @@ -94,6 +96,28 @@ module.exports = function(Chart) { // Any function defined here is inherited by all scale types. // Any function can be extended by the scale type + mergeTicksOptions: function() { + if (this.options.ticks.minor === false) { + this.options.ticks.minor = { + display: false + }; + } + if (this.options.ticks.major === false) { + this.options.ticks.major = { + display: false + }; + } + for (var key in this.options.ticks) { + if (key !== 'major' && key !== 'minor') { + if (typeof this.options.ticks.minor[key] === 'undefined') { + this.options.ticks.minor[key] = this.options.ticks[key]; + } + if (typeof this.options.ticks.major[key] === 'undefined') { + this.options.ticks.major[key] = this.options.ticks[key]; + } + } + } + }, beforeUpdate: function() { helpers.callback(this.options.beforeUpdate, [this]); }, @@ -486,8 +510,8 @@ module.exports = function(Chart) { var context = me.ctx; var globalDefaults = Chart.defaults.global; - var optionTicks = options.ticks; - var optionMajorTicks = options.majorTicks ? options.majorTicks : optionTicks; + var optionTicks = options.ticks.minor; + var optionMajorTicks = options.ticks.major ? options.ticks.major : optionTicks; var gridLines = options.gridLines; var scaleLabel = options.scaleLabel; @@ -547,8 +571,7 @@ module.exports = function(Chart) { var yTickStart = options.position === 'bottom' ? me.top : me.bottom - tl; var yTickEnd = options.position === 'bottom' ? me.top + tl : me.bottom; - helpers.each(me.ticks, function(tick, index) { - var label = typeof tick === 'object' && typeof tick.value !== 'undefined' ? tick.value : tick; + helpers.each(me.ticks, function(label, index) { // If the callback returned a null or undefined value, do not draw this line if (label === undefined || label === null) { return; @@ -629,6 +652,11 @@ module.exports = function(Chart) { ty1 = ty2 = y1 = y2 = yLineValue; } + var major = false; + if (typeof me.majorTicksIndexes !== 'undefined') { + major = me.majorTicksIndexes.indexOf(index) !== -1; + } + itemsToDraw.push({ tx1: tx1, ty1: ty1, @@ -646,7 +674,9 @@ module.exports = function(Chart) { glBorderDashOffset: borderDashOffset, rotation: -1 * labelRotationRadians, label: label, - major: tick.major === true, + tickOptions: major ? optionMajorTicks : optionTicks, + font: major ? majorTickFont.font : tickFont.font, + color: major ? majorTickFontColor : tickFontColor, textBaseline: textBaseline, textAlign: textAlign }); @@ -679,13 +709,13 @@ module.exports = function(Chart) { context.restore(); } - if (optionTicks.display) { + if (itemToDraw.tickOptions.display) { // Make sure we draw text in the correct color and font context.save(); context.translate(itemToDraw.labelX, itemToDraw.labelY); context.rotate(itemToDraw.rotation); - context.font = itemToDraw.major ? majorTickFont.font : tickFont.font; - context.fillStyle = itemToDraw.major ? majorTickFontColor : tickFontColor; + context.font = itemToDraw.font; + context.fillStyle = itemToDraw.color; context.textBaseline = itemToDraw.textBaseline; context.textAlign = itemToDraw.textAlign; diff --git a/src/helpers/helpers.time.js b/src/helpers/helpers.time.js index 670917d1ab0..48999d887d8 100644 --- a/src/helpers/helpers.time.js +++ b/src/helpers/helpers.time.js @@ -215,6 +215,25 @@ module.exports = function(Chart) { min: niceMin, max: niceMax }); + }, + + /** + * return array of major ticks indexes + * @function getMajorTicksIndexes + * @param ticks {Number[]} ticks array to process + * @param majorUnit {String} unit of major tick + * @return {Number[]} array of major ticks indexes + */ + getMajorTicksIndexes: function(ticks, majorUnit) { + var majorTicksIndexes = []; + if (majorUnit) { + for (var i = 0; i < ticks.length; i++) { + if (ticks[i] === moment(ticks[i]).startOf(majorUnit).valueOf()) { + majorTicksIndexes.push(i); + } + } + } + return majorTicksIndexes; } }; diff --git a/src/scales/scale.time.js b/src/scales/scale.time.js index ce98f5679a8..283a0fd892b 100644 --- a/src/scales/scale.time.js +++ b/src/scales/scale.time.js @@ -36,19 +36,6 @@ module.exports = function(Chart) { }, ticks: { autoSkip: false - }, - majorTicks: { - beginAtZero: false, - minRotation: 0, - maxRotation: 50, - mirror: false, - padding: 0, - reverse: false, - display: true, - autoSkip: true, - autoSkipPadding: 0, - labelOffset: 0, - callback: Chart.Ticks.formatters.values } }; @@ -58,17 +45,8 @@ module.exports = function(Chart) { throw new Error('Chart.js - Moment.js could not be found! You must include it before Chart.js to use the time scale. Download at https://momentjs.com'); } - this.mergeTicksOptions(); - Chart.Scale.prototype.initialize.call(this); }, - mergeTicksOptions: function() { - for (var key in this.options.ticks) { - if (typeof this.options.majorTicks[key] === 'undefined') { - this.options.majorTicks[key] = this.options.ticks[key]; - } - } - }, determineDataLimits: function() { var me = this; var timeOpts = me.options.time; @@ -167,6 +145,7 @@ module.exports = function(Chart) { me.majorUnit = majorUnit; var stepSize = timeOpts.stepSize || timeHelpers.determineStepSize(minTimestamp || dataMin, maxTimestamp || dataMax, unit, maxTicks); + me.ticks = timeHelpers.generateTicks({ maxTicks: maxTicks, min: minTimestamp, @@ -180,6 +159,8 @@ module.exports = function(Chart) { max: dataMax }); + me.majorTicksIndexes = timeHelpers.getMajorTicksIndexes(me.ticks, me.majorUnit); + // At this point, we need to update our max and min given the tick values since we have expanded the // range of the scale me.max = helpers.max(me.ticks); @@ -205,33 +186,23 @@ module.exports = function(Chart) { // Function to format an individual tick mark tickFormatFunction: function(tick, index, ticks) { var formattedTick; - var tickClone = tick.clone(); - var tickTimestamp = tick.valueOf(); - var major = false; var tickOpts; - if (this.majorUnit && this.majorDisplayFormat && tickTimestamp === tickClone.startOf(this.majorUnit).valueOf()) { + if (this.majorUnit && this.majorDisplayFormat && this.majorTicksIndexes.indexOf(index) !== -1) { // format as senior unit formattedTick = tick.format(this.majorDisplayFormat); - tickOpts = this.options.majorTicks; - major = true; + tickOpts = this.options.ticks.major; } else { // format as base unit formattedTick = tick.format(this.displayFormat); - tickOpts = this.options.ticks; + tickOpts = this.options.ticks.minor; } var callback = helpers.getValueOrDefault(tickOpts.callback, tickOpts.userCallback); if (callback) { - return { - value: callback(formattedTick, index, ticks), - major: major - }; + return callback(formattedTick, index, ticks); } - return { - value: formattedTick, - major: major - }; + return formattedTick; }, convertTicksToLabels: function() { var me = this; @@ -293,13 +264,14 @@ module.exports = function(Chart) { var cosRotation = Math.cos(helpers.toRadians(ticks.maxRotation)); var sinRotation = Math.sin(helpers.toRadians(ticks.maxRotation)); var tickFontSize = helpers.getValueOrDefault(ticks.fontSize, Chart.defaults.global.defaultFontSize); + return (tickLabelWidth * cosRotation) + (tickFontSize * sinRotation); }, getLabelCapacity: function(exampleTime) { var me = this; me.displayFormat = me.options.time.displayFormats.millisecond; // Pick the longest format for guestimation - var exampleLabel = me.tickFormatFunction(moment(exampleTime), 0, []).value; + var exampleLabel = me.tickFormatFunction(moment(exampleTime), 0, []); var tickLabelWidth = me.getLabelWidth(exampleLabel); var innerWidth = me.isHorizontal() ? me.width : me.height; diff --git a/test/specs/core.helpers.tests.js b/test/specs/core.helpers.tests.js index 65e369d46b3..4ad65b9a579 100644 --- a/test/specs/core.helpers.tests.js +++ b/test/specs/core.helpers.tests.js @@ -215,6 +215,8 @@ describe('Core helper tests', function() { autoSkip: true, autoSkipPadding: 0, labelOffset: 0, + minor: { }, + major: { } }, type: 'linear' }, { @@ -253,6 +255,8 @@ describe('Core helper tests', function() { autoSkip: true, autoSkipPadding: 0, labelOffset: 0, + minor: { }, + major: { } }, type: 'linear' }] diff --git a/test/specs/scale.category.tests.js b/test/specs/scale.category.tests.js index 6eba2c2e2eb..9cbcb461ab2 100644 --- a/test/specs/scale.category.tests.js +++ b/test/specs/scale.category.tests.js @@ -44,7 +44,9 @@ describe('Category scale tests', function() { callback: defaultConfig.ticks.callback, // make this nicer, then check explicitly below autoSkip: true, autoSkipPadding: 0, - labelOffset: 0 + labelOffset: 0, + minor: { }, + major: { } } }); diff --git a/test/specs/scale.linear.tests.js b/test/specs/scale.linear.tests.js index c63cad338f6..c1346185a38 100644 --- a/test/specs/scale.linear.tests.js +++ b/test/specs/scale.linear.tests.js @@ -42,7 +42,9 @@ describe('Linear Scale', function() { callback: defaultConfig.ticks.callback, // make this work nicer, then check below autoSkip: true, autoSkipPadding: 0, - labelOffset: 0 + labelOffset: 0, + minor: { }, + major: { } } }); diff --git a/test/specs/scale.logarithmic.tests.js b/test/specs/scale.logarithmic.tests.js index b1053eefce2..b1986fc6d92 100644 --- a/test/specs/scale.logarithmic.tests.js +++ b/test/specs/scale.logarithmic.tests.js @@ -41,7 +41,9 @@ describe('Logarithmic Scale tests', function() { callback: defaultConfig.ticks.callback, // make this nicer, then check explicitly below autoSkip: true, autoSkipPadding: 0, - labelOffset: 0 + labelOffset: 0, + minor: { }, + major: { } }, }); diff --git a/test/specs/scale.radialLinear.tests.js b/test/specs/scale.radialLinear.tests.js index 3d102cf376f..6454b309895 100644 --- a/test/specs/scale.radialLinear.tests.js +++ b/test/specs/scale.radialLinear.tests.js @@ -58,7 +58,9 @@ describe('Test the radial linear scale', function() { callback: defaultConfig.ticks.callback, // make this nicer, then check explicitly below autoSkip: true, autoSkipPadding: 0, - labelOffset: 0 + labelOffset: 0, + minor: { }, + major: { } }, }); diff --git a/test/specs/scale.time.tests.js b/test/specs/scale.time.tests.js index 94a84bcc543..8cffcd18cd7 100755 --- a/test/specs/scale.time.tests.js +++ b/test/specs/scale.time.tests.js @@ -17,12 +17,6 @@ describe('Time scale tests', function() { return scale; } - function getTicksValues(ticks) { - return ticks.map(function(tick) { - return tick.value; - }); - } - beforeEach(function() { // Need a time matcher for getValueFromPixel jasmine.addMatchers({ @@ -89,20 +83,9 @@ describe('Time scale tests', function() { callback: defaultConfig.ticks.callback, // make this nicer, then check explicitly below, autoSkip: false, autoSkipPadding: 0, - labelOffset: 0 - }, - majorTicks: { - beginAtZero: false, - minRotation: 0, - maxRotation: 50, - mirror: false, - padding: 0, - reverse: false, - display: true, - callback: defaultConfig.majorTicks.callback, // make this nicer, then check explicitly below, - autoSkip: true, - autoSkipPadding: 0, - labelOffset: 0 + labelOffset: 0, + minor: { }, + major: { } }, time: { parser: false, @@ -144,9 +127,8 @@ describe('Time scale tests', function() { var scaleOptions = Chart.scaleService.getScaleDefaults('time'); var scale = createScale(mockData, scaleOptions); scale.update(1000, 200); - var ticks = getTicksValues(scale.ticks); - expect(ticks).toEqual(['Jan 1', 'Jan 2', 'Jan 3', 'Jan 4, 2015', 'Jan 5', 'Jan 6', 'Jan 7', 'Jan 8', 'Jan 9', 'Jan 10', 'Jan 11, 2015']); + expect(scale.ticks).toEqual(['Jan 1', 'Jan 2', 'Jan 3', 'Jan 4, 2015', 'Jan 5', 'Jan 6', 'Jan 7', 'Jan 8', 'Jan 9', 'Jan 10', 'Jan 11, 2015']); }); it('should accept labels as date objects', function() { @@ -155,9 +137,8 @@ describe('Time scale tests', function() { }; var scale = createScale(mockData, Chart.scaleService.getScaleDefaults('time')); scale.update(1000, 200); - var ticks = getTicksValues(scale.ticks); - expect(ticks).toEqual(['Jan 1', 'Jan 2', 'Jan 3', 'Jan 4, 2015', 'Jan 5', 'Jan 6', 'Jan 7', 'Jan 8', 'Jan 9', 'Jan 10', 'Jan 11, 2015']); + expect(scale.ticks).toEqual(['Jan 1', 'Jan 2', 'Jan 3', 'Jan 4, 2015', 'Jan 5', 'Jan 6', 'Jan 7', 'Jan 8', 'Jan 9', 'Jan 10', 'Jan 11, 2015']); }); it('should accept data as xy points', function() { @@ -203,9 +184,8 @@ describe('Time scale tests', function() { var xScale = chart.scales.xScale0; xScale.update(800, 200); - var ticks = getTicksValues(xScale.ticks); - expect(ticks).toEqual(['Jan 1', 'Jan 2', 'Jan 3', 'Jan 4, 2015', 'Jan 5', 'Jan 6', 'Jan 7', 'Jan 8', 'Jan 9', 'Jan 10', 'Jan 11, 2015']); + expect(xScale.ticks).toEqual(['Jan 1', 'Jan 2', 'Jan 3', 'Jan 4, 2015', 'Jan 5', 'Jan 6', 'Jan 7', 'Jan 8', 'Jan 9', 'Jan 10', 'Jan 11, 2015']); }); }); @@ -243,8 +223,8 @@ describe('Time scale tests', function() { var xScale = chart.scales.xScale0; // Counts down because the lines are drawn top to bottom - expect(xScale.ticks[0].value).toEqualOneOf(['Nov 19', 'Nov 20', 'Nov 21']); // handle time zone changes - expect(xScale.ticks[1].value).toEqualOneOf(['Nov 19', 'Nov 20', 'Nov 21']); // handle time zone changes + expect(xScale.ticks[0]).toEqualOneOf(['Nov 19', 'Nov 20', 'Nov 21']); // handle time zone changes + expect(xScale.ticks[1]).toEqualOneOf(['Nov 19', 'Nov 20', 'Nov 21']); // handle time zone changes }); it('should build ticks using the config unit', function() { @@ -257,9 +237,8 @@ describe('Time scale tests', function() { var scale = createScale(mockData, config); scale.update(2500, 200); - var ticks = getTicksValues(scale.ticks); - expect(ticks).toEqual(['8PM', '9PM', '10PM', '11PM', 'Jan 2', '1AM', '2AM', '3AM', '4AM', '5AM', '6AM', '7AM', '8AM', '9AM', '10AM', '11AM', '12PM', '1PM', '2PM', '3PM', '4PM', '5PM', '6PM', '7PM', '8PM', '9PM']); + expect(scale.ticks).toEqual(['8PM', '9PM', '10PM', '11PM', 'Jan 2', '1AM', '2AM', '3AM', '4AM', '5AM', '6AM', '7AM', '8AM', '9AM', '10AM', '11AM', '12PM', '1PM', '2PM', '3PM', '4PM', '5PM', '6PM', '7PM', '8PM', '9PM']); }); it('build ticks honoring the minUnit', function() { @@ -271,9 +250,8 @@ describe('Time scale tests', function() { config.time.minUnit = 'day'; var scale = createScale(mockData, config); - var ticks = getTicksValues(scale.ticks); - expect(ticks).toEqual(['Jan 1', 'Jan 2', 'Jan 3']); + expect(scale.ticks).toEqual(['Jan 1', 'Jan 2', 'Jan 3']); }); it('should build ticks using the config diff', function() { @@ -287,10 +265,9 @@ describe('Time scale tests', function() { var scale = createScale(mockData, config); scale.update(800, 200); - var ticks = getTicksValues(scale.ticks); // last date is feb 15 because we round to start of week - expect(ticks).toEqual(['Dec 28, 2014', 'Jan 4, 2015', 'Jan 11, 2015', 'Jan 18, 2015', 'Jan 25, 2015', 'Feb 2015', 'Feb 8, 2015', 'Feb 15, 2015']); + expect(scale.ticks).toEqual(['Dec 28, 2014', 'Jan 4, 2015', 'Jan 11, 2015', 'Jan 18, 2015', 'Jan 25, 2015', 'Feb 2015', 'Feb 8, 2015', 'Feb 15, 2015']); }); describe('when specifying limits', function() { @@ -308,7 +285,7 @@ describe('Time scale tests', function() { config.time.min = '2014-12-29T04:00:00'; var scale = createScale(mockData, config); - expect(scale.ticks[0].value).toEqual('Dec 29'); + expect(scale.ticks[0]).toEqual('Dec 29'); }); it('should use the max option', function() { @@ -317,7 +294,7 @@ describe('Time scale tests', function() { var scale = createScale(mockData, config); - expect(scale.ticks[scale.ticks.length - 1].value).toEqual('Jan 6'); + expect(scale.ticks[scale.ticks.length - 1]).toEqual('Jan 6'); }); }); @@ -335,9 +312,8 @@ describe('Time scale tests', function() { // Wednesday config.time.isoWeekday = 3; var scale = createScale(mockData, config); - var ticks = getTicksValues(scale.ticks); - expect(ticks).toEqual(['Dec 31, 2014', 'Jan 7, 2015']); + expect(scale.ticks).toEqual(['Dec 31, 2014', 'Jan 7, 2015']); }); describe('when rendering several days', function() { @@ -426,7 +402,7 @@ describe('Time scale tests', function() { it('should build the correct ticks', function() { // Where 'correct' is a two year spacing. - expect(getTicksValues(xScale.ticks)).toEqual(['2005', '2007', '2009', '2011', '2013', '2015', '2017', '2019']); + expect(xScale.ticks).toEqual(['2005', '2007', '2009', '2011', '2013', '2015', '2017', '2019']); }); it('should have ticks with accurate labels', function() { @@ -436,7 +412,7 @@ describe('Time scale tests', function() { for (var i = 0; i < ticks.length - 1; i++) { var offset = 2 * pixelsPerYear * i; expect(xScale.getValueForPixel(xScale.left + offset)).toBeCloseToTime({ - value: moment(ticks[i].value + '-01-01'), + value: moment(ticks[i] + '-01-01'), unit: 'day', threshold: 0.5, });