From d29ec5a48537cb880934200d037a53b51e2ed6d4 Mon Sep 17 00:00:00 2001 From: Akihiko Kusanagi Date: Thu, 20 Dec 2018 17:56:06 +0900 Subject: [PATCH] Add scale.pointLabels.lineHeight and scale.ticks.lineHeight options (#5914) --- docs/axes/radial/linear.md | 1 + docs/axes/styling.md | 3 + src/core/core.defaults.js | 2 +- src/core/core.js | 1 + src/core/core.scale.js | 92 ++++------ src/elements/element.line.js | 7 +- src/elements/element.point.js | 4 +- src/elements/element.rectangle.js | 6 +- src/helpers/helpers.options.js | 42 +++++ src/plugins/plugin.legend.js | 35 ++-- src/plugins/plugin.title.js | 19 +- src/scales/scale.radialLinear.js | 143 ++++++--------- .../fixtures/controller.radar/point-style.png | Bin 7679 -> 7679 bytes .../fill-radar-boundary-origin-spline.png | Bin 10472 -> 11449 bytes .../fill-radar-boundary-origin.png | Bin 9297 -> 10129 bytes test/specs/controller.radar.tests.js | 30 +-- test/specs/core.controller.tests.js | 12 +- test/specs/core.scale.tests.js | 3 - test/specs/helpers.options.tests.js | 173 +++++++++++++----- test/specs/plugin.legend.tests.js | 2 +- test/specs/plugin.title.tests.js | 1 - test/specs/scale.category.tests.js | 6 +- test/specs/scale.linear.tests.js | 14 +- test/specs/scale.radialLinear.tests.js | 10 +- 24 files changed, 328 insertions(+), 278 deletions(-) diff --git a/docs/axes/radial/linear.md b/docs/axes/radial/linear.md index d1edd18be..92f1e35b7 100644 --- a/docs/axes/radial/linear.md +++ b/docs/axes/radial/linear.md @@ -109,3 +109,4 @@ The following options are used to configure the point labels that are shown on t | `fontFamily` | `String` | `"'Helvetica Neue', 'Helvetica', 'Arial', sans-serif"` | Font family to use when rendering labels. | `fontSize` | `Number` | 10 | font size in pixels. | `fontStyle` | `String` | `'normal'` | Font style to use when rendering point labels. +| `lineHeight` | `Number/String` | `1.2` | Height of an individual line of text (see [MDN](https://developer.mozilla.org/en-US/docs/Web/CSS/line-height)). diff --git a/docs/axes/styling.md b/docs/axes/styling.md index 7184bc96f..1f06ca7af 100644 --- a/docs/axes/styling.md +++ b/docs/axes/styling.md @@ -35,6 +35,7 @@ The tick configuration is nested under the scale configuration in the `ticks` ke | `fontFamily` | `String` | `"'Helvetica Neue', 'Helvetica', 'Arial', sans-serif"` | Font family for the tick labels, follows CSS font-family options. | `fontSize` | `Number` | `12` | Font size for the tick labels. | `fontStyle` | `String` | `'normal'` | Font style for the tick labels, follows CSS font-style options (i.e. normal, italic, oblique, initial, inherit). +| `lineHeight` | `Number/String` | `1.2` | Height of an individual line of text (see [MDN](https://developer.mozilla.org/en-US/docs/Web/CSS/line-height)). | `reverse` | `Boolean` | `false` | Reverses order of tick labels. | `minor` | `object` | `{}` | Minor ticks configuration. Omitted options are inherited from options above. | `major` | `object` | `{}` | Major ticks configuration. Omitted options are inherited from options above. @@ -50,6 +51,7 @@ The minorTick configuration is nested under the ticks configuration in the `mino | `fontFamily` | `String` | `"'Helvetica Neue', 'Helvetica', 'Arial', sans-serif"` | Font family for the tick labels, follows CSS font-family options. | `fontSize` | `Number` | `12` | Font size for the tick labels. | `fontStyle` | `String` | `'normal'` | Font style for the tick labels, follows CSS font-style options (i.e. normal, italic, oblique, initial, inherit). +| `lineHeight` | `Number/String` | `1.2` | Height of an individual line of text (see [MDN](https://developer.mozilla.org/en-US/docs/Web/CSS/line-height)). ## Major Tick Configuration The majorTick configuration is nested under the ticks configuration in the `major` key. It defines options for the major tick marks that are generated by the axis. Omitted options are inherited from `ticks` configuration. @@ -61,3 +63,4 @@ The majorTick configuration is nested under the ticks configuration in the `majo | `fontFamily` | `String` | `"'Helvetica Neue', 'Helvetica', 'Arial', sans-serif"` | Font family for the tick labels, follows CSS font-family options. | `fontSize` | `Number` | `12` | Font size for the tick labels. | `fontStyle` | `String` | `'normal'` | Font style for the tick labels, follows CSS font-style options (i.e. normal, italic, oblique, initial, inherit). +| `lineHeight` | `Number/String` | `1.2` | Height of an individual line of text (see [MDN](https://developer.mozilla.org/en-US/docs/Web/CSS/line-height)). diff --git a/src/core/core.defaults.js b/src/core/core.defaults.js index 29bb040d4..4a2bb8e1b 100644 --- a/src/core/core.defaults.js +++ b/src/core/core.defaults.js @@ -1,6 +1,6 @@ 'use strict'; -var helpers = require('../helpers/index'); +var helpers = require('../helpers/helpers.core'); module.exports = { /** diff --git a/src/core/core.js b/src/core/core.js index 906b897c6..8860b9bbc 100644 --- a/src/core/core.js +++ b/src/core/core.js @@ -19,6 +19,7 @@ defaults._set('global', { defaultFontFamily: "'Helvetica Neue', 'Helvetica', 'Arial', sans-serif", defaultFontSize: 12, defaultFontStyle: 'normal', + defaultLineHeight: 1.2, showLines: true, // Element defaults defined in element extensions diff --git a/src/core/core.scale.js b/src/core/core.scale.js index 5a777e658..a538b5efa 100644 --- a/src/core/core.scale.js +++ b/src/core/core.scale.js @@ -36,9 +36,6 @@ defaults._set('scale', { // actual label labelString: '', - // line height - lineHeight: 1.2, - // top/bottom padding padding: { top: 4, @@ -99,27 +96,6 @@ function computeTextSize(context, tick, font) { context.measureText(tick).width; } -function parseFontOptions(options) { - var valueOrDefault = helpers.valueOrDefault; - var globalDefaults = defaults.global; - var size = valueOrDefault(options.fontSize, globalDefaults.defaultFontSize); - var style = valueOrDefault(options.fontStyle, globalDefaults.defaultFontStyle); - var family = valueOrDefault(options.fontFamily, globalDefaults.defaultFontFamily); - - return { - size: size, - style: style, - family: family, - font: helpers.fontString(size, style, family) - }; -} - -function parseLineHeight(options) { - return helpers.options.toLineHeight( - helpers.valueOrDefault(options.lineHeight, 1.2), - helpers.valueOrDefault(options.fontSize, defaults.global.defaultFontSize)); -} - module.exports = Element.extend({ /** * Get the padding needed for the scale @@ -341,13 +317,13 @@ module.exports = Element.extend({ // Get the width of each grid by calculating the difference // between x offsets between 0 and 1. - var tickFont = parseFontOptions(tickOpts); - context.font = tickFont.font; + var tickFont = helpers.options._parseFont(tickOpts); + context.font = tickFont.string; var labelRotation = tickOpts.minRotation || 0; if (labels.length && me.options.display && me.isHorizontal()) { - var originalLabelWidth = helpers.longestText(context, tickFont.font, labels, me.longestTextCache); + var originalLabelWidth = helpers.longestText(context, tickFont.string, labels, me.longestTextCache); var labelWidth = originalLabelWidth; var cosRotation, sinRotation; @@ -400,7 +376,8 @@ module.exports = Element.extend({ var position = opts.position; var isHorizontal = me.isHorizontal(); - var tickFont = parseFontOptions(tickOpts); + var parseFont = helpers.options._parseFont; + var tickFont = parseFont(tickOpts); var tickMarkLength = opts.gridLines.tickMarkLength; // Width @@ -420,9 +397,9 @@ module.exports = Element.extend({ // Are we showing a title for the scale? if (scaleLabelOpts.display && display) { - var scaleLabelLineHeight = parseLineHeight(scaleLabelOpts); + var scaleLabelFont = parseFont(scaleLabelOpts); var scaleLabelPadding = helpers.options.toPadding(scaleLabelOpts.padding); - var deltaHeight = scaleLabelLineHeight + scaleLabelPadding.height; + var deltaHeight = scaleLabelFont.lineHeight + scaleLabelPadding.height; if (isHorizontal) { minSize.height += deltaHeight; @@ -433,7 +410,7 @@ module.exports = Element.extend({ // Don't bother fitting the ticks if we are not showing them if (tickOpts.display && display) { - var largestTextWidth = helpers.longestText(me.ctx, tickFont.font, labels, me.longestTextCache); + var largestTextWidth = helpers.longestText(me.ctx, tickFont.string, labels, me.longestTextCache); var tallestLabelHeightInLines = helpers.numberOfLabelLines(labels); var lineSpace = tickFont.size * 0.5; var tickPadding = me.options.ticks.padding; @@ -448,15 +425,14 @@ module.exports = Element.extend({ // TODO - improve this calculation var labelHeight = (sinRotation * largestTextWidth) - + (tickFont.size * tallestLabelHeightInLines) - + (lineSpace * (tallestLabelHeightInLines - 1)) + + (tickFont.lineHeight * tallestLabelHeightInLines) + lineSpace; // padding minSize.height = Math.min(me.maxHeight, minSize.height + labelHeight + tickPadding); - me.ctx.font = tickFont.font; - var firstLabelWidth = computeTextSize(me.ctx, labels[0], tickFont.font); - var lastLabelWidth = computeTextSize(me.ctx, labels[labels.length - 1], tickFont.font); + me.ctx.font = tickFont.string; + var firstLabelWidth = computeTextSize(me.ctx, labels[0], tickFont.string); + var lastLabelWidth = computeTextSize(me.ctx, labels[labels.length - 1], tickFont.string); var offsetLeft = me.getPixelForTick(0) - me.left; var offsetRight = me.right - me.getPixelForTick(labels.length - 1); var paddingLeft, paddingRight; @@ -717,6 +693,7 @@ module.exports = Element.extend({ var chart = me.chart; var context = me.ctx; var globalDefaults = defaults.global; + var defaultFontColor = globalDefaults.defaultFontColor; var optionTicks = options.ticks.minor; var optionMajorTicks = options.ticks.major || optionTicks; var gridLines = options.gridLines; @@ -727,18 +704,20 @@ module.exports = Element.extend({ var isMirrored = optionTicks.mirror; var isHorizontal = me.isHorizontal(); + var parseFont = helpers.options._parseFont; var ticks = optionTicks.autoSkip ? me._autoSkip(me.getTicks()) : me.getTicks(); - var tickFontColor = helpers.valueOrDefault(optionTicks.fontColor, globalDefaults.defaultFontColor); - var tickFont = parseFontOptions(optionTicks); - var majorTickFontColor = helpers.valueOrDefault(optionMajorTicks.fontColor, globalDefaults.defaultFontColor); - var majorTickFont = parseFontOptions(optionMajorTicks); + var tickFontColor = helpers.valueOrDefault(optionTicks.fontColor, defaultFontColor); + var tickFont = parseFont(optionTicks); + var lineHeight = tickFont.lineHeight; + var majorTickFontColor = helpers.valueOrDefault(optionMajorTicks.fontColor, defaultFontColor); + var majorTickFont = parseFont(optionMajorTicks); var tickPadding = optionTicks.padding; var labelOffset = optionTicks.labelOffset; var tl = gridLines.drawTicks ? gridLines.tickMarkLength : 0; - var scaleLabelFontColor = helpers.valueOrDefault(scaleLabel.fontColor, globalDefaults.defaultFontColor); - var scaleLabelFont = parseFontOptions(scaleLabel); + var scaleLabelFontColor = helpers.valueOrDefault(scaleLabel.fontColor, defaultFontColor); + var scaleLabelFont = parseFont(scaleLabel); var scaleLabelPadding = helpers.options.toPadding(scaleLabel.padding); var labelRotationRadians = helpers.toRadians(me.labelRotation); @@ -790,8 +769,8 @@ module.exports = Element.extend({ } // Common properties - var tx1, ty1, tx2, ty2, x1, y1, x2, y2, labelX, labelY, textAlign; - var textBaseline = 'middle'; + 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) { @@ -809,13 +788,13 @@ module.exports = Element.extend({ if (position === 'top') { y1 = alignPixel(chart, chartArea.top, axisWidth) + axisWidth / 2; y2 = chartArea.bottom; - textBaseline = !isRotated ? 'bottom' : 'middle'; + 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; - textBaseline = !isRotated ? 'top' : 'middle'; + textOffset = (!isRotated ? 0.5 : 0) * lineHeight; textAlign = !isRotated ? 'center' : 'right'; labelY = me.top + labelYOffset; } @@ -830,6 +809,7 @@ module.exports = Element.extend({ 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; @@ -862,7 +842,7 @@ module.exports = Element.extend({ rotation: -1 * labelRotationRadians, label: label, major: tick.major, - textBaseline: textBaseline, + textOffset: textOffset, textAlign: textAlign }); }); @@ -902,25 +882,21 @@ module.exports = Element.extend({ context.save(); context.translate(itemToDraw.labelX, itemToDraw.labelY); context.rotate(itemToDraw.rotation); - context.font = itemToDraw.major ? majorTickFont.font : tickFont.font; + context.font = itemToDraw.major ? majorTickFont.string : tickFont.string; context.fillStyle = itemToDraw.major ? majorTickFontColor : tickFontColor; - context.textBaseline = itemToDraw.textBaseline; + context.textBaseline = 'middle'; context.textAlign = itemToDraw.textAlign; var label = itemToDraw.label; + var y = itemToDraw.textOffset; if (helpers.isArray(label)) { - var lineCount = label.length; - var lineHeight = tickFont.size * 1.5; - var y = isHorizontal ? 0 : -lineHeight * (lineCount - 1) / 2; - - for (var i = 0; i < lineCount; ++i) { + for (var i = 0; i < label.length; ++i) { // We just make sure the multiline element is a string here.. context.fillText('' + label[i], 0, y); - // apply same lineSpacing as calculated @ L#320 y += lineHeight; } } else { - context.fillText(label, 0, 0); + context.fillText(label, 0, y); } context.restore(); } @@ -931,7 +907,7 @@ module.exports = Element.extend({ var scaleLabelX; var scaleLabelY; var rotation = 0; - var halfLineHeight = parseLineHeight(scaleLabel) / 2; + var halfLineHeight = scaleLabelFont.lineHeight / 2; if (isHorizontal) { scaleLabelX = me.left + ((me.right - me.left) / 2); // midpoint of the width @@ -953,7 +929,7 @@ module.exports = Element.extend({ context.textAlign = 'center'; context.textBaseline = 'middle'; context.fillStyle = scaleLabelFontColor; // render in correct colour - context.font = scaleLabelFont.font; + context.font = scaleLabelFont.string; context.fillText(scaleLabel.labelString, 0, 0); context.restore(); } diff --git a/src/elements/element.line.js b/src/elements/element.line.js index 1500d353c..96bb5be38 100644 --- a/src/elements/element.line.js +++ b/src/elements/element.line.js @@ -4,15 +4,15 @@ var defaults = require('../core/core.defaults'); var Element = require('../core/core.element'); var helpers = require('../helpers/index'); -var globalDefaults = defaults.global; +var defaultColor = defaults.global.defaultColor; defaults._set('global', { elements: { line: { tension: 0.4, - backgroundColor: globalDefaults.defaultColor, + backgroundColor: defaultColor, borderWidth: 3, - borderColor: globalDefaults.defaultColor, + borderColor: defaultColor, borderCapStyle: 'butt', borderDash: [], borderDashOffset: 0.0, @@ -30,6 +30,7 @@ module.exports = Element.extend({ var ctx = me._chart.ctx; var spanGaps = vm.spanGaps; var points = me._children.slice(); // clone array + var globalDefaults = defaults.global; var globalOptionLineElements = globalDefaults.elements.line; var lastDrawnIndex = -1; var index, current, previous, currentVM; diff --git a/src/elements/element.point.js b/src/elements/element.point.js index 56eb57966..b7909116a 100644 --- a/src/elements/element.point.js +++ b/src/elements/element.point.js @@ -73,6 +73,8 @@ module.exports = Element.extend({ var x = vm.x; var y = vm.y; var epsilon = 0.0000001; // 0.0000001 is margin in pixels for Accumulated error. + var globalDefaults = defaults.global; + var defaultColor = globalDefaults.defaultColor; // eslint-disable-line no-shadow if (vm.skip) { return; @@ -81,7 +83,7 @@ module.exports = Element.extend({ // Clipping for Points. if (chartArea === undefined || (model.x > chartArea.left - epsilon && chartArea.right + epsilon > model.x && model.y > chartArea.top - epsilon && chartArea.bottom + epsilon > model.y)) { ctx.strokeStyle = vm.borderColor || defaultColor; - ctx.lineWidth = helpers.valueOrDefault(vm.borderWidth, defaults.global.elements.point.borderWidth); + ctx.lineWidth = helpers.valueOrDefault(vm.borderWidth, globalDefaults.elements.point.borderWidth); ctx.fillStyle = vm.backgroundColor || defaultColor; helpers.canvas.drawPoint(ctx, pointStyle, radius, x, y, rotation); } diff --git a/src/elements/element.rectangle.js b/src/elements/element.rectangle.js index ef59ac0dd..1cb6fbb53 100644 --- a/src/elements/element.rectangle.js +++ b/src/elements/element.rectangle.js @@ -3,11 +3,13 @@ var defaults = require('../core/core.defaults'); var Element = require('../core/core.element'); +var defaultColor = defaults.global.defaultColor; + defaults._set('global', { elements: { rectangle: { - backgroundColor: defaults.global.defaultColor, - borderColor: defaults.global.defaultColor, + backgroundColor: defaultColor, + borderColor: defaultColor, borderSkipped: 'bottom', borderWidth: 0 } diff --git a/src/helpers/helpers.options.js b/src/helpers/helpers.options.js index 8e6c0aadf..0b107e40d 100644 --- a/src/helpers/helpers.options.js +++ b/src/helpers/helpers.options.js @@ -1,7 +1,25 @@ 'use strict'; +var defaults = require('../core/core.defaults'); var helpers = require('./helpers.core'); +/** + * Converts the given font object into a CSS font string. + * @param {Object} font - A font object. + * @return {Stringg} The CSS font string. See https://developer.mozilla.org/en-US/docs/Web/CSS/font + * @private + */ +function toFontString(font) { + if (!font || helpers.isNullOrUndef(font.size) || helpers.isNullOrUndef(font.family)) { + return null; + } + + return (font.style ? font.style + ' ' : '') + + (font.weight ? font.weight + ' ' : '') + + font.size + 'px ' + + font.family; +} + /** * @alias Chart.helpers.options * @namespace @@ -65,6 +83,30 @@ module.exports = { }; }, + /** + * Parses font options and returns the font object. + * @param {Object} options - A object that contains font opttons to be parsed. + * @return {Object} The font object. + * @todo Support font.* options and renamed to toFont(). + * @private + */ + _parseFont: function(options) { + var valueOrDefault = helpers.valueOrDefault; + var globalDefaults = defaults.global; + var size = valueOrDefault(options.fontSize, globalDefaults.defaultFontSize); + var font = { + family: valueOrDefault(options.fontFamily, globalDefaults.defaultFontFamily), + lineHeight: helpers.options.toLineHeight(valueOrDefault(options.lineHeight, globalDefaults.defaultLineHeight), size), + size: size, + style: valueOrDefault(options.fontStyle, globalDefaults.defaultFontStyle), + weight: null, + string: '' + }; + + font.string = toFontString(font); + return font; + }, + /** * Evaluates the given `inputs` sequentially and returns the first defined value. * @param {Array[]} inputs - An array of values, falling back to the last value. diff --git a/src/plugins/plugin.legend.js b/src/plugins/plugin.legend.js index 8e6b75f65..f11b823ea 100644 --- a/src/plugins/plugin.legend.js +++ b/src/plugins/plugin.legend.js @@ -211,12 +211,8 @@ var Legend = Element.extend({ var ctx = me.ctx; - var globalDefault = defaults.global; - var valueOrDefault = helpers.valueOrDefault; - var fontSize = valueOrDefault(labelOpts.fontSize, globalDefault.defaultFontSize); - var fontStyle = valueOrDefault(labelOpts.fontStyle, globalDefault.defaultFontStyle); - var fontFamily = valueOrDefault(labelOpts.fontFamily, globalDefault.defaultFontFamily); - var labelFont = helpers.fontString(fontSize, fontStyle, fontFamily); + var labelFont = helpers.options._parseFont(labelOpts); + var fontSize = labelFont.size; // Reset hit boxes var hitboxes = me.legendHitBoxes = []; @@ -234,7 +230,7 @@ var Legend = Element.extend({ // Increase sizes here if (display) { - ctx.font = labelFont; + ctx.font = labelFont.string; if (isHorizontal) { // Labels @@ -323,19 +319,18 @@ var Legend = Element.extend({ var me = this; var opts = me.options; var labelOpts = opts.labels; - var globalDefault = defaults.global; - var lineDefault = globalDefault.elements.line; + var globalDefaults = defaults.global; + var defaultColor = globalDefaults.defaultColor; + var lineDefault = globalDefaults.elements.line; var legendWidth = me.width; var lineWidths = me.lineWidths; if (opts.display) { var ctx = me.ctx; var valueOrDefault = helpers.valueOrDefault; - var fontColor = valueOrDefault(labelOpts.fontColor, globalDefault.defaultFontColor); - var fontSize = valueOrDefault(labelOpts.fontSize, globalDefault.defaultFontSize); - var fontStyle = valueOrDefault(labelOpts.fontStyle, globalDefault.defaultFontStyle); - var fontFamily = valueOrDefault(labelOpts.fontFamily, globalDefault.defaultFontFamily); - var labelFont = helpers.fontString(fontSize, fontStyle, fontFamily); + var fontColor = valueOrDefault(labelOpts.fontColor, globalDefaults.defaultFontColor); + var labelFont = helpers.options._parseFont(labelOpts); + var fontSize = labelFont.size; var cursor; // Canvas setup @@ -344,7 +339,7 @@ var Legend = Element.extend({ ctx.lineWidth = 0.5; ctx.strokeStyle = fontColor; // for strikethrough effect ctx.fillStyle = fontColor; // render in correct colour - ctx.font = labelFont; + ctx.font = labelFont.string; var boxWidth = getBoxWidth(labelOpts, fontSize); var hitboxes = me.legendHitBoxes; @@ -358,13 +353,13 @@ var Legend = Element.extend({ // Set the ctx for the box ctx.save(); - ctx.fillStyle = valueOrDefault(legendItem.fillStyle, globalDefault.defaultColor); + var lineWidth = valueOrDefault(legendItem.lineWidth, lineDefault.borderWidth); + ctx.fillStyle = valueOrDefault(legendItem.fillStyle, defaultColor); ctx.lineCap = valueOrDefault(legendItem.lineCap, lineDefault.borderCapStyle); ctx.lineDashOffset = valueOrDefault(legendItem.lineDashOffset, lineDefault.borderDashOffset); ctx.lineJoin = valueOrDefault(legendItem.lineJoin, lineDefault.borderJoinStyle); - ctx.lineWidth = valueOrDefault(legendItem.lineWidth, lineDefault.borderWidth); - ctx.strokeStyle = valueOrDefault(legendItem.strokeStyle, globalDefault.defaultColor); - var isLineWidthZero = (valueOrDefault(legendItem.lineWidth, lineDefault.borderWidth) === 0); + ctx.lineWidth = lineWidth; + ctx.strokeStyle = valueOrDefault(legendItem.strokeStyle, defaultColor); if (ctx.setLineDash) { // IE 9 and 10 do not support line dash @@ -383,7 +378,7 @@ var Legend = Element.extend({ helpers.canvas.drawPoint(ctx, legendItem.pointStyle, radius, centerX, centerY); } else { // Draw box as legend symbol - if (!isLineWidthZero) { + if (lineWidth !== 0) { ctx.strokeRect(x, y, boxWidth, fontSize); } ctx.fillRect(x, y, boxWidth, fontSize); diff --git a/src/plugins/plugin.title.js b/src/plugins/plugin.title.js index 47588844d..eb09aa0e2 100644 --- a/src/plugins/plugin.title.js +++ b/src/plugins/plugin.title.js @@ -12,7 +12,6 @@ defaults._set('global', { display: false, fontStyle: 'bold', fullWidth: true, - lineHeight: 1.2, padding: 10, position: 'top', text: '', @@ -111,14 +110,12 @@ var Title = Element.extend({ beforeFit: noop, fit: function() { var me = this; - var valueOrDefault = helpers.valueOrDefault; var opts = me.options; var display = opts.display; - var fontSize = valueOrDefault(opts.fontSize, defaults.global.defaultFontSize); var minSize = me.minSize; var lineCount = helpers.isArray(opts.text) ? opts.text.length : 1; - var lineHeight = helpers.options.toLineHeight(opts.lineHeight, fontSize); - var textSize = display ? (lineCount * lineHeight) + (opts.padding * 2) : 0; + var fontOpts = helpers.options._parseFont(opts); + var textSize = display ? (lineCount * fontOpts.lineHeight) + (opts.padding * 2) : 0; if (me.isHorizontal()) { minSize.width = me.maxWidth; // fill all the width @@ -146,14 +143,10 @@ var Title = Element.extend({ var ctx = me.ctx; var valueOrDefault = helpers.valueOrDefault; var opts = me.options; - var globalDefaults = defaults.global; if (opts.display) { - var fontSize = valueOrDefault(opts.fontSize, globalDefaults.defaultFontSize); - var fontStyle = valueOrDefault(opts.fontStyle, globalDefaults.defaultFontStyle); - var fontFamily = valueOrDefault(opts.fontFamily, globalDefaults.defaultFontFamily); - var titleFont = helpers.fontString(fontSize, fontStyle, fontFamily); - var lineHeight = helpers.options.toLineHeight(opts.lineHeight, fontSize); + var fontOpts = helpers.options._parseFont(opts); + var lineHeight = fontOpts.lineHeight; var offset = lineHeight / 2 + opts.padding; var rotation = 0; var top = me.top; @@ -162,8 +155,8 @@ var Title = Element.extend({ var right = me.right; var maxWidth, titleX, titleY; - ctx.fillStyle = valueOrDefault(opts.fontColor, globalDefaults.defaultFontColor); // render in correct colour - ctx.font = titleFont; + ctx.fillStyle = valueOrDefault(opts.fontColor, defaults.global.defaultFontColor); // render in correct colour + ctx.font = fontOpts.string; // Horizontal if (me.isHorizontal()) { diff --git a/src/scales/scale.radialLinear.js b/src/scales/scale.radialLinear.js index 4a76a773c..f0c38317e 100644 --- a/src/scales/scale.radialLinear.js +++ b/src/scales/scale.radialLinear.js @@ -7,8 +7,6 @@ var Ticks = require('../core/core.ticks'); module.exports = function(Chart) { - var globalDefaults = defaults.global; - var defaultConfig = { display: true, @@ -64,42 +62,26 @@ module.exports = function(Chart) { return opts.angleLines.display || opts.pointLabels.display ? scale.chart.data.labels.length : 0; } - function getPointLabelFontOptions(scale) { - var pointLabelOptions = scale.options.pointLabels; - var fontSize = helpers.valueOrDefault(pointLabelOptions.fontSize, globalDefaults.defaultFontSize); - var fontStyle = helpers.valueOrDefault(pointLabelOptions.fontStyle, globalDefaults.defaultFontStyle); - var fontFamily = helpers.valueOrDefault(pointLabelOptions.fontFamily, globalDefaults.defaultFontFamily); - var font = helpers.fontString(fontSize, fontStyle, fontFamily); - - return { - size: fontSize, - style: fontStyle, - family: fontFamily, - font: font - }; - } - - function getTickFontSize(scale) { - var opts = scale.options; + function getTickBackdropHeight(opts) { var tickOpts = opts.ticks; if (tickOpts.display && opts.display) { - return helpers.valueOrDefault(tickOpts.fontSize, globalDefaults.defaultFontSize); + return helpers.valueOrDefault(tickOpts.fontSize, defaults.global.defaultFontSize) + tickOpts.backdropPaddingY * 2; } return 0; } - function measureLabelSize(ctx, fontSize, label) { + function measureLabelSize(ctx, lineHeight, label) { if (helpers.isArray(label)) { return { w: helpers.longestText(ctx, ctx.font, label), - h: (label.length * fontSize) + ((label.length - 1) * 1.5 * fontSize) + h: label.length * lineHeight }; } return { w: ctx.measureText(label).width, - h: fontSize + h: lineHeight }; } @@ -111,14 +93,14 @@ module.exports = function(Chart) { }; } else if (angle < min || angle > max) { return { - start: pos - size - 5, + start: pos - size, end: pos }; } return { start: pos, - end: pos + size + 5 + end: pos + size }; } @@ -154,28 +136,26 @@ module.exports = function(Chart) { * https://dl.dropboxusercontent.com/u/34601363/yeahscience.gif */ - var plFont = getPointLabelFontOptions(scale); - var paddingTop = getTickFontSize(scale) / 2; + var plFont = helpers.options._parseFont(scale.options.pointLabels); // Get maximum radius of the polygon. Either half the height (minus the text width) or half the width. // Use this to calculate the offset + change. - Make sure L/R protrusion is at least 0 to stop issues with centre points - var largestPossibleRadius = Math.min(scale.height / 2, scale.width / 2); var furthestLimits = { - r: scale.width, l: 0, - t: scale.height, - b: 0 + r: scale.width, + t: 0, + b: scale.height }; var furthestAngles = {}; var i, textSize, pointPosition; - scale.ctx.font = plFont.font; + scale.ctx.font = plFont.string; scale._pointLabelSizes = []; var valueCount = getValueCount(scale); for (i = 0; i < valueCount; i++) { - pointPosition = scale.getPointPosition(i, largestPossibleRadius); - textSize = measureLabelSize(scale.ctx, plFont.size, scale.pointLabels[i] || ''); + pointPosition = scale.getPointPosition(i, scale.drawingArea + 5); + textSize = measureLabelSize(scale.ctx, plFont.lineHeight, scale.pointLabels[i] || ''); scale._pointLabelSizes[i] = textSize; // Add quarter circle to make degree 0 mean top of circle @@ -205,22 +185,7 @@ module.exports = function(Chart) { } } - if (paddingTop && -paddingTop < furthestLimits.t) { - furthestLimits.t = -paddingTop; - furthestAngles.t = 0; - } - - scale.setReductions(largestPossibleRadius, furthestLimits, furthestAngles); - } - - /** - * Helper function to fit a radial linear scale with no point labels - */ - function fit(scale) { - var paddingTop = getTickFontSize(scale) / 2; - var largestPossibleRadius = Math.min((scale.height - paddingTop) / 2, scale.width / 2); - scale.drawingArea = Math.floor(largestPossibleRadius); - scale.setCenterPoint(0, 0, paddingTop, 0); + scale.setReductions(scale.drawingArea, furthestLimits, furthestAngles); } function getTextAlignForAngle(angle) { @@ -233,17 +198,17 @@ module.exports = function(Chart) { return 'right'; } - function fillText(ctx, text, position, fontSize) { - if (helpers.isArray(text)) { - var y = position.y; - var spacing = 1.5 * fontSize; + function fillText(ctx, text, position, lineHeight) { + var y = position.y + lineHeight / 2; + var i, ilen; - for (var i = 0; i < text.length; ++i) { + if (helpers.isArray(text)) { + for (i = 0, ilen = text.length; i < ilen; ++i) { ctx.fillText(text[i], position.x, y); - y += spacing; + y += lineHeight; } } else { - ctx.fillText(text, position.x, position.y); + ctx.fillText(text, position.x, y); } } @@ -263,6 +228,7 @@ module.exports = function(Chart) { var pointLabelOpts = opts.pointLabels; var lineWidth = helpers.valueOrDefault(angleLineOpts.lineWidth, gridLineOpts.lineWidth); var lineColor = helpers.valueOrDefault(angleLineOpts.color, gridLineOpts.color); + var tickBackdropHeight = getTickBackdropHeight(opts); ctx.save(); ctx.lineWidth = lineWidth; @@ -275,10 +241,10 @@ module.exports = function(Chart) { var outerDistance = scale.getDistanceFromCenterForValue(opts.ticks.reverse ? scale.min : scale.max); // Point Label Font - var plFont = getPointLabelFontOptions(scale); + var plFont = helpers.options._parseFont(pointLabelOpts); - ctx.font = plFont.font; - ctx.textBaseline = 'top'; + ctx.font = plFont.string; + ctx.textBaseline = 'middle'; for (var i = getValueCount(scale) - 1; i >= 0; i--) { if (angleLineOpts.display && lineWidth && lineColor) { @@ -290,18 +256,19 @@ module.exports = function(Chart) { } if (pointLabelOpts.display) { - // Extra 3px out for some label spacing - var pointLabelPosition = scale.getPointPosition(i, outerDistance + 5); + // Extra pixels out for some label spacing + var extra = (i === 0 ? tickBackdropHeight / 2 : 0); + var pointLabelPosition = scale.getPointPosition(i, outerDistance + extra + 5); // Keep this in loop since we may support array properties here - var pointLabelFontColor = helpers.valueAtIndexOrDefault(pointLabelOpts.fontColor, i, globalDefaults.defaultFontColor); + var pointLabelFontColor = helpers.valueAtIndexOrDefault(pointLabelOpts.fontColor, i, defaults.global.defaultFontColor); ctx.fillStyle = pointLabelFontColor; var angleRadians = scale.getIndexAngle(i); var angle = helpers.toDegrees(angleRadians); ctx.textAlign = getTextAlignForAngle(angle); adjustPointPositionForLabelHeight(angle, scale._pointLabelSizes[i], pointLabelPosition); - fillText(ctx, scale.pointLabels[i] || '', pointLabelPosition, plFont.size); + fillText(ctx, scale.pointLabels[i] || '', pointLabelPosition, plFont.lineHeight); } } ctx.restore(); @@ -353,17 +320,14 @@ module.exports = function(Chart) { var LinearRadialScale = Chart.LinearScaleBase.extend({ setDimensions: function() { var me = this; - var opts = me.options; - var tickOpts = opts.ticks; + // Set the unconstrained dimension before label rotation me.width = me.maxWidth; me.height = me.maxHeight; + me.paddingTop = getTickBackdropHeight(me.options) / 2; me.xCenter = Math.floor(me.width / 2); - me.yCenter = Math.floor(me.height / 2); - - var minSize = helpers.min([me.height, me.width]); - var tickFontSize = helpers.valueOrDefault(tickOpts.fontSize, globalDefaults.defaultFontSize); - me.drawingArea = opts.display ? (minSize / 2) - (tickFontSize / 2 + tickOpts.backdropPaddingY) : (minSize / 2); + me.yCenter = Math.floor((me.height - me.paddingTop) / 2); + me.drawingArea = Math.min(me.height - me.paddingTop, me.width) / 2; }, determineDataLimits: function() { var me = this; @@ -394,9 +358,10 @@ module.exports = function(Chart) { me.handleTickRangeOptions(); }, getTickLimit: function() { - var tickOpts = this.options.ticks; - var tickFontSize = helpers.valueOrDefault(tickOpts.fontSize, globalDefaults.defaultFontSize); - return Math.min(tickOpts.maxTicksLimit ? tickOpts.maxTicksLimit : 11, Math.ceil(this.drawingArea / (1.5 * tickFontSize))); + var opts = this.options; + var tickOpts = opts.ticks; + var tickBackdropHeight = getTickBackdropHeight(opts); + return Math.min(tickOpts.maxTicksLimit ? tickOpts.maxTicksLimit : 11, Math.ceil(this.drawingArea / tickBackdropHeight)); }, convertTicksToLabels: function() { var me = this; @@ -410,10 +375,13 @@ module.exports = function(Chart) { return +this.getRightValue(this.chart.data.datasets[datasetIndex].data[index]); }, fit: function() { - if (this.options.pointLabels.display) { - fitWithPointLabels(this); + var me = this; + var opts = me.options; + + if (opts.display && opts.pointLabels.display) { + fitWithPointLabels(me); } else { - fit(this); + me.setCenterPoint(0, 0, 0, 0); } }, /** @@ -425,7 +393,7 @@ module.exports = function(Chart) { var radiusReductionLeft = furthestLimits.l / Math.sin(furthestAngles.l); var radiusReductionRight = Math.max(furthestLimits.r - me.width, 0) / Math.sin(furthestAngles.r); var radiusReductionTop = -furthestLimits.t / Math.cos(furthestAngles.t); - var radiusReductionBottom = -Math.max(furthestLimits.b - me.height, 0) / Math.cos(furthestAngles.b); + var radiusReductionBottom = -Math.max(furthestLimits.b - (me.height - me.paddingTop), 0) / Math.cos(furthestAngles.b); radiusReductionLeft = numberOrZero(radiusReductionLeft); radiusReductionRight = numberOrZero(radiusReductionRight); @@ -442,10 +410,10 @@ module.exports = function(Chart) { var maxRight = me.width - rightMovement - me.drawingArea; var maxLeft = leftMovement + me.drawingArea; var maxTop = topMovement + me.drawingArea; - var maxBottom = me.height - bottomMovement - me.drawingArea; + var maxBottom = (me.height - me.paddingTop) - bottomMovement - me.drawingArea; me.xCenter = Math.floor(((maxLeft + maxRight) / 2) + me.left); - me.yCenter = Math.floor(((maxTop + maxBottom) / 2) + me.top); + me.yCenter = Math.floor(((maxTop + maxBottom) / 2) + me.top + me.paddingTop); }, getIndexAngle: function(index) { @@ -507,12 +475,7 @@ module.exports = function(Chart) { if (opts.display) { var ctx = me.ctx; var startAngle = this.getIndexAngle(0); - - // Tick Font - var tickFontSize = valueOrDefault(tickOpts.fontSize, globalDefaults.defaultFontSize); - var tickFontStyle = valueOrDefault(tickOpts.fontStyle, globalDefaults.defaultFontStyle); - var tickFontFamily = valueOrDefault(tickOpts.fontFamily, globalDefaults.defaultFontFamily); - var tickLabelFont = helpers.fontString(tickFontSize, tickFontStyle, tickFontFamily); + var tickFont = helpers.options._parseFont(tickOpts); if (opts.angleLines.display || opts.pointLabels.display) { drawPointLabels(me); @@ -529,8 +492,8 @@ module.exports = function(Chart) { } if (tickOpts.display) { - var tickFontColor = valueOrDefault(tickOpts.fontColor, globalDefaults.defaultFontColor); - ctx.font = tickLabelFont; + var tickFontColor = valueOrDefault(tickOpts.fontColor, defaults.global.defaultFontColor); + ctx.font = tickFont.string; ctx.save(); ctx.translate(me.xCenter, me.yCenter); @@ -541,9 +504,9 @@ module.exports = function(Chart) { ctx.fillStyle = tickOpts.backdropColor; ctx.fillRect( -labelWidth / 2 - tickOpts.backdropPaddingX, - -yCenterOffset - tickFontSize / 2 - tickOpts.backdropPaddingY, + -yCenterOffset - tickFont.size / 2 - tickOpts.backdropPaddingY, labelWidth + tickOpts.backdropPaddingX * 2, - tickFontSize + tickOpts.backdropPaddingY * 2 + tickFont.size + tickOpts.backdropPaddingY * 2 ); } diff --git a/test/fixtures/controller.radar/point-style.png b/test/fixtures/controller.radar/point-style.png index c64bf330755377abdca1aac7fc18d0c94a5269fa..723fdb826472b2dbc18f6de68d96c2a8e9dd7a7c 100644 GIT binary patch literal 7679 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 W`A?R-M$X8;(I=daSN!Pn^M3(PxiKFA literal 7679 zc-pO52~d;Smc9WL5D}DCR0Ko?UW)=Qh^)aCBeDe$lvNQ?5D+jd0g|}Vwk$pnL6Aj_ zV%P;C5(r5*3JuEg+7M(3v55gpfQXntfFyInR87^Jnt3x-Gk;Yo|9_Wr&wuWB&iT&$ z@4C4-s;}C-3IL#vI&l;YfC7;JR93>j9|?>F0Cd|>M-O=>Jf31DJ_rd(H2bWdWxk!I zzI*@Hl9i<Km-x|R;mESFUK8#xm!f!isr`Ug<4jt zfR!(6%f{mo2ip4)iu84EiI+1GpPyI0UjdASv96$Bsz}fJQ$hk!5_scjRr^e{#wusK zZ>}= z?QOIIx4!25eB&NjG7vs0Y61V<<>6WM39AMiig%_GV?1^m*N?0My}6xMLoGbvpPr&f zsxBxA&k2s3ubMl_&8Z|Sfor7N*q!YBbs+L2_de5C5m3;Hq6cG0tM1EG3s9n#L-lC9 zcn7$Vw)A1|0s0%S04t~2YR{Iu&hARGDoAtUwhVO(dn}4r*mBtI*{$pBvoy0&?3yv_o4XR6Dvdd@A%1}E$00He6@Z6V z_tzS+w~@>4$wA5~5QOHw%#dS=iXj0yu~R!~74St{5iyOxAtci?V|wP`8%)Q4ws@;M zGM8b1Vp)(1eFZDQ#8Js%wAm@%VqXqV?0N(vKSUa(&@}BiX)!|9R;o7%2Pz8 zn322WY@Vn%d(iqy>VYzw-py6mycY>E~A(E<809HGXMP0a&}u zp?FLpE_ijUojs`_n=(E+4qMU6hQdnX^!_$0ygf;;w zzRo?#7~`+OAHL6+W>qVKv=aLVj3;&#v6)2sPZDcD-@^NflK5xx3kw-hb7VbWo<4Y= z!PD#aEns9W`f7l@r$rALIyx0e7tzK-nkl$Xw9jHh9q^e`wO0e6%hTfQoQsyFjsn1* zh70TKRN-5o#ts7BJh40CjWjjex`DL-%ufi1i6gyNC6-3Q$8mf``qn=E9l{%T__Uz& z8o(iw7oUv{Zxym1GJc+7DiaQswHpcV-r*PLGJ*#yHNZ_z#~s3MINF%B*x;)VR+gJO z@k&Xpvn2MrOWEccthwu;L2qTD9mxWD`@fE-jqcj_PEt79| zaa5529FAZvpaZ9(sny09nv|&sF1q0#F*@ujW^GWCGjn_ZwwI|j*_j2>9y3Cl3iN?b zi8YcGI2knZY!ElQ6_}?kV9~CdILWR~yr(l=${^lN*$~siZ1R!vSSYFq>BxpX?dP=O zH&?``Mfl$I3eTp*^8LZCbHW^AL&M0sPB&T(Ln9k|2CY%l&e7XDbBMNpL$}8_P!EwT zjfIoUCgAgwcUi0Y0RP8b*ZO^W!Zc#b1IBt3HF>oAI%cV1b^+R26=fIY`;oSngm>nB zt&FGEssK>;ayM$KehMG>cLt?l@yz8gR)hJs1!A`|#8QeqNyA7;x%(ne`Sl35a|Zw) zN~AHPIWmz9?}ye8BkON8Mu4Er^^v3NWxCR(eJHy!nS}mE6@ZT~V@qW|6 zsXZN!?6p9Et*AnXhYjD}95I@X(0HE?qjrE@;)4SiV|G2mc}#6e#P1bySB-dp^2 za0jV)M?K%CL6NEn2#3nGU7BwM{;+j*V!m(7a5WY-9J7^@&mv&}^Nd3AHU&QxtVN|nCeI$qeXIah zlBSCp?{q&TxuA{H@Bwn`Y$PrWzcmrP>S>BX^V^YTX$4SMmqPQIlhitKU_Qf%QlQ@k z4Yab9>Dr7ITuTqnKKpTMbInEtVE#8@)?M$3!s^)bXaOIm>+?4#E{zh4_TKe<@T+)B z?hecae%U$f^2qXg&2d=!)N+K+?@9n{+=_rAPXItbz`Pwq_~)jwTtnd(h^&8o1zBVI zi7w8(t~2r6+l{khf(yl6vLcK(ΞsvDkFeSFjFjyk~7uZYd2FQAG=nXF@H zd7A*&$y{M6C2@3Jhw?3SOvC**0Q2mdxF1t~$#37BmEqIPWaqi}ooE9TTM$&%r$fsz z+hQSw+0B0Lm(G0kz$kwsVE&b0GLjc>8<8Bf*t35mu9Lat>VVE!$TWz050danPUA39 z4NJ1RPY0~ z@-#HIq;H5}GkX-k>Qi<3`?Ce}q;_#-0# zVld?cZC);Zp})6$fwmgu>ttgSGQ*LUJt;?;z*Qc5D=b1LvP8&8h7GLo4K$q-cp^1%Tk}2g4 z2UG(XtDBa-xX#|74cJD+CcpcN>?li@BnOY+_-22kW=#jmLz zmLSb0pTD>aBkPZd@dDRw4w8ZkP@jDi8qqOrS;Ul#g=J30%ubdXw(W^)5k9;-G}EvR zfL^kHWQ#$8=n?+oDM>}KI`d|1rqclob4g=SI!4TxEPc2s{3ie={!!#B+i8XyV6L#} z?za;d4s?`> z?8CF2;Rli%`%?sXq49TiOQL_v{C%-tE>Q1uV-K19g-~vPK$_mChni}lbE>ouLBD0P zElI4lKrM%H-fom#ozV2oGrdR3ezt>h)+n3#s;=H*JO0FK{!g`9QU3145q?xB&GyQu zHT3;X#ht}T3EVmMRS#xBY|5IJNtm!+KC`3lB86*5Gq&l=h9h|j6AwUdxlNQ5*An;k z;2@s+UC|_IxaNto0mcW7&pTo&xh5KkNkJpu^U{6mupD)imuRHcFx78?M3inN9T}R8 z{>cRaZW@9g5XwdQrOlxnAGw95`U>j$F=m(j2X9%MlQ5O>dyopC<~*YcRr#pBr)M8v zAW&2LWhsBRVZ}K`O2yF)vb(R|cUijb-#g|CCLE_8Fr>QOw2Px``;E;DT}1o6qdnc* z0mF+Wb!@&dRVPjtzyH|ONSz*X5nnzXvEaa8murkDL1Kc?x$)+L_AKo1{P^vVaE7LN z80cNnu3FQD$uj|$r1v~%Man7MLBFOb49-@h)gLnM>w4LL=KD7A z`qd}R5^?J<7u67TD?k_iAJffWg#(f{ow{~)mVOiaYA~|`S&Hd+e-HF_A3j1D)0o}w zp_2DAl)Cyl=w&9$o7DFEtK^x=n{I>Nk(UMzv#H1OH!HkY3A&06)`Ew38V|!!!L`9E zpZ3T{rGuJlAAVBUkKb6=p=rJe?8o0Z0esHBxC52G=-%YKq+DCV;i4o7qxy6#PhmXa z8b1_lZ~;D8Bl&W%FH^D?=C@@GWlsp~KjV+(1TOfFj zI}AR2ykaSSBpd>_LCJPCs$A0AE=($IK*3>~(hk&AAtT;bpl~|l-N+Y56@pb-OcJ|M z-o>y-MMOf?H??6>kp|$RIgcuFR0SP1M+g9L_Dl~gu0og}qh1=_P`X_*`3qtziWh*g zOZ;o`Y?Tu5X~u@3=OCS>W-V}Ldelrtk`?GD(Q_xlx_&|0^Z+-0mUY6p#q(lPlM%+j zF}51Tn`V7bUp?%m=#a%&%(9NYRb1c*UTPZ(JKs&+0pCbQ2 zY~8e5DQz>@FFU6bs?xi3y<6Q5?1)VzxGNCGT=y112q{uu9?2f87Mczg>{j})9`0$} z>pXe;eshSI-EiAag&II)C||ln(KPpf@O{*kEQdvn(kXSsM~J;*_q25o69mHx?V9Em zaKeZ7EAk0GH;j(ZGY}55p$0G1ApYs=P79oP@y2lJp7WC%WSP z1a=vCAg7R1>4aP0xOr!(?vVnUli z6I_f0W9Dqg1}o%;QLFF6J4IM7&_SOIiuGJ*`mY%zs@yKydIyPhR-gnX-sh{2b7y2% zy-o)%jxl`^V4()vnwTiahLj^we6PBjmmU>HH3TJ2HV0%K21VW?&)G}yAL8Vv? z0csS||6IBaDafI*kIVmv4gdb!w&CBO@3j46`Tuy^!1Dh}$|YznSZw(pz#Bk5QevM^ z(*(fjL~$3~5CSu-r$RE*DE{M>4mLuX&FL%ke`LWAEM!Dh7OOtIfjxzeI`eksEpt8C zr9ZbIGMR1ekBC?Ya1yP4ZU7s1oL@?K{xstR@6o-3dV>@N0{8hbh;KxY>cl>!OHxpC z>&IFMFu3uQ)ew4eHEzKm1s-0HmIo^W_ZiNLwD@}Dm_OtZa&eT*${?sg?mRWg zX@g5-S+l0Ozg#UJ`>~-VGSUUcQMq=z^wlDyL;7Y)w%^Llx>H&({u#{^KUb0!z;>T2 zvuyiJXsGwCZ}Md#@hxb|!8XM0hoDZVNK^qywJ zHVSNe?cQ^Om!sY+-IX#BQ3E4eu*#Hh(>n zLoP&%zeVS0*hasGH9&c@n9+H&rFM_9zes@69inUl`Fs=jzG^XaubvJMHs*|Uwq~H^ zQo@gDF{M*bh`~(9brh3L)d^!1x%e9uP2U+?WWT8gUtaQP-`7L=ITb&i#xzvu_49@{ z4$QMQa!u(EDcmpXm}&0!&ta%mnye^kj8295@3%G0OHIK^L--f3#N9cPs>Y`n64<56 zStVDyZ+on)x>E33?3xkW&3?d$t~6PBsk*jIxM9%u3fv#<(trp1@YR{3z{DO@j$(C! zKmS+6V41C5j*zr;o?rQz+zU6=Y_QCEe5}xTP7Bye`8ks6g~R+T#2{%Yawj=rF*N$f zPK_(n=b|wkh5fQu2fw5icRT9$yMp;8_ndlfJPFeQB8FSUu4@doc1*z6(JxS}p7*m) z_Cq2*|L*9?yfS|rid(Qufo>NWS_K!(%zvKXpO3VI@1<5!Qv#0=ik8)M1ORZLtPxm`nzQ`37Ksi8K=+{IUVl#*0 z%C;mAtVgjfS0rh}vSDYrhnVal`|ELHvqetfSvXip_||qj9Xb7jEDSMg*%MwZZG9Ex z4?zB9Ef_i4_(g2Vkgcp9`SlA~n1)va#0>33s_PSonGpFU*V`P9={Gb9Ro)PA(VcGcW&h?1k!!!nUZ)YZcV7_l-k_ zf>XRNM2akBJQO2OGbGA;D&yS^h`LOn{CGDFlIP~T43W%tRK2~`l^(?LukzE(g3?0SiP;A zwGA=V)BOl=+zk(_Y^=|C9x-4@ejcmB3zwH(^|-+v__QmGOpKh2rdAtaF5vkH@bP7n zvG9prf0E=a34oF(6mK;9Ycy5E7~{9xt@)qMeS!>0tgMB?JMF5_EeUH?LB)=GS9GYQ z?eN4ZAJhNh%!v1mIKt*&A>(@`sZ<81o{3Ol9+~_U1Jbhv`DyT>lmjF$yQ+FhaXlcM zVU%ZK!!-D|FWFIgO5mbOgki7irZg$2b+C7lQdG)*5_uQS6qE!Dd+)yR&ovg}vlrMXYWQ2udQ=K_s8oW? zV?@2KS_$%U2ltU2jWEe*FG#!oAM6H}QasNOf7*cX^pK zCmz4DT#nJXle`}p8Lhrh#eceLE_khR;`Wi2w$_)t`Z|+NxImT_a1c0RQg!bx?Cu<^S}8Ihj8gOO|B$mpV_Y=cV9k93PD!=z5t> z8-OiL`NtKgkH^69xmo0M%w0ggMo~kQzBW|{dCnnTJI!=GG!G69LDDqrnc>2H6LCF= z!Fdpo>Dz~0{NK5*n){jD&R4p+{Ij`mw?m)0$6I7X`0D|4-G6lw`v27QvNqCJWn$E) hpJMwzYr3&qrh#@Tey&n$-IGp;=}u!v|^ocb%Br^v<1b!YlgB`>6`YL)L_0??smPq!iY#=QgBcrVU=4k*TjzNM z-2p4;V=CAq6I{f~Y=B$vggFI+)eDF(hci%REyaPwQd-@4K?L-2WIV-gSgwx1VEqdA zhys68gyOM6-146~b!W6uk}Mb8`_d3RC5iDU;!ETUZ@kC&U5#I9VYn$d5-6q$J;D9- z1}ltGoBjvBVJ!tj%(REd(DC51VP*#Jdt>3R^$z2F*vrO9@V91oXddFhodAm$XMhNF z!U%k!1SM-AGk<}R|EWGDSD*feG$?L>EP`Mj3U!Yg!7zLR9DMKiaS+sr3;G^>v64k< zMH0hsnk@AN1Y_-AQKl9*_lX!18- zUO|a|tf_2ulziYDNC|ot4R2BPS>(&5{+`kyrULn_ZB1-lc$Dx)&mT^T2bo)T?#@6Y zn&wKtQ=aHbnh9fkF-1R<=Ldy-DpRd_BQPxBQMFUTYm-54&A%@dq3#omjsz*D;eN9^ z`p@?R2U#=la0aju{fxl^?`vPGLMXismK-vkPwPx(OfQX<+6z+)g+1~MT{hCxoYX0( z+Tph2+8wXkzG>06Dc!4dD0grywy#c(RPI}zY<2t9{RD{fiowBm2s zhv?8YF=a5_xs)pyf2LG!59`_U7cGB>a8wu&M7p^so)`zS!3uJDhYDy1vv4Q~2-w1s zM6+npv5p??AO9qYY8cpj;WLOeF5M+<;|FU^?4xVVdA@JZ{OZ_LOrId~JggpJ*Bkso zFwkE`Ur}H6VH;&YL#^@AdG$N)5dLeaH`FUhA3q-o@O=9ho373-b=plwM4>UUrN0Rc zx6L!EaF-&`y^?WrWxH5eb6`Ce7Y49^{FelxpN~apz>*;#N(*dxb>);;XebEnzEWtO zP@i{+!k@lltfQgW70#gfyukCUX)1VzzAUqzpN$9<8p6OCsj&Pc?Rg0s4W}}aJ^rKK z3zea}(C$Xf{gKEzCoo)&UGpSkJtut|27cAxJ&?+z8wizwO6E&s(>#Vkh9qQ}=rwN( ze?~5RzO|uQ$$K<8xDcDVVwkwJ#e;pDk?Jtg5o7*0j)354V$>w#ECC})1et%mmf?u( zLN6(DvwbS1hBNZBFXJtZH;hB1r*A!bt4CnW;2PU_F6};iI!+Y&pujV*xt(e2HH){>lA!uedI* z|0Nai^-%q*ykmf4iecR0BZ4gztXu9<(rLYx-&l)IMPrPI1iXcOiOjt;sHYa%RItf* z0i%PEf*0zDhV}-7uY;Gsy{ZBnF4I+rQv6MX>k5}+1ONy+ z3gYsSNZ}Fa1$WkH(SbU#%6{6@JebBK4D;lQ+x4E?jRW^x1*D=qV&vf2v^?r{+N&BK zH-NPWCnG|Hqr&DID^T)WO+5XX=_@!0<2Se_-oL=oOZhe+#r*O^Q*pA?mlSAhg*B^6 zPzM(>^AjD;K=bS0D8Iy;pG3$v#H^Co{yoH?I~ULnwc3zIso#6hn32yfU*CcaPAj~TSj6nMy8%+R!t5)& zie#G2Ipg2v^dq{y}_Y=(T>!gG4Yq!+TuFawQ{s0i;* zpX7Z1=&aKzx$PGvS*M97mo^K?S0CokYRqq#*~P#q@nkW8UaI9%U(!`Bjb~&|PX<;hor?o;` z?b3y*%!goEtiA*#m*fT21(WaL@N|-!PLU*$`U!}WI2PQ!`(h#ZUnfKsCg@?J$4bsV zzkYXMGxKkm=rDm5JtDw9R&Ow}vKH8Y-7L_&^WIcxuqRMVZo%SvFVQW}W2TF#wvzAG zC1?1BVVuQaoP0>IxiOWg74xrpj5>_HF*PW93~3EuSJ=%CRu<^~DBYERNT`oxiS14H zY&T`S{Hn7NTHmFP3`;OTPgOLvn7l~aV`iq2miTSWeKD^xZA=q__DZ|<#d%WuAtal+ zxDcjL(p!pg-w~*ZckCo4D7I@ur(>e)$A6Adv>oU@{sSk6@F7SRCm-PyDj+ z^*D39v>TEq=D%-Qr|EL)r;_Woeh&FJgJ_atljih+0A^V%*!rK;V`$`|KJ-WHLl}k3H*>$8!`K|*b9PvFWX`{Sosp7p zF2MI#7(Iq;d}5xi=>Tn;>so42TL$OfG|R9Y*b?V-DN`e`2mgRQ1PBUy_m&mj%^Hs< z(K}#~DY_IPf)UE{hMLI=+50lK!xkvDV&dU!e#8;K$^qQ+X`}DoC~3iC<`^=?y|N;@ zmz96cl1`c~BExJgvT6BZ92kbu+c6CZVdN89M!MKxNKEUGEAL-q`J?7r6Kf+39evvr zO6XfMY$|Y(3o5qSJ@Wi9QWXrtnSNh#&4PX`6LLOb7PIz=#bfPZ&-Udx9^T=}P^XBl z2sQV2ESPrV-Xf-KafXK`3Gr(Y{4R4`9(0imoH%|_y70mDwXH!%7b)^xJ_tBJc=>o< z118)#94DqHl%gfTIFYuJ73XGgS=(G11Zhl=#$;(EM|^!6c2h=vh_qbL4jqfajOo zwKAvvKN@hbE`5Vs=(Q_&-Q}~RyW@4a)8?fCnGnJ_j{#ty%wbXaXmyh#1%Da1iklF% z!i=H~^%RP}mO-@2V}d?H=DOE3kH38^xeTKQLUU3)V{{gJck#GuC}2y0!6nT+&eOAZ zs@bl0L_d!ydtlCEHfT?S=nfM08NcYm9EdDq(%a%}YK9-4;<&2egT|d?^*BFfsXk1Z z06akj8+H_zLmgj)w4e*0wQzI@@((nLF$LufjQhXO{qJeZrpCFXfYPR4?{Z)UIaBk+ zdI5;tuJ_r1BMgOpV?8vW@_$DT zj6#)3)WHxZb{Z3W1OwfOqHrSDC zh78NkUP9b+zGy1~r75H-`6|U4NmlPWn{Xv)nVXC-Jtez8QWJVdCZL!3alvJk!^pVX zx`055s{lO-ykcE|bd z50D658TR5NU3$lAV`bmmt%M}sIhh>^yH_Oa_fqpMOX~}zAqaqNbdmWlr<~+r3 zjaG6t=yJ}i=txDgElEp~(=(H+kCj=6PpK;Ai`5ICcQx;l={hp`oqATOMHaOM)+R5> zp+V}@d5#MlPf8i`zDj&GZEZ#CT7c1{$r-10e8)p%lO-(sEZrhZaHjd&bV^cuKI0Atb!bp(jc zKCPmKLltJeE{!rhrznTOb)I+VIsg1B*ZdKaYY$g0I)}aprV^1LzYwgo)=_dz{Wz~6 zWeg%>QXry_SwUzWUoGpY3Y*wbn0O)be8b0%RImU|!sjAKpF)YzLs;@{$Cu5dVq5^; zR~Lyg!ylWEHD_?%o{N;@J$|zs<*E%NfZSNE=5|u}Ut)joAx5Vg+1#Ksl|4U1i56Q0F zm@1M^3xGVF3BORjytg6jv!qGZ5_++=D6v|E>sdSt^@xQ`#>NDDnVg)4Nsu4_$ z=j&ySs2|FAFfiyab7_Lg*xM;BfTN8`vnzO{TNZj#j zCQ#*{cyPB%3UtTFBie%MqwCdWy>H8K2UL*D@tB!qUBPY`zxa|L+0eZplbui&jkASr z^&&pxygZk$(1Q`IhlwTDAHEOX%E3^?uoMz8&Vj8DQ24LFgJKtB%78vQaXOBEPnOmg zY7n17$&g``SNkSP>*r@#ycqApw>tIEs(N1u?ZjcBKWZ0Ra(_mUA zRUD$O%!L^Qf}y^%D4z{Kxqpga+r0>s_lHl7uRtV4ijV`Kmm2R`hvkx?+ODvGkhY~< zs?otedWLL^C&c)zyG(4=9F%=({O7wA(Xi^;b4&v-1XjF@y6TS``mjed$vT{DlNagb z%#yPqbklvCP^@w^!{)ul_tZ&w&iZQ5^}1s@W}E@D z!*C%l)9=1w~FJ4wt&8*%A`);z-i#^48*OKUn^ z`45*mC-3mC#E8cXGkhODhb(d-`<7MO-)s+azpQjK`|@q=e$WJPltkOkAh0p}{W;xc zM@G@Uus%AWgO_G*8QZ1!?}Mgx{AubjiA@GSZE21{wU{ezn?2InwKTv zcPY%RT;4;0;B8lMyBOB)f~xU35ui*CO>#(iOEldlDA2iFp5p;%xo0&lqU*D$@!&yN;fc!M&}YJJ z=|mzp=&Y(o^OkF*WPv@R>f7Isif@!FN9b z5~v{LPr;r`aJl!@?M-h7WbY1reB1cvlB9R?wAyH&)EUMr%h+KO7(oFTD80Y0banIU;sAH z?7z3Cew4x_B$Qd$3oeCQUmaf6v=FFSvoq%3G@1{Wa_y21U6mE5roEv{tn=f1+Sg5! z69lI0 zGqcy)Yh$}M?L!8P+&^7o@(D&Sy&t+v$A6EN{PJ#dy#65cQN^|5lYWMr;>+@;5F)@+ zU%|~Dfh@a$6HC6@Z}kDWv}lzn7w5!Jm29$06SGXwx)c=5B^=(InCX3Q+hl1!Sk9k% zM}tzNP_E2gmI%Grg?zz#7tSj(ZE@T*_79#!&l!pB1rH*PoEY-j;xFN;NqTEm^Hv$xVPHz|3Q}S1pkhHx+^Q$J zjB;ZL@l{^4K+lkb1wgTW{C8%?^pP31S5KgQW^|M&;4Y5*Xqj=ZH1+dl;p4ZZDcXl3 z0YFA&$m*os2z8oG&{B@XvRi?}?2#7v9uW&Y`7tfd8S{{IniN$4wMt)>LG*gFE0Qrn zf0M7lg!9x#$ltZ)dwy-tBmCOHz5A?DuH&AZ&Mgw?Kd*d|S>lyrKIcOk@& zZSvoowoXf}5{R$lnKu1ULs=q~Dh zL#fJFDF5iA%3rKpa11R(jRs-BX&h^*id=mSlFeEV?pIPxJ@i(Ma$rha+Hkq}WlO2> zfNiPJ!*aP}rknkhBj2-zJPLcVW|EkA4NO4vqS(^Q&p#)A50d^SvSpvN6-z1*x-02g zs}kV72=I9|f8L)fK9psm8Fjv*+`Qk83mGGI^JuP2e^+N zx%%ZIC|et!-hZh)7!ls4@Wyf(Dx?=Ky; zb*U}=;lnK}>}QCxk;O%idOEsdC<)(@toQ4lB(Q5^zbrm6T?umJ$wtFYu&)rw|9C;b z^cnr7xT7<9AC(EsBC78Tdot0gYT`O3yuG%pB?J!7ABXrcE3X?gdd~*uxbz>sQxTKX ziNh5|H882bpH+0sj%X+zHbeJ@b(WKIwZ+9!SQbjR&D9hJt~}*?R&32 zKBHH(6s<&<`aY%)#@1wXNa@7~@Fr;7u(Cg976Z9GBz}HWt)~%IrDm=xg%EK@mqZQAg22z-(b~6>re}>xP}Ij(%zm=KZf9k^8(Cdp8$0a8rx)Tdvo

UF3$(7Yp%bIfUM>4&dOLKk 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 O@2#xL>q=?Up#K9}p4wFa literal 10472 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 HArb!rXRrfz diff --git a/test/fixtures/plugin.filler/fill-radar-boundary-origin.png b/test/fixtures/plugin.filler/fill-radar-boundary-origin.png index 66c6e563a796d1c50039329dafff1e339a841f53..82b1f60478d1e0f83d5900986b43d400d906a1d9 100644 GIT binary patch literal 10129 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 YbW$4*rF7S@0Qk{-sHa||Y8Uzc0IX3l4FCWD diff --git a/test/specs/controller.radar.tests.js b/test/specs/controller.radar.tests.js index a013274bb..a911f9302 100644 --- a/test/specs/controller.radar.tests.js +++ b/test/specs/controller.radar.tests.js @@ -131,10 +131,10 @@ describe('Chart.controllers.radar', function() { })); [ - {x: 256, y: 256, cppx: 256, cppy: 256, cpnx: 256, cpny: 256}, - {x: 256, y: 256, cppx: 256, cppy: 256, cpnx: 256, cpny: 256}, - {x: 256, y: 256, cppx: 256, cppy: 256, cpnx: 256, cpny: 256}, - {x: 256, y: 256, cppx: 256, cppy: 256, cpnx: 256, cpny: 256}, + {x: 256, y: 260, cppx: 256, cppy: 260, cpnx: 256, cpny: 260}, + {x: 256, y: 260, cppx: 256, cppy: 260, cpnx: 256, cpny: 260}, + {x: 256, y: 260, cppx: 256, cppy: 260, cpnx: 256, cpny: 260}, + {x: 256, y: 260, cppx: 256, cppy: 260, cpnx: 256, cpny: 260}, ].forEach(function(expected, i) { expect(meta.data[i]._model.x).toBeCloseToPixel(expected.x); expect(meta.data[i]._model.y).toBeCloseToPixel(expected.y); @@ -158,10 +158,10 @@ describe('Chart.controllers.radar', function() { meta.controller.update(); [ - {x: 256, y: 117, cppx: 246, cppy: 117, cpnx: 272, cpny: 117}, - {x: 464, y: 256, cppx: 464, cppy: 248, cpnx: 464, cpny: 262}, - {x: 256, y: 256, cppx: 277, cppy: 256, cpnx: 250, cpny: 256}, - {x: 200, y: 256, cppx: 200, cppy: 259, cpnx: 200, cpny: 245}, + {x: 256, y: 120, cppx: 246, cppy: 120, cpnx: 272, cpny: 120}, + {x: 464, y: 260, cppx: 464, cppy: 252, cpnx: 464, cpny: 266}, + {x: 256, y: 260, cppx: 277, cppy: 260, cpnx: 250, cpny: 260}, + {x: 200, y: 260, cppx: 200, cppy: 264, cpnx: 200, cpny: 250}, ].forEach(function(expected, i) { expect(meta.data[i]._model.x).toBeCloseToPixel(expected.x); expect(meta.data[i]._model.y).toBeCloseToPixel(expected.y); @@ -215,10 +215,10 @@ describe('Chart.controllers.radar', function() { // Since tension is now 0, we don't care about the control points [ - {x: 256, y: 117}, - {x: 464, y: 256}, - {x: 256, y: 256}, - {x: 200, y: 256}, + {x: 256, y: 120}, + {x: 464, y: 260}, + {x: 256, y: 260}, + {x: 200, y: 260}, ].forEach(function(expected, i) { expect(meta.data[i]._model.x).toBeCloseToPixel(expected.x); expect(meta.data[i]._model.y).toBeCloseToPixel(expected.y); @@ -274,11 +274,11 @@ describe('Chart.controllers.radar', function() { })); expect(meta.data[0]._model.x).toBeCloseToPixel(256); - expect(meta.data[0]._model.y).toBeCloseToPixel(117); + expect(meta.data[0]._model.y).toBeCloseToPixel(120); expect(meta.data[0]._model.controlPointPreviousX).toBeCloseToPixel(241); - expect(meta.data[0]._model.controlPointPreviousY).toBeCloseToPixel(117); + expect(meta.data[0]._model.controlPointPreviousY).toBeCloseToPixel(120); expect(meta.data[0]._model.controlPointNextX).toBeCloseToPixel(281); - expect(meta.data[0]._model.controlPointNextY).toBeCloseToPixel(117); + expect(meta.data[0]._model.controlPointNextY).toBeCloseToPixel(120); expect(meta.data[0]._model).toEqual(jasmine.objectContaining({ radius: 2.2, backgroundColor: 'rgb(0, 1, 3)', diff --git a/test/specs/core.controller.tests.js b/test/specs/core.controller.tests.js index cf9eb30a2..443cceace 100644 --- a/test/specs/core.controller.tests.js +++ b/test/specs/core.controller.tests.js @@ -761,16 +761,16 @@ describe('Chart', function() { // then we will reset and see that they moved expect(meta.data[0]._model.y).toBeCloseToPixel(333); expect(meta.data[1]._model.y).toBeCloseToPixel(183); - expect(meta.data[2]._model.y).toBe(32); - expect(meta.data[3]._model.y).toBe(484); + expect(meta.data[2]._model.y).toBeCloseToPixel(32); + expect(meta.data[3]._model.y).toBeCloseToPixel(482); chart.reset(); // For a line chart, the animation state is the bottom - expect(meta.data[0]._model.y).toBe(484); - expect(meta.data[1]._model.y).toBe(484); - expect(meta.data[2]._model.y).toBe(484); - expect(meta.data[3]._model.y).toBe(484); + expect(meta.data[0]._model.y).toBeCloseToPixel(482); + expect(meta.data[1]._model.y).toBeCloseToPixel(482); + expect(meta.data[2]._model.y).toBeCloseToPixel(482); + expect(meta.data[3]._model.y).toBeCloseToPixel(482); }); }); diff --git a/test/specs/core.scale.tests.js b/test/specs/core.scale.tests.js index 93545a217..175f47403 100644 --- a/test/specs/core.scale.tests.js +++ b/test/specs/core.scale.tests.js @@ -9,9 +9,6 @@ describe('Core.scale', function() { // actual label labelString: '', - // actual label - lineHeight: 1.2, - // top/bottom padding padding: { top: 4, diff --git a/test/specs/helpers.options.tests.js b/test/specs/helpers.options.tests.js index 3188888cb..1afbd59bd 100644 --- a/test/specs/helpers.options.tests.js +++ b/test/specs/helpers.options.tests.js @@ -4,122 +4,197 @@ describe('Chart.helpers.options', function() { var options = Chart.helpers.options; describe('toLineHeight', function() { + var toLineHeight = options.toLineHeight; + it ('should support keyword values', function() { - expect(options.toLineHeight('normal', 16)).toBe(16 * 1.2); + expect(toLineHeight('normal', 16)).toBe(16 * 1.2); }); it ('should support unitless values', function() { - expect(options.toLineHeight(1.4, 16)).toBe(16 * 1.4); - expect(options.toLineHeight('1.4', 16)).toBe(16 * 1.4); + expect(toLineHeight(1.4, 16)).toBe(16 * 1.4); + expect(toLineHeight('1.4', 16)).toBe(16 * 1.4); }); it ('should support length values', function() { - expect(options.toLineHeight('42px', 16)).toBe(42); - expect(options.toLineHeight('1.4em', 16)).toBe(16 * 1.4); + expect(toLineHeight('42px', 16)).toBe(42); + expect(toLineHeight('1.4em', 16)).toBe(16 * 1.4); }); it ('should support percentage values', function() { - expect(options.toLineHeight('140%', 16)).toBe(16 * 1.4); + expect(toLineHeight('140%', 16)).toBe(16 * 1.4); }); it ('should fallback to default (1.2) for invalid values', function() { - expect(options.toLineHeight(null, 16)).toBe(16 * 1.2); - expect(options.toLineHeight(undefined, 16)).toBe(16 * 1.2); - expect(options.toLineHeight('foobar', 16)).toBe(16 * 1.2); + expect(toLineHeight(null, 16)).toBe(16 * 1.2); + expect(toLineHeight(undefined, 16)).toBe(16 * 1.2); + expect(toLineHeight('foobar', 16)).toBe(16 * 1.2); }); }); describe('toPadding', function() { + var toPadding = options.toPadding; + it ('should support number values', function() { - expect(options.toPadding(4)).toEqual( + expect(toPadding(4)).toEqual( {top: 4, right: 4, bottom: 4, left: 4, height: 8, width: 8}); - expect(options.toPadding(4.5)).toEqual( + expect(toPadding(4.5)).toEqual( {top: 4.5, right: 4.5, bottom: 4.5, left: 4.5, height: 9, width: 9}); }); it ('should support string values', function() { - expect(options.toPadding('4')).toEqual( + expect(toPadding('4')).toEqual( {top: 4, right: 4, bottom: 4, left: 4, height: 8, width: 8}); - expect(options.toPadding('4.5')).toEqual( + expect(toPadding('4.5')).toEqual( {top: 4.5, right: 4.5, bottom: 4.5, left: 4.5, height: 9, width: 9}); }); it ('should support object values', function() { - expect(options.toPadding({top: 1, right: 2, bottom: 3, left: 4})).toEqual( + expect(toPadding({top: 1, right: 2, bottom: 3, left: 4})).toEqual( {top: 1, right: 2, bottom: 3, left: 4, height: 4, width: 6}); - expect(options.toPadding({top: 1.5, right: 2.5, bottom: 3.5, left: 4.5})).toEqual( + expect(toPadding({top: 1.5, right: 2.5, bottom: 3.5, left: 4.5})).toEqual( {top: 1.5, right: 2.5, bottom: 3.5, left: 4.5, height: 5, width: 7}); - expect(options.toPadding({top: '1', right: '2', bottom: '3', left: '4'})).toEqual( + expect(toPadding({top: '1', right: '2', bottom: '3', left: '4'})).toEqual( {top: 1, right: 2, bottom: 3, left: 4, height: 4, width: 6}); }); it ('should fallback to 0 for invalid values', function() { - expect(options.toPadding({top: 'foo', right: 'foo', bottom: 'foo', left: 'foo'})).toEqual( + expect(toPadding({top: 'foo', right: 'foo', bottom: 'foo', left: 'foo'})).toEqual( {top: 0, right: 0, bottom: 0, left: 0, height: 0, width: 0}); - expect(options.toPadding({top: null, right: null, bottom: null, left: null})).toEqual( + expect(toPadding({top: null, right: null, bottom: null, left: null})).toEqual( {top: 0, right: 0, bottom: 0, left: 0, height: 0, width: 0}); - expect(options.toPadding({})).toEqual( + expect(toPadding({})).toEqual( {top: 0, right: 0, bottom: 0, left: 0, height: 0, width: 0}); - expect(options.toPadding('foo')).toEqual( + expect(toPadding('foo')).toEqual( {top: 0, right: 0, bottom: 0, left: 0, height: 0, width: 0}); - expect(options.toPadding(null)).toEqual( + expect(toPadding(null)).toEqual( {top: 0, right: 0, bottom: 0, left: 0, height: 0, width: 0}); - expect(options.toPadding(undefined)).toEqual( + expect(toPadding(undefined)).toEqual( {top: 0, right: 0, bottom: 0, left: 0, height: 0, width: 0}); }); }); + describe('_parseFont', function() { + var parseFont = options._parseFont; + + it ('should return a font with default values', function() { + var global = Chart.defaults.global; + + Chart.defaults.global = { + defaultFontFamily: 'foobar', + defaultFontSize: 42, + defaultFontStyle: 'xxxyyy', + defaultLineHeight: 1.5 + }; + + expect(parseFont({})).toEqual({ + family: 'foobar', + lineHeight: 63, + size: 42, + string: 'xxxyyy 42px foobar', + style: 'xxxyyy', + weight: null + }); + + Chart.defaults.global = global; + }); + it ('should return a font with given values', function() { + expect(parseFont({ + fontFamily: 'bla', + lineHeight: 8, + fontSize: 21, + fontStyle: 'zzz' + })).toEqual({ + family: 'bla', + lineHeight: 8 * 21, + size: 21, + string: 'zzz 21px bla', + style: 'zzz', + weight: null + }); + }); + it('should return null as a font string if fontSize or fontFamily are missing', function() { + var global = Chart.defaults.global; + + Chart.defaults.global = {}; + + expect(parseFont({ + fontStyle: 'italic', + fontSize: 12 + }).string).toBeNull(); + expect(parseFont({ + fontStyle: 'italic', + fontFamily: 'serif' + }).string).toBeNull(); + + Chart.defaults.global = global; + }); + it('fontStyle should be optional for font strings', function() { + var global = Chart.defaults.global; + + Chart.defaults.global = {}; + + expect(parseFont({ + fontSize: 12, + fontFamily: 'serif' + }).string).toBe('12px serif'); + + Chart.defaults.global = global; + }); + }); + describe('resolve', function() { + var resolve = options.resolve; + it ('should fallback to the first defined input', function() { - expect(options.resolve([42])).toBe(42); - expect(options.resolve([42, 'foo'])).toBe(42); - expect(options.resolve([undefined, 42, 'foo'])).toBe(42); - expect(options.resolve([42, 'foo', undefined])).toBe(42); - expect(options.resolve([undefined])).toBe(undefined); + expect(resolve([42])).toBe(42); + expect(resolve([42, 'foo'])).toBe(42); + expect(resolve([undefined, 42, 'foo'])).toBe(42); + expect(resolve([42, 'foo', undefined])).toBe(42); + expect(resolve([undefined])).toBe(undefined); }); it ('should correctly handle empty values (null, 0, "")', function() { - expect(options.resolve([0, 'foo'])).toBe(0); - expect(options.resolve(['', 'foo'])).toBe(''); - expect(options.resolve([null, 'foo'])).toBe(null); + expect(resolve([0, 'foo'])).toBe(0); + expect(resolve(['', 'foo'])).toBe(''); + expect(resolve([null, 'foo'])).toBe(null); }); it ('should support indexable options if index is provided', function() { var input = [42, 'foo', 'bar']; - expect(options.resolve([input], undefined, 0)).toBe(42); - expect(options.resolve([input], undefined, 1)).toBe('foo'); - expect(options.resolve([input], undefined, 2)).toBe('bar'); + expect(resolve([input], undefined, 0)).toBe(42); + expect(resolve([input], undefined, 1)).toBe('foo'); + expect(resolve([input], undefined, 2)).toBe('bar'); }); it ('should fallback if an indexable option value is undefined', function() { var input = [42, undefined, 'bar']; - expect(options.resolve([input], undefined, 5)).toBe(undefined); - expect(options.resolve([input, 'foo'], undefined, 1)).toBe('foo'); - expect(options.resolve([input, 'foo'], undefined, 5)).toBe('foo'); + expect(resolve([input], undefined, 5)).toBe(undefined); + expect(resolve([input, 'foo'], undefined, 1)).toBe('foo'); + expect(resolve([input, 'foo'], undefined, 5)).toBe('foo'); }); it ('should not handle indexable options if index is undefined', function() { var array = [42, 'foo', 'bar']; - expect(options.resolve([array])).toBe(array); - expect(options.resolve([array], undefined, undefined)).toBe(array); + expect(resolve([array])).toBe(array); + expect(resolve([array], undefined, undefined)).toBe(array); }); it ('should support scriptable options if context is provided', function() { var input = function(context) { return context.v * 2; }; - expect(options.resolve([42], {v: 42})).toBe(42); - expect(options.resolve([input], {v: 42})).toBe(84); + expect(resolve([42], {v: 42})).toBe(42); + expect(resolve([input], {v: 42})).toBe(84); }); it ('should fallback if a scriptable option returns undefined', function() { var input = function() {}; - expect(options.resolve([input], {v: 42})).toBe(undefined); - expect(options.resolve([input, 'foo'], {v: 42})).toBe('foo'); - expect(options.resolve([input, undefined, 'foo'], {v: 42})).toBe('foo'); + expect(resolve([input], {v: 42})).toBe(undefined); + expect(resolve([input, 'foo'], {v: 42})).toBe('foo'); + expect(resolve([input, undefined, 'foo'], {v: 42})).toBe('foo'); }); it ('should not handle scriptable options if context is undefined', function() { var input = function(context) { return context.v * 2; }; - expect(options.resolve([input])).toBe(input); - expect(options.resolve([input], undefined)).toBe(input); + expect(resolve([input])).toBe(input); + expect(resolve([input], undefined)).toBe(input); }); it ('should handle scriptable and indexable option', function() { var input = function(context) { return [context.v, undefined, 'bar']; }; - expect(options.resolve([input, 'foo'], {v: 42}, 0)).toBe(42); - expect(options.resolve([input, 'foo'], {v: 42}, 1)).toBe('foo'); - expect(options.resolve([input, 'foo'], {v: 42}, 5)).toBe('foo'); - expect(options.resolve([input, ['foo', 'bar']], {v: 42}, 1)).toBe('bar'); + expect(resolve([input, 'foo'], {v: 42}, 0)).toBe(42); + expect(resolve([input, 'foo'], {v: 42}, 1)).toBe('foo'); + expect(resolve([input, 'foo'], {v: 42}, 5)).toBe('foo'); + expect(resolve([input, ['foo', 'bar']], {v: 42}, 1)).toBe('bar'); }); }); }); diff --git a/test/specs/plugin.legend.tests.js b/test/specs/plugin.legend.tests.js index 451f7400d..fee715bdb 100644 --- a/test/specs/plugin.legend.tests.js +++ b/test/specs/plugin.legend.tests.js @@ -494,7 +494,7 @@ describe('Legend block tests', function() { expect(chart.legend.left).toBeCloseToPixel(0); expect(chart.legend.top).toBeCloseToPixel(6); expect(chart.legend.width).toBeCloseToPixel(128); - expect(chart.legend.height).toBeCloseToPixel(478); + expect(chart.legend.height).toBeCloseToPixel(476); expect(chart.legend.legendHitBoxes.length).toBe(22); [ diff --git a/test/specs/plugin.title.tests.js b/test/specs/plugin.title.tests.js index 28786f054..4f00ee892 100644 --- a/test/specs/plugin.title.tests.js +++ b/test/specs/plugin.title.tests.js @@ -8,7 +8,6 @@ describe('Title block tests', function() { fullWidth: true, weight: 2000, fontStyle: 'bold', - lineHeight: 1.2, padding: 10, text: '' }); diff --git a/test/specs/scale.category.tests.js b/test/specs/scale.category.tests.js index 0ce39e5b3..7bc6a8a57 100644 --- a/test/specs/scale.category.tests.js +++ b/test/specs/scale.category.tests.js @@ -340,8 +340,8 @@ describe('Category scale tests', function() { expect(yScale.getPixelForValue(0, 0, 0)).toBeCloseToPixel(77); expect(yScale.getValueForPixel(77)).toBe(0); - expect(yScale.getPixelForValue(0, 4, 0)).toBeCloseToPixel(439); - expect(yScale.getValueForPixel(439)).toBe(4); + expect(yScale.getPixelForValue(0, 4, 0)).toBeCloseToPixel(437); + expect(yScale.getValueForPixel(437)).toBe(4); }); it ('should get the correct pixel for a value when vertical and zoomed', function() { @@ -385,6 +385,6 @@ describe('Category scale tests', function() { chart.update(); expect(yScale.getPixelForValue(0, 1, 0)).toBeCloseToPixel(107); - expect(yScale.getPixelForValue(0, 3, 0)).toBeCloseToPixel(409); + expect(yScale.getPixelForValue(0, 3, 0)).toBeCloseToPixel(407); }); }); diff --git a/test/specs/scale.linear.tests.js b/test/specs/scale.linear.tests.js index 9ec21116e..ad35e2a43 100644 --- a/test/specs/scale.linear.tests.js +++ b/test/specs/scale.linear.tests.js @@ -817,9 +817,9 @@ describe('Linear Scale', function() { expect(yScale.getPixelForValue(-1, 0, 0)).toBeCloseToPixel(484); // left + paddingLeft expect(yScale.getPixelForValue(0, 0, 0)).toBeCloseToPixel(258); // halfway*/ - expect(yScale.getValueForPixel(32)).toBe(1); - expect(yScale.getValueForPixel(484)).toBe(-1); - expect(yScale.getValueForPixel(258)).toBe(0); + expect(yScale.getValueForPixel(32)).toBeCloseTo(1, 1e-2); + expect(yScale.getValueForPixel(484)).toBeCloseTo(-1, 1e-2); + expect(yScale.getValueForPixel(258)).toBeCloseTo(0, 1e-2); }); it('should fit correctly', function() { @@ -865,7 +865,7 @@ describe('Linear Scale', function() { expect(xScale.paddingLeft).toBeCloseToPixel(0); expect(xScale.paddingRight).toBeCloseToPixel(0); expect(xScale.width).toBeCloseToPixel(468 - 6); // minus lineSpace - expect(xScale.height).toBeCloseToPixel(28); + expect(xScale.height).toBeCloseToPixel(30); var yScale = chart.scales.yScale0; expect(yScale.paddingTop).toBeCloseToPixel(0); @@ -873,7 +873,7 @@ describe('Linear Scale', function() { expect(yScale.paddingLeft).toBeCloseToPixel(0); expect(yScale.paddingRight).toBeCloseToPixel(0); expect(yScale.width).toBeCloseToPixel(30 + 6); // plus lineSpace - expect(yScale.height).toBeCloseToPixel(452); + expect(yScale.height).toBeCloseToPixel(450); // Extra size when scale label showing xScale.options.scaleLabel.display = true; @@ -885,14 +885,14 @@ describe('Linear Scale', function() { expect(xScale.paddingLeft).toBeCloseToPixel(0); expect(xScale.paddingRight).toBeCloseToPixel(0); expect(xScale.width).toBeCloseToPixel(440); - expect(xScale.height).toBeCloseToPixel(50); + expect(xScale.height).toBeCloseToPixel(53); expect(yScale.paddingTop).toBeCloseToPixel(0); expect(yScale.paddingBottom).toBeCloseToPixel(0); expect(yScale.paddingLeft).toBeCloseToPixel(0); expect(yScale.paddingRight).toBeCloseToPixel(0); expect(yScale.width).toBeCloseToPixel(58); - expect(yScale.height).toBeCloseToPixel(430); + expect(yScale.height).toBeCloseToPixel(427); }); it('should fit correctly when display is turned off', function() { diff --git a/test/specs/scale.radialLinear.tests.js b/test/specs/scale.radialLinear.tests.js index 5459eac31..49ef34b46 100644 --- a/test/specs/scale.radialLinear.tests.js +++ b/test/specs/scale.radialLinear.tests.js @@ -349,9 +349,9 @@ describe('Test the radial linear scale', function() { } }); - expect(chart.scale.drawingArea).toBe(232); + expect(chart.scale.drawingArea).toBe(227); expect(chart.scale.xCenter).toBe(256); - expect(chart.scale.yCenter).toBe(279); + expect(chart.scale.yCenter).toBe(284); }); it('should correctly get the label for a given data index', function() { @@ -397,16 +397,16 @@ describe('Test the radial linear scale', function() { }); expect(chart.scale.getDistanceFromCenterForValue(chart.scale.min)).toBe(0); - expect(chart.scale.getDistanceFromCenterForValue(chart.scale.max)).toBe(232); + expect(chart.scale.getDistanceFromCenterForValue(chart.scale.max)).toBe(227); var position = chart.scale.getPointPositionForValue(1, 5); expect(position.x).toBeCloseToPixel(270); - expect(position.y).toBeCloseToPixel(275); + expect(position.y).toBeCloseToPixel(278); chart.scale.options.ticks.reverse = true; chart.update(); - expect(chart.scale.getDistanceFromCenterForValue(chart.scale.min)).toBe(232); + expect(chart.scale.getDistanceFromCenterForValue(chart.scale.min)).toBe(227); expect(chart.scale.getDistanceFromCenterForValue(chart.scale.max)).toBe(0); }); -- 2.39.5