Skip to content

Commit

Permalink
Make offsetGridLines behavior consistent and add ticks.offset option
Browse files Browse the repository at this point in the history
- 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
  • Loading branch information
nagix committed Aug 5, 2017
1 parent 8dca88c commit cffd2bc
Show file tree
Hide file tree
Showing 18 changed files with 451 additions and 122 deletions.
3 changes: 2 additions & 1 deletion docs/axes/cartesian/README.md
Expand Up @@ -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
Expand Down Expand Up @@ -101,4 +102,4 @@ var myChart = new Chart(ctx, {
}
}
});
```
```
2 changes: 1 addition & 1 deletion docs/axes/styling.md
Expand Up @@ -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.
Expand Down
14 changes: 7 additions & 7 deletions docs/charts/bar.md
Expand Up @@ -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 = {
Expand Down
108 changes: 81 additions & 27 deletions src/controllers/controller.bar.js
Expand Up @@ -17,6 +17,11 @@ defaults._set('bar', {
categoryPercentage: 0.8,
barPercentage: 0.9,

// ticks settings
ticks: {
offset: true
},

// grid line settings
gridLines: {
offsetGridLines: true
Expand Down Expand Up @@ -49,6 +54,11 @@ defaults._set('horizontalBar', {
categoryPercentage: 0.8,
barPercentage: 0.9,

// ticks settings
ticks: {
offset: true
},

// grid line settings
gridLines: {
offsetGridLines: true
Expand Down Expand Up @@ -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);

Expand Down Expand Up @@ -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;
},

/**
Expand Down Expand Up @@ -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,
Expand Down
2 changes: 1 addition & 1 deletion src/controllers/controller.bubble.js
Expand Up @@ -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),
Expand Down
4 changes: 1 addition & 3 deletions src/controllers/controller.line.js
Expand Up @@ -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)) {
Expand All @@ -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
Expand Down
9 changes: 0 additions & 9 deletions src/core/core.controller.js
Expand Up @@ -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;
},

Expand Down
47 changes: 38 additions & 9 deletions src/core/core.scale.js
Expand Up @@ -47,6 +47,7 @@ defaults._set('scale', {
padding: 0,
reverse: false,
display: true,
offset: false,
autoSkip: true,
autoSkipPadding: 0,
labelOffset: 0,
Expand Down Expand Up @@ -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;
}

Expand All @@ -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);
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand All @@ -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;
Expand Down
20 changes: 11 additions & 9 deletions src/scales/scale.category.js
Expand Up @@ -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.
Expand All @@ -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);
}

Expand All @@ -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);
}

Expand All @@ -119,7 +121,7 @@ module.exports = function(Chart) {
value = Math.round(pixel / valueDimension);
}

return value;
return value + me.minIndex;
},
getBasePixel: function() {
return this.bottom;
Expand Down

0 comments on commit cffd2bc

Please sign in to comment.