mirror of
https://github.com/chartjs/Chart.js.git
synced 2026-03-07 08:46:48 +01:00
1192 lines
33 KiB
JavaScript
1192 lines
33 KiB
JavaScript
'use strict';
|
|
|
|
var defaults = require('./core.defaults');
|
|
var Element = require('./core.element');
|
|
var helpers = require('../helpers/index');
|
|
var Ticks = require('./core.ticks');
|
|
|
|
var valueOrDefault = helpers.valueOrDefault;
|
|
var valueAtIndexOrDefault = helpers.valueAtIndexOrDefault;
|
|
|
|
defaults._set('scale', {
|
|
display: true,
|
|
position: 'left',
|
|
offset: false,
|
|
|
|
// grid line settings
|
|
gridLines: {
|
|
display: true,
|
|
color: 'rgba(0, 0, 0, 0.1)',
|
|
lineWidth: 1,
|
|
drawBorder: true,
|
|
drawOnChartArea: true,
|
|
drawTicks: true,
|
|
tickMarkLength: 10,
|
|
zeroLineWidth: 1,
|
|
zeroLineColor: 'rgba(0,0,0,0.25)',
|
|
zeroLineBorderDash: [],
|
|
zeroLineBorderDashOffset: 0.0,
|
|
offsetGridLines: false,
|
|
borderDash: [],
|
|
borderDashOffset: 0.0
|
|
},
|
|
|
|
// scale label
|
|
scaleLabel: {
|
|
// display property
|
|
display: false,
|
|
|
|
// actual label
|
|
labelString: '',
|
|
|
|
// top/bottom padding
|
|
padding: {
|
|
top: 4,
|
|
bottom: 4
|
|
}
|
|
},
|
|
|
|
// label settings
|
|
ticks: {
|
|
beginAtZero: false,
|
|
minRotation: 0,
|
|
maxRotation: 50,
|
|
mirror: false,
|
|
padding: 0,
|
|
reverse: false,
|
|
display: true,
|
|
autoSkip: true,
|
|
autoSkipPadding: 0,
|
|
labelOffset: 0,
|
|
// We pass through arrays to be rendered as multiline labels, we convert Others to strings here.
|
|
callback: Ticks.formatters.values,
|
|
minor: {},
|
|
major: {}
|
|
}
|
|
});
|
|
|
|
function getPixelForGridLine(scale, index, offsetGridLines) {
|
|
var lineValue = scale.getPixelForTick(index);
|
|
|
|
if (offsetGridLines) {
|
|
if (scale.getTicks().length === 1) {
|
|
lineValue -= scale.isHorizontal() ?
|
|
Math.max(lineValue - scale.left, scale.right - lineValue) :
|
|
Math.max(lineValue - scale.top, scale.bottom - lineValue);
|
|
} else if (index === 0) {
|
|
lineValue -= (scale.getPixelForTick(1) - lineValue) / 2;
|
|
} else {
|
|
lineValue -= (lineValue - scale.getPixelForTick(index - 1)) / 2;
|
|
}
|
|
}
|
|
return lineValue;
|
|
}
|
|
|
|
function garbageCollect(caches, length) {
|
|
helpers.each(caches, function(cache) {
|
|
var gc = cache.gc;
|
|
var gcLen = gc.length / 2;
|
|
var i;
|
|
if (gcLen > length) {
|
|
for (i = 0; i < gcLen; ++i) {
|
|
delete cache.data[gc[i]];
|
|
}
|
|
gc.splice(0, gcLen);
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Returns {width, height, offset} objects for the first, last, widest, highest tick
|
|
* labels where offset indicates the anchor point offset from the top in pixels.
|
|
*/
|
|
function computeLabelSizes(ctx, tickFonts, ticks, caches) {
|
|
var length = ticks.length;
|
|
var widths = [];
|
|
var heights = [];
|
|
var offsets = [];
|
|
var i, j, jlen, label, tickFont, fontString, cache, lineHeight, width, height, nestedLabel, widest, highest;
|
|
|
|
for (i = 0; i < length; ++i) {
|
|
label = ticks[i].label;
|
|
tickFont = ticks[i].major ? tickFonts.major : tickFonts.minor;
|
|
ctx.font = fontString = tickFont.string;
|
|
cache = caches[fontString] = caches[fontString] || {data: {}, gc: []};
|
|
lineHeight = tickFont.lineHeight;
|
|
width = height = 0;
|
|
// Undefined labels and arrays should not be measured
|
|
if (!helpers.isNullOrUndef(label) && !helpers.isArray(label)) {
|
|
width = helpers.measureText(ctx, cache.data, cache.gc, width, label);
|
|
height = lineHeight;
|
|
} else if (helpers.isArray(label)) {
|
|
// if it is an array let's measure each element
|
|
for (j = 0, jlen = label.length; j < jlen; ++j) {
|
|
nestedLabel = label[j];
|
|
// Undefined labels and arrays should not be measured
|
|
if (!helpers.isNullOrUndef(nestedLabel) && !helpers.isArray(nestedLabel)) {
|
|
width = helpers.measureText(ctx, cache.data, cache.gc, width, nestedLabel);
|
|
height += lineHeight;
|
|
}
|
|
}
|
|
}
|
|
widths.push(width);
|
|
heights.push(height);
|
|
offsets.push(lineHeight / 2);
|
|
}
|
|
garbageCollect(caches, length);
|
|
|
|
widest = widths.indexOf(Math.max.apply(null, widths));
|
|
highest = heights.indexOf(Math.max.apply(null, heights));
|
|
|
|
function valueAt(idx) {
|
|
return {
|
|
width: widths[idx] || 0,
|
|
height: heights[idx] || 0,
|
|
offset: offsets[idx] || 0
|
|
};
|
|
}
|
|
|
|
return {
|
|
first: valueAt(0),
|
|
last: valueAt(length - 1),
|
|
widest: valueAt(widest),
|
|
highest: valueAt(highest)
|
|
};
|
|
}
|
|
|
|
function getTickMarkLength(options) {
|
|
return options.drawTicks ? options.tickMarkLength : 0;
|
|
}
|
|
|
|
function getScaleLabelHeight(options) {
|
|
var font, padding;
|
|
|
|
if (!options.display) {
|
|
return 0;
|
|
}
|
|
|
|
font = helpers.options._parseFont(options);
|
|
padding = helpers.options.toPadding(options.padding);
|
|
|
|
return font.lineHeight + padding.height;
|
|
}
|
|
|
|
function parseFontOptions(options, nestedOpts) {
|
|
return helpers.extend(helpers.options._parseFont({
|
|
fontFamily: valueOrDefault(nestedOpts.fontFamily, options.fontFamily),
|
|
fontSize: valueOrDefault(nestedOpts.fontSize, options.fontSize),
|
|
fontStyle: valueOrDefault(nestedOpts.fontStyle, options.fontStyle),
|
|
lineHeight: valueOrDefault(nestedOpts.lineHeight, options.lineHeight)
|
|
}), {
|
|
color: helpers.options.resolve([nestedOpts.fontColor, options.fontColor, defaults.global.defaultFontColor])
|
|
});
|
|
}
|
|
|
|
function parseTickFontOptions(options) {
|
|
var minor = parseFontOptions(options, options.minor);
|
|
var major = options.major.enabled ? parseFontOptions(options, options.major) : minor;
|
|
|
|
return {minor: minor, major: major};
|
|
}
|
|
|
|
var Scale = Element.extend({
|
|
/**
|
|
* Get the padding needed for the scale
|
|
* @method getPadding
|
|
* @private
|
|
* @returns {Padding} the necessary padding
|
|
*/
|
|
getPadding: function() {
|
|
var me = this;
|
|
return {
|
|
left: me.paddingLeft || 0,
|
|
top: me.paddingTop || 0,
|
|
right: me.paddingRight || 0,
|
|
bottom: me.paddingBottom || 0
|
|
};
|
|
},
|
|
|
|
/**
|
|
* Returns the scale tick objects ({label, major})
|
|
* @since 2.7
|
|
*/
|
|
getTicks: function() {
|
|
return this._ticks;
|
|
},
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
_getLabels: function() {
|
|
var data = this.chart.data;
|
|
return this.options.labels || (this.isHorizontal() ? data.xLabels : data.yLabels) || data.labels;
|
|
},
|
|
|
|
// These methods are ordered by lifecyle. Utilities then follow.
|
|
// Any function defined here is inherited by all scale types.
|
|
// Any function can be extended by the scale type
|
|
|
|
/**
|
|
* Provided for backward compatibility, not available anymore
|
|
* @function Chart.Scale.mergeTicksOptions
|
|
* @deprecated since version 2.8.0
|
|
* @todo remove at version 3
|
|
*/
|
|
mergeTicksOptions: function() {
|
|
// noop
|
|
},
|
|
|
|
beforeUpdate: function() {
|
|
helpers.callback(this.options.beforeUpdate, [this]);
|
|
},
|
|
|
|
update: function(maxWidth, maxHeight, margins) {
|
|
var me = this;
|
|
var i, ilen, labels, label, ticks, tick;
|
|
|
|
// Update Lifecycle - Probably don't want to ever extend or overwrite this function ;)
|
|
me.beforeUpdate();
|
|
|
|
// Absorb the master measurements
|
|
me.maxWidth = maxWidth;
|
|
me.maxHeight = maxHeight;
|
|
me.margins = helpers.extend({
|
|
left: 0,
|
|
right: 0,
|
|
top: 0,
|
|
bottom: 0
|
|
}, margins);
|
|
|
|
me._maxLabelLines = 0;
|
|
me.longestLabelWidth = 0;
|
|
me.longestTextCache = me.longestTextCache || {};
|
|
me._itemsToDraw = null;
|
|
|
|
// Dimensions
|
|
me.beforeSetDimensions();
|
|
me.setDimensions();
|
|
me.afterSetDimensions();
|
|
|
|
// Data min/max
|
|
me.beforeDataLimits();
|
|
me.determineDataLimits();
|
|
me.afterDataLimits();
|
|
|
|
// Ticks - `this.ticks` is now DEPRECATED!
|
|
// Internal ticks are now stored as objects in the PRIVATE `this._ticks` member
|
|
// and must not be accessed directly from outside this class. `this.ticks` being
|
|
// around for long time and not marked as private, we can't change its structure
|
|
// without unexpected breaking changes. If you need to access the scale ticks,
|
|
// use scale.getTicks() instead.
|
|
|
|
me.beforeBuildTicks();
|
|
|
|
// New implementations should return an array of objects but for BACKWARD COMPAT,
|
|
// we still support no return (`this.ticks` internally set by calling this method).
|
|
ticks = me.buildTicks() || [];
|
|
|
|
// Allow modification of ticks in callback.
|
|
ticks = me.afterBuildTicks(ticks) || ticks;
|
|
|
|
me.beforeTickToLabelConversion();
|
|
|
|
// New implementations should return the formatted tick labels but for BACKWARD
|
|
// COMPAT, we still support no return (`this.ticks` internally changed by calling
|
|
// this method and supposed to contain only string values).
|
|
labels = me.convertTicksToLabels(ticks) || me.ticks;
|
|
|
|
me.afterTickToLabelConversion();
|
|
|
|
me.ticks = labels; // BACKWARD COMPATIBILITY
|
|
|
|
// IMPORTANT: from this point, we consider that `this.ticks` will NEVER change!
|
|
|
|
// BACKWARD COMPAT: synchronize `_ticks` with labels (so potentially `this.ticks`)
|
|
for (i = 0, ilen = labels.length; i < ilen; ++i) {
|
|
label = labels[i];
|
|
tick = ticks[i];
|
|
if (!tick) {
|
|
ticks.push(tick = {
|
|
label: label,
|
|
major: false
|
|
});
|
|
} else {
|
|
tick.label = label;
|
|
}
|
|
}
|
|
|
|
me._ticks = ticks;
|
|
|
|
// Tick Rotation
|
|
me.beforeCalculateTickRotation();
|
|
me.calculateTickRotation();
|
|
me.afterCalculateTickRotation();
|
|
// Fit
|
|
me.beforeFit();
|
|
me.fit();
|
|
me.afterFit();
|
|
//
|
|
me.afterUpdate();
|
|
|
|
return me.minSize;
|
|
|
|
},
|
|
afterUpdate: function() {
|
|
helpers.callback(this.options.afterUpdate, [this]);
|
|
},
|
|
|
|
//
|
|
|
|
beforeSetDimensions: function() {
|
|
helpers.callback(this.options.beforeSetDimensions, [this]);
|
|
},
|
|
setDimensions: function() {
|
|
var me = this;
|
|
// Set the unconstrained dimension before label rotation
|
|
if (me.isHorizontal()) {
|
|
// Reset position before calculating rotation
|
|
me.width = me.maxWidth;
|
|
me.left = 0;
|
|
me.right = me.width;
|
|
} else {
|
|
me.height = me.maxHeight;
|
|
|
|
// Reset position before calculating rotation
|
|
me.top = 0;
|
|
me.bottom = me.height;
|
|
}
|
|
|
|
// Reset padding
|
|
me.paddingLeft = 0;
|
|
me.paddingTop = 0;
|
|
me.paddingRight = 0;
|
|
me.paddingBottom = 0;
|
|
},
|
|
afterSetDimensions: function() {
|
|
helpers.callback(this.options.afterSetDimensions, [this]);
|
|
},
|
|
|
|
// Data limits
|
|
beforeDataLimits: function() {
|
|
helpers.callback(this.options.beforeDataLimits, [this]);
|
|
},
|
|
determineDataLimits: helpers.noop,
|
|
afterDataLimits: function() {
|
|
helpers.callback(this.options.afterDataLimits, [this]);
|
|
},
|
|
|
|
//
|
|
beforeBuildTicks: function() {
|
|
helpers.callback(this.options.beforeBuildTicks, [this]);
|
|
},
|
|
buildTicks: helpers.noop,
|
|
afterBuildTicks: function(ticks) {
|
|
var me = this;
|
|
// ticks is empty for old axis implementations here
|
|
if (helpers.isArray(ticks) && ticks.length) {
|
|
return helpers.callback(me.options.afterBuildTicks, [me, ticks]);
|
|
}
|
|
// Support old implementations (that modified `this.ticks` directly in buildTicks)
|
|
me.ticks = helpers.callback(me.options.afterBuildTicks, [me, me.ticks]) || me.ticks;
|
|
return ticks;
|
|
},
|
|
|
|
beforeTickToLabelConversion: function() {
|
|
helpers.callback(this.options.beforeTickToLabelConversion, [this]);
|
|
},
|
|
convertTicksToLabels: function() {
|
|
var me = this;
|
|
// Convert ticks to strings
|
|
var tickOpts = me.options.ticks;
|
|
me.ticks = me.ticks.map(tickOpts.userCallback || tickOpts.callback, this);
|
|
},
|
|
afterTickToLabelConversion: function() {
|
|
helpers.callback(this.options.afterTickToLabelConversion, [this]);
|
|
},
|
|
|
|
//
|
|
|
|
beforeCalculateTickRotation: function() {
|
|
helpers.callback(this.options.beforeCalculateTickRotation, [this]);
|
|
},
|
|
calculateTickRotation: function() {
|
|
var me = this;
|
|
var options = me.options;
|
|
var tickOpts = options.ticks;
|
|
var ticks = me.getTicks();
|
|
var minRotation = tickOpts.minRotation || 0;
|
|
var maxRotation = tickOpts.maxRotation;
|
|
var labelRotation = minRotation;
|
|
var labelSizes, maxLabelWidth, maxLabelHeight, maxWidth, tickWidth, maxHeight, maxLabelDiagonal;
|
|
|
|
if (me._isVisible() && tickOpts.display) {
|
|
labelSizes = me._labelSizes = computeLabelSizes(me.ctx, parseTickFontOptions(tickOpts), ticks, me.longestTextCache);
|
|
|
|
if (minRotation < maxRotation && ticks.length > 1 && me.isHorizontal()) {
|
|
maxLabelWidth = labelSizes.widest.width;
|
|
maxLabelHeight = labelSizes.highest.height - labelSizes.highest.offset;
|
|
|
|
// Estimate the width of each grid based on the canvas width, the maximum
|
|
// label width and the number of tick intervals
|
|
maxWidth = Math.min(me.maxWidth, me.chart.width - maxLabelWidth);
|
|
tickWidth = options.offset ? me.maxWidth / ticks.length : maxWidth / (ticks.length - 1);
|
|
|
|
// Allow 3 pixels x2 padding either side for label readability
|
|
if (maxLabelWidth + 6 > tickWidth) {
|
|
tickWidth = maxWidth / (ticks.length - (options.offset ? 0.5 : 1));
|
|
maxHeight = me.maxHeight - getTickMarkLength(options.gridLines)
|
|
- tickOpts.padding - getScaleLabelHeight(options.scaleLabel);
|
|
maxLabelDiagonal = Math.sqrt(maxLabelWidth * maxLabelWidth + maxLabelHeight * maxLabelHeight);
|
|
labelRotation = helpers.toDegrees(Math.min(
|
|
Math.asin(Math.min((labelSizes.highest.height + 6) / tickWidth, 1)),
|
|
Math.asin(Math.min(maxHeight / maxLabelDiagonal, 1)) - Math.asin(maxLabelHeight / maxLabelDiagonal)
|
|
));
|
|
labelRotation = Math.max(minRotation, Math.min(maxRotation, labelRotation));
|
|
}
|
|
}
|
|
}
|
|
|
|
me.labelRotation = labelRotation;
|
|
},
|
|
afterCalculateTickRotation: function() {
|
|
helpers.callback(this.options.afterCalculateTickRotation, [this]);
|
|
},
|
|
|
|
//
|
|
|
|
beforeFit: function() {
|
|
helpers.callback(this.options.beforeFit, [this]);
|
|
},
|
|
fit: function() {
|
|
var me = this;
|
|
// Reset
|
|
var minSize = me.minSize = {
|
|
width: 0,
|
|
height: 0
|
|
};
|
|
|
|
var ticks = me.getTicks();
|
|
var opts = me.options;
|
|
var tickOpts = opts.ticks;
|
|
var scaleLabelOpts = opts.scaleLabel;
|
|
var gridLineOpts = opts.gridLines;
|
|
var display = me._isVisible();
|
|
var position = opts.position;
|
|
var isHorizontal = me.isHorizontal();
|
|
|
|
// Width
|
|
if (isHorizontal) {
|
|
minSize.width = me.maxWidth;
|
|
} else if (display) {
|
|
minSize.width = getTickMarkLength(gridLineOpts) + getScaleLabelHeight(scaleLabelOpts);
|
|
}
|
|
|
|
// height
|
|
if (!isHorizontal) {
|
|
minSize.height = me.maxHeight; // fill all the height
|
|
} else if (display) {
|
|
minSize.height = getTickMarkLength(gridLineOpts) + getScaleLabelHeight(scaleLabelOpts);
|
|
}
|
|
|
|
// Don't bother fitting the ticks if we are not showing the labels
|
|
if (tickOpts.display && display) {
|
|
var tickFonts = parseTickFontOptions(tickOpts);
|
|
var labelSizes = me._labelSizes;
|
|
var firstLabelSize = labelSizes.first;
|
|
var lastLabelSize = labelSizes.last;
|
|
var widestLabelSize = labelSizes.widest;
|
|
var highestLabelSize = labelSizes.highest;
|
|
var lineSpace = tickFonts.minor.lineHeight * 0.4;
|
|
var tickPadding = tickOpts.padding;
|
|
|
|
if (isHorizontal) {
|
|
// A horizontal axis is more constrained by the height.
|
|
me.longestLabelWidth = widestLabelSize.width;
|
|
|
|
var isRotated = me.labelRotation !== 0;
|
|
var angleRadians = helpers.toRadians(me.labelRotation);
|
|
var cosRotation = Math.cos(angleRadians);
|
|
var sinRotation = Math.sin(angleRadians);
|
|
|
|
var labelHeight = sinRotation * widestLabelSize.width
|
|
+ cosRotation * (highestLabelSize.height - (isRotated ? highestLabelSize.offset : 0))
|
|
+ (isRotated ? 0 : lineSpace); // padding
|
|
|
|
minSize.height = Math.min(me.maxHeight, minSize.height + labelHeight + tickPadding);
|
|
|
|
var offsetLeft = me.getPixelForTick(0) - me.left;
|
|
var offsetRight = me.right - me.getPixelForTick(ticks.length - 1);
|
|
var paddingLeft, paddingRight;
|
|
|
|
// Ensure that our ticks are always inside the canvas. When rotated, ticks are right aligned
|
|
// which means that the right padding is dominated by the font height
|
|
if (isRotated) {
|
|
paddingLeft = position === 'bottom' ?
|
|
cosRotation * firstLabelSize.width + sinRotation * firstLabelSize.offset :
|
|
sinRotation * (firstLabelSize.height - firstLabelSize.offset);
|
|
paddingRight = position === 'bottom' ?
|
|
sinRotation * (lastLabelSize.height - lastLabelSize.offset) :
|
|
cosRotation * lastLabelSize.width + sinRotation * lastLabelSize.offset;
|
|
} else {
|
|
paddingLeft = firstLabelSize.width / 2;
|
|
paddingRight = lastLabelSize.width / 2;
|
|
}
|
|
|
|
// Adjust padding taking into account changes in offsets
|
|
// and add 3 px to move away from canvas edges
|
|
me.paddingLeft = Math.max((paddingLeft - offsetLeft) * me.width / (me.width - offsetLeft), 0) + 3;
|
|
me.paddingRight = Math.max((paddingRight - offsetRight) * me.width / (me.width - offsetRight), 0) + 3;
|
|
} else {
|
|
// A vertical axis is more constrained by the width. Labels are the
|
|
// dominant factor here, so get that length first and account for padding
|
|
var labelWidth = tickOpts.mirror ? 0 :
|
|
// use lineSpace for consistency with horizontal axis
|
|
// tickPadding is not implemented for horizontal
|
|
widestLabelSize.width + tickPadding + lineSpace;
|
|
|
|
minSize.width = Math.min(me.maxWidth, minSize.width + labelWidth);
|
|
|
|
me.paddingTop = firstLabelSize.height / 2;
|
|
me.paddingBottom = lastLabelSize.height / 2;
|
|
}
|
|
}
|
|
|
|
me.handleMargins();
|
|
|
|
me.width = minSize.width;
|
|
me.height = minSize.height;
|
|
},
|
|
|
|
/**
|
|
* Handle margins and padding interactions
|
|
* @private
|
|
*/
|
|
handleMargins: function() {
|
|
var me = this;
|
|
if (me.margins) {
|
|
me.margins.left = Math.max(me.paddingLeft, me.margins.left);
|
|
me.margins.top = Math.max(me.paddingTop, me.margins.top);
|
|
me.margins.right = Math.max(me.paddingRight, me.margins.right);
|
|
me.margins.bottom = Math.max(me.paddingBottom, me.margins.bottom);
|
|
}
|
|
},
|
|
|
|
afterFit: function() {
|
|
helpers.callback(this.options.afterFit, [this]);
|
|
},
|
|
|
|
// Shared Methods
|
|
isHorizontal: function() {
|
|
return this.options.position === 'top' || this.options.position === 'bottom';
|
|
},
|
|
isFullWidth: function() {
|
|
return (this.options.fullWidth);
|
|
},
|
|
|
|
// Get the correct value. NaN bad inputs, If the value type is object get the x or y based on whether we are horizontal or not
|
|
getRightValue: function(rawValue) {
|
|
// Null and undefined values first
|
|
if (helpers.isNullOrUndef(rawValue)) {
|
|
return NaN;
|
|
}
|
|
// isNaN(object) returns true, so make sure NaN is checking for a number; Discard Infinite values
|
|
if ((typeof rawValue === 'number' || rawValue instanceof Number) && !isFinite(rawValue)) {
|
|
return NaN;
|
|
}
|
|
|
|
// If it is in fact an object, dive in one more level
|
|
if (rawValue) {
|
|
if (this.isHorizontal()) {
|
|
if (rawValue.x !== undefined) {
|
|
return this.getRightValue(rawValue.x);
|
|
}
|
|
} else if (rawValue.y !== undefined) {
|
|
return this.getRightValue(rawValue.y);
|
|
}
|
|
}
|
|
|
|
// Value is good, return it
|
|
return rawValue;
|
|
},
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
_parseValue: function(value) {
|
|
var start, end, min, max;
|
|
|
|
if (helpers.isArray(value)) {
|
|
start = +this.getRightValue(value[0]);
|
|
end = +this.getRightValue(value[1]);
|
|
min = Math.min(start, end);
|
|
max = Math.max(start, end);
|
|
} else {
|
|
value = +this.getRightValue(value);
|
|
start = undefined;
|
|
end = value;
|
|
min = value;
|
|
max = value;
|
|
}
|
|
|
|
return {
|
|
min: min,
|
|
max: max,
|
|
start: start,
|
|
end: end
|
|
};
|
|
},
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
_getScaleLabel: function(rawValue) {
|
|
var v = this._parseValue(rawValue);
|
|
if (v.start !== undefined) {
|
|
return '[' + v.start + ', ' + v.end + ']';
|
|
}
|
|
|
|
return +this.getRightValue(rawValue);
|
|
},
|
|
|
|
/**
|
|
* Used to get the value to display in the tooltip for the data at the given index
|
|
* @param index
|
|
* @param datasetIndex
|
|
*/
|
|
getLabelForIndex: helpers.noop,
|
|
|
|
/**
|
|
* Returns the location of the given data point. Value can either be an index or a numerical value
|
|
* The coordinate (0, 0) is at the upper-left corner of the canvas
|
|
* @param value
|
|
* @param index
|
|
* @param datasetIndex
|
|
*/
|
|
getPixelForValue: helpers.noop,
|
|
|
|
/**
|
|
* Used to get the data value from a given pixel. This is the inverse of getPixelForValue
|
|
* The coordinate (0, 0) is at the upper-left corner of the canvas
|
|
* @param pixel
|
|
*/
|
|
getValueForPixel: helpers.noop,
|
|
|
|
/**
|
|
* Returns the location of the tick at the given index
|
|
* The coordinate (0, 0) is at the upper-left corner of the canvas
|
|
*/
|
|
getPixelForTick: function(index) {
|
|
var me = this;
|
|
var offset = me.options.offset;
|
|
var numTicks = me._ticks.length;
|
|
if (index < 0 || index > numTicks - 1) {
|
|
return null;
|
|
}
|
|
if (me.isHorizontal()) {
|
|
var tickWidth = me.width / Math.max((numTicks - (offset ? 0 : 1)), 1);
|
|
var pixel = (tickWidth * index);
|
|
|
|
if (offset) {
|
|
pixel += tickWidth / 2;
|
|
}
|
|
|
|
return me.left + pixel;
|
|
}
|
|
return me.top + (index * (me.height / (numTicks - 1)));
|
|
},
|
|
|
|
/**
|
|
* Utility for getting the pixel location of a percentage of scale
|
|
* The coordinate (0, 0) is at the upper-left corner of the canvas
|
|
*/
|
|
getPixelForDecimal: function(decimal) {
|
|
var me = this;
|
|
return me.isHorizontal()
|
|
? me.left + decimal * me.width
|
|
: me.top + decimal * me.height;
|
|
},
|
|
|
|
/**
|
|
* Returns the pixel for the minimum chart value
|
|
* The coordinate (0, 0) is at the upper-left corner of the canvas
|
|
*/
|
|
getBasePixel: function() {
|
|
return this.getPixelForValue(this.getBaseValue());
|
|
},
|
|
|
|
getBaseValue: function() {
|
|
var me = this;
|
|
var min = me.min;
|
|
var max = me.max;
|
|
|
|
return me.beginAtZero ? 0 :
|
|
min < 0 && max < 0 ? max :
|
|
min > 0 && max > 0 ? min :
|
|
0;
|
|
},
|
|
|
|
/**
|
|
* Returns a subset of ticks to be plotted to avoid overlapping labels.
|
|
* @private
|
|
*/
|
|
_autoSkip: function(ticks) {
|
|
var me = this;
|
|
var isHorizontal = me.isHorizontal();
|
|
var optionTicks = me.options.ticks;
|
|
var tickCount = ticks.length;
|
|
var skipRatio = false;
|
|
var maxTicks = optionTicks.maxTicksLimit;
|
|
|
|
// Total space needed to display all ticks. First and last ticks are
|
|
// drawn as their center at end of axis, so tickCount-1
|
|
var ticksLength = me._tickSize() * (tickCount - 1);
|
|
|
|
// Axis length
|
|
var axisLength = isHorizontal ? me.width : me.height;
|
|
|
|
var result = [];
|
|
var i, tick;
|
|
|
|
if (ticksLength > axisLength) {
|
|
skipRatio = 1 + Math.floor(ticksLength / axisLength);
|
|
}
|
|
|
|
// if they defined a max number of optionTicks,
|
|
// increase skipRatio until that number is met
|
|
if (tickCount > maxTicks) {
|
|
skipRatio = Math.max(skipRatio, 1 + Math.floor(tickCount / maxTicks));
|
|
}
|
|
|
|
for (i = 0; i < tickCount; i++) {
|
|
tick = ticks[i];
|
|
|
|
if (skipRatio > 1 && i % skipRatio > 0) {
|
|
// leave tick in place but make sure it's not displayed (#4635)
|
|
delete tick.label;
|
|
}
|
|
result.push(tick);
|
|
}
|
|
return result;
|
|
},
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
_tickSize: function() {
|
|
var me = this;
|
|
var isHorizontal = me.isHorizontal();
|
|
var optionTicks = me.options.ticks;
|
|
|
|
// Calculate space needed by label in axis direction.
|
|
var rot = helpers.toRadians(me.labelRotation);
|
|
var cos = Math.abs(Math.cos(rot));
|
|
var sin = Math.abs(Math.sin(rot));
|
|
|
|
var labelSizes = me._labelSizes;
|
|
var padding = optionTicks.autoSkipPadding || 0;
|
|
var w = labelSizes ? labelSizes.widest.width + padding : 0;
|
|
var h = labelSizes ? labelSizes.highest.height + padding : 0;
|
|
|
|
// Calculate space needed for 1 tick in axis direction.
|
|
return isHorizontal
|
|
? h * cos > w * sin ? w / cos : h / sin
|
|
: h * sin < w * cos ? h / cos : w / sin;
|
|
},
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
_isVisible: function() {
|
|
var me = this;
|
|
var chart = me.chart;
|
|
var display = me.options.display;
|
|
var i, ilen, meta;
|
|
|
|
if (display !== 'auto') {
|
|
return !!display;
|
|
}
|
|
|
|
// When 'auto', the scale is visible if at least one associated dataset is visible.
|
|
for (i = 0, ilen = chart.data.datasets.length; i < ilen; ++i) {
|
|
if (chart.isDatasetVisible(i)) {
|
|
meta = chart.getDatasetMeta(i);
|
|
if (meta.xAxisID === me.id || meta.yAxisID === me.id) {
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
|
|
return false;
|
|
},
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
_computeItemsToDraw: function(chartArea) {
|
|
var me = this;
|
|
var chart = me.chart;
|
|
var options = me.options;
|
|
var optionTicks = options.ticks;
|
|
var gridLines = options.gridLines;
|
|
var position = options.position;
|
|
|
|
var isRotated = me.labelRotation !== 0;
|
|
var isMirrored = optionTicks.mirror;
|
|
var isHorizontal = me.isHorizontal();
|
|
|
|
var ticks = optionTicks.display && optionTicks.autoSkip ? me._autoSkip(me.getTicks()) : me.getTicks();
|
|
var tickFonts = parseTickFontOptions(optionTicks);
|
|
var tickPadding = optionTicks.padding;
|
|
var labelOffset = optionTicks.labelOffset;
|
|
|
|
var tl = getTickMarkLength(gridLines);
|
|
|
|
var labelRotationRadians = helpers.toRadians(me.labelRotation);
|
|
|
|
var items = [];
|
|
|
|
var epsilon = 0.0000001; // 0.0000001 is margin in pixels for Accumulated error.
|
|
|
|
var axisWidth = gridLines.drawBorder ? valueAtIndexOrDefault(gridLines.lineWidth, 0, 0) : 0;
|
|
var alignPixel = helpers._alignPixel;
|
|
var borderValue, tickStart, tickEnd;
|
|
|
|
if (position === 'top') {
|
|
borderValue = alignPixel(chart, me.bottom, axisWidth);
|
|
tickStart = me.bottom - tl;
|
|
tickEnd = borderValue - axisWidth / 2;
|
|
} else if (position === 'bottom') {
|
|
borderValue = alignPixel(chart, me.top, axisWidth);
|
|
tickStart = borderValue + axisWidth / 2;
|
|
tickEnd = me.top + tl;
|
|
} else if (position === 'left') {
|
|
borderValue = alignPixel(chart, me.right, axisWidth);
|
|
tickStart = me.right - tl;
|
|
tickEnd = borderValue - axisWidth / 2;
|
|
} else {
|
|
borderValue = alignPixel(chart, me.left, axisWidth);
|
|
tickStart = borderValue + axisWidth / 2;
|
|
tickEnd = me.left + tl;
|
|
}
|
|
|
|
helpers.each(ticks, function(tick, index) {
|
|
// autoskipper skipped this tick (#4635)
|
|
if (helpers.isNullOrUndef(tick.label)) {
|
|
return;
|
|
}
|
|
|
|
var label = tick.label;
|
|
var tickFont = tick.major ? tickFonts.major : tickFonts.minor;
|
|
var lineHeight = tickFont.lineHeight;
|
|
var lineWidth, lineColor, borderDash, borderDashOffset;
|
|
if (index === me.zeroLineIndex && options.offset === gridLines.offsetGridLines) {
|
|
// Draw the first index specially
|
|
lineWidth = gridLines.zeroLineWidth;
|
|
lineColor = gridLines.zeroLineColor;
|
|
borderDash = gridLines.zeroLineBorderDash || [];
|
|
borderDashOffset = gridLines.zeroLineBorderDashOffset || 0.0;
|
|
} else {
|
|
lineWidth = valueAtIndexOrDefault(gridLines.lineWidth, index);
|
|
lineColor = valueAtIndexOrDefault(gridLines.color, index);
|
|
borderDash = gridLines.borderDash || [];
|
|
borderDashOffset = gridLines.borderDashOffset || 0.0;
|
|
}
|
|
|
|
// Common properties
|
|
var tx1, ty1, tx2, ty2, x1, y1, x2, y2, labelX, labelY, textOffset, textAlign;
|
|
var labelCount = helpers.isArray(label) ? label.length : 1;
|
|
var lineValue = getPixelForGridLine(me, index, gridLines.offsetGridLines);
|
|
|
|
if (isHorizontal) {
|
|
var labelYOffset = tl + tickPadding;
|
|
|
|
if (lineValue < me.left - epsilon) {
|
|
lineColor = 'rgba(0,0,0,0)';
|
|
}
|
|
|
|
tx1 = tx2 = x1 = x2 = alignPixel(chart, lineValue, lineWidth);
|
|
ty1 = tickStart;
|
|
ty2 = tickEnd;
|
|
labelX = me.getPixelForTick(index) + labelOffset; // x values for optionTicks (need to consider offsetLabel option)
|
|
|
|
if (position === 'top') {
|
|
y1 = alignPixel(chart, chartArea.top, axisWidth) + axisWidth / 2;
|
|
y2 = chartArea.bottom;
|
|
textOffset = ((!isRotated ? 0.5 : 1) - labelCount) * lineHeight;
|
|
textAlign = !isRotated ? 'center' : 'left';
|
|
labelY = me.bottom - labelYOffset;
|
|
} else {
|
|
y1 = chartArea.top;
|
|
y2 = alignPixel(chart, chartArea.bottom, axisWidth) - axisWidth / 2;
|
|
textOffset = (!isRotated ? 0.5 : 0) * lineHeight;
|
|
textAlign = !isRotated ? 'center' : 'right';
|
|
labelY = me.top + labelYOffset;
|
|
}
|
|
} else {
|
|
var labelXOffset = (isMirrored ? 0 : tl) + tickPadding;
|
|
|
|
if (lineValue < me.top - epsilon) {
|
|
lineColor = 'rgba(0,0,0,0)';
|
|
}
|
|
|
|
tx1 = tickStart;
|
|
tx2 = tickEnd;
|
|
ty1 = ty2 = y1 = y2 = alignPixel(chart, lineValue, lineWidth);
|
|
labelY = me.getPixelForTick(index) + labelOffset;
|
|
textOffset = (1 - labelCount) * lineHeight / 2;
|
|
|
|
if (position === 'left') {
|
|
x1 = alignPixel(chart, chartArea.left, axisWidth) + axisWidth / 2;
|
|
x2 = chartArea.right;
|
|
textAlign = isMirrored ? 'left' : 'right';
|
|
labelX = me.right - labelXOffset;
|
|
} else {
|
|
x1 = chartArea.left;
|
|
x2 = alignPixel(chart, chartArea.right, axisWidth) - axisWidth / 2;
|
|
textAlign = isMirrored ? 'right' : 'left';
|
|
labelX = me.left + labelXOffset;
|
|
}
|
|
}
|
|
|
|
items.push({
|
|
tx1: tx1,
|
|
ty1: ty1,
|
|
tx2: tx2,
|
|
ty2: ty2,
|
|
x1: x1,
|
|
y1: y1,
|
|
x2: x2,
|
|
y2: y2,
|
|
labelX: labelX,
|
|
labelY: labelY,
|
|
glWidth: lineWidth,
|
|
glColor: lineColor,
|
|
glBorderDash: borderDash,
|
|
glBorderDashOffset: borderDashOffset,
|
|
rotation: -1 * labelRotationRadians,
|
|
label: label,
|
|
major: tick.major,
|
|
font: tick.major ? tickFonts.major : tickFonts.minor,
|
|
textOffset: textOffset,
|
|
textAlign: textAlign
|
|
});
|
|
});
|
|
|
|
items.ticksLength = ticks.length;
|
|
items.borderValue = borderValue;
|
|
|
|
return items;
|
|
},
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
_drawGrid: function(chartArea) {
|
|
var me = this;
|
|
var ctx = me.ctx;
|
|
var chart = me.chart;
|
|
var gridLines = me.options.gridLines;
|
|
|
|
if (!gridLines.display) {
|
|
return;
|
|
}
|
|
|
|
var alignPixel = helpers._alignPixel;
|
|
var axisWidth = gridLines.drawBorder ? valueAtIndexOrDefault(gridLines.lineWidth, 0, 0) : 0;
|
|
var items = me._itemsToDraw || (me._itemsToDraw = me._computeItemsToDraw(chartArea));
|
|
var glWidth, glColor;
|
|
|
|
helpers.each(items, function(item) {
|
|
glWidth = item.glWidth;
|
|
glColor = item.glColor;
|
|
|
|
if (glWidth && glColor) {
|
|
ctx.save();
|
|
ctx.lineWidth = glWidth;
|
|
ctx.strokeStyle = glColor;
|
|
if (ctx.setLineDash) {
|
|
ctx.setLineDash(item.glBorderDash);
|
|
ctx.lineDashOffset = item.glBorderDashOffset;
|
|
}
|
|
|
|
ctx.beginPath();
|
|
|
|
if (gridLines.drawTicks) {
|
|
ctx.moveTo(item.tx1, item.ty1);
|
|
ctx.lineTo(item.tx2, item.ty2);
|
|
}
|
|
|
|
if (gridLines.drawOnChartArea) {
|
|
ctx.moveTo(item.x1, item.y1);
|
|
ctx.lineTo(item.x2, item.y2);
|
|
}
|
|
|
|
ctx.stroke();
|
|
ctx.restore();
|
|
}
|
|
});
|
|
|
|
if (axisWidth) {
|
|
// Draw the line at the edge of the axis
|
|
var firstLineWidth = axisWidth;
|
|
var lastLineWidth = valueAtIndexOrDefault(gridLines.lineWidth, items.ticksLength - 1, 0);
|
|
var borderValue = items.borderValue;
|
|
var x1, x2, y1, y2;
|
|
|
|
if (me.isHorizontal()) {
|
|
x1 = alignPixel(chart, me.left, firstLineWidth) - firstLineWidth / 2;
|
|
x2 = alignPixel(chart, me.right, lastLineWidth) + lastLineWidth / 2;
|
|
y1 = y2 = borderValue;
|
|
} else {
|
|
y1 = alignPixel(chart, me.top, firstLineWidth) - firstLineWidth / 2;
|
|
y2 = alignPixel(chart, me.bottom, lastLineWidth) + lastLineWidth / 2;
|
|
x1 = x2 = borderValue;
|
|
}
|
|
|
|
ctx.lineWidth = axisWidth;
|
|
ctx.strokeStyle = valueAtIndexOrDefault(gridLines.color, 0);
|
|
ctx.beginPath();
|
|
ctx.moveTo(x1, y1);
|
|
ctx.lineTo(x2, y2);
|
|
ctx.stroke();
|
|
}
|
|
},
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
_drawLabels: function(chartArea) {
|
|
var me = this;
|
|
var ctx = me.ctx;
|
|
var optionTicks = me.options.ticks;
|
|
|
|
if (!optionTicks.display) {
|
|
return;
|
|
}
|
|
|
|
var items = me._itemsToDraw || (me._itemsToDraw = me._computeItemsToDraw(chartArea));
|
|
var tickFont;
|
|
|
|
helpers.each(items, function(item) {
|
|
tickFont = item.font;
|
|
|
|
// Make sure we draw text in the correct color and font
|
|
ctx.save();
|
|
ctx.translate(item.labelX, item.labelY);
|
|
ctx.rotate(item.rotation);
|
|
ctx.font = tickFont.string;
|
|
ctx.fillStyle = tickFont.color;
|
|
ctx.textBaseline = 'middle';
|
|
ctx.textAlign = item.textAlign;
|
|
|
|
var label = item.label;
|
|
var y = item.textOffset;
|
|
if (helpers.isArray(label)) {
|
|
for (var i = 0; i < label.length; ++i) {
|
|
// We just make sure the multiline element is a string here..
|
|
ctx.fillText('' + label[i], 0, y);
|
|
y += tickFont.lineHeight;
|
|
}
|
|
} else {
|
|
ctx.fillText(label, 0, y);
|
|
}
|
|
ctx.restore();
|
|
});
|
|
},
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
_drawTitle: function() {
|
|
var me = this;
|
|
var ctx = me.ctx;
|
|
var options = me.options;
|
|
var scaleLabel = options.scaleLabel;
|
|
|
|
if (!scaleLabel.display) {
|
|
return;
|
|
}
|
|
|
|
var scaleLabelFontColor = valueOrDefault(scaleLabel.fontColor, defaults.global.defaultFontColor);
|
|
var scaleLabelFont = helpers.options._parseFont(scaleLabel);
|
|
var scaleLabelPadding = helpers.options.toPadding(scaleLabel.padding);
|
|
var halfLineHeight = scaleLabelFont.lineHeight / 2;
|
|
var position = options.position;
|
|
var rotation = 0;
|
|
var scaleLabelX, scaleLabelY;
|
|
|
|
if (me.isHorizontal()) {
|
|
scaleLabelX = me.left + ((me.right - me.left) / 2); // midpoint of the width
|
|
scaleLabelY = position === 'bottom'
|
|
? me.bottom - halfLineHeight - scaleLabelPadding.bottom
|
|
: me.top + halfLineHeight + scaleLabelPadding.top;
|
|
} else {
|
|
var isLeft = position === 'left';
|
|
scaleLabelX = isLeft
|
|
? me.left + halfLineHeight + scaleLabelPadding.top
|
|
: me.right - halfLineHeight - scaleLabelPadding.top;
|
|
scaleLabelY = me.top + ((me.bottom - me.top) / 2);
|
|
rotation = isLeft ? -0.5 * Math.PI : 0.5 * Math.PI;
|
|
}
|
|
|
|
ctx.save();
|
|
ctx.translate(scaleLabelX, scaleLabelY);
|
|
ctx.rotate(rotation);
|
|
ctx.textAlign = 'center';
|
|
ctx.textBaseline = 'middle';
|
|
ctx.fillStyle = scaleLabelFontColor; // render in correct colour
|
|
ctx.font = scaleLabelFont.string;
|
|
ctx.fillText(scaleLabel.labelString, 0, 0);
|
|
ctx.restore();
|
|
},
|
|
|
|
draw: function(chartArea) {
|
|
var me = this;
|
|
|
|
if (!me._isVisible()) {
|
|
return;
|
|
}
|
|
|
|
me._drawGrid(chartArea);
|
|
me._drawTitle(chartArea);
|
|
me._drawLabels(chartArea);
|
|
},
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
_layers: function() {
|
|
var me = this;
|
|
var opts = me.options;
|
|
var tz = opts.ticks && opts.ticks.z || 0;
|
|
var gz = opts.gridLines && opts.gridLines.z || 0;
|
|
|
|
if (!me._isVisible() || tz === gz || me.draw !== me._draw) {
|
|
// backward compatibility: draw has been overridden by custom scale
|
|
return [{
|
|
z: tz,
|
|
draw: function() {
|
|
me.draw.apply(me, arguments);
|
|
}
|
|
}];
|
|
}
|
|
|
|
return [{
|
|
z: gz,
|
|
draw: function() {
|
|
me._drawGrid.apply(me, arguments);
|
|
me._drawTitle.apply(me, arguments);
|
|
}
|
|
}, {
|
|
z: tz,
|
|
draw: function() {
|
|
me._drawLabels.apply(me, arguments);
|
|
}
|
|
}];
|
|
}
|
|
});
|
|
|
|
Scale.prototype._draw = Scale.prototype.draw;
|
|
|
|
module.exports = Scale;
|