From cffd2bc3b826047b533182900b59f57745f0e7ee Mon Sep 17 00:00:00 2001 From: Akihiko Kusanagi Date: Sat, 22 Jul 2017 02:07:49 +0800 Subject: [PATCH] Make `offsetGridLines` behavior consistent and add `ticks.offset` option - Remove `chart.isCombo` - Remove the includeOffset argument from `getPixelForValue()` and `getPixelForTick()` - Add a new `ticks.offset` option to add extra space at edges - Support `'max'` for the `barThickness` option - Bar controller automatically calcurates the bar width to avoid opverlaps - When `offsetGridLines` is true, grid lines move to the left by one half of the tick interval, and labels don't move - Add tests to scales and bar controller --- docs/axes/cartesian/README.md | 3 +- docs/axes/styling.md | 2 +- docs/charts/bar.md | 14 +-- src/controllers/controller.bar.js | 108 ++++++++++++----- src/controllers/controller.bubble.js | 2 +- src/controllers/controller.line.js | 4 +- src/core/core.controller.js | 9 -- src/core/core.scale.js | 47 ++++++-- src/scales/scale.category.js | 20 ++-- src/scales/scale.time.js | 44 ++++++- test/specs/controller.bar.tests.js | 154 +++++++++++++++++++++++++ test/specs/controller.line.tests.js | 2 +- test/specs/core.helpers.tests.js | 2 + test/specs/scale.category.tests.js | 77 +++++-------- test/specs/scale.linear.tests.js | 1 + test/specs/scale.logarithmic.tests.js | 1 + test/specs/scale.radialLinear.tests.js | 1 + test/specs/scale.time.tests.js | 82 +++++++++++++ 18 files changed, 451 insertions(+), 122 deletions(-) diff --git a/docs/axes/cartesian/README.md b/docs/axes/cartesian/README.md index 7d27c625b48..6825d88063d 100644 --- a/docs/axes/cartesian/README.md +++ b/docs/axes/cartesian/README.md @@ -31,6 +31,7 @@ The following options are common to all cartesian axes but do not apply to other | `maxRotation` | `Number` | `90` | Maximum rotation for tick labels when rotating to condense labels. Note: Rotation doesn't occur until necessary. *Note: Only applicable to horizontal scales.* | `minRotation` | `Number` | `0` | Minimum rotation for tick labels. *Note: Only applicable to horizontal scales.* | `mirror` | `Boolean` | `false` | Flips tick labels around axis, displaying the labels inside the chart instead of outside. *Note: Only applicable to vertical scales.* +| `offset` | `Boolean` | `false` | If true, extra space is added to the both edges and the axis is scaled to fit into the chart area. This is set to `true` in the bar chart by default. | `padding` | `Number` | `10` | Padding between the tick label and the axis. When set on a vertical axis, this applies in the horizontal (X) direction. When set on a horizontal axis, this applies in the vertical (Y) direction. ## Axis ID @@ -101,4 +102,4 @@ var myChart = new Chart(ctx, { } } }); -``` \ No newline at end of file +``` diff --git a/docs/axes/styling.md b/docs/axes/styling.md index 255b779d6e3..79cb4e9d66f 100644 --- a/docs/axes/styling.md +++ b/docs/axes/styling.md @@ -21,7 +21,7 @@ The grid line configuration is nested under the scale configuration in the `grid | `zeroLineColor` | Color | `'rgba(0, 0, 0, 0.25)'` | Stroke color of the grid line for the first index (index 0). | `zeroLineBorderDash` | `Number[]` | `[]` | Length and spacing of dashes of the grid line for the first index (index 0). See [MDN](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/setLineDash) | `zeroLineBorderDashOffset` | `Number` | `0` | Offset for line dashes of the grid line for the first index (index 0). See [MDN](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/lineDashOffset) -| `offsetGridLines` | `Boolean` | `false` | If true, labels are shifted to be between grid lines. This is used in the bar chart and should not generally be used. +| `offsetGridLines` | `Boolean` | `false` | If true, grid lines will be shifted to be between labels. This is set to `true` in the bar chart by default. ## Tick Configuration The tick configuration is nested under the scale configuration in the `ticks` key. It defines options for the tick marks that are generated by the axis. diff --git a/docs/charts/bar.md b/docs/charts/bar.md index 8dbc1b48602..7b8ba840ae2 100644 --- a/docs/charts/bar.md +++ b/docs/charts/bar.md @@ -93,16 +93,16 @@ The bar chart defines the following configuration options. These options are mer | Name | Type | Default | Description | ---- | ---- | ------- | ----------- -| `barPercentage` | `Number` | `0.9` | Percent (0-1) of the available width each bar should be within the category percentage. 1.0 will take the whole category width and put the bars right next to each other. [more...](#barpercentage-vs-categorypercentage) -| `categoryPercentage` | `Number` | `0.8` | Percent (0-1) of the available width (the space between the gridlines for small datasets) for each data-point to use for the bars. [more...](#barpercentage-vs-categorypercentage) -| `barThickness` | `Number` | | Manually set width of each bar in pixels. If not set, the bars are sized automatically using `barPercentage` and `categoryPercentage`; -| `maxBarThickness` | `Number` | | Set this to ensure that the automatically sized bars are not sized thicker than this. Only works if barThickness is not set (automatic sizing is enabled). -| `gridLines.offsetGridLines` | `Boolean` | `true` | If true, the bars for a particular data point fall between the grid lines. If false, the grid line will go right down the middle of the bars. [more...](#offsetgridlines) +| `barPercentage` | `Number` | `0.9` | Percent (0-1) of the available width each bar should be within the category width. 1.0 will take the whole category width and put the bars right next to each other. [more...](#barpercentage-vs-categorypercentage) +| `categoryPercentage` | `Number` | `0.8` | Percent (0-1) of the available width each category should be within the sample width. [more...](#barpercentage-vs-categorypercentage) +| `barThickness` | `Number` or `String` | | Manually set width of each bar in pixels. If not set, the bars are sized automatically using `barPercentage` and `categoryPercentage` based on the same sample width. If `'max'` is set, bars are still sized using `barPercentage` and `categoryPercentage` but the sample width for each bar will be maximized without overlaps. +| `maxBarThickness` | `Number` | | Set this to ensure that bars are not sized thicker than this. +| `gridLines.offsetGridLines` | `Boolean` | `true` | If true, the bars for a particular data point fall between the grid lines. The grid line will move to the left by one half of the tick interval. If false, the grid line will go right down the middle of the bars. [more...](#offsetgridlines) ### offsetGridLines -If true, the bars for a particular data point fall between the grid lines. If false, the grid line will go right down the middle of the bars. It is unlikely that this will ever need to be changed in practice. It exists more as a way to reuse the axis code by configuring the existing axis slightly differently. +If true, the bars for a particular data point fall between the grid lines. The grid line will move to the left by one half of the tick interval, which is the space between the grid lines. If false, the grid line will go right down the middle of the bars. This is set to true for a bar chart while false for other charts by default. -This setting applies to the axis configuration for a bar chart. If axes are added to the chart, this setting will need to be set for each new axis. +This setting applies to the axis configuration. If axes are added to the chart, this setting will need to be set for each new axis. ```javascript options = { diff --git a/src/controllers/controller.bar.js b/src/controllers/controller.bar.js index 9c2eb2a1afd..46a487059fc 100644 --- a/src/controllers/controller.bar.js +++ b/src/controllers/controller.bar.js @@ -17,6 +17,11 @@ defaults._set('bar', { categoryPercentage: 0.8, barPercentage: 0.9, + // ticks settings + ticks: { + offset: true + }, + // grid line settings gridLines: { offsetGridLines: true @@ -49,6 +54,11 @@ defaults._set('horizontalBar', { categoryPercentage: 0.8, barPercentage: 0.9, + // ticks settings + ticks: { + offset: true + }, + // grid line settings gridLines: { offsetGridLines: true @@ -154,7 +164,7 @@ module.exports = function(Chart) { var vscale = me.getValueScale(); var base = vscale.getBasePixel(); var horizontal = vscale.isHorizontal(); - var ruler = me._ruler || me.getRuler(); + var ruler = (!me._ruler || index >= me._ruler.length) ? me.getRuler() : me._ruler; var vpixels = me.calculateBarValuePixels(me.index, index); var ipixels = me.calculateBarIndexPixels(me.index, index, ruler); @@ -235,27 +245,76 @@ module.exports = function(Chart) { var me = this; var scale = me.getIndexScale(); var options = scale.options; + var ticksOffset = options.ticks.offset; + var barThickness = options.barThickness; var stackCount = me.getStackCount(); - var fullSize = scale.isHorizontal() ? scale.width : scale.height; - var tickSize = fullSize / scale.getTicks().length; - var categorySize = tickSize * options.categoryPercentage; - var fullBarSize = categorySize / stackCount; - var barSize = fullBarSize * options.barPercentage; + var datasetIndex = me.index; + var length = me.getMeta().data.length; + var data = []; + var ruler = []; + var isHorizontal = scale.isHorizontal(); + var min = isHorizontal ? scale.left : scale.top; + var max = min + (isHorizontal ? scale.width : scale.height); + var minInterval = Infinity; + var prev = -Infinity; + var i, curr, leftSampleSize, rightSampleSize, leftCategorySize, rightCategorySize, fullBarSize, barSize; + + // First pass: store data pixels and calculate the minimum interval + for (i = 0; i < length; ++i) { + curr = scale.getPixelForValue(null, i, datasetIndex); + data.push(curr); + minInterval = Math.min(minInterval, curr - prev); + if (ticksOffset) { + if (curr > min && curr < max) { + minInterval = Math.min(minInterval, Math.min(curr - min, max - curr) * 2); + } + } else { + minInterval = Math.min(minInterval, Math.max(curr - min, max - curr) * 2); + } + prev = curr; + } - barSize = Math.min( - helpers.valueOrDefault(options.barThickness, barSize), - helpers.valueOrDefault(options.maxBarThickness, Infinity)); + // Second pass: calculate the left and right half of bar size separately + for (i = 0; i < length; ++i) { + if (barThickness !== 'max') { + leftSampleSize = rightSampleSize = minInterval / 2; + } else { + if (i > 0) { + leftSampleSize = (data[i] - data[i - 1]) / 2; // half of the left interval + } else if (ticksOffset) { + leftSampleSize = data[0] - min; // offset from the left edge + } else if (length > 1) { + leftSampleSize = (data[1] - data[0]) / 2; // half of the right interval + } else { + leftSampleSize = minInterval / 2; // offset from the farthest edge + } + if (i < length - 1) { + rightSampleSize = (data[i + 1] - data[i]) / 2; // half of the right interval + } else if (ticksOffset) { + rightSampleSize = max - data[length - 1]; // offset from the right edge + } else if (length > 1) { + rightSampleSize = (data[length - 1] - data[length - 2]) / 2; // half of the left interval + } else { + rightSampleSize = minInterval / 2; // offset from the farthest edge + } + } + leftCategorySize = leftSampleSize * options.categoryPercentage; + rightCategorySize = rightSampleSize * options.categoryPercentage; + fullBarSize = (leftCategorySize + rightCategorySize) / stackCount; + barSize = fullBarSize * options.barPercentage; + + barSize = Math.min( + isNaN(barThickness) ? barSize : barThickness, + helpers.valueOrDefault(options.maxBarThickness, Infinity)); + + ruler.push({ + base: data[i] - leftCategorySize + (fullBarSize - barSize) / 2, + fullBarSize: fullBarSize, + barSize: barSize + }); + } - return { - stackCount: stackCount, - tickSize: tickSize, - categorySize: categorySize, - categorySpacing: tickSize - categorySize, - fullBarSize: fullBarSize, - barSize: barSize, - barSpacing: fullBarSize - barSize, - scale: scale - }; + return ruler; }, /** @@ -308,16 +367,11 @@ module.exports = function(Chart) { */ calculateBarIndexPixels: function(datasetIndex, index, ruler) { var me = this; - var scale = ruler.scale; - var isCombo = me.chart.isCombo; var stackIndex = me.getStackIndex(datasetIndex); - var base = scale.getPixelForValue(null, index, datasetIndex, isCombo); - var size = ruler.barSize; + var base = ruler[index].base; + var size = ruler[index].barSize; - base -= isCombo ? ruler.tickSize / 2 : 0; - base += ruler.fullBarSize * stackIndex; - base += ruler.categorySpacing / 2; - base += ruler.barSpacing / 2; + base += ruler[index].fullBarSize * stackIndex; return { size: size, diff --git a/src/controllers/controller.bubble.js b/src/controllers/controller.bubble.js index 58e3793e96e..be229da9def 100644 --- a/src/controllers/controller.bubble.js +++ b/src/controllers/controller.bubble.js @@ -76,7 +76,7 @@ module.exports = function(Chart) { // Desired view properties _model: { - x: reset ? xScale.getPixelForDecimal(0.5) : xScale.getPixelForValue(typeof data === 'object' ? data : NaN, index, dsIndex, me.chart.isCombo), + x: reset ? xScale.getPixelForDecimal(0.5) : xScale.getPixelForValue(typeof data === 'object' ? data : NaN, index, dsIndex), y: reset ? yScale.getBasePixel() : yScale.getPixelForValue(data, index, dsIndex), // Appearance radius: reset ? 0 : custom.radius ? custom.radius : me.getRadius(data), diff --git a/src/controllers/controller.line.js b/src/controllers/controller.line.js index 8fe352ed40a..9d9206d5cb2 100644 --- a/src/controllers/controller.line.js +++ b/src/controllers/controller.line.js @@ -159,8 +159,6 @@ module.exports = function(Chart) { var xScale = me.getScaleForId(meta.xAxisID); var pointOptions = me.chart.options.elements.point; var x, y; - var labels = me.chart.data.labels || []; - var includeOffset = (labels.length === 1 || dataset.data.length === 1) || me.chart.isCombo; // Compatibility: If the properties are defined with only the old name, use those values if ((dataset.radius !== undefined) && (dataset.pointRadius === undefined)) { @@ -170,7 +168,7 @@ module.exports = function(Chart) { dataset.pointHitRadius = dataset.hitRadius; } - x = xScale.getPixelForValue(typeof value === 'object' ? value : NaN, index, datasetIndex, includeOffset); + x = xScale.getPixelForValue(typeof value === 'object' ? value : NaN, index, datasetIndex); y = reset ? yScale.getBasePixel() : me.calculatePointY(value, index, datasetIndex); // Utility diff --git a/src/core/core.controller.js b/src/core/core.controller.js index 28dd9765095..b4739c6af09 100644 --- a/src/core/core.controller.js +++ b/src/core/core.controller.js @@ -312,15 +312,6 @@ module.exports = function(Chart) { } }, me); - if (types.length > 1) { - for (var i = 1; i < types.length; i++) { - if (types[i] !== types[i - 1]) { - me.isCombo = true; - break; - } - } - } - return newControllers; }, diff --git a/src/core/core.scale.js b/src/core/core.scale.js index c584514fbf0..1596b6af8f2 100644 --- a/src/core/core.scale.js +++ b/src/core/core.scale.js @@ -47,6 +47,7 @@ defaults._set('scale', { padding: 0, reverse: false, display: true, + offset: false, autoSkip: true, autoSkipPadding: 0, labelOffset: 0, @@ -521,14 +522,15 @@ module.exports = function(Chart) { getValueForPixel: helpers.noop, // Used for tick location, should - getPixelForTick: function(index, includeOffset) { + getPixelForTick: function(index) { var me = this; + var ticksOffset = me.options.ticks.offset; if (me.isHorizontal()) { var innerWidth = me.width - (me.paddingLeft + me.paddingRight); - var tickWidth = innerWidth / Math.max((me._ticks.length - ((me.options.gridLines.offsetGridLines) ? 0 : 1)), 1); + var tickWidth = innerWidth / Math.max((me.ticks.length - (ticksOffset ? 0 : 1)), 1); var pixel = (tickWidth * index) + me.paddingLeft; - if (includeOffset) { + if (ticksOffset) { pixel += tickWidth / 2; } @@ -541,7 +543,7 @@ module.exports = function(Chart) { }, // Utility for getting the pixel location of a percentage of scale - getPixelForDecimal: function(decimal /* , includeOffset*/) { + getPixelForDecimal: function(decimal) { var me = this; if (me.isHorizontal()) { var innerWidth = me.width - (me.paddingLeft + me.paddingRight); @@ -658,7 +660,7 @@ module.exports = function(Chart) { } var lineWidth, lineColor, borderDash, borderDashOffset; - if (index === (typeof me.zeroLineIndex !== 'undefined' ? me.zeroLineIndex : 0)) { + if (index === (typeof me.zeroLineIndex !== 'undefined' ? me.zeroLineIndex : 0) && (optionTicks.offset === gridLines.offsetGridLines)) { // Draw the first index specially lineWidth = gridLines.zeroLineWidth; lineColor = gridLines.zeroLineColor; @@ -692,8 +694,22 @@ module.exports = function(Chart) { labelY = me.bottom - labelYOffset; } - var xLineValue = me.getPixelForTick(index) + helpers.aliasPixel(lineWidth); // xvalues for grid lines - labelX = me.getPixelForTick(index, gridLines.offsetGridLines) + optionTicks.labelOffset; // x values for optionTicks (need to consider offsetLabel option) + var xLineValue; // xvalues for grid lines + if (gridLines.offsetGridLines && me.ticks.length > 1) { + if (index === 0) { + xLineValue = (me.getPixelForTick(0) * 3 - me.getPixelForTick(1)) / 2; + if (xLineValue < me.left) { + lineColor = 'rgba(0,0,0,0)'; + } + } else { + xLineValue = (me.getPixelForTick(index - 1) + me.getPixelForTick(index)) / 2; + } + } else { + xLineValue = me.getPixelForTick(index); + } + xLineValue += helpers.aliasPixel(lineWidth); + + labelX = me.getPixelForTick(index) + optionTicks.labelOffset; // x values for optionTicks (need to consider offsetLabel option) tx1 = tx2 = x1 = x2 = xLineValue; ty1 = yTickStart; @@ -714,9 +730,22 @@ module.exports = function(Chart) { labelX = isLeft ? me.right - labelXOffset : me.left + labelXOffset; - var yLineValue = me.getPixelForTick(index); // xvalues for grid lines + var yLineValue; // yvalues for grid lines + if (gridLines.offsetGridLines && me.ticks.length > 1) { + if (index === 0) { + yLineValue = (me.getPixelForTick(0) * 3 - me.getPixelForTick(1)) / 2; + if (yLineValue < me.top) { + lineColor = 'rgba(0,0,0,0)'; + } + } else { + yLineValue = (me.getPixelForTick(index - 1) + me.getPixelForTick(index)) / 2; + } + } else { + yLineValue = me.getPixelForTick(index); + } yLineValue += helpers.aliasPixel(lineWidth); - labelY = me.getPixelForTick(index, gridLines.offsetGridLines) + optionTicks.labelOffset; + + labelY = me.getPixelForTick(index) + optionTicks.labelOffset; tx1 = xTickStart; tx2 = xTickEnd; diff --git a/src/scales/scale.category.js b/src/scales/scale.category.js index d5c21e788e4..7af0d006571 100644 --- a/src/scales/scale.category.js +++ b/src/scales/scale.category.js @@ -60,10 +60,11 @@ module.exports = function(Chart) { }, // Used to get data value locations. Value can either be an index or a numerical value - getPixelForValue: function(value, index, datasetIndex, includeOffset) { + getPixelForValue: function(value, index) { var me = this; + var ticksOffset = me.options.ticks.offset; // 1 is added because we need the length but we have the indexes - var offsetAmt = Math.max((me.maxIndex + 1 - me.minIndex - ((me.options.gridLines.offsetGridLines) ? 0 : 1)), 1); + var offsetAmt = Math.max((me.maxIndex + 1 - me.minIndex - (ticksOffset ? 0 : 1)), 1); // If value is a data object, then index is the index in the data array, // not the index of the scale. We need to change that. @@ -82,7 +83,7 @@ module.exports = function(Chart) { var valueWidth = me.width / offsetAmt; var widthOffset = (valueWidth * (index - me.minIndex)); - if (me.options.gridLines.offsetGridLines && includeOffset || me.maxIndex === me.minIndex && includeOffset) { + if (ticksOffset) { widthOffset += (valueWidth / 2); } @@ -91,25 +92,26 @@ module.exports = function(Chart) { var valueHeight = me.height / offsetAmt; var heightOffset = (valueHeight * (index - me.minIndex)); - if (me.options.gridLines.offsetGridLines && includeOffset) { + if (ticksOffset) { heightOffset += (valueHeight / 2); } return me.top + Math.round(heightOffset); }, - getPixelForTick: function(index, includeOffset) { - return this.getPixelForValue(this.ticks[index], index + this.minIndex, null, includeOffset); + getPixelForTick: function(index) { + return this.getPixelForValue(this.ticks[index], index + this.minIndex, null); }, getValueForPixel: function(pixel) { var me = this; + var ticksOffset = me.options.ticks.offset; var value; - var offsetAmt = Math.max((me.ticks.length - ((me.options.gridLines.offsetGridLines) ? 0 : 1)), 1); + var offsetAmt = Math.max((me.ticks.length - (ticksOffset ? 0 : 1)), 1); var horz = me.isHorizontal(); var valueDimension = (horz ? me.width : me.height) / offsetAmt; pixel -= horz ? me.left : me.top; - if (me.options.gridLines.offsetGridLines) { + if (ticksOffset) { pixel -= (valueDimension / 2); } @@ -119,7 +121,7 @@ module.exports = function(Chart) { value = Math.round(pixel / valueDimension); } - return value; + return value + me.minIndex; }, getBasePixel: function() { return this.bottom; diff --git a/src/scales/scale.time.js b/src/scales/scale.time.js index 035991a765b..3f87dd3f07e 100644 --- a/src/scales/scale.time.js +++ b/src/scales/scale.time.js @@ -443,9 +443,9 @@ module.exports = function(Chart) { determineDataLimits: function() { var me = this; var chart = me.chart; - var options = me.options; - var min = parse(options.time.min, me) || MAX_INTEGER; - var max = parse(options.time.max, me) || MIN_INTEGER; + var timeOpts = me.options.time; + var min = parse(timeOpts.min, me) || MAX_INTEGER; + var max = parse(timeOpts.max, me) || MIN_INTEGER; var timestamps = []; var datasets = []; var labels = []; @@ -515,6 +515,7 @@ module.exports = function(Chart) { var min = me.min; var max = me.max; var options = me.options; + var ticksOpts = options.ticks; var timeOpts = options.time; var formats = timeOpts.displayFormats; var capacity = me.getLabelCapacity(min); @@ -524,7 +525,7 @@ module.exports = function(Chart) { var ticks = []; var i, ilen, timestamp; - switch (options.ticks.source) { + switch (ticksOpts.source) { case 'data': timestamps = me._timestamps.data; break; @@ -562,6 +563,37 @@ module.exports = function(Chart) { me._minorFormat = formats[unit]; me._majorFormat = formats[majorUnit]; me._table = buildLookupTable(me._timestamps.data, min, max, options.distribution); + me._leftOffset = 0; + me._rightOffset = 0; + + if (ticksOpts.offset && ticks.length) { + if (!timeOpts.min) { + if (ticks.length > 1) { + me._leftOffset = ( + interpolate(me._table, 'time', ticks[1], 'pos') - + interpolate(me._table, 'time', ticks[0], 'pos') + ) / 2; + } else { + me._leftOffset = ( + interpolate(me._table, 'time', max, 'pos') - + interpolate(me._table, 'time', ticks[0], 'pos') + ); + } + } + if (!timeOpts.max) { + if (ticks.length > 1) { + me._rightOffset = ( + interpolate(me._table, 'time', ticks[ticks.length - 1], 'pos') - + interpolate(me._table, 'time', ticks[ticks.length - 2], 'pos') + ) / 2; + } else { + me._rightOffset = ( + interpolate(me._table, 'time', ticks[ticks.length - 1], 'pos') - + interpolate(me._table, 'time', min, 'pos') + ); + } + } + } return ticksFromTimestamps(ticks, majorUnit); }, @@ -622,7 +654,7 @@ module.exports = function(Chart) { var start = me._horizontal ? me.left : me.top; var pos = interpolate(me._table, 'time', time, 'pos'); - return start + size * pos; + return start + size * (me._leftOffset + pos) / (me._leftOffset + 1 + me._rightOffset); }, getPixelForValue: function(value, index, datasetIndex) { @@ -653,7 +685,7 @@ module.exports = function(Chart) { var me = this; var size = me._horizontal ? me.width : me.height; var start = me._horizontal ? me.left : me.top; - var pos = size ? (pixel - start) / size : 0; + var pos = (size ? (pixel - start) / size : 0) * (me._leftOffset + 1 + me._rightOffset) - me._leftOffset; var time = interpolate(me._table, 'pos', pos, 'time'); return moment(time); diff --git a/test/specs/controller.bar.tests.js b/test/specs/controller.bar.tests.js index 0c69ddc302f..a07a9c4e87d 100644 --- a/test/specs/controller.bar.tests.js +++ b/test/specs/controller.bar.tests.js @@ -1519,4 +1519,158 @@ describe('Bar controller tests', function() { }; }); }); + + describe('Bar thickness with a category scale', function() { + [20, undefined, 'max'].forEach(function(barThickness) { + describe('When barThickness is ' + barThickness, function() { + beforeEach(function() { + this.chart = window.acquireChart({ + type: 'bar', + data: { + datasets: [{ + data: [1, 2] + }, { + data: [1, 2] + }], + labels: ['label1', 'label2', 'label3'] + }, + options: { + scales: { + xAxes: [{ + id: 'x', + type: 'category', + barThickness: barThickness + }], + yAxes: [{ + type: 'linear' + }] + } + } + }); + }); + + it('should correctly set bar width', function() { + var chart = this.chart; + var firstExpected, lastExpected, i, ilen, meta; + + if (!isNaN(barThickness)) { + firstExpected = barThickness; + lastExpected = barThickness; + } else { + var scale = chart.scales.x; + var options = chart.options.scales.xAxes[0]; + var categoryPercentage = options.categoryPercentage; + var barPercentage = options.barPercentage; + var firstInterval = scale.getPixelForTick(1) - scale.getPixelForTick(0); + var lastInterval = scale.left + scale.width - scale.getPixelForTick(1); + + firstExpected = firstInterval * categoryPercentage / 2 * barPercentage; + lastExpected = !barThickness ? firstExpected : + (firstInterval / 2 + lastInterval) * categoryPercentage / 2 * barPercentage; + } + + for (i = 0, ilen = chart.data.datasets.length; i < ilen; ++i) { + meta = chart.getDatasetMeta(i); + expect(meta.data[0]._model.width).toBeCloseToPixel(firstExpected); + expect(meta.data[1]._model.width).toBeCloseToPixel(lastExpected); + } + }); + + it('should correctly set bar width if maxBarThickness is specified', function() { + var chart = this.chart; + var options = chart.options.scales.xAxes[0]; + var i, ilen, meta; + + options.maxBarThickness = 10; + chart.update(); + + for (i = 0, ilen = chart.data.datasets.length; i < ilen; ++i) { + meta = chart.getDatasetMeta(i); + expect(meta.data[0]._model.width).toBeCloseToPixel(10); + expect(meta.data[1]._model.width).toBeCloseToPixel(10); + } + }); + }); + }); + }); + + describe('Bar thickness with a time scale', function() { + ['auto', 'data', 'labels'].forEach(function(source) { + ['series', 'linear'].forEach(function(distribution) { + [undefined, 'max'].forEach(function(barThickness) { + describe('When ticks.source is "' + source + '", distribution is "' + distribution + '" and barThickness is ' + barThickness + '"', function() { + beforeEach(function() { + this.chart = window.acquireChart({ + type: 'bar', + data: { + datasets: [{ + data: [1, 2] + }, { + data: [1, 2] + }], + labels: ['2017', '2018', '2020'] + }, + options: { + scales: { + xAxes: [{ + id: 'x', + type: 'time', + time: { + unit: 'year', + parser: 'YYYY' + }, + ticks: { + source: source, + offset: true + }, + distribution: distribution, + barThickness: barThickness + }], + yAxes: [{ + type: 'linear' + }] + } + } + }); + }); + + it('should correctly set bar width', function() { + var chart = this.chart; + var scale = chart.scales.x; + var options = chart.options.scales.xAxes[0]; + var categoryPercentage = options.categoryPercentage; + var barPercentage = options.barPercentage; + var firstInterval = scale.getPixelForValue('2018') - scale.getPixelForValue('2017'); + var firstExpected = firstInterval * categoryPercentage / 2 * barPercentage; + var lastInterval = scale.left + scale.width - scale.getPixelForValue('2018'); + var lastExpected = barThickness !== 'max' ? firstExpected : + (firstInterval / 2 + lastInterval) * categoryPercentage / 2 * barPercentage; + var i, ilen, meta; + + for (i = 0, ilen = chart.data.datasets.length; i < ilen; ++i) { + meta = chart.getDatasetMeta(i); + expect(meta.data[0]._model.width).toBeCloseToPixel(firstExpected); + expect(meta.data[1]._model.width).toBeCloseToPixel(lastExpected); + } + }); + + it('should correctly set bar width if maxBarThickness is specified', function() { + var chart = this.chart; + var options = chart.options.scales.xAxes[0]; + var i, ilen, meta; + + options.maxBarThickness = 10; + chart.update(); + + for (i = 0, ilen = chart.data.datasets.length; i < ilen; ++i) { + meta = chart.getDatasetMeta(i); + expect(meta.data[0]._model.width).toBeCloseToPixel(10); + expect(meta.data[1]._model.width).toBeCloseToPixel(10); + } + }); + }); + }); + }); + }); + }); }); diff --git a/test/specs/controller.line.tests.js b/test/specs/controller.line.tests.js index 4c9471db935..e1231dc692c 100644 --- a/test/specs/controller.line.tests.js +++ b/test/specs/controller.line.tests.js @@ -250,7 +250,7 @@ describe('Line controller tests', function() { var meta = chart.getDatasetMeta(0); // 1 point var point = meta.data[0]; - expect(point._model.x).toBeCloseToPixel(262); + expect(point._model.x).toBeCloseToPixel(27); // 2 points chart.data.labels = ['One', 'Two']; diff --git a/test/specs/core.helpers.tests.js b/test/specs/core.helpers.tests.js index e6f6264395a..4f4e0493e21 100644 --- a/test/specs/core.helpers.tests.js +++ b/test/specs/core.helpers.tests.js @@ -140,6 +140,7 @@ describe('Core helper tests', function() { padding: 0, reverse: false, display: true, + offset: false, callback: merged.scales.yAxes[1].ticks.callback, // make it nicer, then check explicitly below autoSkip: true, autoSkipPadding: 0, @@ -181,6 +182,7 @@ describe('Core helper tests', function() { padding: 0, reverse: false, display: true, + offset: false, callback: merged.scales.yAxes[2].ticks.callback, // make it nicer, then check explicitly below autoSkip: true, autoSkipPadding: 0, diff --git a/test/specs/scale.category.tests.js b/test/specs/scale.category.tests.js index e7774bf443f..76d4619b9a2 100644 --- a/test/specs/scale.category.tests.js +++ b/test/specs/scale.category.tests.js @@ -42,6 +42,7 @@ describe('Category scale tests', function() { padding: 0, reverse: false, display: true, + offset: false, callback: defaultConfig.ticks.callback, // make this nicer, then check explicitly below autoSkip: true, autoSkipPadding: 0, @@ -214,25 +215,20 @@ describe('Category scale tests', function() { }); var xScale = chart.scales.xScale0; - expect(xScale.getPixelForValue(0, 0, 0, false)).toBeCloseToPixel(23); - expect(xScale.getPixelForValue(0, 0, 0, true)).toBeCloseToPixel(23); - expect(xScale.getValueForPixel(33)).toBe(0); + expect(xScale.getPixelForValue(0, 0, 0)).toBeCloseToPixel(23); + expect(xScale.getValueForPixel(23)).toBe(0); - expect(xScale.getPixelForValue(0, 4, 0, false)).toBeCloseToPixel(487); - expect(xScale.getPixelForValue(0, 4, 0, true)).toBeCloseToPixel(487); + expect(xScale.getPixelForValue(0, 4, 0)).toBeCloseToPixel(487); expect(xScale.getValueForPixel(487)).toBe(4); - xScale.options.gridLines.offsetGridLines = true; + xScale.options.ticks.offset = true; + chart.update(); - expect(xScale.getPixelForValue(0, 0, 0, false)).toBeCloseToPixel(23); - expect(xScale.getPixelForValue(0, 0, 0, true)).toBeCloseToPixel(69); - expect(xScale.getValueForPixel(33)).toBe(0); - expect(xScale.getValueForPixel(78)).toBe(0); + expect(xScale.getPixelForValue(0, 0, 0)).toBeCloseToPixel(69); + expect(xScale.getValueForPixel(69)).toBe(0); - expect(xScale.getPixelForValue(0, 4, 0, false)).toBeCloseToPixel(395); - expect(xScale.getPixelForValue(0, 4, 0, true)).toBeCloseToPixel(441); + expect(xScale.getPixelForValue(0, 4, 0)).toBeCloseToPixel(441); expect(xScale.getValueForPixel(397)).toBe(4); - expect(xScale.getValueForPixel(441)).toBe(4); }); it ('Should get the correct pixel for a value when there are repeated labels', function() { @@ -262,8 +258,8 @@ describe('Category scale tests', function() { }); var xScale = chart.scales.xScale0; - expect(xScale.getPixelForValue('tick_1', 0, 0, false)).toBeCloseToPixel(23); - expect(xScale.getPixelForValue('tick_1', 1, 0, false)).toBeCloseToPixel(139); + expect(xScale.getPixelForValue('tick_1', 0, 0)).toBeCloseToPixel(23); + expect(xScale.getPixelForValue('tick_1', 1, 0)).toBeCloseToPixel(139); }); it ('Should get the correct pixel for a value when horizontal and zoomed', function() { @@ -297,19 +293,14 @@ describe('Category scale tests', function() { }); var xScale = chart.scales.xScale0; - expect(xScale.getPixelForValue(0, 1, 0, false)).toBeCloseToPixel(23); - expect(xScale.getPixelForValue(0, 1, 0, true)).toBeCloseToPixel(23); + expect(xScale.getPixelForValue(0, 1, 0)).toBeCloseToPixel(23); + expect(xScale.getPixelForValue(0, 3, 0)).toBeCloseToPixel(496); - expect(xScale.getPixelForValue(0, 3, 0, false)).toBeCloseToPixel(496); - expect(xScale.getPixelForValue(0, 3, 0, true)).toBeCloseToPixel(496); + xScale.options.ticks.offset = true; + chart.update(); - xScale.options.gridLines.offsetGridLines = true; - - expect(xScale.getPixelForValue(0, 1, 0, false)).toBeCloseToPixel(23); - expect(xScale.getPixelForValue(0, 1, 0, true)).toBeCloseToPixel(102); - - expect(xScale.getPixelForValue(0, 3, 0, false)).toBeCloseToPixel(338); - expect(xScale.getPixelForValue(0, 3, 0, true)).toBeCloseToPixel(419); + expect(xScale.getPixelForValue(0, 1, 0)).toBeCloseToPixel(102); + expect(xScale.getPixelForValue(0, 3, 0)).toBeCloseToPixel(417); }); it ('should get the correct pixel for a value when vertical', function() { @@ -341,24 +332,19 @@ describe('Category scale tests', function() { }); var yScale = chart.scales.yScale0; - expect(yScale.getPixelForValue(0, 0, 0, false)).toBe(32); - expect(yScale.getPixelForValue(0, 0, 0, true)).toBe(32); + expect(yScale.getPixelForValue(0, 0, 0)).toBe(32); expect(yScale.getValueForPixel(32)).toBe(0); - expect(yScale.getPixelForValue(0, 4, 0, false)).toBe(484); - expect(yScale.getPixelForValue(0, 4, 0, true)).toBe(484); + expect(yScale.getPixelForValue(0, 4, 0)).toBe(484); expect(yScale.getValueForPixel(484)).toBe(4); - yScale.options.gridLines.offsetGridLines = true; + yScale.options.ticks.offset = true; + chart.update(); - expect(yScale.getPixelForValue(0, 0, 0, false)).toBe(32); - expect(yScale.getPixelForValue(0, 0, 0, true)).toBe(77); - expect(yScale.getValueForPixel(32)).toBe(0); + expect(yScale.getPixelForValue(0, 0, 0)).toBe(77); expect(yScale.getValueForPixel(77)).toBe(0); - expect(yScale.getPixelForValue(0, 4, 0, false)).toBe(394); - expect(yScale.getPixelForValue(0, 4, 0, true)).toBe(439); - expect(yScale.getValueForPixel(394)).toBe(4); + expect(yScale.getPixelForValue(0, 4, 0)).toBe(439); expect(yScale.getValueForPixel(439)).toBe(4); }); @@ -396,18 +382,13 @@ describe('Category scale tests', function() { var yScale = chart.scales.yScale0; - expect(yScale.getPixelForValue(0, 1, 0, false)).toBe(32); - expect(yScale.getPixelForValue(0, 1, 0, true)).toBe(32); - - expect(yScale.getPixelForValue(0, 3, 0, false)).toBe(484); - expect(yScale.getPixelForValue(0, 3, 0, true)).toBe(484); - - yScale.options.gridLines.offsetGridLines = true; + expect(yScale.getPixelForValue(0, 1, 0)).toBe(32); + expect(yScale.getPixelForValue(0, 3, 0)).toBe(484); - expect(yScale.getPixelForValue(0, 1, 0, false)).toBe(32); - expect(yScale.getPixelForValue(0, 1, 0, true)).toBe(107); + yScale.options.ticks.offset = true; + chart.update(); - expect(yScale.getPixelForValue(0, 3, 0, false)).toBe(333); - expect(yScale.getPixelForValue(0, 3, 0, true)).toBe(409); + expect(yScale.getPixelForValue(0, 1, 0)).toBe(107); + expect(yScale.getPixelForValue(0, 3, 0)).toBe(409); }); }); diff --git a/test/specs/scale.linear.tests.js b/test/specs/scale.linear.tests.js index de653268d73..f5289a59320 100644 --- a/test/specs/scale.linear.tests.js +++ b/test/specs/scale.linear.tests.js @@ -40,6 +40,7 @@ describe('Linear Scale', function() { padding: 0, reverse: false, display: true, + offset: false, callback: defaultConfig.ticks.callback, // make this work nicer, then check below autoSkip: true, autoSkipPadding: 0, diff --git a/test/specs/scale.logarithmic.tests.js b/test/specs/scale.logarithmic.tests.js index 863f7477293..6cb9e2c8543 100644 --- a/test/specs/scale.logarithmic.tests.js +++ b/test/specs/scale.logarithmic.tests.js @@ -39,6 +39,7 @@ describe('Logarithmic Scale tests', function() { padding: 0, reverse: false, display: true, + offset: false, callback: defaultConfig.ticks.callback, // make this nicer, then check explicitly below autoSkip: true, autoSkipPadding: 0, diff --git a/test/specs/scale.radialLinear.tests.js b/test/specs/scale.radialLinear.tests.js index 6164249d652..4f16ffa4246 100644 --- a/test/specs/scale.radialLinear.tests.js +++ b/test/specs/scale.radialLinear.tests.js @@ -56,6 +56,7 @@ describe('Test the radial linear scale', function() { reverse: false, showLabelBackdrop: true, display: true, + offset: false, callback: defaultConfig.ticks.callback, // make this nicer, then check explicitly below autoSkip: true, autoSkipPadding: 0, diff --git a/test/specs/scale.time.tests.js b/test/specs/scale.time.tests.js index 267c2b99a2b..8a00b3d4c98 100755 --- a/test/specs/scale.time.tests.js +++ b/test/specs/scale.time.tests.js @@ -88,6 +88,7 @@ describe('Time scale tests', function() { padding: 0, reverse: false, display: true, + offset: false, callback: defaultConfig.ticks.callback, // make this nicer, then check explicitly below, autoSkip: false, autoSkipPadding: 0, @@ -1087,4 +1088,85 @@ describe('Time scale tests', function() { }); }); }); + + ['auto', 'data', 'labels'].forEach(function(source) { + ['series', 'linear'].forEach(function(distribution) { + describe('when ticks.source is "' + source + '" and distribution is "' + distribution + '"', function() { + beforeEach(function() { + this.chart = window.acquireChart({ + type: 'line', + data: { + labels: ['2017', '2019', '2020', '2025', '2042'], + datasets: [{data: [0, 1, 2, 3, 4, 5]}] + }, + options: { + scales: { + xAxes: [{ + id: 'x', + type: 'time', + time: { + parser: 'YYYY' + }, + ticks: { + source: source + }, + distribution: distribution + }] + } + } + }); + }); + + it ('should not add tick offset from the edges', function() { + var scale = this.chart.scales.x; + + expect(scale.getPixelForValue('2017')).toBeCloseToPixel(scale.left); + expect(scale.getPixelForValue('2042')).toBeCloseToPixel(scale.left + scale.width); + }); + + it ('should add tick offset from the edges if ticks.offset is true', function() { + var chart = this.chart; + var scale = chart.scales.x; + var options = chart.options.scales.xAxes[0]; + + options.ticks.offset = true; + chart.update(); + + var numTicks = scale.ticks.length; + var firstTickInterval = scale.getPixelForTick(1) - scale.getPixelForTick(0); + var lastTickInterval = scale.getPixelForTick(numTicks - 1) - scale.getPixelForTick(numTicks - 2); + + expect(scale.getPixelForValue('2017')).toBeCloseToPixel(scale.left + firstTickInterval / 2); + expect(scale.getPixelForValue('2042')).toBeCloseToPixel(scale.left + scale.width - lastTickInterval / 2); + }); + + it ('should not add offset if min and max extend the labels range', function() { + var chart = this.chart; + var scale = chart.scales.x; + var options = chart.options.scales.xAxes[0]; + + options.time.min = '2012'; + options.time.max = '2051'; + chart.update(); + + expect(scale.getPixelForValue('2012')).toBeCloseToPixel(scale.left); + expect(scale.getPixelForValue('2051')).toBeCloseToPixel(scale.left + scale.width); + }); + + it ('should not add offset if min and max extend the labels range and ticks.offset is true', function() { + var chart = this.chart; + var scale = chart.scales.x; + var options = chart.options.scales.xAxes[0]; + + options.time.min = '2012'; + options.time.max = '2051'; + options.ticks.offset = true; + chart.update(); + + expect(scale.getPixelForValue('2012')).toBeCloseToPixel(scale.left); + expect(scale.getPixelForValue('2051')).toBeCloseToPixel(scale.left + scale.width); + }); + }); + }); + }); });