app/assets/javascripts/pghero/Chart.bundle.js in pghero-2.7.0 vs app/assets/javascripts/pghero/Chart.bundle.js in pghero-2.7.1

- old
+ new

@@ -1,808 +1,1240 @@ /*! - * Chart.js v2.8.0 + * Chart.js v2.9.3 * https://www.chartjs.org * (c) 2019 Chart.js Contributors * Released under the MIT License */ (function (global, factory) { typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : typeof define === 'function' && define.amd ? define(factory) : -(global.Chart = factory()); +(global = global || self, global.Chart = factory()); }(this, (function () { 'use strict'; -/* MIT license */ +var commonjsGlobal = typeof globalThis !== 'undefined' ? globalThis : typeof window !== 'undefined' ? window : typeof global !== 'undefined' ? global : typeof self !== 'undefined' ? self : {}; -var conversions = { - rgb2hsl: rgb2hsl, - rgb2hsv: rgb2hsv, - rgb2hwb: rgb2hwb, - rgb2cmyk: rgb2cmyk, - rgb2keyword: rgb2keyword, - rgb2xyz: rgb2xyz, - rgb2lab: rgb2lab, - rgb2lch: rgb2lch, +function commonjsRequire () { + throw new Error('Dynamic requires are not currently supported by rollup-plugin-commonjs'); +} - hsl2rgb: hsl2rgb, - hsl2hsv: hsl2hsv, - hsl2hwb: hsl2hwb, - hsl2cmyk: hsl2cmyk, - hsl2keyword: hsl2keyword, +function createCommonjsModule(fn, module) { + return module = { exports: {} }, fn(module, module.exports), module.exports; +} - hsv2rgb: hsv2rgb, - hsv2hsl: hsv2hsl, - hsv2hwb: hsv2hwb, - hsv2cmyk: hsv2cmyk, - hsv2keyword: hsv2keyword, +function getCjsExportFromNamespace (n) { + return n && n['default'] || n; +} - hwb2rgb: hwb2rgb, - hwb2hsl: hwb2hsl, - hwb2hsv: hwb2hsv, - hwb2cmyk: hwb2cmyk, - hwb2keyword: hwb2keyword, +var colorName = { + "aliceblue": [240, 248, 255], + "antiquewhite": [250, 235, 215], + "aqua": [0, 255, 255], + "aquamarine": [127, 255, 212], + "azure": [240, 255, 255], + "beige": [245, 245, 220], + "bisque": [255, 228, 196], + "black": [0, 0, 0], + "blanchedalmond": [255, 235, 205], + "blue": [0, 0, 255], + "blueviolet": [138, 43, 226], + "brown": [165, 42, 42], + "burlywood": [222, 184, 135], + "cadetblue": [95, 158, 160], + "chartreuse": [127, 255, 0], + "chocolate": [210, 105, 30], + "coral": [255, 127, 80], + "cornflowerblue": [100, 149, 237], + "cornsilk": [255, 248, 220], + "crimson": [220, 20, 60], + "cyan": [0, 255, 255], + "darkblue": [0, 0, 139], + "darkcyan": [0, 139, 139], + "darkgoldenrod": [184, 134, 11], + "darkgray": [169, 169, 169], + "darkgreen": [0, 100, 0], + "darkgrey": [169, 169, 169], + "darkkhaki": [189, 183, 107], + "darkmagenta": [139, 0, 139], + "darkolivegreen": [85, 107, 47], + "darkorange": [255, 140, 0], + "darkorchid": [153, 50, 204], + "darkred": [139, 0, 0], + "darksalmon": [233, 150, 122], + "darkseagreen": [143, 188, 143], + "darkslateblue": [72, 61, 139], + "darkslategray": [47, 79, 79], + "darkslategrey": [47, 79, 79], + "darkturquoise": [0, 206, 209], + "darkviolet": [148, 0, 211], + "deeppink": [255, 20, 147], + "deepskyblue": [0, 191, 255], + "dimgray": [105, 105, 105], + "dimgrey": [105, 105, 105], + "dodgerblue": [30, 144, 255], + "firebrick": [178, 34, 34], + "floralwhite": [255, 250, 240], + "forestgreen": [34, 139, 34], + "fuchsia": [255, 0, 255], + "gainsboro": [220, 220, 220], + "ghostwhite": [248, 248, 255], + "gold": [255, 215, 0], + "goldenrod": [218, 165, 32], + "gray": [128, 128, 128], + "green": [0, 128, 0], + "greenyellow": [173, 255, 47], + "grey": [128, 128, 128], + "honeydew": [240, 255, 240], + "hotpink": [255, 105, 180], + "indianred": [205, 92, 92], + "indigo": [75, 0, 130], + "ivory": [255, 255, 240], + "khaki": [240, 230, 140], + "lavender": [230, 230, 250], + "lavenderblush": [255, 240, 245], + "lawngreen": [124, 252, 0], + "lemonchiffon": [255, 250, 205], + "lightblue": [173, 216, 230], + "lightcoral": [240, 128, 128], + "lightcyan": [224, 255, 255], + "lightgoldenrodyellow": [250, 250, 210], + "lightgray": [211, 211, 211], + "lightgreen": [144, 238, 144], + "lightgrey": [211, 211, 211], + "lightpink": [255, 182, 193], + "lightsalmon": [255, 160, 122], + "lightseagreen": [32, 178, 170], + "lightskyblue": [135, 206, 250], + "lightslategray": [119, 136, 153], + "lightslategrey": [119, 136, 153], + "lightsteelblue": [176, 196, 222], + "lightyellow": [255, 255, 224], + "lime": [0, 255, 0], + "limegreen": [50, 205, 50], + "linen": [250, 240, 230], + "magenta": [255, 0, 255], + "maroon": [128, 0, 0], + "mediumaquamarine": [102, 205, 170], + "mediumblue": [0, 0, 205], + "mediumorchid": [186, 85, 211], + "mediumpurple": [147, 112, 219], + "mediumseagreen": [60, 179, 113], + "mediumslateblue": [123, 104, 238], + "mediumspringgreen": [0, 250, 154], + "mediumturquoise": [72, 209, 204], + "mediumvioletred": [199, 21, 133], + "midnightblue": [25, 25, 112], + "mintcream": [245, 255, 250], + "mistyrose": [255, 228, 225], + "moccasin": [255, 228, 181], + "navajowhite": [255, 222, 173], + "navy": [0, 0, 128], + "oldlace": [253, 245, 230], + "olive": [128, 128, 0], + "olivedrab": [107, 142, 35], + "orange": [255, 165, 0], + "orangered": [255, 69, 0], + "orchid": [218, 112, 214], + "palegoldenrod": [238, 232, 170], + "palegreen": [152, 251, 152], + "paleturquoise": [175, 238, 238], + "palevioletred": [219, 112, 147], + "papayawhip": [255, 239, 213], + "peachpuff": [255, 218, 185], + "peru": [205, 133, 63], + "pink": [255, 192, 203], + "plum": [221, 160, 221], + "powderblue": [176, 224, 230], + "purple": [128, 0, 128], + "rebeccapurple": [102, 51, 153], + "red": [255, 0, 0], + "rosybrown": [188, 143, 143], + "royalblue": [65, 105, 225], + "saddlebrown": [139, 69, 19], + "salmon": [250, 128, 114], + "sandybrown": [244, 164, 96], + "seagreen": [46, 139, 87], + "seashell": [255, 245, 238], + "sienna": [160, 82, 45], + "silver": [192, 192, 192], + "skyblue": [135, 206, 235], + "slateblue": [106, 90, 205], + "slategray": [112, 128, 144], + "slategrey": [112, 128, 144], + "snow": [255, 250, 250], + "springgreen": [0, 255, 127], + "steelblue": [70, 130, 180], + "tan": [210, 180, 140], + "teal": [0, 128, 128], + "thistle": [216, 191, 216], + "tomato": [255, 99, 71], + "turquoise": [64, 224, 208], + "violet": [238, 130, 238], + "wheat": [245, 222, 179], + "white": [255, 255, 255], + "whitesmoke": [245, 245, 245], + "yellow": [255, 255, 0], + "yellowgreen": [154, 205, 50] +}; - cmyk2rgb: cmyk2rgb, - cmyk2hsl: cmyk2hsl, - cmyk2hsv: cmyk2hsv, - cmyk2hwb: cmyk2hwb, - cmyk2keyword: cmyk2keyword, +var conversions = createCommonjsModule(function (module) { +/* MIT license */ - keyword2rgb: keyword2rgb, - keyword2hsl: keyword2hsl, - keyword2hsv: keyword2hsv, - keyword2hwb: keyword2hwb, - keyword2cmyk: keyword2cmyk, - keyword2lab: keyword2lab, - keyword2xyz: keyword2xyz, - xyz2rgb: xyz2rgb, - xyz2lab: xyz2lab, - xyz2lch: xyz2lch, +// NOTE: conversions should only return primitive values (i.e. arrays, or +// values that give correct `typeof` results). +// do not use box values types (i.e. Number(), String(), etc.) - lab2xyz: lab2xyz, - lab2rgb: lab2rgb, - lab2lch: lab2lch, +var reverseKeywords = {}; +for (var key in colorName) { + if (colorName.hasOwnProperty(key)) { + reverseKeywords[colorName[key]] = key; + } +} - lch2lab: lch2lab, - lch2xyz: lch2xyz, - lch2rgb: lch2rgb +var convert = module.exports = { + rgb: {channels: 3, labels: 'rgb'}, + hsl: {channels: 3, labels: 'hsl'}, + hsv: {channels: 3, labels: 'hsv'}, + hwb: {channels: 3, labels: 'hwb'}, + cmyk: {channels: 4, labels: 'cmyk'}, + xyz: {channels: 3, labels: 'xyz'}, + lab: {channels: 3, labels: 'lab'}, + lch: {channels: 3, labels: 'lch'}, + hex: {channels: 1, labels: ['hex']}, + keyword: {channels: 1, labels: ['keyword']}, + ansi16: {channels: 1, labels: ['ansi16']}, + ansi256: {channels: 1, labels: ['ansi256']}, + hcg: {channels: 3, labels: ['h', 'c', 'g']}, + apple: {channels: 3, labels: ['r16', 'g16', 'b16']}, + gray: {channels: 1, labels: ['gray']} }; +// hide .channels and .labels properties +for (var model in convert) { + if (convert.hasOwnProperty(model)) { + if (!('channels' in convert[model])) { + throw new Error('missing channels property: ' + model); + } -function rgb2hsl(rgb) { - var r = rgb[0]/255, - g = rgb[1]/255, - b = rgb[2]/255, - min = Math.min(r, g, b), - max = Math.max(r, g, b), - delta = max - min, - h, s, l; + if (!('labels' in convert[model])) { + throw new Error('missing channel labels property: ' + model); + } - if (max == min) - h = 0; - else if (r == max) - h = (g - b) / delta; - else if (g == max) - h = 2 + (b - r) / delta; - else if (b == max) - h = 4 + (r - g)/ delta; + if (convert[model].labels.length !== convert[model].channels) { + throw new Error('channel and label counts mismatch: ' + model); + } - h = Math.min(h * 60, 360); + var channels = convert[model].channels; + var labels = convert[model].labels; + delete convert[model].channels; + delete convert[model].labels; + Object.defineProperty(convert[model], 'channels', {value: channels}); + Object.defineProperty(convert[model], 'labels', {value: labels}); + } +} - if (h < 0) - h += 360; +convert.rgb.hsl = function (rgb) { + var r = rgb[0] / 255; + var g = rgb[1] / 255; + var b = rgb[2] / 255; + var min = Math.min(r, g, b); + var max = Math.max(r, g, b); + var delta = max - min; + var h; + var s; + var l; - l = (min + max) / 2; + if (max === min) { + h = 0; + } else if (r === max) { + h = (g - b) / delta; + } else if (g === max) { + h = 2 + (b - r) / delta; + } else if (b === max) { + h = 4 + (r - g) / delta; + } - if (max == min) - s = 0; - else if (l <= 0.5) - s = delta / (max + min); - else - s = delta / (2 - max - min); + h = Math.min(h * 60, 360); - return [h, s * 100, l * 100]; -} + if (h < 0) { + h += 360; + } -function rgb2hsv(rgb) { - var r = rgb[0], - g = rgb[1], - b = rgb[2], - min = Math.min(r, g, b), - max = Math.max(r, g, b), - delta = max - min, - h, s, v; + l = (min + max) / 2; - if (max == 0) - s = 0; - else - s = (delta/max * 1000)/10; + if (max === min) { + s = 0; + } else if (l <= 0.5) { + s = delta / (max + min); + } else { + s = delta / (2 - max - min); + } - if (max == min) - h = 0; - else if (r == max) - h = (g - b) / delta; - else if (g == max) - h = 2 + (b - r) / delta; - else if (b == max) - h = 4 + (r - g) / delta; + return [h, s * 100, l * 100]; +}; - h = Math.min(h * 60, 360); +convert.rgb.hsv = function (rgb) { + var rdif; + var gdif; + var bdif; + var h; + var s; - if (h < 0) - h += 360; + var r = rgb[0] / 255; + var g = rgb[1] / 255; + var b = rgb[2] / 255; + var v = Math.max(r, g, b); + var diff = v - Math.min(r, g, b); + var diffc = function (c) { + return (v - c) / 6 / diff + 1 / 2; + }; - v = ((max / 255) * 1000) / 10; + if (diff === 0) { + h = s = 0; + } else { + s = diff / v; + rdif = diffc(r); + gdif = diffc(g); + bdif = diffc(b); - return [h, s, v]; -} + if (r === v) { + h = bdif - gdif; + } else if (g === v) { + h = (1 / 3) + rdif - bdif; + } else if (b === v) { + h = (2 / 3) + gdif - rdif; + } + if (h < 0) { + h += 1; + } else if (h > 1) { + h -= 1; + } + } -function rgb2hwb(rgb) { - var r = rgb[0], - g = rgb[1], - b = rgb[2], - h = rgb2hsl(rgb)[0], - w = 1/255 * Math.min(r, Math.min(g, b)), - b = 1 - 1/255 * Math.max(r, Math.max(g, b)); + return [ + h * 360, + s * 100, + v * 100 + ]; +}; - return [h, w * 100, b * 100]; -} +convert.rgb.hwb = function (rgb) { + var r = rgb[0]; + var g = rgb[1]; + var b = rgb[2]; + var h = convert.rgb.hsl(rgb)[0]; + var w = 1 / 255 * Math.min(r, Math.min(g, b)); -function rgb2cmyk(rgb) { - var r = rgb[0] / 255, - g = rgb[1] / 255, - b = rgb[2] / 255, - c, m, y, k; + b = 1 - 1 / 255 * Math.max(r, Math.max(g, b)); - k = Math.min(1 - r, 1 - g, 1 - b); - c = (1 - r - k) / (1 - k) || 0; - m = (1 - g - k) / (1 - k) || 0; - y = (1 - b - k) / (1 - k) || 0; - return [c * 100, m * 100, y * 100, k * 100]; -} + return [h, w * 100, b * 100]; +}; -function rgb2keyword(rgb) { - return reverseKeywords[JSON.stringify(rgb)]; +convert.rgb.cmyk = function (rgb) { + var r = rgb[0] / 255; + var g = rgb[1] / 255; + var b = rgb[2] / 255; + var c; + var m; + var y; + var k; + + k = Math.min(1 - r, 1 - g, 1 - b); + c = (1 - r - k) / (1 - k) || 0; + m = (1 - g - k) / (1 - k) || 0; + y = (1 - b - k) / (1 - k) || 0; + + return [c * 100, m * 100, y * 100, k * 100]; +}; + +/** + * See https://en.m.wikipedia.org/wiki/Euclidean_distance#Squared_Euclidean_distance + * */ +function comparativeDistance(x, y) { + return ( + Math.pow(x[0] - y[0], 2) + + Math.pow(x[1] - y[1], 2) + + Math.pow(x[2] - y[2], 2) + ); } -function rgb2xyz(rgb) { - var r = rgb[0] / 255, - g = rgb[1] / 255, - b = rgb[2] / 255; +convert.rgb.keyword = function (rgb) { + var reversed = reverseKeywords[rgb]; + if (reversed) { + return reversed; + } - // assume sRGB - r = r > 0.04045 ? Math.pow(((r + 0.055) / 1.055), 2.4) : (r / 12.92); - g = g > 0.04045 ? Math.pow(((g + 0.055) / 1.055), 2.4) : (g / 12.92); - b = b > 0.04045 ? Math.pow(((b + 0.055) / 1.055), 2.4) : (b / 12.92); + var currentClosestDistance = Infinity; + var currentClosestKeyword; - var x = (r * 0.4124) + (g * 0.3576) + (b * 0.1805); - var y = (r * 0.2126) + (g * 0.7152) + (b * 0.0722); - var z = (r * 0.0193) + (g * 0.1192) + (b * 0.9505); + for (var keyword in colorName) { + if (colorName.hasOwnProperty(keyword)) { + var value = colorName[keyword]; - return [x * 100, y *100, z * 100]; -} + // Compute comparative distance + var distance = comparativeDistance(rgb, value); -function rgb2lab(rgb) { - var xyz = rgb2xyz(rgb), - x = xyz[0], - y = xyz[1], - z = xyz[2], - l, a, b; + // Check if its less, if so set as closest + if (distance < currentClosestDistance) { + currentClosestDistance = distance; + currentClosestKeyword = keyword; + } + } + } - x /= 95.047; - y /= 100; - z /= 108.883; + return currentClosestKeyword; +}; - x = x > 0.008856 ? Math.pow(x, 1/3) : (7.787 * x) + (16 / 116); - y = y > 0.008856 ? Math.pow(y, 1/3) : (7.787 * y) + (16 / 116); - z = z > 0.008856 ? Math.pow(z, 1/3) : (7.787 * z) + (16 / 116); +convert.keyword.rgb = function (keyword) { + return colorName[keyword]; +}; - l = (116 * y) - 16; - a = 500 * (x - y); - b = 200 * (y - z); +convert.rgb.xyz = function (rgb) { + var r = rgb[0] / 255; + var g = rgb[1] / 255; + var b = rgb[2] / 255; - return [l, a, b]; -} + // assume sRGB + r = r > 0.04045 ? Math.pow(((r + 0.055) / 1.055), 2.4) : (r / 12.92); + g = g > 0.04045 ? Math.pow(((g + 0.055) / 1.055), 2.4) : (g / 12.92); + b = b > 0.04045 ? Math.pow(((b + 0.055) / 1.055), 2.4) : (b / 12.92); -function rgb2lch(args) { - return lab2lch(rgb2lab(args)); -} + var x = (r * 0.4124) + (g * 0.3576) + (b * 0.1805); + var y = (r * 0.2126) + (g * 0.7152) + (b * 0.0722); + var z = (r * 0.0193) + (g * 0.1192) + (b * 0.9505); -function hsl2rgb(hsl) { - var h = hsl[0] / 360, - s = hsl[1] / 100, - l = hsl[2] / 100, - t1, t2, t3, rgb, val; + return [x * 100, y * 100, z * 100]; +}; - if (s == 0) { - val = l * 255; - return [val, val, val]; - } +convert.rgb.lab = function (rgb) { + var xyz = convert.rgb.xyz(rgb); + var x = xyz[0]; + var y = xyz[1]; + var z = xyz[2]; + var l; + var a; + var b; - if (l < 0.5) - t2 = l * (1 + s); - else - t2 = l + s - l * s; - t1 = 2 * l - t2; + x /= 95.047; + y /= 100; + z /= 108.883; - rgb = [0, 0, 0]; - for (var i = 0; i < 3; i++) { - t3 = h + 1 / 3 * - (i - 1); - t3 < 0 && t3++; - t3 > 1 && t3--; + x = x > 0.008856 ? Math.pow(x, 1 / 3) : (7.787 * x) + (16 / 116); + y = y > 0.008856 ? Math.pow(y, 1 / 3) : (7.787 * y) + (16 / 116); + z = z > 0.008856 ? Math.pow(z, 1 / 3) : (7.787 * z) + (16 / 116); - if (6 * t3 < 1) - val = t1 + (t2 - t1) * 6 * t3; - else if (2 * t3 < 1) - val = t2; - else if (3 * t3 < 2) - val = t1 + (t2 - t1) * (2 / 3 - t3) * 6; - else - val = t1; + l = (116 * y) - 16; + a = 500 * (x - y); + b = 200 * (y - z); - rgb[i] = val * 255; - } + return [l, a, b]; +}; - return rgb; -} +convert.hsl.rgb = function (hsl) { + var h = hsl[0] / 360; + var s = hsl[1] / 100; + var l = hsl[2] / 100; + var t1; + var t2; + var t3; + var rgb; + var val; -function hsl2hsv(hsl) { - var h = hsl[0], - s = hsl[1] / 100, - l = hsl[2] / 100, - sv, v; + if (s === 0) { + val = l * 255; + return [val, val, val]; + } - if(l === 0) { - // no need to do calc on black - // also avoids divide by 0 error - return [0, 0, 0]; - } + if (l < 0.5) { + t2 = l * (1 + s); + } else { + t2 = l + s - l * s; + } - l *= 2; - s *= (l <= 1) ? l : 2 - l; - v = (l + s) / 2; - sv = (2 * s) / (l + s); - return [h, sv * 100, v * 100]; -} + t1 = 2 * l - t2; -function hsl2hwb(args) { - return rgb2hwb(hsl2rgb(args)); -} + rgb = [0, 0, 0]; + for (var i = 0; i < 3; i++) { + t3 = h + 1 / 3 * -(i - 1); + if (t3 < 0) { + t3++; + } + if (t3 > 1) { + t3--; + } -function hsl2cmyk(args) { - return rgb2cmyk(hsl2rgb(args)); -} + if (6 * t3 < 1) { + val = t1 + (t2 - t1) * 6 * t3; + } else if (2 * t3 < 1) { + val = t2; + } else if (3 * t3 < 2) { + val = t1 + (t2 - t1) * (2 / 3 - t3) * 6; + } else { + val = t1; + } -function hsl2keyword(args) { - return rgb2keyword(hsl2rgb(args)); -} + rgb[i] = val * 255; + } + return rgb; +}; -function hsv2rgb(hsv) { - var h = hsv[0] / 60, - s = hsv[1] / 100, - v = hsv[2] / 100, - hi = Math.floor(h) % 6; +convert.hsl.hsv = function (hsl) { + var h = hsl[0]; + var s = hsl[1] / 100; + var l = hsl[2] / 100; + var smin = s; + var lmin = Math.max(l, 0.01); + var sv; + var v; - var f = h - Math.floor(h), - p = 255 * v * (1 - s), - q = 255 * v * (1 - (s * f)), - t = 255 * v * (1 - (s * (1 - f))), - v = 255 * v; + l *= 2; + s *= (l <= 1) ? l : 2 - l; + smin *= lmin <= 1 ? lmin : 2 - lmin; + v = (l + s) / 2; + sv = l === 0 ? (2 * smin) / (lmin + smin) : (2 * s) / (l + s); - switch(hi) { - case 0: - return [v, t, p]; - case 1: - return [q, v, p]; - case 2: - return [p, v, t]; - case 3: - return [p, q, v]; - case 4: - return [t, p, v]; - case 5: - return [v, p, q]; - } -} + return [h, sv * 100, v * 100]; +}; -function hsv2hsl(hsv) { - var h = hsv[0], - s = hsv[1] / 100, - v = hsv[2] / 100, - sl, l; +convert.hsv.rgb = function (hsv) { + var h = hsv[0] / 60; + var s = hsv[1] / 100; + var v = hsv[2] / 100; + var hi = Math.floor(h) % 6; - l = (2 - s) * v; - sl = s * v; - sl /= (l <= 1) ? l : 2 - l; - sl = sl || 0; - l /= 2; - return [h, sl * 100, l * 100]; -} + var f = h - Math.floor(h); + var p = 255 * v * (1 - s); + var q = 255 * v * (1 - (s * f)); + var t = 255 * v * (1 - (s * (1 - f))); + v *= 255; -function hsv2hwb(args) { - return rgb2hwb(hsv2rgb(args)) -} + switch (hi) { + case 0: + return [v, t, p]; + case 1: + return [q, v, p]; + case 2: + return [p, v, t]; + case 3: + return [p, q, v]; + case 4: + return [t, p, v]; + case 5: + return [v, p, q]; + } +}; -function hsv2cmyk(args) { - return rgb2cmyk(hsv2rgb(args)); -} +convert.hsv.hsl = function (hsv) { + var h = hsv[0]; + var s = hsv[1] / 100; + var v = hsv[2] / 100; + var vmin = Math.max(v, 0.01); + var lmin; + var sl; + var l; -function hsv2keyword(args) { - return rgb2keyword(hsv2rgb(args)); -} + l = (2 - s) * v; + lmin = (2 - s) * vmin; + sl = s * vmin; + sl /= (lmin <= 1) ? lmin : 2 - lmin; + sl = sl || 0; + l /= 2; + return [h, sl * 100, l * 100]; +}; + // http://dev.w3.org/csswg/css-color/#hwb-to-rgb -function hwb2rgb(hwb) { - var h = hwb[0] / 360, - wh = hwb[1] / 100, - bl = hwb[2] / 100, - ratio = wh + bl, - i, v, f, n; +convert.hwb.rgb = function (hwb) { + var h = hwb[0] / 360; + var wh = hwb[1] / 100; + var bl = hwb[2] / 100; + var ratio = wh + bl; + var i; + var v; + var f; + var n; - // wh + bl cant be > 1 - if (ratio > 1) { - wh /= ratio; - bl /= ratio; - } + // wh + bl cant be > 1 + if (ratio > 1) { + wh /= ratio; + bl /= ratio; + } - i = Math.floor(6 * h); - v = 1 - bl; - f = 6 * h - i; - if ((i & 0x01) != 0) { - f = 1 - f; - } - n = wh + f * (v - wh); // linear interpolation + i = Math.floor(6 * h); + v = 1 - bl; + f = 6 * h - i; - switch (i) { - default: - case 6: - case 0: r = v; g = n; b = wh; break; - case 1: r = n; g = v; b = wh; break; - case 2: r = wh; g = v; b = n; break; - case 3: r = wh; g = n; b = v; break; - case 4: r = n; g = wh; b = v; break; - case 5: r = v; g = wh; b = n; break; - } + if ((i & 0x01) !== 0) { + f = 1 - f; + } - return [r * 255, g * 255, b * 255]; -} + n = wh + f * (v - wh); // linear interpolation -function hwb2hsl(args) { - return rgb2hsl(hwb2rgb(args)); -} + var r; + var g; + var b; + switch (i) { + default: + case 6: + case 0: r = v; g = n; b = wh; break; + case 1: r = n; g = v; b = wh; break; + case 2: r = wh; g = v; b = n; break; + case 3: r = wh; g = n; b = v; break; + case 4: r = n; g = wh; b = v; break; + case 5: r = v; g = wh; b = n; break; + } -function hwb2hsv(args) { - return rgb2hsv(hwb2rgb(args)); -} + return [r * 255, g * 255, b * 255]; +}; -function hwb2cmyk(args) { - return rgb2cmyk(hwb2rgb(args)); -} +convert.cmyk.rgb = function (cmyk) { + var c = cmyk[0] / 100; + var m = cmyk[1] / 100; + var y = cmyk[2] / 100; + var k = cmyk[3] / 100; + var r; + var g; + var b; -function hwb2keyword(args) { - return rgb2keyword(hwb2rgb(args)); -} + r = 1 - Math.min(1, c * (1 - k) + k); + g = 1 - Math.min(1, m * (1 - k) + k); + b = 1 - Math.min(1, y * (1 - k) + k); -function cmyk2rgb(cmyk) { - var c = cmyk[0] / 100, - m = cmyk[1] / 100, - y = cmyk[2] / 100, - k = cmyk[3] / 100, - r, g, b; + return [r * 255, g * 255, b * 255]; +}; - r = 1 - Math.min(1, c * (1 - k) + k); - g = 1 - Math.min(1, m * (1 - k) + k); - b = 1 - Math.min(1, y * (1 - k) + k); - return [r * 255, g * 255, b * 255]; -} +convert.xyz.rgb = function (xyz) { + var x = xyz[0] / 100; + var y = xyz[1] / 100; + var z = xyz[2] / 100; + var r; + var g; + var b; -function cmyk2hsl(args) { - return rgb2hsl(cmyk2rgb(args)); -} + r = (x * 3.2406) + (y * -1.5372) + (z * -0.4986); + g = (x * -0.9689) + (y * 1.8758) + (z * 0.0415); + b = (x * 0.0557) + (y * -0.2040) + (z * 1.0570); -function cmyk2hsv(args) { - return rgb2hsv(cmyk2rgb(args)); -} + // assume sRGB + r = r > 0.0031308 + ? ((1.055 * Math.pow(r, 1.0 / 2.4)) - 0.055) + : r * 12.92; -function cmyk2hwb(args) { - return rgb2hwb(cmyk2rgb(args)); -} + g = g > 0.0031308 + ? ((1.055 * Math.pow(g, 1.0 / 2.4)) - 0.055) + : g * 12.92; -function cmyk2keyword(args) { - return rgb2keyword(cmyk2rgb(args)); -} + b = b > 0.0031308 + ? ((1.055 * Math.pow(b, 1.0 / 2.4)) - 0.055) + : b * 12.92; + r = Math.min(Math.max(0, r), 1); + g = Math.min(Math.max(0, g), 1); + b = Math.min(Math.max(0, b), 1); -function xyz2rgb(xyz) { - var x = xyz[0] / 100, - y = xyz[1] / 100, - z = xyz[2] / 100, - r, g, b; + return [r * 255, g * 255, b * 255]; +}; - r = (x * 3.2406) + (y * -1.5372) + (z * -0.4986); - g = (x * -0.9689) + (y * 1.8758) + (z * 0.0415); - b = (x * 0.0557) + (y * -0.2040) + (z * 1.0570); +convert.xyz.lab = function (xyz) { + var x = xyz[0]; + var y = xyz[1]; + var z = xyz[2]; + var l; + var a; + var b; - // assume sRGB - r = r > 0.0031308 ? ((1.055 * Math.pow(r, 1.0 / 2.4)) - 0.055) - : r = (r * 12.92); + x /= 95.047; + y /= 100; + z /= 108.883; - g = g > 0.0031308 ? ((1.055 * Math.pow(g, 1.0 / 2.4)) - 0.055) - : g = (g * 12.92); + x = x > 0.008856 ? Math.pow(x, 1 / 3) : (7.787 * x) + (16 / 116); + y = y > 0.008856 ? Math.pow(y, 1 / 3) : (7.787 * y) + (16 / 116); + z = z > 0.008856 ? Math.pow(z, 1 / 3) : (7.787 * z) + (16 / 116); - b = b > 0.0031308 ? ((1.055 * Math.pow(b, 1.0 / 2.4)) - 0.055) - : b = (b * 12.92); + l = (116 * y) - 16; + a = 500 * (x - y); + b = 200 * (y - z); - r = Math.min(Math.max(0, r), 1); - g = Math.min(Math.max(0, g), 1); - b = Math.min(Math.max(0, b), 1); + return [l, a, b]; +}; - return [r * 255, g * 255, b * 255]; -} +convert.lab.xyz = function (lab) { + var l = lab[0]; + var a = lab[1]; + var b = lab[2]; + var x; + var y; + var z; -function xyz2lab(xyz) { - var x = xyz[0], - y = xyz[1], - z = xyz[2], - l, a, b; + y = (l + 16) / 116; + x = a / 500 + y; + z = y - b / 200; - x /= 95.047; - y /= 100; - z /= 108.883; + var y2 = Math.pow(y, 3); + var x2 = Math.pow(x, 3); + var z2 = Math.pow(z, 3); + y = y2 > 0.008856 ? y2 : (y - 16 / 116) / 7.787; + x = x2 > 0.008856 ? x2 : (x - 16 / 116) / 7.787; + z = z2 > 0.008856 ? z2 : (z - 16 / 116) / 7.787; - x = x > 0.008856 ? Math.pow(x, 1/3) : (7.787 * x) + (16 / 116); - y = y > 0.008856 ? Math.pow(y, 1/3) : (7.787 * y) + (16 / 116); - z = z > 0.008856 ? Math.pow(z, 1/3) : (7.787 * z) + (16 / 116); + x *= 95.047; + y *= 100; + z *= 108.883; - l = (116 * y) - 16; - a = 500 * (x - y); - b = 200 * (y - z); + return [x, y, z]; +}; - return [l, a, b]; -} +convert.lab.lch = function (lab) { + var l = lab[0]; + var a = lab[1]; + var b = lab[2]; + var hr; + var h; + var c; -function xyz2lch(args) { - return lab2lch(xyz2lab(args)); -} + hr = Math.atan2(b, a); + h = hr * 360 / 2 / Math.PI; -function lab2xyz(lab) { - var l = lab[0], - a = lab[1], - b = lab[2], - x, y, z, y2; + if (h < 0) { + h += 360; + } - if (l <= 8) { - y = (l * 100) / 903.3; - y2 = (7.787 * (y / 100)) + (16 / 116); - } else { - y = 100 * Math.pow((l + 16) / 116, 3); - y2 = Math.pow(y / 100, 1/3); - } + c = Math.sqrt(a * a + b * b); - x = x / 95.047 <= 0.008856 ? x = (95.047 * ((a / 500) + y2 - (16 / 116))) / 7.787 : 95.047 * Math.pow((a / 500) + y2, 3); + return [l, c, h]; +}; - z = z / 108.883 <= 0.008859 ? z = (108.883 * (y2 - (b / 200) - (16 / 116))) / 7.787 : 108.883 * Math.pow(y2 - (b / 200), 3); +convert.lch.lab = function (lch) { + var l = lch[0]; + var c = lch[1]; + var h = lch[2]; + var a; + var b; + var hr; - return [x, y, z]; -} + hr = h / 360 * 2 * Math.PI; + a = c * Math.cos(hr); + b = c * Math.sin(hr); -function lab2lch(lab) { - var l = lab[0], - a = lab[1], - b = lab[2], - hr, h, c; + return [l, a, b]; +}; - hr = Math.atan2(b, a); - h = hr * 360 / 2 / Math.PI; - if (h < 0) { - h += 360; - } - c = Math.sqrt(a * a + b * b); - return [l, c, h]; -} +convert.rgb.ansi16 = function (args) { + var r = args[0]; + var g = args[1]; + var b = args[2]; + var value = 1 in arguments ? arguments[1] : convert.rgb.hsv(args)[2]; // hsv -> ansi16 optimization -function lab2rgb(args) { - return xyz2rgb(lab2xyz(args)); -} + value = Math.round(value / 50); -function lch2lab(lch) { - var l = lch[0], - c = lch[1], - h = lch[2], - a, b, hr; + if (value === 0) { + return 30; + } - hr = h / 360 * 2 * Math.PI; - a = c * Math.cos(hr); - b = c * Math.sin(hr); - return [l, a, b]; -} + var ansi = 30 + + ((Math.round(b / 255) << 2) + | (Math.round(g / 255) << 1) + | Math.round(r / 255)); -function lch2xyz(args) { - return lab2xyz(lch2lab(args)); -} + if (value === 2) { + ansi += 60; + } -function lch2rgb(args) { - return lab2rgb(lch2lab(args)); -} + return ansi; +}; -function keyword2rgb(keyword) { - return cssKeywords[keyword]; -} +convert.hsv.ansi16 = function (args) { + // optimization here; we already know the value and don't need to get + // it converted for us. + return convert.rgb.ansi16(convert.hsv.rgb(args), args[2]); +}; -function keyword2hsl(args) { - return rgb2hsl(keyword2rgb(args)); -} +convert.rgb.ansi256 = function (args) { + var r = args[0]; + var g = args[1]; + var b = args[2]; -function keyword2hsv(args) { - return rgb2hsv(keyword2rgb(args)); -} + // we use the extended greyscale palette here, with the exception of + // black and white. normal palette only has 4 greyscale shades. + if (r === g && g === b) { + if (r < 8) { + return 16; + } -function keyword2hwb(args) { - return rgb2hwb(keyword2rgb(args)); -} + if (r > 248) { + return 231; + } -function keyword2cmyk(args) { - return rgb2cmyk(keyword2rgb(args)); -} + return Math.round(((r - 8) / 247) * 24) + 232; + } -function keyword2lab(args) { - return rgb2lab(keyword2rgb(args)); -} + var ansi = 16 + + (36 * Math.round(r / 255 * 5)) + + (6 * Math.round(g / 255 * 5)) + + Math.round(b / 255 * 5); -function keyword2xyz(args) { - return rgb2xyz(keyword2rgb(args)); -} + return ansi; +}; -var cssKeywords = { - aliceblue: [240,248,255], - antiquewhite: [250,235,215], - aqua: [0,255,255], - aquamarine: [127,255,212], - azure: [240,255,255], - beige: [245,245,220], - bisque: [255,228,196], - black: [0,0,0], - blanchedalmond: [255,235,205], - blue: [0,0,255], - blueviolet: [138,43,226], - brown: [165,42,42], - burlywood: [222,184,135], - cadetblue: [95,158,160], - chartreuse: [127,255,0], - chocolate: [210,105,30], - coral: [255,127,80], - cornflowerblue: [100,149,237], - cornsilk: [255,248,220], - crimson: [220,20,60], - cyan: [0,255,255], - darkblue: [0,0,139], - darkcyan: [0,139,139], - darkgoldenrod: [184,134,11], - darkgray: [169,169,169], - darkgreen: [0,100,0], - darkgrey: [169,169,169], - darkkhaki: [189,183,107], - darkmagenta: [139,0,139], - darkolivegreen: [85,107,47], - darkorange: [255,140,0], - darkorchid: [153,50,204], - darkred: [139,0,0], - darksalmon: [233,150,122], - darkseagreen: [143,188,143], - darkslateblue: [72,61,139], - darkslategray: [47,79,79], - darkslategrey: [47,79,79], - darkturquoise: [0,206,209], - darkviolet: [148,0,211], - deeppink: [255,20,147], - deepskyblue: [0,191,255], - dimgray: [105,105,105], - dimgrey: [105,105,105], - dodgerblue: [30,144,255], - firebrick: [178,34,34], - floralwhite: [255,250,240], - forestgreen: [34,139,34], - fuchsia: [255,0,255], - gainsboro: [220,220,220], - ghostwhite: [248,248,255], - gold: [255,215,0], - goldenrod: [218,165,32], - gray: [128,128,128], - green: [0,128,0], - greenyellow: [173,255,47], - grey: [128,128,128], - honeydew: [240,255,240], - hotpink: [255,105,180], - indianred: [205,92,92], - indigo: [75,0,130], - ivory: [255,255,240], - khaki: [240,230,140], - lavender: [230,230,250], - lavenderblush: [255,240,245], - lawngreen: [124,252,0], - lemonchiffon: [255,250,205], - lightblue: [173,216,230], - lightcoral: [240,128,128], - lightcyan: [224,255,255], - lightgoldenrodyellow: [250,250,210], - lightgray: [211,211,211], - lightgreen: [144,238,144], - lightgrey: [211,211,211], - lightpink: [255,182,193], - lightsalmon: [255,160,122], - lightseagreen: [32,178,170], - lightskyblue: [135,206,250], - lightslategray: [119,136,153], - lightslategrey: [119,136,153], - lightsteelblue: [176,196,222], - lightyellow: [255,255,224], - lime: [0,255,0], - limegreen: [50,205,50], - linen: [250,240,230], - magenta: [255,0,255], - maroon: [128,0,0], - mediumaquamarine: [102,205,170], - mediumblue: [0,0,205], - mediumorchid: [186,85,211], - mediumpurple: [147,112,219], - mediumseagreen: [60,179,113], - mediumslateblue: [123,104,238], - mediumspringgreen: [0,250,154], - mediumturquoise: [72,209,204], - mediumvioletred: [199,21,133], - midnightblue: [25,25,112], - mintcream: [245,255,250], - mistyrose: [255,228,225], - moccasin: [255,228,181], - navajowhite: [255,222,173], - navy: [0,0,128], - oldlace: [253,245,230], - olive: [128,128,0], - olivedrab: [107,142,35], - orange: [255,165,0], - orangered: [255,69,0], - orchid: [218,112,214], - palegoldenrod: [238,232,170], - palegreen: [152,251,152], - paleturquoise: [175,238,238], - palevioletred: [219,112,147], - papayawhip: [255,239,213], - peachpuff: [255,218,185], - peru: [205,133,63], - pink: [255,192,203], - plum: [221,160,221], - powderblue: [176,224,230], - purple: [128,0,128], - rebeccapurple: [102, 51, 153], - red: [255,0,0], - rosybrown: [188,143,143], - royalblue: [65,105,225], - saddlebrown: [139,69,19], - salmon: [250,128,114], - sandybrown: [244,164,96], - seagreen: [46,139,87], - seashell: [255,245,238], - sienna: [160,82,45], - silver: [192,192,192], - skyblue: [135,206,235], - slateblue: [106,90,205], - slategray: [112,128,144], - slategrey: [112,128,144], - snow: [255,250,250], - springgreen: [0,255,127], - steelblue: [70,130,180], - tan: [210,180,140], - teal: [0,128,128], - thistle: [216,191,216], - tomato: [255,99,71], - turquoise: [64,224,208], - violet: [238,130,238], - wheat: [245,222,179], - white: [255,255,255], - whitesmoke: [245,245,245], - yellow: [255,255,0], - yellowgreen: [154,205,50] +convert.ansi16.rgb = function (args) { + var color = args % 10; + + // handle greyscale + if (color === 0 || color === 7) { + if (args > 50) { + color += 3.5; + } + + color = color / 10.5 * 255; + + return [color, color, color]; + } + + var mult = (~~(args > 50) + 1) * 0.5; + var r = ((color & 1) * mult) * 255; + var g = (((color >> 1) & 1) * mult) * 255; + var b = (((color >> 2) & 1) * mult) * 255; + + return [r, g, b]; }; -var reverseKeywords = {}; -for (var key in cssKeywords) { - reverseKeywords[JSON.stringify(cssKeywords[key])] = key; -} +convert.ansi256.rgb = function (args) { + // handle greyscale + if (args >= 232) { + var c = (args - 232) * 10 + 8; + return [c, c, c]; + } -var convert = function() { - return new Converter(); + args -= 16; + + var rem; + var r = Math.floor(args / 36) / 5 * 255; + var g = Math.floor((rem = args % 36) / 6) / 5 * 255; + var b = (rem % 6) / 5 * 255; + + return [r, g, b]; }; -for (var func in conversions) { - // export Raw versions - convert[func + "Raw"] = (function(func) { - // accept array or plain args - return function(arg) { - if (typeof arg == "number") - arg = Array.prototype.slice.call(arguments); - return conversions[func](arg); - } - })(func); +convert.rgb.hex = function (args) { + var integer = ((Math.round(args[0]) & 0xFF) << 16) + + ((Math.round(args[1]) & 0xFF) << 8) + + (Math.round(args[2]) & 0xFF); - var pair = /(\w+)2(\w+)/.exec(func), - from = pair[1], - to = pair[2]; + var string = integer.toString(16).toUpperCase(); + return '000000'.substring(string.length) + string; +}; - // export rgb2hsl and ["rgb"]["hsl"] - convert[from] = convert[from] || {}; +convert.hex.rgb = function (args) { + var match = args.toString(16).match(/[a-f0-9]{6}|[a-f0-9]{3}/i); + if (!match) { + return [0, 0, 0]; + } - convert[from][to] = convert[func] = (function(func) { - return function(arg) { - if (typeof arg == "number") - arg = Array.prototype.slice.call(arguments); - - var val = conversions[func](arg); - if (typeof val == "string" || val === undefined) - return val; // keyword + var colorString = match[0]; - for (var i = 0; i < val.length; i++) - val[i] = Math.round(val[i]); - return val; - } - })(func); -} + if (match[0].length === 3) { + colorString = colorString.split('').map(function (char) { + return char + char; + }).join(''); + } + var integer = parseInt(colorString, 16); + var r = (integer >> 16) & 0xFF; + var g = (integer >> 8) & 0xFF; + var b = integer & 0xFF; -/* Converter does lazy conversion and caching */ -var Converter = function() { - this.convs = {}; + return [r, g, b]; }; -/* Either get the values for a space or - set the values for a space, depending on args */ -Converter.prototype.routeSpace = function(space, args) { - var values = args[0]; - if (values === undefined) { - // color.rgb() - return this.getValues(space); - } - // color.rgb(10, 10, 10) - if (typeof values == "number") { - values = Array.prototype.slice.call(args); - } +convert.rgb.hcg = function (rgb) { + var r = rgb[0] / 255; + var g = rgb[1] / 255; + var b = rgb[2] / 255; + var max = Math.max(Math.max(r, g), b); + var min = Math.min(Math.min(r, g), b); + var chroma = (max - min); + var grayscale; + var hue; - return this.setValues(space, values); + if (chroma < 1) { + grayscale = min / (1 - chroma); + } else { + grayscale = 0; + } + + if (chroma <= 0) { + hue = 0; + } else + if (max === r) { + hue = ((g - b) / chroma) % 6; + } else + if (max === g) { + hue = 2 + (b - r) / chroma; + } else { + hue = 4 + (r - g) / chroma + 4; + } + + hue /= 6; + hue %= 1; + + return [hue * 360, chroma * 100, grayscale * 100]; }; - -/* Set the values for a space, invalidating cache */ -Converter.prototype.setValues = function(space, values) { - this.space = space; - this.convs = {}; - this.convs[space] = values; - return this; + +convert.hsl.hcg = function (hsl) { + var s = hsl[1] / 100; + var l = hsl[2] / 100; + var c = 1; + var f = 0; + + if (l < 0.5) { + c = 2.0 * s * l; + } else { + c = 2.0 * s * (1.0 - l); + } + + if (c < 1.0) { + f = (l - 0.5 * c) / (1.0 - c); + } + + return [hsl[0], c * 100, f * 100]; }; -/* Get the values for a space. If there's already - a conversion for the space, fetch it, otherwise - compute it */ -Converter.prototype.getValues = function(space) { - var vals = this.convs[space]; - if (!vals) { - var fspace = this.space, - from = this.convs[fspace]; - vals = convert[fspace][space](from); +convert.hsv.hcg = function (hsv) { + var s = hsv[1] / 100; + var v = hsv[2] / 100; - this.convs[space] = vals; - } - return vals; + var c = s * v; + var f = 0; + + if (c < 1.0) { + f = (v - c) / (1 - c); + } + + return [hsv[0], c * 100, f * 100]; }; -["rgb", "hsl", "hsv", "cmyk", "keyword"].forEach(function(space) { - Converter.prototype[space] = function(vals) { - return this.routeSpace(space, arguments); - }; +convert.hcg.rgb = function (hcg) { + var h = hcg[0] / 360; + var c = hcg[1] / 100; + var g = hcg[2] / 100; + + if (c === 0.0) { + return [g * 255, g * 255, g * 255]; + } + + var pure = [0, 0, 0]; + var hi = (h % 1) * 6; + var v = hi % 1; + var w = 1 - v; + var mg = 0; + + switch (Math.floor(hi)) { + case 0: + pure[0] = 1; pure[1] = v; pure[2] = 0; break; + case 1: + pure[0] = w; pure[1] = 1; pure[2] = 0; break; + case 2: + pure[0] = 0; pure[1] = 1; pure[2] = v; break; + case 3: + pure[0] = 0; pure[1] = w; pure[2] = 1; break; + case 4: + pure[0] = v; pure[1] = 0; pure[2] = 1; break; + default: + pure[0] = 1; pure[1] = 0; pure[2] = w; + } + + mg = (1.0 - c) * g; + + return [ + (c * pure[0] + mg) * 255, + (c * pure[1] + mg) * 255, + (c * pure[2] + mg) * 255 + ]; +}; + +convert.hcg.hsv = function (hcg) { + var c = hcg[1] / 100; + var g = hcg[2] / 100; + + var v = c + g * (1.0 - c); + var f = 0; + + if (v > 0.0) { + f = c / v; + } + + return [hcg[0], f * 100, v * 100]; +}; + +convert.hcg.hsl = function (hcg) { + var c = hcg[1] / 100; + var g = hcg[2] / 100; + + var l = g * (1.0 - c) + 0.5 * c; + var s = 0; + + if (l > 0.0 && l < 0.5) { + s = c / (2 * l); + } else + if (l >= 0.5 && l < 1.0) { + s = c / (2 * (1 - l)); + } + + return [hcg[0], s * 100, l * 100]; +}; + +convert.hcg.hwb = function (hcg) { + var c = hcg[1] / 100; + var g = hcg[2] / 100; + var v = c + g * (1.0 - c); + return [hcg[0], (v - c) * 100, (1 - v) * 100]; +}; + +convert.hwb.hcg = function (hwb) { + var w = hwb[1] / 100; + var b = hwb[2] / 100; + var v = 1 - b; + var c = v - w; + var g = 0; + + if (c < 1) { + g = (v - c) / (1 - c); + } + + return [hwb[0], c * 100, g * 100]; +}; + +convert.apple.rgb = function (apple) { + return [(apple[0] / 65535) * 255, (apple[1] / 65535) * 255, (apple[2] / 65535) * 255]; +}; + +convert.rgb.apple = function (rgb) { + return [(rgb[0] / 255) * 65535, (rgb[1] / 255) * 65535, (rgb[2] / 255) * 65535]; +}; + +convert.gray.rgb = function (args) { + return [args[0] / 100 * 255, args[0] / 100 * 255, args[0] / 100 * 255]; +}; + +convert.gray.hsl = convert.gray.hsv = function (args) { + return [0, 0, args[0]]; +}; + +convert.gray.hwb = function (gray) { + return [0, 100, gray[0]]; +}; + +convert.gray.cmyk = function (gray) { + return [0, 0, 0, gray[0]]; +}; + +convert.gray.lab = function (gray) { + return [gray[0], 0, 0]; +}; + +convert.gray.hex = function (gray) { + var val = Math.round(gray[0] / 100 * 255) & 0xFF; + var integer = (val << 16) + (val << 8) + val; + + var string = integer.toString(16).toUpperCase(); + return '000000'.substring(string.length) + string; +}; + +convert.rgb.gray = function (rgb) { + var val = (rgb[0] + rgb[1] + rgb[2]) / 3; + return [val / 255 * 100]; +}; }); +var conversions_1 = conversions.rgb; +var conversions_2 = conversions.hsl; +var conversions_3 = conversions.hsv; +var conversions_4 = conversions.hwb; +var conversions_5 = conversions.cmyk; +var conversions_6 = conversions.xyz; +var conversions_7 = conversions.lab; +var conversions_8 = conversions.lch; +var conversions_9 = conversions.hex; +var conversions_10 = conversions.keyword; +var conversions_11 = conversions.ansi16; +var conversions_12 = conversions.ansi256; +var conversions_13 = conversions.hcg; +var conversions_14 = conversions.apple; +var conversions_15 = conversions.gray; +/* + this function routes a model to all other models. + + all functions that are routed have a property `.conversion` attached + to the returned synthetic function. This property is an array + of strings, each with the steps in between the 'from' and 'to' + color models (inclusive). + + conversions that are not possible simply are not included. +*/ + +function buildGraph() { + var graph = {}; + // https://jsperf.com/object-keys-vs-for-in-with-closure/3 + var models = Object.keys(conversions); + + for (var len = models.length, i = 0; i < len; i++) { + graph[models[i]] = { + // http://jsperf.com/1-vs-infinity + // micro-opt, but this is simple. + distance: -1, + parent: null + }; + } + + return graph; +} + +// https://en.wikipedia.org/wiki/Breadth-first_search +function deriveBFS(fromModel) { + var graph = buildGraph(); + var queue = [fromModel]; // unshift -> queue -> pop + + graph[fromModel].distance = 0; + + while (queue.length) { + var current = queue.pop(); + var adjacents = Object.keys(conversions[current]); + + for (var len = adjacents.length, i = 0; i < len; i++) { + var adjacent = adjacents[i]; + var node = graph[adjacent]; + + if (node.distance === -1) { + node.distance = graph[current].distance + 1; + node.parent = current; + queue.unshift(adjacent); + } + } + } + + return graph; +} + +function link(from, to) { + return function (args) { + return to(from(args)); + }; +} + +function wrapConversion(toModel, graph) { + var path = [graph[toModel].parent, toModel]; + var fn = conversions[graph[toModel].parent][toModel]; + + var cur = graph[toModel].parent; + while (graph[cur].parent) { + path.unshift(graph[cur].parent); + fn = link(conversions[graph[cur].parent][cur], fn); + cur = graph[cur].parent; + } + + fn.conversion = path; + return fn; +} + +var route = function (fromModel) { + var graph = deriveBFS(fromModel); + var conversion = {}; + + var models = Object.keys(graph); + for (var len = models.length, i = 0; i < len; i++) { + var toModel = models[i]; + var node = graph[toModel]; + + if (node.parent === null) { + // no possible conversion, or this node is the source model. + continue; + } + + conversion[toModel] = wrapConversion(toModel, graph); + } + + return conversion; +}; + +var convert = {}; + +var models = Object.keys(conversions); + +function wrapRaw(fn) { + var wrappedFn = function (args) { + if (args === undefined || args === null) { + return args; + } + + if (arguments.length > 1) { + args = Array.prototype.slice.call(arguments); + } + + return fn(args); + }; + + // preserve .conversion property if there is one + if ('conversion' in fn) { + wrappedFn.conversion = fn.conversion; + } + + return wrappedFn; +} + +function wrapRounded(fn) { + var wrappedFn = function (args) { + if (args === undefined || args === null) { + return args; + } + + if (arguments.length > 1) { + args = Array.prototype.slice.call(arguments); + } + + var result = fn(args); + + // we're assuming the result is an array here. + // see notice in conversions.js; don't use box types + // in conversion functions. + if (typeof result === 'object') { + for (var len = result.length, i = 0; i < len; i++) { + result[i] = Math.round(result[i]); + } + } + + return result; + }; + + // preserve .conversion property if there is one + if ('conversion' in fn) { + wrappedFn.conversion = fn.conversion; + } + + return wrappedFn; +} + +models.forEach(function (fromModel) { + convert[fromModel] = {}; + + Object.defineProperty(convert[fromModel], 'channels', {value: conversions[fromModel].channels}); + Object.defineProperty(convert[fromModel], 'labels', {value: conversions[fromModel].labels}); + + var routes = route(fromModel); + var routeModels = Object.keys(routes); + + routeModels.forEach(function (toModel) { + var fn = routes[toModel]; + + convert[fromModel][toModel] = wrapRounded(fn); + convert[fromModel][toModel].raw = wrapRaw(fn); + }); +}); + var colorConvert = convert; -var colorName = { +var colorName$1 = { "aliceblue": [240, 248, 255], "antiquewhite": [250, 235, 215], "aqua": [0, 255, 255], "aquamarine": [127, 255, 212], "azure": [240, 255, 255], @@ -1021,11 +1453,11 @@ } else if (match = string.match(keyword)) { if (match[1] == "transparent") { return [0, 0, 0, 0]; } - rgb = colorName[match[1]]; + rgb = colorName$1[match[1]]; if (!rgb) { return; } } @@ -1183,12 +1615,12 @@ } //create a list of reverse color names var reverseNames = {}; -for (var name in colorName) { - reverseNames[colorName[name]] = name; +for (var name in colorName$1) { + reverseNames[colorName$1[name]] = name; } /* MIT license */ @@ -1948,18 +2380,16 @@ * @param {object} target - The target object in which all objects are merged into. * @param {object} arg1 - Object containing additional properties to merge in target. * @param {object} argN - Additional objects containing properties to merge in target. * @returns {object} The `target` object. */ - extend: function(target) { - var setFn = function(value, key) { - target[key] = value; - }; - for (var i = 1, ilen = arguments.length; i < ilen; ++i) { - helpers.each(arguments[i], setFn); - } - return target; + extend: Object.assign || function(target) { + return helpers.merge(target, [].slice.call(arguments, 1), { + merger: function(key, dst, src) { + dst[key] = src[key]; + } + }); }, /** * Basic javascript inheritance based on the model created in Backbone.js */ @@ -1981,10 +2411,17 @@ helpers.extend(ChartElement.prototype, extensions); } ChartElement.__super__ = me.prototype; return ChartElement; + }, + + _deprecated: function(scope, value, previous, current) { + if (value !== undefined) { + console.warn(scope + ': "' + previous + + '" is deprecated. Please use "' + current + '" instead'); + } } }; var helpers_core = helpers; @@ -2342,11 +2779,15 @@ var rad = (rotation || 0) * RAD_PER_DEG; if (style && typeof style === 'object') { type = style.toString(); if (type === '[object HTMLImageElement]' || type === '[object HTMLCanvasElement]') { - ctx.drawImage(style, x - style.width / 2, y - style.height / 2, style.width, style.height); + ctx.save(); + ctx.translate(x, y); + ctx.rotate(rad); + ctx.drawImage(style, -style.width / 2, -style.height / 2, style.width, style.height); + ctx.restore(); return; } } if (isNaN(radius) || radius <= 0) { @@ -2534,10 +2975,12 @@ _set: function(scope, values) { return helpers_core.merge(this[scope] || (this[scope] = {}), values); } }; +// TODO(v3): remove 'global' from namespace. all default are global and +// there's inconsistency around which options are under 'global' defaults._set('global', { defaultColor: 'rgba(0,0,0,0.1)', defaultFontColor: '#666', defaultFontFamily: "'Helvetica Neue', 'Helvetica', 'Arial', sans-serif", defaultFontSize: 12, @@ -2592,12 +3035,10 @@ case 'px': return value; case '%': value /= 100; break; - default: - break; } return size * value; }, @@ -2658,40 +3099,180 @@ * @param {Array} inputs - An array of values, falling back to the last value. * @param {object} [context] - If defined and the current value is a function, the value * is called with `context` as first argument and the result becomes the new input. * @param {number} [index] - If defined and the current value is an array, the value * at `index` become the new input. + * @param {object} [info] - object to return information about resolution in + * @param {boolean} [info.cacheable] - Will be set to `false` if option is not cacheable. * @since 2.7.0 */ - resolve: function(inputs, context, index) { + resolve: function(inputs, context, index, info) { + var cacheable = true; var i, ilen, value; for (i = 0, ilen = inputs.length; i < ilen; ++i) { value = inputs[i]; if (value === undefined) { continue; } if (context !== undefined && typeof value === 'function') { value = value(context); + cacheable = false; } if (index !== undefined && helpers_core.isArray(value)) { value = value[index]; + cacheable = false; } if (value !== undefined) { + if (info && !cacheable) { + info.cacheable = false; + } return value; } } } }; +/** + * @alias Chart.helpers.math + * @namespace + */ +var exports$2 = { + /** + * Returns an array of factors sorted from 1 to sqrt(value) + * @private + */ + _factorize: function(value) { + var result = []; + var sqrt = Math.sqrt(value); + var i; + + for (i = 1; i < sqrt; i++) { + if (value % i === 0) { + result.push(i); + result.push(value / i); + } + } + if (sqrt === (sqrt | 0)) { // if value is a square number + result.push(sqrt); + } + + result.sort(function(a, b) { + return a - b; + }).pop(); + return result; + }, + + log10: Math.log10 || function(x) { + var exponent = Math.log(x) * Math.LOG10E; // Math.LOG10E = 1 / Math.LN10. + // Check for whole powers of 10, + // which due to floating point rounding error should be corrected. + var powerOf10 = Math.round(exponent); + var isPowerOf10 = x === Math.pow(10, powerOf10); + + return isPowerOf10 ? powerOf10 : exponent; + } +}; + +var helpers_math = exports$2; + +// DEPRECATIONS + +/** + * Provided for backward compatibility, use Chart.helpers.math.log10 instead. + * @namespace Chart.helpers.log10 + * @deprecated since version 2.9.0 + * @todo remove at version 3 + * @private + */ +helpers_core.log10 = exports$2.log10; + +var getRtlAdapter = function(rectX, width) { + return { + x: function(x) { + return rectX + rectX + width - x; + }, + setWidth: function(w) { + width = w; + }, + textAlign: function(align) { + if (align === 'center') { + return align; + } + return align === 'right' ? 'left' : 'right'; + }, + xPlus: function(x, value) { + return x - value; + }, + leftForLtr: function(x, itemWidth) { + return x - itemWidth; + }, + }; +}; + +var getLtrAdapter = function() { + return { + x: function(x) { + return x; + }, + setWidth: function(w) { // eslint-disable-line no-unused-vars + }, + textAlign: function(align) { + return align; + }, + xPlus: function(x, value) { + return x + value; + }, + leftForLtr: function(x, _itemWidth) { // eslint-disable-line no-unused-vars + return x; + }, + }; +}; + +var getAdapter = function(rtl, rectX, width) { + return rtl ? getRtlAdapter(rectX, width) : getLtrAdapter(); +}; + +var overrideTextDirection = function(ctx, direction) { + var style, original; + if (direction === 'ltr' || direction === 'rtl') { + style = ctx.canvas.style; + original = [ + style.getPropertyValue('direction'), + style.getPropertyPriority('direction'), + ]; + + style.setProperty('direction', direction, 'important'); + ctx.prevTextDirection = original; + } +}; + +var restoreTextDirection = function(ctx) { + var original = ctx.prevTextDirection; + if (original !== undefined) { + delete ctx.prevTextDirection; + ctx.canvas.style.setProperty('direction', original[0], original[1]); + } +}; + +var helpers_rtl = { + getRtlAdapter: getAdapter, + overrideTextDirection: overrideTextDirection, + restoreTextDirection: restoreTextDirection, +}; + var helpers$1 = helpers_core; var easing = helpers_easing; var canvas = helpers_canvas; var options = helpers_options; +var math = helpers_math; +var rtl = helpers_rtl; helpers$1.easing = easing; helpers$1.canvas = canvas; helpers$1.options = options; +helpers$1.math = math; +helpers$1.rtl = rtl; function interpolate(start, view, model, ease) { var keys = Object.keys(model); var i, ilen, key, actual, origin, target, type, c0, c1; @@ -2744,19 +3325,20 @@ helpers$1.extend(this, configuration); this.initialize.apply(this, arguments); }; helpers$1.extend(Element.prototype, { + _type: undefined, initialize: function() { this.hidden = false; }, pivot: function() { var me = this; if (!me._view) { - me._view = helpers$1.clone(me._model); + me._view = helpers$1.extend({}, me._model); } me._start = {}; return me; }, @@ -2766,11 +3348,11 @@ var start = me._start; var view = me._view; // No animation -> No Transition if (!model || ease === 1) { - me._view = model; + me._view = helpers$1.extend({}, model); me._start = null; return me; } if (!view) { @@ -2800,32 +3382,32 @@ Element.extend = helpers$1.inherits; var core_element = Element; -var exports$2 = core_element.extend({ +var exports$3 = core_element.extend({ chart: null, // the animation associated chart instance currentStep: 0, // the current animation step numSteps: 60, // default number of steps easing: '', // the easing to use for this animation render: null, // render function used by the animation service onAnimationProgress: null, // user specified callback to fire on each step of the animation onAnimationComplete: null, // user specified callback to fire when the animation finishes }); -var core_animation = exports$2; +var core_animation = exports$3; // DEPRECATIONS /** * Provided for backward compatibility, use Chart.Animation instead * @prop Chart.Animation#animationObject * @deprecated since version 2.6.0 * @todo remove at version 3 */ -Object.defineProperty(exports$2.prototype, 'animationObject', { +Object.defineProperty(exports$3.prototype, 'animationObject', { get: function() { return this; } }); @@ -2833,11 +3415,11 @@ * Provided for backward compatibility, use Chart.Animation#chart instead * @prop Chart.Animation#chartInstance * @deprecated since version 2.6.0 * @todo remove at version 3 */ -Object.defineProperty(exports$2.prototype, 'chartInstance', { +Object.defineProperty(exports$3.prototype, 'chartInstance', { get: function() { return this.chart; }, set: function(value) { this.chart = value; @@ -3051,32 +3633,65 @@ * Element type used to generate a meta data (e.g. Chart.element.Point). * @type {Chart.core.element} */ dataElementType: null, + /** + * Dataset element option keys to be resolved in _resolveDatasetElementOptions. + * A derived controller may override this to resolve controller-specific options. + * The keys defined here are for backward compatibility for legend styles. + * @private + */ + _datasetElementOptions: [ + 'backgroundColor', + 'borderCapStyle', + 'borderColor', + 'borderDash', + 'borderDashOffset', + 'borderJoinStyle', + 'borderWidth' + ], + + /** + * Data element option keys to be resolved in _resolveDataElementOptions. + * A derived controller may override this to resolve controller-specific options. + * The keys defined here are for backward compatibility for legend styles. + * @private + */ + _dataElementOptions: [ + 'backgroundColor', + 'borderColor', + 'borderWidth', + 'pointStyle' + ], + initialize: function(chart, datasetIndex) { var me = this; me.chart = chart; me.index = datasetIndex; me.linkScales(); me.addElements(); + me._type = me.getMeta().type; }, updateIndex: function(datasetIndex) { this.index = datasetIndex; }, linkScales: function() { var me = this; var meta = me.getMeta(); + var chart = me.chart; + var scales = chart.scales; var dataset = me.getDataset(); + var scalesOpts = chart.options.scales; - if (meta.xAxisID === null || !(meta.xAxisID in me.chart.scales)) { - meta.xAxisID = dataset.xAxisID || me.chart.options.scales.xAxes[0].id; + if (meta.xAxisID === null || !(meta.xAxisID in scales) || dataset.xAxisID) { + meta.xAxisID = dataset.xAxisID || scalesOpts.xAxes[0].id; } - if (meta.yAxisID === null || !(meta.yAxisID in me.chart.scales)) { - meta.yAxisID = dataset.yAxisID || me.chart.options.scales.yAxes[0].id; + if (meta.yAxisID === null || !(meta.yAxisID in scales) || dataset.yAxisID) { + meta.yAxisID = dataset.yAxisID || scalesOpts.yAxes[0].id; } }, getDataset: function() { return this.chart.data.datasets[this.index]; @@ -3117,11 +3732,11 @@ _getIndexScale: function() { return this.getScaleForId(this._getIndexScaleId()); }, reset: function() { - this.update(true); + this._update(true); }, /** * @private */ @@ -3193,10 +3808,35 @@ // Re-sync meta data in case the user replaced the data array or if we missed // any updates and so make sure that we handle number of datapoints changing. me.resyncElements(); }, + /** + * Returns the merged user-supplied and default dataset-level options + * @private + */ + _configure: function() { + var me = this; + me._config = helpers$1.merge({}, [ + me.chart.options.datasets[me._type], + me.getDataset(), + ], { + merger: function(key, target, source) { + if (key !== '_meta' && key !== 'data') { + helpers$1._merger(key, target, source); + } + } + }); + }, + + _update: function(reset) { + var me = this; + me._configure(); + me._cachedDataOpts = null; + me.update(reset); + }, + update: helpers$1.noop, transition: function(easingValue) { var meta = this.getMeta(); var elements = meta.data || []; @@ -3225,10 +3865,131 @@ for (; i < ilen; ++i) { elements[i].draw(); } }, + /** + * Returns a set of predefined style properties that should be used to represent the dataset + * or the data if the index is specified + * @param {number} index - data index + * @return {IStyleInterface} style object + */ + getStyle: function(index) { + var me = this; + var meta = me.getMeta(); + var dataset = meta.dataset; + var style; + + me._configure(); + if (dataset && index === undefined) { + style = me._resolveDatasetElementOptions(dataset || {}); + } else { + index = index || 0; + style = me._resolveDataElementOptions(meta.data[index] || {}, index); + } + + if (style.fill === false || style.fill === null) { + style.backgroundColor = style.borderColor; + } + + return style; + }, + + /** + * @private + */ + _resolveDatasetElementOptions: function(element, hover) { + var me = this; + var chart = me.chart; + var datasetOpts = me._config; + var custom = element.custom || {}; + var options = chart.options.elements[me.datasetElementType.prototype._type] || {}; + var elementOptions = me._datasetElementOptions; + var values = {}; + var i, ilen, key, readKey; + + // Scriptable options + var context = { + chart: chart, + dataset: me.getDataset(), + datasetIndex: me.index, + hover: hover + }; + + for (i = 0, ilen = elementOptions.length; i < ilen; ++i) { + key = elementOptions[i]; + readKey = hover ? 'hover' + key.charAt(0).toUpperCase() + key.slice(1) : key; + values[key] = resolve([ + custom[readKey], + datasetOpts[readKey], + options[readKey] + ], context); + } + + return values; + }, + + /** + * @private + */ + _resolveDataElementOptions: function(element, index) { + var me = this; + var custom = element && element.custom; + var cached = me._cachedDataOpts; + if (cached && !custom) { + return cached; + } + var chart = me.chart; + var datasetOpts = me._config; + var options = chart.options.elements[me.dataElementType.prototype._type] || {}; + var elementOptions = me._dataElementOptions; + var values = {}; + + // Scriptable options + var context = { + chart: chart, + dataIndex: index, + dataset: me.getDataset(), + datasetIndex: me.index + }; + + // `resolve` sets cacheable to `false` if any option is indexed or scripted + var info = {cacheable: !custom}; + + var keys, i, ilen, key; + + custom = custom || {}; + + if (helpers$1.isArray(elementOptions)) { + for (i = 0, ilen = elementOptions.length; i < ilen; ++i) { + key = elementOptions[i]; + values[key] = resolve([ + custom[key], + datasetOpts[key], + options[key] + ], context, index, info); + } + } else { + keys = Object.keys(elementOptions); + for (i = 0, ilen = keys.length; i < ilen; ++i) { + key = keys[i]; + values[key] = resolve([ + custom[key], + datasetOpts[elementOptions[key]], + datasetOpts[key], + options[key] + ], context, index, info); + } + } + + if (info.cacheable) { + me._cachedDataOpts = Object.freeze(values); + } + + return values; + }, + removeHoverStyle: function(element) { helpers$1.merge(element._model, element.$previousStyle || {}); delete element.$previousStyle; }, @@ -3251,10 +4012,46 @@ }, /** * @private */ + _removeDatasetHoverStyle: function() { + var element = this.getMeta().dataset; + + if (element) { + this.removeHoverStyle(element); + } + }, + + /** + * @private + */ + _setDatasetHoverStyle: function() { + var element = this.getMeta().dataset; + var prev = {}; + var i, ilen, key, keys, hoverOptions, model; + + if (!element) { + return; + } + + model = element._model; + hoverOptions = this._resolveDatasetElementOptions(element, true); + + keys = Object.keys(hoverOptions); + for (i = 0, ilen = keys.length; i < ilen; ++i) { + key = keys[i]; + prev[key] = model[key]; + model[key] = hoverOptions[key]; + } + + element.$previousStyle = prev; + }, + + /** + * @private + */ resyncElements: function() { var me = this; var meta = me.getMeta(); var data = me.getDataset().data; var numMeta = meta.data.length; @@ -3316,10 +4113,12 @@ DatasetController.extend = helpers$1.inherits; var core_datasetController = DatasetController; +var TAU = Math.PI * 2; + core_defaults._set('global', { elements: { arc: { backgroundColor: core_defaults.global.defaultColor, borderColor: '#fff', @@ -3327,11 +4126,88 @@ borderAlign: 'center' } } }); +function clipArc(ctx, arc) { + var startAngle = arc.startAngle; + var endAngle = arc.endAngle; + var pixelMargin = arc.pixelMargin; + var angleMargin = pixelMargin / arc.outerRadius; + var x = arc.x; + var y = arc.y; + + // Draw an inner border by cliping the arc and drawing a double-width border + // Enlarge the clipping arc by 0.33 pixels to eliminate glitches between borders + ctx.beginPath(); + ctx.arc(x, y, arc.outerRadius, startAngle - angleMargin, endAngle + angleMargin); + if (arc.innerRadius > pixelMargin) { + angleMargin = pixelMargin / arc.innerRadius; + ctx.arc(x, y, arc.innerRadius - pixelMargin, endAngle + angleMargin, startAngle - angleMargin, true); + } else { + ctx.arc(x, y, pixelMargin, endAngle + Math.PI / 2, startAngle - Math.PI / 2); + } + ctx.closePath(); + ctx.clip(); +} + +function drawFullCircleBorders(ctx, vm, arc, inner) { + var endAngle = arc.endAngle; + var i; + + if (inner) { + arc.endAngle = arc.startAngle + TAU; + clipArc(ctx, arc); + arc.endAngle = endAngle; + if (arc.endAngle === arc.startAngle && arc.fullCircles) { + arc.endAngle += TAU; + arc.fullCircles--; + } + } + + ctx.beginPath(); + ctx.arc(arc.x, arc.y, arc.innerRadius, arc.startAngle + TAU, arc.startAngle, true); + for (i = 0; i < arc.fullCircles; ++i) { + ctx.stroke(); + } + + ctx.beginPath(); + ctx.arc(arc.x, arc.y, vm.outerRadius, arc.startAngle, arc.startAngle + TAU); + for (i = 0; i < arc.fullCircles; ++i) { + ctx.stroke(); + } +} + +function drawBorder(ctx, vm, arc) { + var inner = vm.borderAlign === 'inner'; + + if (inner) { + ctx.lineWidth = vm.borderWidth * 2; + ctx.lineJoin = 'round'; + } else { + ctx.lineWidth = vm.borderWidth; + ctx.lineJoin = 'bevel'; + } + + if (arc.fullCircles) { + drawFullCircleBorders(ctx, vm, arc, inner); + } + + if (inner) { + clipArc(ctx, arc); + } + + ctx.beginPath(); + ctx.arc(arc.x, arc.y, vm.outerRadius, arc.startAngle, arc.endAngle); + ctx.arc(arc.x, arc.y, arc.innerRadius, arc.endAngle, arc.startAngle, true); + ctx.closePath(); + ctx.stroke(); +} + var element_arc = core_element.extend({ + _type: 'arc', + inLabelRange: function(mouseX) { var vm = this._view; if (vm) { return (Math.pow(mouseX - vm.x, 2) < Math.pow(vm.radius + vm.hoverRadius, 2)); @@ -3342,24 +4218,24 @@ inRange: function(chartX, chartY) { var vm = this._view; if (vm) { var pointRelativePosition = helpers$1.getAngleFromPoint(vm, {x: chartX, y: chartY}); - var angle = pointRelativePosition.angle; + var angle = pointRelativePosition.angle; var distance = pointRelativePosition.distance; // Sanitise angle range var startAngle = vm.startAngle; var endAngle = vm.endAngle; while (endAngle < startAngle) { - endAngle += 2.0 * Math.PI; + endAngle += TAU; } while (angle > endAngle) { - angle -= 2.0 * Math.PI; + angle -= TAU; } while (angle < startAngle) { - angle += 2.0 * Math.PI; + angle += TAU; } // Check if within the range of the open/close angle var betweenAngles = (angle >= startAngle && angle <= endAngle); var withinRadius = (distance >= vm.innerRadius && distance <= vm.outerRadius); @@ -3396,55 +4272,48 @@ }, draw: function() { var ctx = this._chart.ctx; var vm = this._view; - var sA = vm.startAngle; - var eA = vm.endAngle; var pixelMargin = (vm.borderAlign === 'inner') ? 0.33 : 0; - var angleMargin; + var arc = { + x: vm.x, + y: vm.y, + innerRadius: vm.innerRadius, + outerRadius: Math.max(vm.outerRadius - pixelMargin, 0), + pixelMargin: pixelMargin, + startAngle: vm.startAngle, + endAngle: vm.endAngle, + fullCircles: Math.floor(vm.circumference / TAU) + }; + var i; ctx.save(); + ctx.fillStyle = vm.backgroundColor; + ctx.strokeStyle = vm.borderColor; + + if (arc.fullCircles) { + arc.endAngle = arc.startAngle + TAU; + ctx.beginPath(); + ctx.arc(arc.x, arc.y, arc.outerRadius, arc.startAngle, arc.endAngle); + ctx.arc(arc.x, arc.y, arc.innerRadius, arc.endAngle, arc.startAngle, true); + ctx.closePath(); + for (i = 0; i < arc.fullCircles; ++i) { + ctx.fill(); + } + arc.endAngle = arc.startAngle + vm.circumference % TAU; + } + ctx.beginPath(); - ctx.arc(vm.x, vm.y, Math.max(vm.outerRadius - pixelMargin, 0), sA, eA); - ctx.arc(vm.x, vm.y, vm.innerRadius, eA, sA, true); + ctx.arc(arc.x, arc.y, arc.outerRadius, arc.startAngle, arc.endAngle); + ctx.arc(arc.x, arc.y, arc.innerRadius, arc.endAngle, arc.startAngle, true); ctx.closePath(); - - ctx.fillStyle = vm.backgroundColor; ctx.fill(); if (vm.borderWidth) { - if (vm.borderAlign === 'inner') { - // Draw an inner border by cliping the arc and drawing a double-width border - // Enlarge the clipping arc by 0.33 pixels to eliminate glitches between borders - ctx.beginPath(); - angleMargin = pixelMargin / vm.outerRadius; - ctx.arc(vm.x, vm.y, vm.outerRadius, sA - angleMargin, eA + angleMargin); - if (vm.innerRadius > pixelMargin) { - angleMargin = pixelMargin / vm.innerRadius; - ctx.arc(vm.x, vm.y, vm.innerRadius - pixelMargin, eA + angleMargin, sA - angleMargin, true); - } else { - ctx.arc(vm.x, vm.y, pixelMargin, eA + Math.PI / 2, sA - Math.PI / 2); - } - ctx.closePath(); - ctx.clip(); - - ctx.beginPath(); - ctx.arc(vm.x, vm.y, vm.outerRadius, sA, eA); - ctx.arc(vm.x, vm.y, vm.innerRadius, eA, sA, true); - ctx.closePath(); - - ctx.lineWidth = vm.borderWidth * 2; - ctx.lineJoin = 'round'; - } else { - ctx.lineWidth = vm.borderWidth; - ctx.lineJoin = 'bevel'; - } - - ctx.strokeStyle = vm.borderColor; - ctx.stroke(); + drawBorder(ctx, vm, arc); } ctx.restore(); } }); @@ -3469,26 +4338,44 @@ } } }); var element_line = core_element.extend({ + _type: 'line', + draw: function() { var me = this; var vm = me._view; var ctx = me._chart.ctx; var spanGaps = vm.spanGaps; var points = me._children.slice(); // clone array var globalDefaults = core_defaults.global; var globalOptionLineElements = globalDefaults.elements.line; var lastDrawnIndex = -1; - var index, current, previous, currentVM; + var closePath = me._loop; + var index, previous, currentVM; - // If we are looping, adding the first point again - if (me._loop && points.length) { - points.push(points[0]); + if (!points.length) { + return; } + if (me._loop) { + for (index = 0; index < points.length; ++index) { + previous = helpers$1.previousItem(points, index); + // If the line has an open path, shift the point array + if (!points[index]._view.skip && previous._view.skip) { + points = points.slice(index).concat(points.slice(0, index)); + closePath = spanGaps; + break; + } + } + // If the line has a close path, add the first point again + if (closePath) { + points.push(points[0]); + } + } + ctx.save(); // Stroke Line Options ctx.lineCap = vm.borderCapStyle || globalOptionLineElements.borderCapStyle; @@ -3502,39 +4389,38 @@ ctx.lineWidth = valueOrDefault$1(vm.borderWidth, globalOptionLineElements.borderWidth); ctx.strokeStyle = vm.borderColor || globalDefaults.defaultColor; // Stroke Line ctx.beginPath(); - lastDrawnIndex = -1; - for (index = 0; index < points.length; ++index) { - current = points[index]; - previous = helpers$1.previousItem(points, index); - currentVM = current._view; + // First point moves to it's starting position no matter what + currentVM = points[0]._view; + if (!currentVM.skip) { + ctx.moveTo(currentVM.x, currentVM.y); + lastDrawnIndex = 0; + } - // First point moves to it's starting position no matter what - if (index === 0) { - if (!currentVM.skip) { + for (index = 1; index < points.length; ++index) { + currentVM = points[index]._view; + previous = lastDrawnIndex === -1 ? helpers$1.previousItem(points, index) : points[lastDrawnIndex]; + + if (!currentVM.skip) { + if ((lastDrawnIndex !== (index - 1) && !spanGaps) || lastDrawnIndex === -1) { + // There was a gap and this is the first point after the gap ctx.moveTo(currentVM.x, currentVM.y); - lastDrawnIndex = index; + } else { + // Line to next point + helpers$1.canvas.lineTo(ctx, previous._view, currentVM); } - } else { - previous = lastDrawnIndex === -1 ? previous : points[lastDrawnIndex]; - - if (!currentVM.skip) { - if ((lastDrawnIndex !== (index - 1) && !spanGaps) || lastDrawnIndex === -1) { - // There was a gap and this is the first point after the gap - ctx.moveTo(currentVM.x, currentVM.y); - } else { - // Line to next point - helpers$1.canvas.lineTo(ctx, previous._view, current._view); - } - lastDrawnIndex = index; - } + lastDrawnIndex = index; } } + if (closePath) { + ctx.closePath(); + } + ctx.stroke(); ctx.restore(); } }); @@ -3567,10 +4453,12 @@ var vm = this._view; return vm ? (Math.abs(mouseY - vm.y) < vm.radius + vm.hitRadius) : false; } var element_point = core_element.extend({ + _type: 'point', + inRange: function(mouseX, mouseY) { var vm = this._view; return vm ? ((Math.pow(mouseX - vm.x, 2) + Math.pow(mouseY - vm.y, 2)) < Math.pow(vm.hitRadius + vm.radius, 2)) : false; }, @@ -3749,10 +4637,12 @@ && (skipX || x >= bounds.left && x <= bounds.right) && (skipY || y >= bounds.top && y <= bounds.bottom); } var element_rectangle = core_element.extend({ + _type: 'rectangle', + draw: function() { var ctx = this._chart.ctx; var vm = this._view; var rects = boundingRects(vm); var outer = rects.outer; @@ -3838,22 +4728,21 @@ elements.Arc = Arc; elements.Line = Line; elements.Point = Point; elements.Rectangle = Rectangle; -var resolve$1 = helpers$1.options.resolve; +var deprecated = helpers$1._deprecated; +var valueOrDefault$3 = helpers$1.valueOrDefault; core_defaults._set('bar', { hover: { mode: 'label' }, scales: { xAxes: [{ type: 'category', - categoryPercentage: 0.8, - barPercentage: 0.9, offset: true, gridLines: { offsetGridLines: true } }], @@ -3862,26 +4751,34 @@ type: 'linear' }] } }); +core_defaults._set('global', { + datasets: { + bar: { + categoryPercentage: 0.8, + barPercentage: 0.9 + } + } +}); + /** * Computes the "optimal" sample size to maintain bars equally sized while preventing overlap. * @private */ function computeMinSampleSize(scale, pixels) { - var min = scale.isHorizontal() ? scale.width : scale.height; - var ticks = scale.getTicks(); + var min = scale._length; var prev, curr, i, ilen; for (i = 1, ilen = pixels.length; i < ilen; ++i) { min = Math.min(min, Math.abs(pixels[i] - pixels[i - 1])); } - for (i = 0, ilen = ticks.length; i < ilen; ++i) { + for (i = 0, ilen = scale.getTicks().length; i < ilen; ++i) { curr = scale.getPixelForTick(i); - min = i > 0 ? Math.min(min, curr - prev) : min; + min = i > 0 ? Math.min(min, Math.abs(curr - prev)) : min; prev = curr; } return min; } @@ -3894,14 +4791,17 @@ */ function computeFitCategoryTraits(index, ruler, options) { var thickness = options.barThickness; var count = ruler.stackCount; var curr = ruler.pixels[index]; + var min = helpers$1.isNullOrUndef(thickness) + ? computeMinSampleSize(ruler.scale, ruler.pixels) + : -1; var size, ratio; if (helpers$1.isNullOrUndef(thickness)) { - size = ruler.min * options.categoryPercentage; + size = min * options.categoryPercentage; ratio = options.barPercentage; } else { // When bar thickness is enforced, category and bar percentages are ignored. // Note(SB): we could add support for relative bar thickness (e.g. barThickness: '50%') // and deprecate barPercentage since this value is ignored when thickness is absolute. @@ -3953,19 +4853,41 @@ var controller_bar = core_datasetController.extend({ dataElementType: elements.Rectangle, + /** + * @private + */ + _dataElementOptions: [ + 'backgroundColor', + 'borderColor', + 'borderSkipped', + 'borderWidth', + 'barPercentage', + 'barThickness', + 'categoryPercentage', + 'maxBarThickness', + 'minBarLength' + ], + initialize: function() { var me = this; - var meta; + var meta, scaleOpts; core_datasetController.prototype.initialize.apply(me, arguments); meta = me.getMeta(); meta.stack = me.getDataset().stack; meta.bar = true; + + scaleOpts = me._getIndexScale().options; + deprecated('bar chart', scaleOpts.barPercentage, 'scales.[x/y]Axes.barPercentage', 'dataset.barPercentage'); + deprecated('bar chart', scaleOpts.barThickness, 'scales.[x/y]Axes.barThickness', 'dataset.barThickness'); + deprecated('bar chart', scaleOpts.categoryPercentage, 'scales.[x/y]Axes.categoryPercentage', 'dataset.categoryPercentage'); + deprecated('bar chart', me._getValueScale().options.minBarLength, 'scales.[x/y]Axes.minBarLength', 'dataset.minBarLength'); + deprecated('bar chart', scaleOpts.maxBarThickness, 'scales.[x/y]Axes.maxBarThickness', 'dataset.maxBarThickness'); }, update: function(reset) { var me = this; var rects = me.getMeta().data; @@ -3980,11 +4902,11 @@ updateElement: function(rectangle, index, reset) { var me = this; var meta = me.getMeta(); var dataset = me.getDataset(); - var options = me._resolveElementOptions(rectangle, index); + var options = me._resolveDataElementOptions(rectangle, index); rectangle._xScale = me.getScaleForId(meta.xAxisID); rectangle._yScale = me.getScaleForId(meta.yAxisID); rectangle._datasetIndex = me.index; rectangle._index = index; @@ -3995,27 +4917,31 @@ borderWidth: options.borderWidth, datasetLabel: dataset.label, label: me.chart.data.labels[index] }; - me._updateElementGeometry(rectangle, index, reset); + if (helpers$1.isArray(dataset.data[index])) { + rectangle._model.borderSkipped = null; + } + me._updateElementGeometry(rectangle, index, reset, options); + rectangle.pivot(); }, /** * @private */ - _updateElementGeometry: function(rectangle, index, reset) { + _updateElementGeometry: function(rectangle, index, reset, options) { var me = this; var model = rectangle._model; var vscale = me._getValueScale(); var base = vscale.getBasePixel(); var horizontal = vscale.isHorizontal(); var ruler = me._ruler || me.getRuler(); - var vpixels = me.calculateBarValuePixels(me.index, index); - var ipixels = me.calculateBarIndexPixels(me.index, index, ruler); + var vpixels = me.calculateBarValuePixels(me.index, index, options); + var ipixels = me.calculateBarIndexPixels(me.index, index, ruler, options); model.horizontal = horizontal; model.base = reset ? base : vpixels.base; model.x = horizontal ? reset ? base : vpixels.head : ipixels.center; model.y = horizontal ? ipixels.center : reset ? base : vpixels.head; @@ -4029,25 +4955,31 @@ * @returns {string[]} The list of stack IDs * @private */ _getStacks: function(last) { var me = this; - var chart = me.chart; var scale = me._getIndexScale(); + var metasets = scale._getMatchingVisibleMetas(me._type); var stacked = scale.options.stacked; - var ilen = last === undefined ? chart.data.datasets.length : last + 1; + var ilen = metasets.length; var stacks = []; var i, meta; for (i = 0; i < ilen; ++i) { - meta = chart.getDatasetMeta(i); - if (meta.bar && chart.isDatasetVisible(i) && - (stacked === false || - (stacked === true && stacks.indexOf(meta.stack) === -1) || - (stacked === undefined && (meta.stack === undefined || stacks.indexOf(meta.stack) === -1)))) { + meta = metasets[i]; + // stacked | meta.stack + // | found | not found | undefined + // false | x | x | x + // true | | x | + // undefined | | x | x + if (stacked === false || stacks.indexOf(meta.stack) === -1 || + (stacked === undefined && meta.stack === undefined)) { stacks.push(meta.stack); } + if (meta.index === last) { + break; + } } return stacks; }, @@ -4081,78 +5013,72 @@ * @private */ getRuler: function() { var me = this; var scale = me._getIndexScale(); - var stackCount = me.getStackCount(); - var datasetIndex = me.index; - var isHorizontal = scale.isHorizontal(); - var start = isHorizontal ? scale.left : scale.top; - var end = start + (isHorizontal ? scale.width : scale.height); var pixels = []; - var i, ilen, min; + var i, ilen; for (i = 0, ilen = me.getMeta().data.length; i < ilen; ++i) { - pixels.push(scale.getPixelForValue(null, i, datasetIndex)); + pixels.push(scale.getPixelForValue(null, i, me.index)); } - min = helpers$1.isNullOrUndef(scale.options.barThickness) - ? computeMinSampleSize(scale, pixels) - : -1; - return { - min: min, pixels: pixels, - start: start, - end: end, - stackCount: stackCount, + start: scale._startPixel, + end: scale._endPixel, + stackCount: me.getStackCount(), scale: scale }; }, /** * Note: pixel values are not clamped to the scale area. * @private */ - calculateBarValuePixels: function(datasetIndex, index) { + calculateBarValuePixels: function(datasetIndex, index, options) { var me = this; var chart = me.chart; - var meta = me.getMeta(); var scale = me._getValueScale(); var isHorizontal = scale.isHorizontal(); var datasets = chart.data.datasets; - var value = +scale.getRightValue(datasets[datasetIndex].data[index]); - var minBarLength = scale.options.minBarLength; + var metasets = scale._getMatchingVisibleMetas(me._type); + var value = scale._parseValue(datasets[datasetIndex].data[index]); + var minBarLength = options.minBarLength; var stacked = scale.options.stacked; - var stack = meta.stack; - var start = 0; - var i, imeta, ivalue, base, head, size; + var stack = me.getMeta().stack; + var start = value.start === undefined ? 0 : value.max >= 0 && value.min >= 0 ? value.min : value.max; + var length = value.start === undefined ? value.end : value.max >= 0 && value.min >= 0 ? value.max - value.min : value.min - value.max; + var ilen = metasets.length; + var i, imeta, ivalue, base, head, size, stackLength; if (stacked || (stacked === undefined && stack !== undefined)) { - for (i = 0; i < datasetIndex; ++i) { - imeta = chart.getDatasetMeta(i); + for (i = 0; i < ilen; ++i) { + imeta = metasets[i]; - if (imeta.bar && - imeta.stack === stack && - imeta.controller._getValueScaleId() === scale.id && - chart.isDatasetVisible(i)) { + if (imeta.index === datasetIndex) { + break; + } - ivalue = +scale.getRightValue(datasets[i].data[index]); - if ((value < 0 && ivalue < 0) || (value >= 0 && ivalue > 0)) { + if (imeta.stack === stack) { + stackLength = scale._parseValue(datasets[imeta.index].data[index]); + ivalue = stackLength.start === undefined ? stackLength.end : stackLength.min >= 0 && stackLength.max >= 0 ? stackLength.max : stackLength.min; + + if ((value.min < 0 && ivalue < 0) || (value.max >= 0 && ivalue > 0)) { start += ivalue; } } } } base = scale.getPixelForValue(start); - head = scale.getPixelForValue(start + value); + head = scale.getPixelForValue(start + length); size = head - base; if (minBarLength !== undefined && Math.abs(size) < minBarLength) { size = minBarLength; - if (value >= 0 && !isHorizontal || value < 0 && isHorizontal) { + if (length >= 0 && !isHorizontal || length < 0 && isHorizontal) { head = base - minBarLength; } else { head = base + minBarLength; } } @@ -4166,21 +5092,20 @@ }, /** * @private */ - calculateBarIndexPixels: function(datasetIndex, index, ruler) { + calculateBarIndexPixels: function(datasetIndex, index, ruler, options) { var me = this; - var options = ruler.scale.options; var range = options.barThickness === 'flex' ? computeFlexCategoryTraits(index, ruler, options) : computeFitCategoryTraits(index, ruler, options); var stackIndex = me.getStackIndex(datasetIndex, me.getMeta().stack); var center = range.start + (range.chunk * stackIndex) + (range.chunk / 2); var size = Math.min( - helpers$1.valueOrDefault(options.maxBarThickness, Infinity), + valueOrDefault$3(options.maxBarThickness, Infinity), range.chunk * range.ratio); return { base: center - size / 2, head: center + size / 2, @@ -4199,61 +5124,41 @@ var i = 0; helpers$1.canvas.clipArea(chart.ctx, chart.chartArea); for (; i < ilen; ++i) { - if (!isNaN(scale.getRightValue(dataset.data[i]))) { + var val = scale._parseValue(dataset.data[i]); + if (!isNaN(val.min) && !isNaN(val.max)) { rects[i].draw(); } } helpers$1.canvas.unclipArea(chart.ctx); }, /** * @private */ - _resolveElementOptions: function(rectangle, index) { + _resolveDataElementOptions: function() { var me = this; - var chart = me.chart; - var datasets = chart.data.datasets; - var dataset = datasets[me.index]; - var custom = rectangle.custom || {}; - var options = chart.options.elements.rectangle; - var values = {}; - var i, ilen, key; + var values = helpers$1.extend({}, core_datasetController.prototype._resolveDataElementOptions.apply(me, arguments)); + var indexOpts = me._getIndexScale().options; + var valueOpts = me._getValueScale().options; - // Scriptable options - var context = { - chart: chart, - dataIndex: index, - dataset: dataset, - datasetIndex: me.index - }; + values.barPercentage = valueOrDefault$3(indexOpts.barPercentage, values.barPercentage); + values.barThickness = valueOrDefault$3(indexOpts.barThickness, values.barThickness); + values.categoryPercentage = valueOrDefault$3(indexOpts.categoryPercentage, values.categoryPercentage); + values.maxBarThickness = valueOrDefault$3(indexOpts.maxBarThickness, values.maxBarThickness); + values.minBarLength = valueOrDefault$3(valueOpts.minBarLength, values.minBarLength); - var keys = [ - 'backgroundColor', - 'borderColor', - 'borderSkipped', - 'borderWidth' - ]; - - for (i = 0, ilen = keys.length; i < ilen; ++i) { - key = keys[i]; - values[key] = resolve$1([ - custom[key], - dataset[key], - options[key] - ], context, index); - } - return values; } + }); -var valueOrDefault$3 = helpers$1.valueOrDefault; -var resolve$2 = helpers$1.options.resolve; +var valueOrDefault$4 = helpers$1.valueOrDefault; +var resolve$1 = helpers$1.options.resolve; core_defaults._set('bubble', { hover: { mode: 'single' }, @@ -4291,10 +5196,26 @@ * @protected */ dataElementType: elements.Point, /** + * @private + */ + _dataElementOptions: [ + 'backgroundColor', + 'borderColor', + 'borderWidth', + 'hoverBackgroundColor', + 'hoverBorderColor', + 'hoverBorderWidth', + 'hoverRadius', + 'hitRadius', + 'pointStyle', + 'rotation' + ], + + /** * @protected */ update: function(reset) { var me = this; var meta = me.getMeta(); @@ -4313,11 +5234,11 @@ var me = this; var meta = me.getMeta(); var custom = point.custom || {}; var xScale = me.getScaleForId(meta.xAxisID); var yScale = me.getScaleForId(meta.yAxisID); - var options = me._resolveElementOptions(point, index); + var options = me._resolveDataElementOptions(point, index); var data = me.getDataset().data[index]; var dsIndex = me.index; var x = reset ? xScale.getPixelForDecimal(0.5) : xScale.getPixelForValue(typeof data === 'object' ? data : NaN, index, dsIndex); var y = reset ? yScale.getBasePixel() : yScale.getPixelForValue(data, index, dsIndex); @@ -4356,75 +5277,58 @@ borderColor: model.borderColor, borderWidth: model.borderWidth, radius: model.radius }; - model.backgroundColor = valueOrDefault$3(options.hoverBackgroundColor, getHoverColor(options.backgroundColor)); - model.borderColor = valueOrDefault$3(options.hoverBorderColor, getHoverColor(options.borderColor)); - model.borderWidth = valueOrDefault$3(options.hoverBorderWidth, options.borderWidth); + model.backgroundColor = valueOrDefault$4(options.hoverBackgroundColor, getHoverColor(options.backgroundColor)); + model.borderColor = valueOrDefault$4(options.hoverBorderColor, getHoverColor(options.borderColor)); + model.borderWidth = valueOrDefault$4(options.hoverBorderWidth, options.borderWidth); model.radius = options.radius + options.hoverRadius; }, /** * @private */ - _resolveElementOptions: function(point, index) { + _resolveDataElementOptions: function(point, index) { var me = this; var chart = me.chart; - var datasets = chart.data.datasets; - var dataset = datasets[me.index]; + var dataset = me.getDataset(); var custom = point.custom || {}; - var options = chart.options.elements.point; - var data = dataset.data[index]; - var values = {}; - var i, ilen, key; + var data = dataset.data[index] || {}; + var values = core_datasetController.prototype._resolveDataElementOptions.apply(me, arguments); // Scriptable options var context = { chart: chart, dataIndex: index, dataset: dataset, datasetIndex: me.index }; - var keys = [ - 'backgroundColor', - 'borderColor', - 'borderWidth', - 'hoverBackgroundColor', - 'hoverBorderColor', - 'hoverBorderWidth', - 'hoverRadius', - 'hitRadius', - 'pointStyle', - 'rotation' - ]; - - for (i = 0, ilen = keys.length; i < ilen; ++i) { - key = keys[i]; - values[key] = resolve$2([ - custom[key], - dataset[key], - options[key] - ], context, index); + // In case values were cached (and thus frozen), we need to clone the values + if (me._cachedDataOpts === values) { + values = helpers$1.extend({}, values); } // Custom radius resolution - values.radius = resolve$2([ + values.radius = resolve$1([ custom.radius, - data ? data.r : undefined, - dataset.radius, - options.radius + data.r, + me._config.radius, + chart.options.elements.point.radius ], context, index); return values; } }); -var resolve$3 = helpers$1.options.resolve; -var valueOrDefault$4 = helpers$1.valueOrDefault; +var valueOrDefault$5 = helpers$1.valueOrDefault; +var PI$1 = Math.PI; +var DOUBLE_PI$1 = PI$1 * 2; +var HALF_PI$1 = PI$1 / 2; + core_defaults._set('doughnut', { animation: { // Boolean - Whether we animate the rotation of the Doughnut animateRotate: true, // Boolean - Whether we animate scaling the Doughnut from the centre @@ -4432,51 +5336,45 @@ }, hover: { mode: 'single' }, legendCallback: function(chart) { - var text = []; - text.push('<ul class="' + chart.id + '-legend">'); - + var list = document.createElement('ul'); var data = chart.data; var datasets = data.datasets; var labels = data.labels; + var i, ilen, listItem, listItemSpan; + list.setAttribute('class', chart.id + '-legend'); if (datasets.length) { - for (var i = 0; i < datasets[0].data.length; ++i) { - text.push('<li><span style="background-color:' + datasets[0].backgroundColor[i] + '"></span>'); + for (i = 0, ilen = datasets[0].data.length; i < ilen; ++i) { + listItem = list.appendChild(document.createElement('li')); + listItemSpan = listItem.appendChild(document.createElement('span')); + listItemSpan.style.backgroundColor = datasets[0].backgroundColor[i]; if (labels[i]) { - text.push(labels[i]); + listItem.appendChild(document.createTextNode(labels[i])); } - text.push('</li>'); } } - text.push('</ul>'); - return text.join(''); + return list.outerHTML; }, legend: { labels: { generateLabels: function(chart) { var data = chart.data; if (data.labels.length && data.datasets.length) { return data.labels.map(function(label, i) { var meta = chart.getDatasetMeta(0); - var ds = data.datasets[0]; - var arc = meta.data[i]; - var custom = arc && arc.custom || {}; - var arcOpts = chart.options.elements.arc; - var fill = resolve$3([custom.backgroundColor, ds.backgroundColor, arcOpts.backgroundColor], undefined, i); - var stroke = resolve$3([custom.borderColor, ds.borderColor, arcOpts.borderColor], undefined, i); - var bw = resolve$3([custom.borderWidth, ds.borderWidth, arcOpts.borderWidth], undefined, i); + var style = meta.controller.getStyle(i); return { text: label, - fillStyle: fill, - strokeStyle: stroke, - lineWidth: bw, - hidden: isNaN(ds.data[i]) || meta.data[i].hidden, + fillStyle: style.backgroundColor, + strokeStyle: style.borderColor, + lineWidth: style.borderWidth, + hidden: isNaN(data.datasets[0].data[i]) || meta.data[i].hidden, // Extra data used for toggling the correct item index: i }; }); @@ -4504,14 +5402,14 @@ // The percentage of the chart that we cut out of the middle. cutoutPercentage: 50, // The rotation of the chart, where the first data arc begins. - rotation: Math.PI * -0.5, + rotation: -HALF_PI$1, // The total circumference of the chart. - circumference: Math.PI * 2.0, + circumference: DOUBLE_PI$1, // Need to override these to give a nice default tooltips: { callbacks: { title: function() { @@ -4540,10 +5438,23 @@ dataElementType: elements.Arc, linkScales: helpers$1.noop, + /** + * @private + */ + _dataElementOptions: [ + 'backgroundColor', + 'borderColor', + 'borderWidth', + 'borderAlign', + 'hoverBackgroundColor', + 'hoverBorderColor', + 'hoverBorderWidth', + ], + // Get index of the dataset in relation to the visible datasets. This allows determining the inner and outer radius correctly getRingIndex: function(datasetIndex) { var ringIndex = 0; for (var j = 0; j < datasetIndex; ++j) { @@ -4558,50 +5469,56 @@ update: function(reset) { var me = this; var chart = me.chart; var chartArea = chart.chartArea; var opts = chart.options; - var availableWidth = chartArea.right - chartArea.left; - var availableHeight = chartArea.bottom - chartArea.top; - var minSize = Math.min(availableWidth, availableHeight); - var offset = {x: 0, y: 0}; + var ratioX = 1; + var ratioY = 1; + var offsetX = 0; + var offsetY = 0; var meta = me.getMeta(); var arcs = meta.data; - var cutoutPercentage = opts.cutoutPercentage; + var cutout = opts.cutoutPercentage / 100 || 0; var circumference = opts.circumference; var chartWeight = me._getRingWeight(me.index); - var i, ilen; + var maxWidth, maxHeight, i, ilen; - // If the chart's circumference isn't a full circle, calculate minSize as a ratio of the width/height of the arc - if (circumference < Math.PI * 2.0) { - var startAngle = opts.rotation % (Math.PI * 2.0); - startAngle += Math.PI * 2.0 * (startAngle >= Math.PI ? -1 : startAngle < -Math.PI ? 1 : 0); + // If the chart's circumference isn't a full circle, calculate size as a ratio of the width/height of the arc + if (circumference < DOUBLE_PI$1) { + var startAngle = opts.rotation % DOUBLE_PI$1; + startAngle += startAngle >= PI$1 ? -DOUBLE_PI$1 : startAngle < -PI$1 ? DOUBLE_PI$1 : 0; var endAngle = startAngle + circumference; - var start = {x: Math.cos(startAngle), y: Math.sin(startAngle)}; - var end = {x: Math.cos(endAngle), y: Math.sin(endAngle)}; - var contains0 = (startAngle <= 0 && endAngle >= 0) || (startAngle <= Math.PI * 2.0 && Math.PI * 2.0 <= endAngle); - var contains90 = (startAngle <= Math.PI * 0.5 && Math.PI * 0.5 <= endAngle) || (startAngle <= Math.PI * 2.5 && Math.PI * 2.5 <= endAngle); - var contains180 = (startAngle <= -Math.PI && -Math.PI <= endAngle) || (startAngle <= Math.PI && Math.PI <= endAngle); - var contains270 = (startAngle <= -Math.PI * 0.5 && -Math.PI * 0.5 <= endAngle) || (startAngle <= Math.PI * 1.5 && Math.PI * 1.5 <= endAngle); - var cutout = cutoutPercentage / 100.0; - var min = {x: contains180 ? -1 : Math.min(start.x * (start.x < 0 ? 1 : cutout), end.x * (end.x < 0 ? 1 : cutout)), y: contains270 ? -1 : Math.min(start.y * (start.y < 0 ? 1 : cutout), end.y * (end.y < 0 ? 1 : cutout))}; - var max = {x: contains0 ? 1 : Math.max(start.x * (start.x > 0 ? 1 : cutout), end.x * (end.x > 0 ? 1 : cutout)), y: contains90 ? 1 : Math.max(start.y * (start.y > 0 ? 1 : cutout), end.y * (end.y > 0 ? 1 : cutout))}; - var size = {width: (max.x - min.x) * 0.5, height: (max.y - min.y) * 0.5}; - minSize = Math.min(availableWidth / size.width, availableHeight / size.height); - offset = {x: (max.x + min.x) * -0.5, y: (max.y + min.y) * -0.5}; + var startX = Math.cos(startAngle); + var startY = Math.sin(startAngle); + var endX = Math.cos(endAngle); + var endY = Math.sin(endAngle); + var contains0 = (startAngle <= 0 && endAngle >= 0) || endAngle >= DOUBLE_PI$1; + var contains90 = (startAngle <= HALF_PI$1 && endAngle >= HALF_PI$1) || endAngle >= DOUBLE_PI$1 + HALF_PI$1; + var contains180 = startAngle === -PI$1 || endAngle >= PI$1; + var contains270 = (startAngle <= -HALF_PI$1 && endAngle >= -HALF_PI$1) || endAngle >= PI$1 + HALF_PI$1; + var minX = contains180 ? -1 : Math.min(startX, startX * cutout, endX, endX * cutout); + var minY = contains270 ? -1 : Math.min(startY, startY * cutout, endY, endY * cutout); + var maxX = contains0 ? 1 : Math.max(startX, startX * cutout, endX, endX * cutout); + var maxY = contains90 ? 1 : Math.max(startY, startY * cutout, endY, endY * cutout); + ratioX = (maxX - minX) / 2; + ratioY = (maxY - minY) / 2; + offsetX = -(maxX + minX) / 2; + offsetY = -(maxY + minY) / 2; } for (i = 0, ilen = arcs.length; i < ilen; ++i) { - arcs[i]._options = me._resolveElementOptions(arcs[i], i); + arcs[i]._options = me._resolveDataElementOptions(arcs[i], i); } chart.borderWidth = me.getMaxBorderWidth(); - chart.outerRadius = Math.max((minSize - chart.borderWidth) / 2, 0); - chart.innerRadius = Math.max(cutoutPercentage ? (chart.outerRadius / 100) * (cutoutPercentage) : 0, 0); + maxWidth = (chartArea.right - chartArea.left - chart.borderWidth) / ratioX; + maxHeight = (chartArea.bottom - chartArea.top - chart.borderWidth) / ratioY; + chart.outerRadius = Math.max(Math.min(maxWidth, maxHeight) / 2, 0); + chart.innerRadius = Math.max(chart.outerRadius * cutout, 0); chart.radiusLength = (chart.outerRadius - chart.innerRadius) / (me._getVisibleDatasetWeightTotal() || 1); - chart.offsetX = offset.x * chart.outerRadius; - chart.offsetY = offset.y * chart.outerRadius; + chart.offsetX = offsetX * chart.outerRadius; + chart.offsetY = offsetY * chart.outerRadius; meta.total = me.calculateTotal(); me.outerRadius = chart.outerRadius - chart.radiusLength * me._getRingWeightOffset(me.index); me.innerRadius = Math.max(me.outerRadius - chart.radiusLength * chartWeight, 0); @@ -4620,11 +5537,11 @@ var centerX = (chartArea.left + chartArea.right) / 2; var centerY = (chartArea.top + chartArea.bottom) / 2; var startAngle = opts.rotation; // non reset case handled later var endAngle = opts.rotation; // non reset case handled later var dataset = me.getDataset(); - var circumference = reset && animationOpts.animateRotate ? 0 : arc.hidden ? 0 : me.calculateCircumference(dataset.data[index]) * (opts.circumference / (2.0 * Math.PI)); + var circumference = reset && animationOpts.animateRotate ? 0 : arc.hidden ? 0 : me.calculateCircumference(dataset.data[index]) * (opts.circumference / DOUBLE_PI$1); var innerRadius = reset && animationOpts.animateScale ? 0 : me.innerRadius; var outerRadius = reset && animationOpts.animateScale ? 0 : me.outerRadius; var options = arc._options || {}; helpers$1.extend(arc, { @@ -4686,11 +5603,11 @@ }, calculateCircumference: function(value) { var total = this.getMeta().total; if (total > 0 && !isNaN(value)) { - return (Math.PI * 2.0) * (Math.abs(value) / total); + return DOUBLE_PI$1 * (Math.abs(value) / total); } return 0; }, // gets the max border or hover width to properly scale pie charts @@ -4718,11 +5635,16 @@ return 0; } for (i = 0, ilen = arcs.length; i < ilen; ++i) { arc = arcs[i]; - options = controller ? controller._resolveElementOptions(arc, i) : arc._options; + if (controller) { + controller._configure(); + options = controller._resolveDataElementOptions(arc, i); + } else { + options = arc._options; + } if (options.borderAlign !== 'inner') { borderWidth = options.borderWidth; hoverWidth = options.hoverBorderWidth; max = borderWidth > max ? borderWidth : max; @@ -4744,58 +5666,16 @@ backgroundColor: model.backgroundColor, borderColor: model.borderColor, borderWidth: model.borderWidth, }; - model.backgroundColor = valueOrDefault$4(options.hoverBackgroundColor, getHoverColor(options.backgroundColor)); - model.borderColor = valueOrDefault$4(options.hoverBorderColor, getHoverColor(options.borderColor)); - model.borderWidth = valueOrDefault$4(options.hoverBorderWidth, options.borderWidth); + model.backgroundColor = valueOrDefault$5(options.hoverBackgroundColor, getHoverColor(options.backgroundColor)); + model.borderColor = valueOrDefault$5(options.hoverBorderColor, getHoverColor(options.borderColor)); + model.borderWidth = valueOrDefault$5(options.hoverBorderWidth, options.borderWidth); }, /** - * @private - */ - _resolveElementOptions: function(arc, index) { - var me = this; - var chart = me.chart; - var dataset = me.getDataset(); - var custom = arc.custom || {}; - var options = chart.options.elements.arc; - var values = {}; - var i, ilen, key; - - // Scriptable options - var context = { - chart: chart, - dataIndex: index, - dataset: dataset, - datasetIndex: me.index - }; - - var keys = [ - 'backgroundColor', - 'borderColor', - 'borderWidth', - 'borderAlign', - 'hoverBackgroundColor', - 'hoverBorderColor', - 'hoverBorderWidth', - ]; - - for (i = 0, ilen = keys.length; i < ilen; ++i) { - key = keys[i]; - values[key] = resolve$3([ - custom[key], - dataset[key], - options[key] - ], context, index); - } - - return values; - }, - - /** * Get radius length offset of the dataset in relation to the visible datasets weights. This allows determining the inner and outer radius correctly * @private */ _getRingWeightOffset: function(datasetIndex) { var ringWeightOffset = 0; @@ -4811,11 +5691,11 @@ /** * @private */ _getRingWeight: function(dataSetIndex) { - return Math.max(valueOrDefault$4(this.chart.data.datasets[dataSetIndex].weight, 1), 0); + return Math.max(valueOrDefault$5(this.chart.data.datasets[dataSetIndex].weight, 1), 0); }, /** * Returns the sum of all visibile data set weights. This value can be 0. * @private @@ -4838,12 +5718,10 @@ }], yAxes: [{ type: 'category', position: 'left', - categoryPercentage: 0.8, - barPercentage: 0.9, offset: true, gridLines: { offsetGridLines: true } }] @@ -4859,10 +5737,19 @@ mode: 'index', axis: 'y' } }); +core_defaults._set('global', { + datasets: { + horizontalBar: { + categoryPercentage: 0.8, + barPercentage: 0.9 + } + } +}); + var controller_horizontalBar = controller_bar.extend({ /** * @private */ _getValueScaleId: function() { @@ -4875,12 +5762,12 @@ _getIndexScaleId: function() { return this.getMeta().yAxisID; } }); -var valueOrDefault$5 = helpers$1.valueOrDefault; -var resolve$4 = helpers$1.options.resolve; +var valueOrDefault$6 = helpers$1.valueOrDefault; +var resolve$2 = helpers$1.options.resolve; var isPointInArea = helpers$1.canvas._isPointInArea; core_defaults._set('line', { showLines: true, spanGaps: false, @@ -4899,44 +5786,120 @@ id: 'y-axis-0' }] } }); -function lineEnabled(dataset, options) { - return valueOrDefault$5(dataset.showLine, options.showLines); +function scaleClip(scale, halfBorderWidth) { + var tickOpts = scale && scale.options.ticks || {}; + var reverse = tickOpts.reverse; + var min = tickOpts.min === undefined ? halfBorderWidth : 0; + var max = tickOpts.max === undefined ? halfBorderWidth : 0; + return { + start: reverse ? max : min, + end: reverse ? min : max + }; } +function defaultClip(xScale, yScale, borderWidth) { + var halfBorderWidth = borderWidth / 2; + var x = scaleClip(xScale, halfBorderWidth); + var y = scaleClip(yScale, halfBorderWidth); + + return { + top: y.end, + right: x.end, + bottom: y.start, + left: x.start + }; +} + +function toClip(value) { + var t, r, b, l; + + if (helpers$1.isObject(value)) { + t = value.top; + r = value.right; + b = value.bottom; + l = value.left; + } else { + t = r = b = l = value; + } + + return { + top: t, + right: r, + bottom: b, + left: l + }; +} + + var controller_line = core_datasetController.extend({ datasetElementType: elements.Line, dataElementType: elements.Point, + /** + * @private + */ + _datasetElementOptions: [ + 'backgroundColor', + 'borderCapStyle', + 'borderColor', + 'borderDash', + 'borderDashOffset', + 'borderJoinStyle', + 'borderWidth', + 'cubicInterpolationMode', + 'fill' + ], + + /** + * @private + */ + _dataElementOptions: { + backgroundColor: 'pointBackgroundColor', + borderColor: 'pointBorderColor', + borderWidth: 'pointBorderWidth', + hitRadius: 'pointHitRadius', + hoverBackgroundColor: 'pointHoverBackgroundColor', + hoverBorderColor: 'pointHoverBorderColor', + hoverBorderWidth: 'pointHoverBorderWidth', + hoverRadius: 'pointHoverRadius', + pointStyle: 'pointStyle', + radius: 'pointRadius', + rotation: 'pointRotation' + }, + update: function(reset) { var me = this; var meta = me.getMeta(); var line = meta.dataset; var points = meta.data || []; - var scale = me.getScaleForId(meta.yAxisID); - var dataset = me.getDataset(); - var showLine = lineEnabled(dataset, me.chart.options); + var options = me.chart.options; + var config = me._config; + var showLine = me._showLine = valueOrDefault$6(config.showLine, options.showLines); var i, ilen; + me._xScale = me.getScaleForId(meta.xAxisID); + me._yScale = me.getScaleForId(meta.yAxisID); + // Update Line if (showLine) { // Compatibility: If the properties are defined with only the old name, use those values - if ((dataset.tension !== undefined) && (dataset.lineTension === undefined)) { - dataset.lineTension = dataset.tension; + if (config.tension !== undefined && config.lineTension === undefined) { + config.lineTension = config.tension; } // Utility - line._scale = scale; + line._scale = me._yScale; line._datasetIndex = me.index; // Data line._children = points; // Model - line._model = me._resolveLineOptions(line); + line._model = me._resolveDatasetElementOptions(line); line.pivot(); } // Update Points @@ -4959,16 +5922,16 @@ var meta = me.getMeta(); var custom = point.custom || {}; var dataset = me.getDataset(); var datasetIndex = me.index; var value = dataset.data[index]; - var yScale = me.getScaleForId(meta.yAxisID); - var xScale = me.getScaleForId(meta.xAxisID); + var xScale = me._xScale; + var yScale = me._yScale; var lineModel = meta.dataset._model; var x, y; - var options = me._resolvePointOptions(point, index); + var options = me._resolveDataElementOptions(point, index); x = xScale.getPixelForValue(typeof value === 'object' ? value : NaN, index, datasetIndex); y = reset ? yScale.getBasePixel() : me.calculatePointY(value, index, datasetIndex); // Utility @@ -4988,139 +5951,74 @@ pointStyle: options.pointStyle, rotation: options.rotation, backgroundColor: options.backgroundColor, borderColor: options.borderColor, borderWidth: options.borderWidth, - tension: valueOrDefault$5(custom.tension, lineModel ? lineModel.tension : 0), + tension: valueOrDefault$6(custom.tension, lineModel ? lineModel.tension : 0), steppedLine: lineModel ? lineModel.steppedLine : false, // Tooltip hitRadius: options.hitRadius }; }, /** * @private */ - _resolvePointOptions: function(element, index) { + _resolveDatasetElementOptions: function(element) { var me = this; - var chart = me.chart; - var dataset = chart.data.datasets[me.index]; + var config = me._config; var custom = element.custom || {}; - var options = chart.options.elements.point; - var values = {}; - var i, ilen, key; + var options = me.chart.options; + var lineOptions = options.elements.line; + var values = core_datasetController.prototype._resolveDatasetElementOptions.apply(me, arguments); - // Scriptable options - var context = { - chart: chart, - dataIndex: index, - dataset: dataset, - datasetIndex: me.index - }; - - var ELEMENT_OPTIONS = { - backgroundColor: 'pointBackgroundColor', - borderColor: 'pointBorderColor', - borderWidth: 'pointBorderWidth', - hitRadius: 'pointHitRadius', - hoverBackgroundColor: 'pointHoverBackgroundColor', - hoverBorderColor: 'pointHoverBorderColor', - hoverBorderWidth: 'pointHoverBorderWidth', - hoverRadius: 'pointHoverRadius', - pointStyle: 'pointStyle', - radius: 'pointRadius', - rotation: 'pointRotation' - }; - var keys = Object.keys(ELEMENT_OPTIONS); - - for (i = 0, ilen = keys.length; i < ilen; ++i) { - key = keys[i]; - values[key] = resolve$4([ - custom[key], - dataset[ELEMENT_OPTIONS[key]], - dataset[key], - options[key] - ], context, index); - } - - return values; - }, - - /** - * @private - */ - _resolveLineOptions: function(element) { - var me = this; - var chart = me.chart; - var dataset = chart.data.datasets[me.index]; - var custom = element.custom || {}; - var options = chart.options; - var elementOptions = options.elements.line; - var values = {}; - var i, ilen, key; - - var keys = [ - 'backgroundColor', - 'borderWidth', - 'borderColor', - 'borderCapStyle', - 'borderDash', - 'borderDashOffset', - 'borderJoinStyle', - 'fill', - 'cubicInterpolationMode' - ]; - - for (i = 0, ilen = keys.length; i < ilen; ++i) { - key = keys[i]; - values[key] = resolve$4([ - custom[key], - dataset[key], - elementOptions[key] - ]); - } - // The default behavior of lines is to break at null values, according // to https://github.com/chartjs/Chart.js/issues/2435#issuecomment-216718158 // This option gives lines the ability to span gaps - values.spanGaps = valueOrDefault$5(dataset.spanGaps, options.spanGaps); - values.tension = valueOrDefault$5(dataset.lineTension, elementOptions.tension); - values.steppedLine = resolve$4([custom.steppedLine, dataset.steppedLine, elementOptions.stepped]); + values.spanGaps = valueOrDefault$6(config.spanGaps, options.spanGaps); + values.tension = valueOrDefault$6(config.lineTension, lineOptions.tension); + values.steppedLine = resolve$2([custom.steppedLine, config.steppedLine, lineOptions.stepped]); + values.clip = toClip(valueOrDefault$6(config.clip, defaultClip(me._xScale, me._yScale, values.borderWidth))); return values; }, calculatePointY: function(value, index, datasetIndex) { var me = this; var chart = me.chart; - var meta = me.getMeta(); - var yScale = me.getScaleForId(meta.yAxisID); + var yScale = me._yScale; var sumPos = 0; var sumNeg = 0; - var i, ds, dsMeta; + var i, ds, dsMeta, stackedRightValue, rightValue, metasets, ilen; if (yScale.options.stacked) { - for (i = 0; i < datasetIndex; i++) { - ds = chart.data.datasets[i]; - dsMeta = chart.getDatasetMeta(i); - if (dsMeta.type === 'line' && dsMeta.yAxisID === yScale.id && chart.isDatasetVisible(i)) { - var stackedRightValue = Number(yScale.getRightValue(ds.data[index])); + rightValue = +yScale.getRightValue(value); + metasets = chart._getSortedVisibleDatasetMetas(); + ilen = metasets.length; + + for (i = 0; i < ilen; ++i) { + dsMeta = metasets[i]; + if (dsMeta.index === datasetIndex) { + break; + } + + ds = chart.data.datasets[dsMeta.index]; + if (dsMeta.type === 'line' && dsMeta.yAxisID === yScale.id) { + stackedRightValue = +yScale.getRightValue(ds.data[index]); if (stackedRightValue < 0) { sumNeg += stackedRightValue || 0; } else { sumPos += stackedRightValue || 0; } } } - var rightValue = Number(yScale.getRightValue(value)); if (rightValue < 0) { return yScale.getPixelForValue(sumNeg + rightValue); } return yScale.getPixelForValue(sumPos + rightValue); } - return yScale.getPixelForValue(value); }, updateBezierControlPoints: function() { var me = this; @@ -5181,22 +6079,23 @@ var me = this; var chart = me.chart; var meta = me.getMeta(); var points = meta.data || []; var area = chart.chartArea; - var ilen = points.length; - var halfBorderWidth; + var canvas = chart.canvas; var i = 0; + var ilen = points.length; + var clip; - if (lineEnabled(me.getDataset(), chart.options)) { - halfBorderWidth = (meta.dataset._model.borderWidth || 0) / 2; + if (me._showLine) { + clip = meta.dataset._model.clip; helpers$1.canvas.clipArea(chart.ctx, { - left: area.left, - right: area.right, - top: area.top - halfBorderWidth, - bottom: area.bottom + halfBorderWidth + left: clip.left === false ? 0 : area.left - clip.left, + right: clip.right === false ? canvas.width : area.right + clip.right, + top: clip.top === false ? 0 : area.top - clip.top, + bottom: clip.bottom === false ? canvas.height : area.bottom + clip.bottom }); meta.dataset.draw(); helpers$1.canvas.unclipArea(chart.ctx); @@ -5221,18 +6120,18 @@ borderColor: model.borderColor, borderWidth: model.borderWidth, radius: model.radius }; - model.backgroundColor = valueOrDefault$5(options.hoverBackgroundColor, getHoverColor(options.backgroundColor)); - model.borderColor = valueOrDefault$5(options.hoverBorderColor, getHoverColor(options.borderColor)); - model.borderWidth = valueOrDefault$5(options.hoverBorderWidth, options.borderWidth); - model.radius = valueOrDefault$5(options.hoverRadius, options.radius); + model.backgroundColor = valueOrDefault$6(options.hoverBackgroundColor, getHoverColor(options.backgroundColor)); + model.borderColor = valueOrDefault$6(options.hoverBorderColor, getHoverColor(options.borderColor)); + model.borderWidth = valueOrDefault$6(options.hoverBorderWidth, options.borderWidth); + model.radius = valueOrDefault$6(options.hoverRadius, options.radius); }, }); -var resolve$5 = helpers$1.options.resolve; +var resolve$3 = helpers$1.options.resolve; core_defaults._set('polarArea', { scale: { type: 'radialLinear', angleLines: { @@ -5255,51 +6154,45 @@ animateScale: true }, startAngle: -0.5 * Math.PI, legendCallback: function(chart) { - var text = []; - text.push('<ul class="' + chart.id + '-legend">'); - + var list = document.createElement('ul'); var data = chart.data; var datasets = data.datasets; var labels = data.labels; + var i, ilen, listItem, listItemSpan; + list.setAttribute('class', chart.id + '-legend'); if (datasets.length) { - for (var i = 0; i < datasets[0].data.length; ++i) { - text.push('<li><span style="background-color:' + datasets[0].backgroundColor[i] + '"></span>'); + for (i = 0, ilen = datasets[0].data.length; i < ilen; ++i) { + listItem = list.appendChild(document.createElement('li')); + listItemSpan = listItem.appendChild(document.createElement('span')); + listItemSpan.style.backgroundColor = datasets[0].backgroundColor[i]; if (labels[i]) { - text.push(labels[i]); + listItem.appendChild(document.createTextNode(labels[i])); } - text.push('</li>'); } } - text.push('</ul>'); - return text.join(''); + return list.outerHTML; }, legend: { labels: { generateLabels: function(chart) { var data = chart.data; if (data.labels.length && data.datasets.length) { return data.labels.map(function(label, i) { var meta = chart.getDatasetMeta(0); - var ds = data.datasets[0]; - var arc = meta.data[i]; - var custom = arc.custom || {}; - var arcOpts = chart.options.elements.arc; - var fill = resolve$5([custom.backgroundColor, ds.backgroundColor, arcOpts.backgroundColor], undefined, i); - var stroke = resolve$5([custom.borderColor, ds.borderColor, arcOpts.borderColor], undefined, i); - var bw = resolve$5([custom.borderWidth, ds.borderWidth, arcOpts.borderWidth], undefined, i); + var style = meta.controller.getStyle(i); return { text: label, - fillStyle: fill, - strokeStyle: stroke, - lineWidth: bw, - hidden: isNaN(ds.data[i]) || meta.data[i].hidden, + fillStyle: style.backgroundColor, + strokeStyle: style.borderColor, + lineWidth: style.borderWidth, + hidden: isNaN(data.datasets[0].data[i]) || meta.data[i].hidden, // Extra data used for toggling the correct item index: i }; }); @@ -5339,10 +6232,37 @@ dataElementType: elements.Arc, linkScales: helpers$1.noop, + /** + * @private + */ + _dataElementOptions: [ + 'backgroundColor', + 'borderColor', + 'borderWidth', + 'borderAlign', + 'hoverBackgroundColor', + 'hoverBorderColor', + 'hoverBorderWidth', + ], + + /** + * @private + */ + _getIndexScaleId: function() { + return this.chart.scale.id; + }, + + /** + * @private + */ + _getValueScaleId: function() { + return this.chart.scale.id; + }, + update: function(reset) { var me = this; var dataset = me.getDataset(); var meta = me.getMeta(); var start = me.chart.options.startAngle || 0; @@ -5361,11 +6281,11 @@ angles[i] = angle; start += angle; } for (i = 0, ilen = arcs.length; i < ilen; ++i) { - arcs[i]._options = me._resolveElementOptions(arcs[i], i); + arcs[i]._options = me._resolveDataElementOptions(arcs[i], i); me.updateElement(arcs[i], i, reset); } }, /** @@ -5467,52 +6387,10 @@ }, /** * @private */ - _resolveElementOptions: function(arc, index) { - var me = this; - var chart = me.chart; - var dataset = me.getDataset(); - var custom = arc.custom || {}; - var options = chart.options.elements.arc; - var values = {}; - var i, ilen, key; - - // Scriptable options - var context = { - chart: chart, - dataIndex: index, - dataset: dataset, - datasetIndex: me.index - }; - - var keys = [ - 'backgroundColor', - 'borderColor', - 'borderWidth', - 'borderAlign', - 'hoverBackgroundColor', - 'hoverBorderColor', - 'hoverBorderWidth', - ]; - - for (i = 0, ilen = keys.length; i < ilen; ++i) { - key = keys[i]; - values[key] = resolve$5([ - custom[key], - dataset[key], - options[key] - ], context, index); - } - - return values; - }, - - /** - * @private - */ _computeAngle: function(index) { var me = this; var count = this.getMeta().count; var dataset = me.getDataset(); var meta = me.getMeta(); @@ -5527,11 +6405,11 @@ dataIndex: index, dataset: dataset, datasetIndex: me.index }; - return resolve$5([ + return resolve$3([ me.chart.options.elements.arc.angle, (2 * Math.PI) / count ], context, index); } }); @@ -5542,54 +6420,99 @@ }); // Pie charts are Doughnut chart with different defaults var controller_pie = controller_doughnut; -var valueOrDefault$6 = helpers$1.valueOrDefault; -var resolve$6 = helpers$1.options.resolve; +var valueOrDefault$7 = helpers$1.valueOrDefault; core_defaults._set('radar', { + spanGaps: false, scale: { type: 'radialLinear' }, elements: { line: { + fill: 'start', tension: 0 // no bezier in radar } } }); var controller_radar = core_datasetController.extend({ - datasetElementType: elements.Line, dataElementType: elements.Point, linkScales: helpers$1.noop, + /** + * @private + */ + _datasetElementOptions: [ + 'backgroundColor', + 'borderWidth', + 'borderColor', + 'borderCapStyle', + 'borderDash', + 'borderDashOffset', + 'borderJoinStyle', + 'fill' + ], + + /** + * @private + */ + _dataElementOptions: { + backgroundColor: 'pointBackgroundColor', + borderColor: 'pointBorderColor', + borderWidth: 'pointBorderWidth', + hitRadius: 'pointHitRadius', + hoverBackgroundColor: 'pointHoverBackgroundColor', + hoverBorderColor: 'pointHoverBorderColor', + hoverBorderWidth: 'pointHoverBorderWidth', + hoverRadius: 'pointHoverRadius', + pointStyle: 'pointStyle', + radius: 'pointRadius', + rotation: 'pointRotation' + }, + + /** + * @private + */ + _getIndexScaleId: function() { + return this.chart.scale.id; + }, + + /** + * @private + */ + _getValueScaleId: function() { + return this.chart.scale.id; + }, + update: function(reset) { var me = this; var meta = me.getMeta(); var line = meta.dataset; var points = meta.data || []; var scale = me.chart.scale; - var dataset = me.getDataset(); + var config = me._config; var i, ilen; // Compatibility: If the properties are defined with only the old name, use those values - if ((dataset.tension !== undefined) && (dataset.lineTension === undefined)) { - dataset.lineTension = dataset.tension; + if (config.tension !== undefined && config.lineTension === undefined) { + config.lineTension = config.tension; } // Utility line._scale = scale; line._datasetIndex = me.index; // Data line._children = points; line._loop = true; // Model - line._model = me._resolveLineOptions(line); + line._model = me._resolveDatasetElementOptions(line); line.pivot(); // Update Points for (i = 0, ilen = points.length; i < ilen; ++i) { @@ -5609,11 +6532,11 @@ var me = this; var custom = point.custom || {}; var dataset = me.getDataset(); var scale = me.chart.scale; var pointPosition = scale.getPointPositionForValue(index, dataset.data[index]); - var options = me._resolvePointOptions(point, index); + var options = me._resolveDataElementOptions(point, index); var lineModel = me.getMeta().dataset._model; var x = reset ? scale.xCenter : pointPosition.x; var y = reset ? scale.yCenter : pointPosition.y; // Utility @@ -5632,109 +6555,46 @@ pointStyle: options.pointStyle, rotation: options.rotation, backgroundColor: options.backgroundColor, borderColor: options.borderColor, borderWidth: options.borderWidth, - tension: valueOrDefault$6(custom.tension, lineModel ? lineModel.tension : 0), + tension: valueOrDefault$7(custom.tension, lineModel ? lineModel.tension : 0), // Tooltip hitRadius: options.hitRadius }; }, /** * @private */ - _resolvePointOptions: function(element, index) { + _resolveDatasetElementOptions: function() { var me = this; - var chart = me.chart; - var dataset = chart.data.datasets[me.index]; - var custom = element.custom || {}; - var options = chart.options.elements.point; - var values = {}; - var i, ilen, key; + var config = me._config; + var options = me.chart.options; + var values = core_datasetController.prototype._resolveDatasetElementOptions.apply(me, arguments); - // Scriptable options - var context = { - chart: chart, - dataIndex: index, - dataset: dataset, - datasetIndex: me.index - }; + values.spanGaps = valueOrDefault$7(config.spanGaps, options.spanGaps); + values.tension = valueOrDefault$7(config.lineTension, options.elements.line.tension); - var ELEMENT_OPTIONS = { - backgroundColor: 'pointBackgroundColor', - borderColor: 'pointBorderColor', - borderWidth: 'pointBorderWidth', - hitRadius: 'pointHitRadius', - hoverBackgroundColor: 'pointHoverBackgroundColor', - hoverBorderColor: 'pointHoverBorderColor', - hoverBorderWidth: 'pointHoverBorderWidth', - hoverRadius: 'pointHoverRadius', - pointStyle: 'pointStyle', - radius: 'pointRadius', - rotation: 'pointRotation' - }; - var keys = Object.keys(ELEMENT_OPTIONS); - - for (i = 0, ilen = keys.length; i < ilen; ++i) { - key = keys[i]; - values[key] = resolve$6([ - custom[key], - dataset[ELEMENT_OPTIONS[key]], - dataset[key], - options[key] - ], context, index); - } - return values; }, - /** - * @private - */ - _resolveLineOptions: function(element) { - var me = this; - var chart = me.chart; - var dataset = chart.data.datasets[me.index]; - var custom = element.custom || {}; - var options = chart.options.elements.line; - var values = {}; - var i, ilen, key; - - var keys = [ - 'backgroundColor', - 'borderWidth', - 'borderColor', - 'borderCapStyle', - 'borderDash', - 'borderDashOffset', - 'borderJoinStyle', - 'fill' - ]; - - for (i = 0, ilen = keys.length; i < ilen; ++i) { - key = keys[i]; - values[key] = resolve$6([ - custom[key], - dataset[key], - options[key] - ]); - } - - values.tension = valueOrDefault$6(dataset.lineTension, options.tension); - - return values; - }, - updateBezierControlPoints: function() { var me = this; var meta = me.getMeta(); var area = me.chart.chartArea; var points = meta.data || []; var i, ilen, model, controlPoints; + // Only consider points that are drawn in case the spanGaps option is used + if (meta.dataset._model.spanGaps) { + points = points.filter(function(pt) { + return !pt._model.skip; + }); + } + function capControlPoint(pt, min, max) { return Math.max(Math.min(pt, max), min); } for (i = 0, ilen = points.length; i < ilen; ++i) { @@ -5764,14 +6624,14 @@ borderColor: model.borderColor, borderWidth: model.borderWidth, radius: model.radius }; - model.backgroundColor = valueOrDefault$6(options.hoverBackgroundColor, getHoverColor(options.backgroundColor)); - model.borderColor = valueOrDefault$6(options.hoverBorderColor, getHoverColor(options.borderColor)); - model.borderWidth = valueOrDefault$6(options.hoverBorderWidth, options.borderWidth); - model.radius = valueOrDefault$6(options.hoverRadius, options.radius); + model.backgroundColor = valueOrDefault$7(options.hoverBackgroundColor, getHoverColor(options.backgroundColor)); + model.borderColor = valueOrDefault$7(options.hoverBorderColor, getHoverColor(options.borderColor)); + model.borderWidth = valueOrDefault$7(options.hoverBorderWidth, options.borderWidth); + model.radius = valueOrDefault$7(options.hoverRadius, options.radius); } }); core_defaults._set('scatter', { hover: { @@ -5789,12 +6649,10 @@ type: 'linear', position: 'left' }] }, - showLines: false, - tooltips: { callbacks: { title: function() { return ''; // doesn't make sense for scatter since data are formatted as a point }, @@ -5803,10 +6661,18 @@ } } } }); +core_defaults._set('global', { + datasets: { + scatter: { + showLine: false + } + } +}); + // Scatter charts use line controllers var controller_scatter = controller_line; // NOTE export a map in which the key represents the controller type, not // the class, and so must be CamelCase in order to be correctly retrieved @@ -5845,21 +6711,17 @@ * Helper function to traverse all of the visible elements in the chart * @param {Chart} chart - the chart * @param {function} handler - the callback to execute for each visible item */ function parseVisibleItems(chart, handler) { - var datasets = chart.data.datasets; - var meta, i, j, ilen, jlen; + var metasets = chart._getSortedVisibleDatasetMetas(); + var metadata, i, j, ilen, jlen, element; - for (i = 0, ilen = datasets.length; i < ilen; ++i) { - if (!chart.isDatasetVisible(i)) { - continue; - } - - meta = chart.getDatasetMeta(i); - for (j = 0, jlen = meta.data.length; j < jlen; ++j) { - var element = meta.data[j]; + for (i = 0, ilen = metasets.length; i < ilen; ++i) { + metadata = metasets[i].data; + for (j = 0, jlen = metadata.length; j < jlen; ++j) { + element = metadata[j]; if (!element._view.skip) { handler(element); } } } @@ -5940,19 +6802,16 @@ if (!items.length) { return []; } - chart.data.datasets.forEach(function(dataset, datasetIndex) { - if (chart.isDatasetVisible(datasetIndex)) { - var meta = chart.getDatasetMeta(datasetIndex); - var element = meta.data[items[0]._index]; + chart._getSortedVisibleDatasetMetas().forEach(function(meta) { + var element = meta.data[items[0]._index]; - // don't count items that are skipped (null data) - if (element && !element._view.skip) { - elements.push(element); - } + // don't count items that are skipped (null data) + if (element && !element._view.skip) { + elements.push(element); } }); return elements; } @@ -6129,61 +6988,199 @@ return items; } } }; +var extend = helpers$1.extend; + function filterByPosition(array, position) { return helpers$1.where(array, function(v) { - return v.position === position; + return v.pos === position; }); } function sortByWeight(array, reverse) { - array.forEach(function(v, i) { - v._tmpIndex_ = i; - return v; - }); - array.sort(function(a, b) { + return array.sort(function(a, b) { var v0 = reverse ? b : a; var v1 = reverse ? a : b; return v0.weight === v1.weight ? - v0._tmpIndex_ - v1._tmpIndex_ : + v0.index - v1.index : v0.weight - v1.weight; }); - array.forEach(function(v) { - delete v._tmpIndex_; - }); } -function findMaxPadding(boxes) { - var top = 0; - var left = 0; - var bottom = 0; - var right = 0; - helpers$1.each(boxes, function(box) { - if (box.getPadding) { - var boxPadding = box.getPadding(); - top = Math.max(top, boxPadding.top); - left = Math.max(left, boxPadding.left); - bottom = Math.max(bottom, boxPadding.bottom); - right = Math.max(right, boxPadding.right); - } - }); +function wrapBoxes(boxes) { + var layoutBoxes = []; + var i, ilen, box; + + for (i = 0, ilen = (boxes || []).length; i < ilen; ++i) { + box = boxes[i]; + layoutBoxes.push({ + index: i, + box: box, + pos: box.position, + horizontal: box.isHorizontal(), + weight: box.weight + }); + } + return layoutBoxes; +} + +function setLayoutDims(layouts, params) { + var i, ilen, layout; + for (i = 0, ilen = layouts.length; i < ilen; ++i) { + layout = layouts[i]; + // store width used instead of chartArea.w in fitBoxes + layout.width = layout.horizontal + ? layout.box.fullWidth && params.availableWidth + : params.vBoxMaxWidth; + // store height used instead of chartArea.h in fitBoxes + layout.height = layout.horizontal && params.hBoxMaxHeight; + } +} + +function buildLayoutBoxes(boxes) { + var layoutBoxes = wrapBoxes(boxes); + var left = sortByWeight(filterByPosition(layoutBoxes, 'left'), true); + var right = sortByWeight(filterByPosition(layoutBoxes, 'right')); + var top = sortByWeight(filterByPosition(layoutBoxes, 'top'), true); + var bottom = sortByWeight(filterByPosition(layoutBoxes, 'bottom')); + return { - top: top, - left: left, - bottom: bottom, - right: right + leftAndTop: left.concat(top), + rightAndBottom: right.concat(bottom), + chartArea: filterByPosition(layoutBoxes, 'chartArea'), + vertical: left.concat(right), + horizontal: top.concat(bottom) }; } -function addSizeByPosition(boxes, size) { - helpers$1.each(boxes, function(box) { - size[box.position] += box.isHorizontal() ? box.height : box.width; - }); +function getCombinedMax(maxPadding, chartArea, a, b) { + return Math.max(maxPadding[a], chartArea[a]) + Math.max(maxPadding[b], chartArea[b]); } +function updateDims(chartArea, params, layout) { + var box = layout.box; + var maxPadding = chartArea.maxPadding; + var newWidth, newHeight; + + if (layout.size) { + // this layout was already counted for, lets first reduce old size + chartArea[layout.pos] -= layout.size; + } + layout.size = layout.horizontal ? box.height : box.width; + chartArea[layout.pos] += layout.size; + + if (box.getPadding) { + var boxPadding = box.getPadding(); + maxPadding.top = Math.max(maxPadding.top, boxPadding.top); + maxPadding.left = Math.max(maxPadding.left, boxPadding.left); + maxPadding.bottom = Math.max(maxPadding.bottom, boxPadding.bottom); + maxPadding.right = Math.max(maxPadding.right, boxPadding.right); + } + + newWidth = params.outerWidth - getCombinedMax(maxPadding, chartArea, 'left', 'right'); + newHeight = params.outerHeight - getCombinedMax(maxPadding, chartArea, 'top', 'bottom'); + + if (newWidth !== chartArea.w || newHeight !== chartArea.h) { + chartArea.w = newWidth; + chartArea.h = newHeight; + + // return true if chart area changed in layout's direction + return layout.horizontal ? newWidth !== chartArea.w : newHeight !== chartArea.h; + } +} + +function handleMaxPadding(chartArea) { + var maxPadding = chartArea.maxPadding; + + function updatePos(pos) { + var change = Math.max(maxPadding[pos] - chartArea[pos], 0); + chartArea[pos] += change; + return change; + } + chartArea.y += updatePos('top'); + chartArea.x += updatePos('left'); + updatePos('right'); + updatePos('bottom'); +} + +function getMargins(horizontal, chartArea) { + var maxPadding = chartArea.maxPadding; + + function marginForPositions(positions) { + var margin = {left: 0, top: 0, right: 0, bottom: 0}; + positions.forEach(function(pos) { + margin[pos] = Math.max(chartArea[pos], maxPadding[pos]); + }); + return margin; + } + + return horizontal + ? marginForPositions(['left', 'right']) + : marginForPositions(['top', 'bottom']); +} + +function fitBoxes(boxes, chartArea, params) { + var refitBoxes = []; + var i, ilen, layout, box, refit, changed; + + for (i = 0, ilen = boxes.length; i < ilen; ++i) { + layout = boxes[i]; + box = layout.box; + + box.update( + layout.width || chartArea.w, + layout.height || chartArea.h, + getMargins(layout.horizontal, chartArea) + ); + if (updateDims(chartArea, params, layout)) { + changed = true; + if (refitBoxes.length) { + // Dimensions changed and there were non full width boxes before this + // -> we have to refit those + refit = true; + } + } + if (!box.fullWidth) { // fullWidth boxes don't need to be re-fitted in any case + refitBoxes.push(layout); + } + } + + return refit ? fitBoxes(refitBoxes, chartArea, params) || changed : changed; +} + +function placeBoxes(boxes, chartArea, params) { + var userPadding = params.padding; + var x = chartArea.x; + var y = chartArea.y; + var i, ilen, layout, box; + + for (i = 0, ilen = boxes.length; i < ilen; ++i) { + layout = boxes[i]; + box = layout.box; + if (layout.horizontal) { + box.left = box.fullWidth ? userPadding.left : chartArea.left; + box.right = box.fullWidth ? params.outerWidth - userPadding.right : chartArea.left + chartArea.w; + box.top = y; + box.bottom = y + box.height; + box.width = box.right - box.left; + y = box.bottom; + } else { + box.left = x; + box.right = x + box.width; + box.top = chartArea.top; + box.bottom = chartArea.top + chartArea.h; + box.height = box.bottom - box.top; + x = box.right; + } + } + + chartArea.x = x; + chartArea.y = y; +} + core_defaults._set('global', { layout: { padding: { top: 0, right: 0, @@ -6229,10 +7226,18 @@ // initialize item with default values item.fullWidth = item.fullWidth || false; item.position = item.position || 'top'; item.weight = item.weight || 0; + item._layers = item._layers || function() { + return [{ + z: 0, + draw: function() { + item.draw.apply(item, arguments); + } + }]; + }; chart.boxes.push(item); }, /** @@ -6279,31 +7284,17 @@ return; } var layoutOptions = chart.options.layout || {}; var padding = helpers$1.options.toPadding(layoutOptions.padding); - var leftPadding = padding.left; - var rightPadding = padding.right; - var topPadding = padding.top; - var bottomPadding = padding.bottom; - var leftBoxes = filterByPosition(chart.boxes, 'left'); - var rightBoxes = filterByPosition(chart.boxes, 'right'); - var topBoxes = filterByPosition(chart.boxes, 'top'); - var bottomBoxes = filterByPosition(chart.boxes, 'bottom'); - var chartAreaBoxes = filterByPosition(chart.boxes, 'chartArea'); + var availableWidth = width - padding.width; + var availableHeight = height - padding.height; + var boxes = buildLayoutBoxes(chart.boxes); + var verticalBoxes = boxes.vertical; + var horizontalBoxes = boxes.horizontal; - // Sort boxes by weight. A higher weight is further away from the chart area - sortByWeight(leftBoxes, true); - sortByWeight(rightBoxes, false); - sortByWeight(topBoxes, true); - sortByWeight(bottomBoxes, false); - - var verticalBoxes = leftBoxes.concat(rightBoxes); - var horizontalBoxes = topBoxes.concat(bottomBoxes); - var outerBoxes = verticalBoxes.concat(horizontalBoxes); - // Essentially we now have any number of boxes on each of the 4 sides. // Our canvas looks like the following. // The areas L1 and L2 are the left axes. R1 is the right axis, T1 is the top axis and // B1 is the bottom axis // There are also 4 quadrant-like locations (left to right instead of clockwise) reserved for chart overlays @@ -6326,205 +7317,61 @@ // | | | B1 | | // |----------------------------------------------------| // | B2 (Full Width) | // |----------------------------------------------------| // - // What we do to find the best sizing, we do the following - // 1. Determine the minimum size of the chart area. - // 2. Split the remaining width equally between each vertical axis - // 3. Split the remaining height equally between each horizontal axis - // 4. Give each layout the maximum size it can be. The layout will return it's minimum size - // 5. Adjust the sizes of each axis based on it's minimum reported size. - // 6. Refit each axis - // 7. Position each axis in the final location - // 8. Tell the chart the final location of the chart area - // 9. Tell any axes that overlay the chart area the positions of the chart area - // Step 1 - var chartWidth = width - leftPadding - rightPadding; - var chartHeight = height - topPadding - bottomPadding; - var chartAreaWidth = chartWidth / 2; // min 50% + var params = Object.freeze({ + outerWidth: width, + outerHeight: height, + padding: padding, + availableWidth: availableWidth, + vBoxMaxWidth: availableWidth / 2 / verticalBoxes.length, + hBoxMaxHeight: availableHeight / 2 + }); + var chartArea = extend({ + maxPadding: extend({}, padding), + w: availableWidth, + h: availableHeight, + x: padding.left, + y: padding.top + }, padding); - // Step 2 - var verticalBoxWidth = (width - chartAreaWidth) / verticalBoxes.length; + setLayoutDims(verticalBoxes.concat(horizontalBoxes), params); - // Step 3 - // TODO re-limit horizontal axis height (this limit has affected only padding calculation since PR 1837) - // var horizontalBoxHeight = (height - chartAreaHeight) / horizontalBoxes.length; + // First fit vertical boxes + fitBoxes(verticalBoxes, chartArea, params); - // Step 4 - var maxChartAreaWidth = chartWidth; - var maxChartAreaHeight = chartHeight; - var outerBoxSizes = {top: topPadding, left: leftPadding, bottom: bottomPadding, right: rightPadding}; - var minBoxSizes = []; - var maxPadding; - - function getMinimumBoxSize(box) { - var minSize; - var isHorizontal = box.isHorizontal(); - - if (isHorizontal) { - minSize = box.update(box.fullWidth ? chartWidth : maxChartAreaWidth, chartHeight / 2); - maxChartAreaHeight -= minSize.height; - } else { - minSize = box.update(verticalBoxWidth, maxChartAreaHeight); - maxChartAreaWidth -= minSize.width; - } - - minBoxSizes.push({ - horizontal: isHorizontal, - width: minSize.width, - box: box, - }); + // Then fit horizontal boxes + if (fitBoxes(horizontalBoxes, chartArea, params)) { + // if the area changed, re-fit vertical boxes + fitBoxes(verticalBoxes, chartArea, params); } - helpers$1.each(outerBoxes, getMinimumBoxSize); + handleMaxPadding(chartArea); - // If a horizontal box has padding, we move the left boxes over to avoid ugly charts (see issue #2478) - maxPadding = findMaxPadding(outerBoxes); + // Finally place the boxes to correct coordinates + placeBoxes(boxes.leftAndTop, chartArea, params); - // At this point, maxChartAreaHeight and maxChartAreaWidth are the size the chart area could - // be if the axes are drawn at their minimum sizes. - // Steps 5 & 6 + // Move to opposite side of chart + chartArea.x += chartArea.w; + chartArea.y += chartArea.h; - // Function to fit a box - function fitBox(box) { - var minBoxSize = helpers$1.findNextWhere(minBoxSizes, function(minBox) { - return minBox.box === box; - }); + placeBoxes(boxes.rightAndBottom, chartArea, params); - if (minBoxSize) { - if (minBoxSize.horizontal) { - var scaleMargin = { - left: Math.max(outerBoxSizes.left, maxPadding.left), - right: Math.max(outerBoxSizes.right, maxPadding.right), - top: 0, - bottom: 0 - }; - - // Don't use min size here because of label rotation. When the labels are rotated, their rotation highly depends - // on the margin. Sometimes they need to increase in size slightly - box.update(box.fullWidth ? chartWidth : maxChartAreaWidth, chartHeight / 2, scaleMargin); - } else { - box.update(minBoxSize.width, maxChartAreaHeight); - } - } - } - - // Update, and calculate the left and right margins for the horizontal boxes - helpers$1.each(verticalBoxes, fitBox); - addSizeByPosition(verticalBoxes, outerBoxSizes); - - // Set the Left and Right margins for the horizontal boxes - helpers$1.each(horizontalBoxes, fitBox); - addSizeByPosition(horizontalBoxes, outerBoxSizes); - - function finalFitVerticalBox(box) { - var minBoxSize = helpers$1.findNextWhere(minBoxSizes, function(minSize) { - return minSize.box === box; - }); - - var scaleMargin = { - left: 0, - right: 0, - top: outerBoxSizes.top, - bottom: outerBoxSizes.bottom - }; - - if (minBoxSize) { - box.update(minBoxSize.width, maxChartAreaHeight, scaleMargin); - } - } - - // Let the left layout know the final margin - helpers$1.each(verticalBoxes, finalFitVerticalBox); - - // Recalculate because the size of each layout might have changed slightly due to the margins (label rotation for instance) - outerBoxSizes = {top: topPadding, left: leftPadding, bottom: bottomPadding, right: rightPadding}; - addSizeByPosition(outerBoxes, outerBoxSizes); - - // We may be adding some padding to account for rotated x axis labels - var leftPaddingAddition = Math.max(maxPadding.left - outerBoxSizes.left, 0); - outerBoxSizes.left += leftPaddingAddition; - outerBoxSizes.right += Math.max(maxPadding.right - outerBoxSizes.right, 0); - - var topPaddingAddition = Math.max(maxPadding.top - outerBoxSizes.top, 0); - outerBoxSizes.top += topPaddingAddition; - outerBoxSizes.bottom += Math.max(maxPadding.bottom - outerBoxSizes.bottom, 0); - - // Figure out if our chart area changed. This would occur if the dataset layout label rotation - // changed due to the application of the margins in step 6. Since we can only get bigger, this is safe to do - // without calling `fit` again - var newMaxChartAreaHeight = height - outerBoxSizes.top - outerBoxSizes.bottom; - var newMaxChartAreaWidth = width - outerBoxSizes.left - outerBoxSizes.right; - - if (newMaxChartAreaWidth !== maxChartAreaWidth || newMaxChartAreaHeight !== maxChartAreaHeight) { - helpers$1.each(verticalBoxes, function(box) { - box.height = newMaxChartAreaHeight; - }); - - helpers$1.each(horizontalBoxes, function(box) { - if (!box.fullWidth) { - box.width = newMaxChartAreaWidth; - } - }); - - maxChartAreaHeight = newMaxChartAreaHeight; - maxChartAreaWidth = newMaxChartAreaWidth; - } - - // Step 7 - Position the boxes - var left = leftPadding + leftPaddingAddition; - var top = topPadding + topPaddingAddition; - - function placeBox(box) { - if (box.isHorizontal()) { - box.left = box.fullWidth ? leftPadding : outerBoxSizes.left; - box.right = box.fullWidth ? width - rightPadding : outerBoxSizes.left + maxChartAreaWidth; - box.top = top; - box.bottom = top + box.height; - - // Move to next point - top = box.bottom; - - } else { - - box.left = left; - box.right = left + box.width; - box.top = outerBoxSizes.top; - box.bottom = outerBoxSizes.top + maxChartAreaHeight; - - // Move to next point - left = box.right; - } - } - - helpers$1.each(leftBoxes.concat(topBoxes), placeBox); - - // Account for chart width and height - left += maxChartAreaWidth; - top += maxChartAreaHeight; - - helpers$1.each(rightBoxes, placeBox); - helpers$1.each(bottomBoxes, placeBox); - - // Step 8 chart.chartArea = { - left: outerBoxSizes.left, - top: outerBoxSizes.top, - right: outerBoxSizes.left + maxChartAreaWidth, - bottom: outerBoxSizes.top + maxChartAreaHeight + left: chartArea.left, + top: chartArea.top, + right: chartArea.left + chartArea.w, + bottom: chartArea.top + chartArea.h }; - // Step 9 - helpers$1.each(chartAreaBoxes, function(box) { - box.left = chart.chartArea.left; - box.top = chart.chartArea.top; - box.right = chart.chartArea.right; - box.bottom = chart.chartArea.bottom; - - box.update(maxChartAreaWidth, maxChartAreaHeight); + // Finally update boxes in chartArea (radial scale for example) + helpers$1.each(boxes.chartArea, function(layout) { + var box = layout.box; + extend(box, chart.chartArea); + box.update(chartArea.w, chartArea.h); }); } }; /** @@ -6544,27 +7391,14 @@ }; var platform_dom = "/*\n * DOM element rendering detection\n * https://davidwalsh.name/detect-node-insertion\n */\n@keyframes chartjs-render-animation {\n\tfrom { opacity: 0.99; }\n\tto { opacity: 1; }\n}\n\n.chartjs-render-monitor {\n\tanimation: chartjs-render-animation 0.001s;\n}\n\n/*\n * DOM element resizing detection\n * https://github.com/marcj/css-element-queries\n */\n.chartjs-size-monitor,\n.chartjs-size-monitor-expand,\n.chartjs-size-monitor-shrink {\n\tposition: absolute;\n\tdirection: ltr;\n\tleft: 0;\n\ttop: 0;\n\tright: 0;\n\tbottom: 0;\n\toverflow: hidden;\n\tpointer-events: none;\n\tvisibility: hidden;\n\tz-index: -1;\n}\n\n.chartjs-size-monitor-expand > div {\n\tposition: absolute;\n\twidth: 1000000px;\n\theight: 1000000px;\n\tleft: 0;\n\ttop: 0;\n}\n\n.chartjs-size-monitor-shrink > div {\n\tposition: absolute;\n\twidth: 200%;\n\theight: 200%;\n\tleft: 0;\n\ttop: 0;\n}\n"; var platform_dom$1 = /*#__PURE__*/Object.freeze({ -default: platform_dom +__proto__: null, +'default': platform_dom }); -var commonjsGlobal = typeof window !== 'undefined' ? window : typeof global !== 'undefined' ? global : typeof self !== 'undefined' ? self : {}; - -function commonjsRequire () { - throw new Error('Dynamic requires are not currently supported by rollup-plugin-commonjs'); -} - -function createCommonjsModule(fn, module) { - return module = { exports: {} }, fn(module, module.exports), module.exports; -} - -function getCjsExportFromNamespace (n) { - return n && n.default || n; -} - var stylesheet = getCjsExportFromNamespace(platform_dom$1); var EXPANDO_KEY = '$chartjs'; var CSS_PREFIX = 'chartjs-'; var CSS_SIZE_MONITOR = CSS_PREFIX + 'size-monitor'; @@ -6850,21 +7684,26 @@ if (resizer && resizer.parentNode) { resizer.parentNode.removeChild(resizer); } } -function injectCSS(platform, css) { +/** + * Injects CSS styles inline if the styles are not already present. + * @param {HTMLDocument|ShadowRoot} rootNode - the node to contain the <style>. + * @param {string} css - the CSS to be injected. + */ +function injectCSS(rootNode, css) { // https://stackoverflow.com/q/3922139 - var style = platform._style || document.createElement('style'); - if (!platform._style) { - platform._style = style; + var expando = rootNode[EXPANDO_KEY] || (rootNode[EXPANDO_KEY] = {}); + if (!expando.containsStyles) { + expando.containsStyles = true; css = '/* Chart.js */\n' + css; + var style = document.createElement('style'); style.setAttribute('type', 'text/css'); - document.getElementsByTagName('head')[0].appendChild(style); + style.appendChild(document.createTextNode(css)); + rootNode.appendChild(style); } - - style.appendChild(document.createTextNode(css)); } var platform_dom$2 = { /** * When `true`, prevents the automatic injection of the stylesheet required to @@ -6881,22 +7720,22 @@ * @private */ _enabled: typeof window !== 'undefined' && typeof document !== 'undefined', /** + * Initializes resources that depend on platform options. + * @param {HTMLCanvasElement} canvas - The Canvas element. * @private */ - _ensureLoaded: function() { - if (this._loaded) { - return; - } - - this._loaded = true; - - // https://github.com/chartjs/Chart.js/issues/5208 + _ensureLoaded: function(canvas) { if (!this.disableCSSInjection) { - injectCSS(this, stylesheet); + // If the canvas is in a shadow DOM, then the styles must also be inserted + // into the same shadow DOM. + // https://github.com/chartjs/Chart.js/issues/5763 + var root = canvas.getRootNode ? canvas.getRootNode() : document; + var targetNode = root.host ? root : document.head; + injectCSS(targetNode, stylesheet); } }, acquireContext: function(item, config) { if (typeof item === 'string') { @@ -6914,22 +7753,21 @@ // To prevent canvas fingerprinting, some add-ons undefine the getContext // method, for example: https://github.com/kkapsner/CanvasBlocker // https://github.com/chartjs/Chart.js/issues/2807 var context = item && item.getContext && item.getContext('2d'); - // Load platform resources on first chart creation, to make possible to change - // platform options after importing the library (e.g. `disableCSSInjection`). - this._ensureLoaded(); - // `instanceof HTMLCanvasElement/CanvasRenderingContext2D` fails when the item is // inside an iframe or when running in a protected environment. We could guess the // types from their toString() value but let's keep things flexible and assume it's // a sufficient condition if the item has a context2D which has item as `canvas`. // https://github.com/chartjs/Chart.js/issues/3887 // https://github.com/chartjs/Chart.js/issues/4102 // https://github.com/chartjs/Chart.js/issues/4152 if (context && context.canvas === item) { + // Load platform resources on first chart creation, to make it possible to + // import the library before setting platform options. + this._ensureLoaded(item); initCanvas(item, config); return context; } return null; @@ -7278,11 +8116,12 @@ core_layouts.addBox(chart, scale); }); } }; -var valueOrDefault$7 = helpers$1.valueOrDefault; +var valueOrDefault$8 = helpers$1.valueOrDefault; +var getRtlHelper = helpers$1.rtl.getRtlAdapter; core_defaults._set('global', { tooltips: { enabled: true, custom: null, @@ -7516,32 +8355,36 @@ xPadding: tooltipOpts.xPadding, yPadding: tooltipOpts.yPadding, xAlign: tooltipOpts.xAlign, yAlign: tooltipOpts.yAlign, + // Drawing direction and text direction + rtl: tooltipOpts.rtl, + textDirection: tooltipOpts.textDirection, + // Body bodyFontColor: tooltipOpts.bodyFontColor, - _bodyFontFamily: valueOrDefault$7(tooltipOpts.bodyFontFamily, globalDefaults.defaultFontFamily), - _bodyFontStyle: valueOrDefault$7(tooltipOpts.bodyFontStyle, globalDefaults.defaultFontStyle), + _bodyFontFamily: valueOrDefault$8(tooltipOpts.bodyFontFamily, globalDefaults.defaultFontFamily), + _bodyFontStyle: valueOrDefault$8(tooltipOpts.bodyFontStyle, globalDefaults.defaultFontStyle), _bodyAlign: tooltipOpts.bodyAlign, - bodyFontSize: valueOrDefault$7(tooltipOpts.bodyFontSize, globalDefaults.defaultFontSize), + bodyFontSize: valueOrDefault$8(tooltipOpts.bodyFontSize, globalDefaults.defaultFontSize), bodySpacing: tooltipOpts.bodySpacing, // Title titleFontColor: tooltipOpts.titleFontColor, - _titleFontFamily: valueOrDefault$7(tooltipOpts.titleFontFamily, globalDefaults.defaultFontFamily), - _titleFontStyle: valueOrDefault$7(tooltipOpts.titleFontStyle, globalDefaults.defaultFontStyle), - titleFontSize: valueOrDefault$7(tooltipOpts.titleFontSize, globalDefaults.defaultFontSize), + _titleFontFamily: valueOrDefault$8(tooltipOpts.titleFontFamily, globalDefaults.defaultFontFamily), + _titleFontStyle: valueOrDefault$8(tooltipOpts.titleFontStyle, globalDefaults.defaultFontStyle), + titleFontSize: valueOrDefault$8(tooltipOpts.titleFontSize, globalDefaults.defaultFontSize), _titleAlign: tooltipOpts.titleAlign, titleSpacing: tooltipOpts.titleSpacing, titleMarginBottom: tooltipOpts.titleMarginBottom, // Footer footerFontColor: tooltipOpts.footerFontColor, - _footerFontFamily: valueOrDefault$7(tooltipOpts.footerFontFamily, globalDefaults.defaultFontFamily), - _footerFontStyle: valueOrDefault$7(tooltipOpts.footerFontStyle, globalDefaults.defaultFontStyle), - footerFontSize: valueOrDefault$7(tooltipOpts.footerFontSize, globalDefaults.defaultFontSize), + _footerFontFamily: valueOrDefault$8(tooltipOpts.footerFontFamily, globalDefaults.defaultFontFamily), + _footerFontStyle: valueOrDefault$8(tooltipOpts.footerFontStyle, globalDefaults.defaultFontStyle), + footerFontSize: valueOrDefault$8(tooltipOpts.footerFontSize, globalDefaults.defaultFontSize), _footerAlign: tooltipOpts.footerAlign, footerSpacing: tooltipOpts.footerSpacing, footerMarginTop: tooltipOpts.footerMarginTop, // Appearance @@ -7764,11 +8607,11 @@ */ function getBeforeAfterBodyLines(callback) { return pushOrConcat([], splitNewlines(callback)); } -var exports$3 = core_element.extend({ +var exports$4 = core_element.extend({ initialize: function() { this._model = getBaseModel(this._options); this._lastActive = []; }, @@ -8022,29 +8865,32 @@ return {x1: x1, x2: x2, x3: x3, y1: y1, y2: y2, y3: y3}; }, drawTitle: function(pt, vm, ctx) { var title = vm.title; + var length = title.length; + var titleFontSize, titleSpacing, i; - if (title.length) { + if (length) { + var rtlHelper = getRtlHelper(vm.rtl, vm.x, vm.width); + pt.x = getAlignedX(vm, vm._titleAlign); - ctx.textAlign = vm._titleAlign; - ctx.textBaseline = 'top'; + ctx.textAlign = rtlHelper.textAlign(vm._titleAlign); + ctx.textBaseline = 'middle'; - var titleFontSize = vm.titleFontSize; - var titleSpacing = vm.titleSpacing; + titleFontSize = vm.titleFontSize; + titleSpacing = vm.titleSpacing; ctx.fillStyle = vm.titleFontColor; ctx.font = helpers$1.fontString(titleFontSize, vm._titleFontStyle, vm._titleFontFamily); - var i, len; - for (i = 0, len = title.length; i < len; ++i) { - ctx.fillText(title[i], pt.x, pt.y); + for (i = 0; i < length; ++i) { + ctx.fillText(title[i], rtlHelper.x(pt.x), pt.y + titleFontSize / 2); pt.y += titleFontSize + titleSpacing; // Line Height and spacing - if (i + 1 === title.length) { + if (i + 1 === length) { pt.y += vm.titleMarginBottom - titleSpacing; // If Last, add margin, remove spacing } } } }, @@ -8053,64 +8899,72 @@ var bodyFontSize = vm.bodyFontSize; var bodySpacing = vm.bodySpacing; var bodyAlign = vm._bodyAlign; var body = vm.body; var drawColorBoxes = vm.displayColors; - var labelColors = vm.labelColors; var xLinePadding = 0; var colorX = drawColorBoxes ? getAlignedX(vm, 'left') : 0; - var textColor; - ctx.textAlign = bodyAlign; - ctx.textBaseline = 'top'; - ctx.font = helpers$1.fontString(bodyFontSize, vm._bodyFontStyle, vm._bodyFontFamily); + var rtlHelper = getRtlHelper(vm.rtl, vm.x, vm.width); - pt.x = getAlignedX(vm, bodyAlign); - - // Before Body var fillLineOfText = function(line) { - ctx.fillText(line, pt.x + xLinePadding, pt.y); + ctx.fillText(line, rtlHelper.x(pt.x + xLinePadding), pt.y + bodyFontSize / 2); pt.y += bodyFontSize + bodySpacing; }; + var bodyItem, textColor, labelColors, lines, i, j, ilen, jlen; + var bodyAlignForCalculation = rtlHelper.textAlign(bodyAlign); + + ctx.textAlign = bodyAlign; + ctx.textBaseline = 'middle'; + ctx.font = helpers$1.fontString(bodyFontSize, vm._bodyFontStyle, vm._bodyFontFamily); + + pt.x = getAlignedX(vm, bodyAlignForCalculation); + // Before body lines ctx.fillStyle = vm.bodyFontColor; helpers$1.each(vm.beforeBody, fillLineOfText); - xLinePadding = drawColorBoxes && bodyAlign !== 'right' + xLinePadding = drawColorBoxes && bodyAlignForCalculation !== 'right' ? bodyAlign === 'center' ? (bodyFontSize / 2 + 1) : (bodyFontSize + 2) : 0; // Draw body lines now - helpers$1.each(body, function(bodyItem, i) { + for (i = 0, ilen = body.length; i < ilen; ++i) { + bodyItem = body[i]; textColor = vm.labelTextColors[i]; + labelColors = vm.labelColors[i]; + ctx.fillStyle = textColor; helpers$1.each(bodyItem.before, fillLineOfText); - helpers$1.each(bodyItem.lines, function(line) { + lines = bodyItem.lines; + for (j = 0, jlen = lines.length; j < jlen; ++j) { // Draw Legend-like boxes if needed if (drawColorBoxes) { + var rtlColorX = rtlHelper.x(colorX); + // Fill a white rect so that colours merge nicely if the opacity is < 1 ctx.fillStyle = vm.legendColorBackground; - ctx.fillRect(colorX, pt.y, bodyFontSize, bodyFontSize); + ctx.fillRect(rtlHelper.leftForLtr(rtlColorX, bodyFontSize), pt.y, bodyFontSize, bodyFontSize); // Border ctx.lineWidth = 1; - ctx.strokeStyle = labelColors[i].borderColor; - ctx.strokeRect(colorX, pt.y, bodyFontSize, bodyFontSize); + ctx.strokeStyle = labelColors.borderColor; + ctx.strokeRect(rtlHelper.leftForLtr(rtlColorX, bodyFontSize), pt.y, bodyFontSize, bodyFontSize); // Inner square - ctx.fillStyle = labelColors[i].backgroundColor; - ctx.fillRect(colorX + 1, pt.y + 1, bodyFontSize - 2, bodyFontSize - 2); + ctx.fillStyle = labelColors.backgroundColor; + ctx.fillRect(rtlHelper.leftForLtr(rtlHelper.xPlus(rtlColorX, 1), bodyFontSize - 2), pt.y + 1, bodyFontSize - 2, bodyFontSize - 2); ctx.fillStyle = textColor; } - fillLineOfText(line); - }); + fillLineOfText(lines[j]); + } helpers$1.each(bodyItem.after, fillLineOfText); - }); + } // Reset back to 0 for after body xLinePadding = 0; // After body lines @@ -8118,25 +8972,31 @@ pt.y -= bodySpacing; // Remove last body spacing }, drawFooter: function(pt, vm, ctx) { var footer = vm.footer; + var length = footer.length; + var footerFontSize, i; - if (footer.length) { + if (length) { + var rtlHelper = getRtlHelper(vm.rtl, vm.x, vm.width); + pt.x = getAlignedX(vm, vm._footerAlign); pt.y += vm.footerMarginTop; - ctx.textAlign = vm._footerAlign; - ctx.textBaseline = 'top'; + ctx.textAlign = rtlHelper.textAlign(vm._footerAlign); + ctx.textBaseline = 'middle'; + footerFontSize = vm.footerFontSize; + ctx.fillStyle = vm.footerFontColor; - ctx.font = helpers$1.fontString(vm.footerFontSize, vm._footerFontStyle, vm._footerFontFamily); + ctx.font = helpers$1.fontString(footerFontSize, vm._footerFontStyle, vm._footerFontFamily); - helpers$1.each(footer, function(line) { - ctx.fillText(line, pt.x, pt.y); - pt.y += vm.footerFontSize + vm.footerSpacing; - }); + for (i = 0; i < length; ++i) { + ctx.fillText(footer[i], rtlHelper.x(pt.x), pt.y + footerFontSize / 2); + pt.y += footerFontSize + vm.footerSpacing; + } } }, drawBackground: function(pt, vm, ctx, tooltipSize) { ctx.fillStyle = vm.backgroundColor; @@ -8212,19 +9072,23 @@ this.drawBackground(pt, vm, ctx, tooltipSize); // Draw Title, Body, and Footer pt.y += vm.yPadding; + helpers$1.rtl.overrideTextDirection(ctx, vm.textDirection); + // Titles this.drawTitle(pt, vm, ctx); // Body this.drawBody(pt, vm, ctx); // Footer this.drawFooter(pt, vm, ctx); + helpers$1.rtl.restoreTextDirection(ctx, vm.textDirection); + ctx.restore(); } }, /** @@ -8243,10 +9107,13 @@ // Find Active Elements for tooltips if (e.type === 'mouseout') { me._active = []; } else { me._active = me._chart.getElementsAtEventForMode(e, options.mode, options); + if (options.reverse) { + me._active.reverse(); + } } // Remember Last Actives changed = !helpers$1.arrayEquals(me._active, me._lastActive); @@ -8272,14 +9139,14 @@ /** * @namespace Chart.Tooltip.positioners */ var positioners_1 = positioners; -var core_tooltip = exports$3; +var core_tooltip = exports$4; core_tooltip.positioners = positioners_1; -var valueOrDefault$8 = helpers$1.valueOrDefault; +var valueOrDefault$9 = helpers$1.valueOrDefault; core_defaults._set('global', { elements: {}, events: [ 'mousemove', @@ -8316,11 +9183,11 @@ target[key] = []; } for (i = 0; i < slen; ++i) { scale = source[key][i]; - type = valueOrDefault$8(scale.type, key === 'xAxes' ? 'category' : 'linear'); + type = valueOrDefault$9(scale.type, key === 'xAxes' ? 'category' : 'linear'); if (i >= target[key].length) { target[key].push({}); } @@ -8400,14 +9267,35 @@ // Tooltip chart.tooltip._options = newOptions.tooltips; chart.tooltip.initialize(); } +function nextAvailableScaleId(axesOpts, prefix, index) { + var id; + var hasId = function(obj) { + return obj.id === id; + }; + + do { + id = prefix + index++; + } while (helpers$1.findIndex(axesOpts, hasId) >= 0); + + return id; +} + function positionIsHorizontal(position) { return position === 'top' || position === 'bottom'; } +function compare2Level(l1, l2) { + return function(a, b) { + return a[l1] === b[l1] + ? a[l2] - b[l2] + : a[l1] - b[l1]; + }; +} + var Chart = function(item, config) { this.construct(item, config); return this; }; @@ -8432,10 +9320,11 @@ me.width = width; me.height = height; me.aspectRatio = height ? width / height : null; me.options = config.options; me._bufferedRender = false; + me._layers = []; /** * Provided for backward compatibility, Chart and Chart.Controller have been merged, * the "instance" still need to be defined since it might be called from plugins. * @prop Chart#chart @@ -8488,13 +9377,10 @@ if (me.options.responsive) { // Initial resize before chart draws (must be silent to preserve initial animations). me.resize(true); } - // Make sure scales have IDs and are built before we build any controllers. - me.ensureScalesHaveIDs(); - me.buildOrUpdateScales(); me.initToolTip(); // After init plugin notification core_plugins.notify(me, 'afterInit'); @@ -8557,15 +9443,19 @@ var options = this.options; var scalesOptions = options.scales || {}; var scaleOptions = options.scale; helpers$1.each(scalesOptions.xAxes, function(xAxisOptions, index) { - xAxisOptions.id = xAxisOptions.id || ('x-axis-' + index); + if (!xAxisOptions.id) { + xAxisOptions.id = nextAvailableScaleId(scalesOptions.xAxes, 'x-axis-', index); + } }); helpers$1.each(scalesOptions.yAxes, function(yAxisOptions, index) { - yAxisOptions.id = yAxisOptions.id || ('y-axis-' + index); + if (!yAxisOptions.id) { + yAxisOptions.id = nextAvailableScaleId(scalesOptions.yAxes, 'y-axis-', index); + } }); if (scaleOptions) { scaleOptions.id = scaleOptions.id || 'scale'; } @@ -8605,11 +9495,11 @@ } helpers$1.each(items, function(item) { var scaleOptions = item.options; var id = scaleOptions.id; - var scaleType = valueOrDefault$8(scaleOptions.type, item.dtype); + var scaleType = valueOrDefault$9(scaleOptions.type, item.dtype); if (positionIsHorizontal(scaleOptions.position) !== positionIsHorizontal(item.dposition)) { scaleOptions.position = item.dposition; } @@ -8657,34 +9547,39 @@ }, buildOrUpdateControllers: function() { var me = this; var newControllers = []; + var datasets = me.data.datasets; + var i, ilen; - helpers$1.each(me.data.datasets, function(dataset, datasetIndex) { - var meta = me.getDatasetMeta(datasetIndex); + for (i = 0, ilen = datasets.length; i < ilen; i++) { + var dataset = datasets[i]; + var meta = me.getDatasetMeta(i); var type = dataset.type || me.config.type; if (meta.type && meta.type !== type) { - me.destroyDatasetMeta(datasetIndex); - meta = me.getDatasetMeta(datasetIndex); + me.destroyDatasetMeta(i); + meta = me.getDatasetMeta(i); } meta.type = type; + meta.order = dataset.order || 0; + meta.index = i; if (meta.controller) { - meta.controller.updateIndex(datasetIndex); + meta.controller.updateIndex(i); meta.controller.linkScales(); } else { var ControllerClass = controllers[meta.type]; if (ControllerClass === undefined) { throw new Error('"' + meta.type + '" is not a chart type.'); } - meta.controller = new ControllerClass(me, datasetIndex); + meta.controller = new ControllerClass(me, i); newControllers.push(meta.controller); } - }, me); + } return newControllers; }, /** @@ -8706,10 +9601,11 @@ this.tooltip.initialize(); }, update: function(config) { var me = this; + var i, ilen; if (!config || typeof config !== 'object') { // backwards compatibility config = { duration: config, @@ -8732,13 +9628,13 @@ // Make sure dataset controllers are updated and new controllers are reset var newControllers = me.buildOrUpdateControllers(); // Make sure all dataset controllers have correct meta data counts - helpers$1.each(me.data.datasets, function(dataset, datasetIndex) { - me.getDatasetMeta(datasetIndex).controller.buildOrUpdateElements(); - }, me); + for (i = 0, ilen = me.data.datasets.length; i < ilen; i++) { + me.getDatasetMeta(i).controller.buildOrUpdateElements(); + } me.updateLayout(); // Can only reset the new controllers after the scales have been updated if (me.options.animation && me.options.animation.duration) { @@ -8758,10 +9654,12 @@ me.lastActive = []; // Do this before render so that any plugins that need final scale updates can use it core_plugins.notify(me, 'afterUpdate'); + me._layers.sort(compare2Level('z', '_idx')); + if (me._bufferedRender) { me._bufferedRequest = { duration: config.duration, easing: config.easing, lazy: config.lazy @@ -8783,10 +9681,24 @@ return; } core_layouts.update(this, this.width, this.height); + me._layers = []; + helpers$1.each(me.boxes, function(box) { + // _configure is called twice, once in core.scale.update and once here. + // Here the boxes are fully updated and at their final positions. + if (box._configure) { + box._configure(); + } + me._layers.push.apply(me._layers, box._layers()); + }, me); + + me._layers.forEach(function(item, index) { + item._idx = index; + }); + /** * Provided for backward compatibility, use `afterLayout` instead. * @method IPlugin#afterScaleUpdate * @deprecated since version 2.5.0 * @todo remove at version 3 @@ -8830,11 +9742,11 @@ if (core_plugins.notify(me, 'beforeDatasetUpdate', [args]) === false) { return; } - meta.controller.update(); + meta.controller._update(); core_plugins.notify(me, 'afterDatasetUpdate', [args]); }, render: function(config) { @@ -8847,11 +9759,11 @@ lazy: arguments[1] }; } var animationOptions = me.options.animation; - var duration = valueOrDefault$8(config.duration, animationOptions && animationOptions.duration); + var duration = valueOrDefault$9(config.duration, animationOptions && animationOptions.duration); var lazy = config.lazy; if (core_plugins.notify(me, 'beforeRender') === false) { return; } @@ -8889,10 +9801,11 @@ return me; }, draw: function(easingValue) { var me = this; + var i, layers; me.clear(); if (helpers$1.isNullOrUndef(easingValue)) { easingValue = 1; @@ -8906,16 +9819,25 @@ if (core_plugins.notify(me, 'beforeDraw', [easingValue]) === false) { return; } - // Draw all the scales - helpers$1.each(me.boxes, function(box) { - box.draw(me.chartArea); - }, me); + // Because of plugin hooks (before/afterDatasetsDraw), datasets can't + // currently be part of layers. Instead, we draw + // layers <= 0 before(default, backward compat), and the rest after + layers = me._layers; + for (i = 0; i < layers.length && layers[i].z <= 0; ++i) { + layers[i].draw(me.chartArea); + } me.drawDatasets(easingValue); + + // Rest of layers + for (; i < layers.length; ++i) { + layers[i].draw(me.chartArea); + } + me._drawTooltip(easingValue); core_plugins.notify(me, 'afterDraw', [easingValue]); }, @@ -8933,42 +9855,67 @@ me.tooltip.transition(easingValue); }, /** + * @private + */ + _getSortedDatasetMetas: function(filterVisible) { + var me = this; + var datasets = me.data.datasets || []; + var result = []; + var i, ilen; + + for (i = 0, ilen = datasets.length; i < ilen; ++i) { + if (!filterVisible || me.isDatasetVisible(i)) { + result.push(me.getDatasetMeta(i)); + } + } + + result.sort(compare2Level('order', 'index')); + + return result; + }, + + /** + * @private + */ + _getSortedVisibleDatasetMetas: function() { + return this._getSortedDatasetMetas(true); + }, + + /** * Draws all datasets unless a plugin returns `false` to the `beforeDatasetsDraw` * hook, in which case, plugins will not be called on `afterDatasetsDraw`. * @private */ drawDatasets: function(easingValue) { var me = this; + var metasets, i; if (core_plugins.notify(me, 'beforeDatasetsDraw', [easingValue]) === false) { return; } - // Draw datasets reversed to support proper line stacking - for (var i = (me.data.datasets || []).length - 1; i >= 0; --i) { - if (me.isDatasetVisible(i)) { - me.drawDataset(i, easingValue); - } + metasets = me._getSortedVisibleDatasetMetas(); + for (i = metasets.length - 1; i >= 0; --i) { + me.drawDataset(metasets[i], easingValue); } core_plugins.notify(me, 'afterDatasetsDraw', [easingValue]); }, /** * Draws dataset at index unless a plugin returns `false` to the `beforeDatasetDraw` * hook, in which case, plugins will not be called on `afterDatasetDraw`. * @private */ - drawDataset: function(index, easingValue) { + drawDataset: function(meta, easingValue) { var me = this; - var meta = me.getDatasetMeta(index); var args = { meta: meta, - index: index, + index: meta.index, easingValue: easingValue }; if (core_plugins.notify(me, 'beforeDatasetDraw', [args]) === false) { return; @@ -9044,11 +9991,13 @@ data: [], dataset: null, controller: null, hidden: null, // See isDatasetVisible() comment xAxisID: null, - yAxisID: null + yAxisID: null, + order: dataset.order || 0, + index: datasetIndex }; } return meta; }, @@ -9170,19 +10119,23 @@ platform.removeEventListener(me, type, listener); }); }, updateHoverStyle: function(elements, mode, enabled) { - var method = enabled ? 'setHoverStyle' : 'removeHoverStyle'; + var prefix = enabled ? 'set' : 'remove'; var element, i, ilen; for (i = 0, ilen = elements.length; i < ilen; ++i) { element = elements[i]; if (element) { - this.getDatasetMeta(element._datasetIndex).controller[method](element); + this.getDatasetMeta(element._datasetIndex).controller[prefix + 'HoverStyle'](element); } } + + if (mode === 'dataset') { + this.getDatasetMeta(elements[0]._datasetIndex).controller['_' + prefix + 'DatasetHoverStyle'](); + } }, /** * @private */ @@ -9394,11 +10347,11 @@ helpers$1.almostEquals = function(x, y, epsilon) { return Math.abs(x - y) < epsilon; }; helpers$1.almostWhole = function(x, epsilon) { var rounded = Math.round(x); - return (((rounded - epsilon) < x) && ((rounded + epsilon) > x)); + return ((rounded - epsilon) <= x) && ((rounded + epsilon) >= x); }; helpers$1.max = function(array) { return array.reduce(function(max, value) { if (!isNaN(value)) { return Math.max(max, value); @@ -9423,23 +10376,10 @@ if (x === 0 || isNaN(x)) { return x; } return x > 0 ? 1 : -1; }; - helpers$1.log10 = Math.log10 ? - function(x) { - return Math.log10(x); - } : - function(x) { - var exponent = Math.log(x) * Math.LOG10E; // Math.LOG10E = 1 / Math.LN10. - // Check for whole powers of 10, - // which due to floating point rounding error should be corrected. - var powerOf10 = Math.round(exponent); - var isPowerOf10 = x === Math.pow(10, powerOf10); - - return isPowerOf10 ? powerOf10 : exponent; - }; helpers$1.toRadians = function(degrees) { return degrees * (Math.PI / 180); }; helpers$1.toDegrees = function(radians) { return radians * (180 / Math.PI); @@ -9876,29 +10816,34 @@ cache.font = font; } ctx.font = font; var longest = 0; - helpers$1.each(arrayOfThings, function(thing) { + var ilen = arrayOfThings.length; + var i, j, jlen, thing, nestedThing; + for (i = 0; i < ilen; i++) { + thing = arrayOfThings[i]; + // Undefined strings and arrays should not be measured if (thing !== undefined && thing !== null && helpers$1.isArray(thing) !== true) { longest = helpers$1.measureText(ctx, data, gc, longest, thing); } else if (helpers$1.isArray(thing)) { // if it is an array lets measure each element // to do maybe simplify this function a bit so we can do this more recursively? - helpers$1.each(thing, function(nestedThing) { + for (j = 0, jlen = thing.length; j < jlen; j++) { + nestedThing = thing[j]; // Undefined strings and arrays should not be measured if (nestedThing !== undefined && nestedThing !== null && !helpers$1.isArray(nestedThing)) { longest = helpers$1.measureText(ctx, data, gc, longest, nestedThing); } - }); + } } - }); + } var gcLen = gc.length / 2; if (gcLen > arrayOfThings.length) { - for (var i = 0; i < gcLen; i++) { + for (i = 0; i < gcLen; i++) { delete data[gc[i]]; } gc.splice(0, gcLen); } return longest; @@ -9912,10 +10857,14 @@ if (textWidth > longest) { longest = textWidth; } return longest; }; + + /** + * @deprecated + */ helpers$1.numberOfLabelLines = function(arrayOfThings) { var numberOfLines = 1; helpers$1.each(arrayOfThings, function(thing) { if (helpers$1.isArray(thing)) { if (thing.length > numberOfLines) { @@ -10109,11 +11058,13 @@ if (tickValue !== 0) { var maxTick = Math.max(Math.abs(ticks[0]), Math.abs(ticks[ticks.length - 1])); if (maxTick < 1e-4) { // all ticks are small numbers; use scientific notation var logTick = helpers$1.log10(Math.abs(tickValue)); - tickString = tickValue.toExponential(Math.floor(logTick) - Math.floor(logDelta)); + var numExponential = Math.floor(logTick) - Math.floor(logDelta); + numExponential = Math.max(Math.min(numExponential, 20), 0); + tickString = tickValue.toExponential(numExponential); } else { var numDecimal = -1 * Math.floor(logDelta); numDecimal = Math.max(Math.min(numDecimal, 20), 0); // toFixed has a max of 20 decimal places tickString = tickValue.toFixed(numDecimal); } @@ -10135,22 +11086,24 @@ return ''; } } }; -var valueOrDefault$9 = helpers$1.valueOrDefault; +var isArray = helpers$1.isArray; +var isNullOrUndef = helpers$1.isNullOrUndef; +var valueOrDefault$a = helpers$1.valueOrDefault; var valueAtIndexOrDefault = helpers$1.valueAtIndexOrDefault; core_defaults._set('scale', { display: true, position: 'left', offset: false, // grid line settings gridLines: { display: true, - color: 'rgba(0, 0, 0, 0.1)', + color: 'rgba(0,0,0,0.1)', lineWidth: 1, drawBorder: true, drawOnChartArea: true, drawTicks: true, tickMarkLength: 10, @@ -10195,45 +11148,270 @@ minor: {}, major: {} } }); -function labelsFromTicks(ticks) { - var labels = []; - var i, ilen; +/** Returns a new array containing numItems from arr */ +function sample(arr, numItems) { + var result = []; + var increment = arr.length / numItems; + var i = 0; + var len = arr.length; - for (i = 0, ilen = ticks.length; i < ilen; ++i) { - labels.push(ticks[i].label); + for (; i < len; i += increment) { + result.push(arr[Math.floor(i)]); } - - return labels; + return result; } function getPixelForGridLine(scale, index, offsetGridLines) { - var lineValue = scale.getPixelForTick(index); + var length = scale.getTicks().length; + var validIndex = Math.min(index, length - 1); + var lineValue = scale.getPixelForTick(validIndex); + var start = scale._startPixel; + var end = scale._endPixel; + var epsilon = 1e-6; // 1e-6 is margin in pixels for accumulated error. + var offset; 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); + if (length === 1) { + offset = Math.max(lineValue - start, end - lineValue); } else if (index === 0) { - lineValue -= (scale.getPixelForTick(1) - lineValue) / 2; + offset = (scale.getPixelForTick(1) - lineValue) / 2; } else { - lineValue -= (lineValue - scale.getPixelForTick(index - 1)) / 2; + offset = (lineValue - scale.getPixelForTick(validIndex - 1)) / 2; } + lineValue += validIndex < index ? offset : -offset; + + // Return undefined if the pixel is out of the range + if (lineValue < start - epsilon || lineValue > end + epsilon) { + return; + } } return lineValue; } -function computeTextSize(context, tick, font) { - return helpers$1.isArray(tick) ? - helpers$1.longestText(context, font, tick) : - context.measureText(tick).width; +function garbageCollect(caches, length) { + helpers$1.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); + } + }); } -var core_scale = core_element.extend({ +/** + * 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 (!isNullOrUndef(label) && !isArray(label)) { + width = helpers$1.measureText(ctx, cache.data, cache.gc, width, label); + height = lineHeight; + } else if (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 (!isNullOrUndef(nestedLabel) && !isArray(nestedLabel)) { + width = helpers$1.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$1.options._parseFont(options); + padding = helpers$1.options.toPadding(options.padding); + + return font.lineHeight + padding.height; +} + +function parseFontOptions(options, nestedOpts) { + return helpers$1.extend(helpers$1.options._parseFont({ + fontFamily: valueOrDefault$a(nestedOpts.fontFamily, options.fontFamily), + fontSize: valueOrDefault$a(nestedOpts.fontSize, options.fontSize), + fontStyle: valueOrDefault$a(nestedOpts.fontStyle, options.fontStyle), + lineHeight: valueOrDefault$a(nestedOpts.lineHeight, options.lineHeight) + }), { + color: helpers$1.options.resolve([nestedOpts.fontColor, options.fontColor, core_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}; +} + +function nonSkipped(ticksToFilter) { + var filtered = []; + var item, index, len; + for (index = 0, len = ticksToFilter.length; index < len; ++index) { + item = ticksToFilter[index]; + if (typeof item._index !== 'undefined') { + filtered.push(item); + } + } + return filtered; +} + +function getEvenSpacing(arr) { + var len = arr.length; + var i, diff; + + if (len < 2) { + return false; + } + + for (diff = arr[0], i = 1; i < len; ++i) { + if (arr[i] - arr[i - 1] !== diff) { + return false; + } + } + return diff; +} + +function calculateSpacing(majorIndices, ticks, axisLength, ticksLimit) { + var evenMajorSpacing = getEvenSpacing(majorIndices); + var spacing = (ticks.length - 1) / ticksLimit; + var factors, factor, i, ilen; + + // If the major ticks are evenly spaced apart, place the minor ticks + // so that they divide the major ticks into even chunks + if (!evenMajorSpacing) { + return Math.max(spacing, 1); + } + + factors = helpers$1.math._factorize(evenMajorSpacing); + for (i = 0, ilen = factors.length - 1; i < ilen; i++) { + factor = factors[i]; + if (factor > spacing) { + return factor; + } + } + return Math.max(spacing, 1); +} + +function getMajorIndices(ticks) { + var result = []; + var i, ilen; + for (i = 0, ilen = ticks.length; i < ilen; i++) { + if (ticks[i].major) { + result.push(i); + } + } + return result; +} + +function skipMajors(ticks, majorIndices, spacing) { + var count = 0; + var next = majorIndices[0]; + var i, tick; + + spacing = Math.ceil(spacing); + for (i = 0; i < ticks.length; i++) { + tick = ticks[i]; + if (i === next) { + tick._index = i; + count++; + next = majorIndices[count * spacing]; + } else { + delete tick.label; + } + } +} + +function skip(ticks, spacing, majorStart, majorEnd) { + var start = valueOrDefault$a(majorStart, 0); + var end = Math.min(valueOrDefault$a(majorEnd, ticks.length), ticks.length); + var count = 0; + var length, i, tick, next; + + spacing = Math.ceil(spacing); + if (majorEnd) { + length = majorEnd - majorStart; + spacing = length / Math.floor(length / spacing); + } + + next = start; + + while (next < 0) { + count++; + next = Math.round(start + count * spacing); + } + + for (i = Math.max(start, 0); i < end; i++) { + tick = ticks[i]; + if (i === next) { + tick._index = i; + count++; + next = Math.round(start + count * spacing); + } else { + delete tick.label; + } + } +} + +var Scale = core_element.extend({ + + zeroLineIndex: 0, + /** * Get the padding needed for the scale * @method getPadding * @private * @returns {Padding} the necessary padding @@ -10254,44 +11432,49 @@ */ 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() { - var ticks = this.options.ticks; - if (ticks.minor === false) { - ticks.minor = { - display: false - }; - } - if (ticks.major === false) { - ticks.major = { - display: false - }; - } - for (var key in ticks) { - if (key !== 'major' && key !== 'minor') { - if (typeof ticks.minor[key] === 'undefined') { - ticks.minor[key] = ticks[key]; - } - if (typeof ticks.major[key] === 'undefined') { - ticks.major[key] = ticks[key]; - } - } - } + // noop }, + beforeUpdate: function() { helpers$1.callback(this.options.beforeUpdate, [this]); }, + /** + * @param {number} maxWidth - the max width in pixels + * @param {number} maxHeight - the max height in pixels + * @param {object} margins - the space between the edge of the other scales and edge of the chart + * This space comes from two sources: + * - padding - space that's required to show the labels at the edges of the scale + * - thickness of scales or legends in another orientation + */ update: function(maxWidth, maxHeight, margins) { var me = this; - var i, ilen, labels, label, ticks, tick; + var tickOpts = me.options.ticks; + var sampleSize = tickOpts.sampleSize; + var i, ilen, labels, ticks, samplingEnabled; // Update Lifecycle - Probably don't want to ever extend or overwrite this function ;) me.beforeUpdate(); // Absorb the master measurements @@ -10302,13 +11485,18 @@ right: 0, top: 0, bottom: 0 }, margins); + me._ticks = null; + me.ticks = null; + me._labelSizes = null; me._maxLabelLines = 0; me.longestLabelWidth = 0; me.longestTextCache = me.longestTextCache || {}; + me._gridLineItems = null; + me._labelItems = null; // Dimensions me.beforeSetDimensions(); me.setDimensions(); me.afterSetDimensions(); @@ -10332,53 +11520,85 @@ 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, + // Ensure ticks contains ticks in new tick format + if ((!ticks || !ticks.length) && me.ticks) { + ticks = []; + for (i = 0, ilen = me.ticks.length; i < ilen; ++i) { + ticks.push({ + value: me.ticks[i], major: false }); - } else { - tick.label = label; } } me._ticks = ticks; + // Compute tick rotation and fit using a sampled subset of labels + // We generally don't need to compute the size of every single label for determining scale size + samplingEnabled = sampleSize < ticks.length; + labels = me._convertTicksToLabels(samplingEnabled ? sample(ticks, sampleSize) : ticks); + + // _configure is called twice, once here, once from core.controller.updateLayout. + // Here we haven't been positioned yet, but dimensions are correct. + // Variables set in _configure are needed for calculateTickRotation, and + // it's ok that coordinates are not correct there, only dimensions matter. + me._configure(); + // Tick Rotation me.beforeCalculateTickRotation(); me.calculateTickRotation(); me.afterCalculateTickRotation(); - // Fit + me.beforeFit(); me.fit(); me.afterFit(); - // + + // Auto-skip + me._ticksToDraw = tickOpts.display && (tickOpts.autoSkip || tickOpts.source === 'auto') ? me._autoSkip(ticks) : ticks; + + if (samplingEnabled) { + // Generate labels using all non-skipped ticks + labels = me._convertTicksToLabels(me._ticksToDraw); + } + + me.ticks = labels; // BACKWARD COMPATIBILITY + + // IMPORTANT: after this point, we consider that `this.ticks` will NEVER change! + me.afterUpdate(); + // TODO(v3): remove minSize as a public property and return value from all layout boxes. It is unused + // make maxWidth and maxHeight private return me.minSize; + }, + /** + * @private + */ + _configure: function() { + var me = this; + var reversePixels = me.options.ticks.reverse; + var startPixel, endPixel; + + if (me.isHorizontal()) { + startPixel = me.left; + endPixel = me.right; + } else { + startPixel = me.top; + endPixel = me.bottom; + // by default vertical scales are from bottom to top, so pixels are reversed + reversePixels = !reversePixels; + } + me._startPixel = startPixel; + me._endPixel = endPixel; + me._reversePixels = reversePixels; + me._length = endPixel - startPixel; }, + afterUpdate: function() { helpers$1.callback(this.options.afterUpdate, [this]); }, // @@ -10427,11 +11647,11 @@ }, buildTicks: helpers$1.noop, afterBuildTicks: function(ticks) { var me = this; // ticks is empty for old axis implementations here - if (helpers$1.isArray(ticks) && ticks.length) { + if (isArray(ticks) && ticks.length) { return helpers$1.callback(me.options.afterBuildTicks, [me, ticks]); } // Support old implementations (that modified `this.ticks` directly in buildTicks) me.ticks = helpers$1.callback(me.options.afterBuildTicks, [me, me.ticks]) || me.ticks; return ticks; @@ -10455,44 +11675,43 @@ beforeCalculateTickRotation: function() { helpers$1.callback(this.options.beforeCalculateTickRotation, [this]); }, calculateTickRotation: function() { var me = this; - var context = me.ctx; - var tickOpts = me.options.ticks; - var labels = labelsFromTicks(me._ticks); + var options = me.options; + var tickOpts = options.ticks; + var numTicks = me.getTicks().length; + var minRotation = tickOpts.minRotation || 0; + var maxRotation = tickOpts.maxRotation; + var labelRotation = minRotation; + var labelSizes, maxLabelWidth, maxLabelHeight, maxWidth, tickWidth, maxHeight, maxLabelDiagonal; - // Get the width of each grid by calculating the difference - // between x offsets between 0 and 1. - var tickFont = helpers$1.options._parseFont(tickOpts); - context.font = tickFont.string; + if (!me._isVisible() || !tickOpts.display || minRotation >= maxRotation || numTicks <= 1 || !me.isHorizontal()) { + me.labelRotation = minRotation; + return; + } - var labelRotation = tickOpts.minRotation || 0; + labelSizes = me._getLabelSizes(); + maxLabelWidth = labelSizes.widest.width; + maxLabelHeight = labelSizes.highest.height - labelSizes.highest.offset; - if (labels.length && me.options.display && me.isHorizontal()) { - var originalLabelWidth = helpers$1.longestText(context, tickFont.string, labels, me.longestTextCache); - var labelWidth = originalLabelWidth; - var cosRotation, sinRotation; + // 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 / numTicks : maxWidth / (numTicks - 1); - // Allow 3 pixels x2 padding either side for label readability - var tickWidth = me.getPixelForTick(1) - me.getPixelForTick(0) - 6; - - // Max label rotation can be set or default to 90 - also act as a loop counter - while (labelWidth > tickWidth && labelRotation < tickOpts.maxRotation) { - var angleRadians = helpers$1.toRadians(labelRotation); - cosRotation = Math.cos(angleRadians); - sinRotation = Math.sin(angleRadians); - - if (sinRotation * originalLabelWidth > me.maxHeight) { - // go back one step - labelRotation--; - break; - } - - labelRotation++; - labelWidth = cosRotation * originalLabelWidth; - } + // Allow 3 pixels x2 padding either side for label readability + if (maxLabelWidth + 6 > tickWidth) { + tickWidth = maxWidth / (numTicks - (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$1.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() { @@ -10510,153 +11729,143 @@ var minSize = me.minSize = { width: 0, height: 0 }; - var labels = labelsFromTicks(me._ticks); - + var chart = me.chart; 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 isBottom = opts.position === 'bottom'; var isHorizontal = me.isHorizontal(); - var parseFont = helpers$1.options._parseFont; - var tickFont = parseFont(tickOpts); - var tickMarkLength = opts.gridLines.tickMarkLength; - // Width if (isHorizontal) { - // subtract the margins to line up with the chartArea if we are a full width scale - minSize.width = me.isFullWidth() ? me.maxWidth - me.margins.left - me.margins.right : me.maxWidth; - } else { - minSize.width = display && gridLineOpts.drawTicks ? tickMarkLength : 0; + minSize.width = me.maxWidth; + } else if (display) { + minSize.width = getTickMarkLength(gridLineOpts) + getScaleLabelHeight(scaleLabelOpts); } // height - if (isHorizontal) { - minSize.height = display && gridLineOpts.drawTicks ? tickMarkLength : 0; - } else { + if (!isHorizontal) { minSize.height = me.maxHeight; // fill all the height + } else if (display) { + minSize.height = getTickMarkLength(gridLineOpts) + getScaleLabelHeight(scaleLabelOpts); } - // Are we showing a title for the scale? - if (scaleLabelOpts.display && display) { - var scaleLabelFont = parseFont(scaleLabelOpts); - var scaleLabelPadding = helpers$1.options.toPadding(scaleLabelOpts.padding); - var deltaHeight = scaleLabelFont.lineHeight + scaleLabelPadding.height; - - if (isHorizontal) { - minSize.height += deltaHeight; - } else { - minSize.width += deltaHeight; - } - } - // Don't bother fitting the ticks if we are not showing the labels if (tickOpts.display && display) { - var largestTextWidth = helpers$1.longestText(me.ctx, tickFont.string, labels, me.longestTextCache); - var tallestLabelHeightInLines = helpers$1.numberOfLabelLines(labels); - var lineSpace = tickFont.size * 0.5; - var tickPadding = me.options.ticks.padding; + var tickFonts = parseTickFontOptions(tickOpts); + var labelSizes = me._getLabelSizes(); + 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; - // Store max number of lines and widest label for _autoSkip - me._maxLabelLines = tallestLabelHeightInLines; - me.longestLabelWidth = largestTextWidth; - if (isHorizontal) { + // A horizontal axis is more constrained by the height. + var isRotated = me.labelRotation !== 0; var angleRadians = helpers$1.toRadians(me.labelRotation); var cosRotation = Math.cos(angleRadians); var sinRotation = Math.sin(angleRadians); - // TODO - improve this calculation - var labelHeight = (sinRotation * largestTextWidth) - + (tickFont.lineHeight * tallestLabelHeightInLines) - + lineSpace; // padding + 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); - 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 offsetRight = me.right - me.getPixelForTick(me.getTicks().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 (me.labelRotation !== 0) { - paddingLeft = position === 'bottom' ? (cosRotation * firstLabelWidth) : (cosRotation * lineSpace); - paddingRight = position === 'bottom' ? (cosRotation * lineSpace) : (cosRotation * lastLabelWidth); + if (isRotated) { + paddingLeft = isBottom ? + cosRotation * firstLabelSize.width + sinRotation * firstLabelSize.offset : + sinRotation * (firstLabelSize.height - firstLabelSize.offset); + paddingRight = isBottom ? + sinRotation * (lastLabelSize.height - lastLabelSize.offset) : + cosRotation * lastLabelSize.width + sinRotation * lastLabelSize.offset; } else { - paddingLeft = firstLabelWidth / 2; - paddingRight = lastLabelWidth / 2; + paddingLeft = firstLabelSize.width / 2; + paddingRight = lastLabelSize.width / 2; } - me.paddingLeft = Math.max(paddingLeft - offsetLeft, 0) + 3; // add 3 px to move away from canvas edges - me.paddingRight = Math.max(paddingRight - offsetRight, 0) + 3; + + // 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 - if (tickOpts.mirror) { - largestTextWidth = 0; - } else { + var labelWidth = tickOpts.mirror ? 0 : // use lineSpace for consistency with horizontal axis // tickPadding is not implemented for horizontal - largestTextWidth += tickPadding + lineSpace; - } + widestLabelSize.width + tickPadding + lineSpace; - minSize.width = Math.min(me.maxWidth, minSize.width + largestTextWidth); + minSize.width = Math.min(me.maxWidth, minSize.width + labelWidth); - me.paddingTop = tickFont.size / 2; - me.paddingBottom = tickFont.size / 2; + me.paddingTop = firstLabelSize.height / 2; + me.paddingBottom = lastLabelSize.height / 2; } } me.handleMargins(); - me.width = minSize.width; - me.height = minSize.height; + if (isHorizontal) { + me.width = me._length = chart.width - me.margins.left - me.margins.right; + me.height = minSize.height; + } else { + me.width = minSize.width; + me.height = me._length = chart.height - me.margins.top - me.margins.bottom; + } }, /** * Handle margins and padding interactions * @private */ handleMargins: function() { var me = this; if (me.margins) { - me.paddingLeft = Math.max(me.paddingLeft - me.margins.left, 0); - me.paddingTop = Math.max(me.paddingTop - me.margins.top, 0); - me.paddingRight = Math.max(me.paddingRight - me.margins.right, 0); - me.paddingBottom = Math.max(me.paddingBottom - me.margins.bottom, 0); + 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$1.callback(this.options.afterFit, [this]); }, // Shared Methods isHorizontal: function() { - return this.options.position === 'top' || this.options.position === 'bottom'; + var pos = this.options.position; + return pos === 'top' || pos === 'bottom'; }, isFullWidth: function() { - return (this.options.fullWidth); + 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$1.isNullOrUndef(rawValue)) { + if (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); @@ -10668,11 +11877,90 @@ // Value is good, return it return rawValue; }, + _convertTicksToLabels: function(ticks) { + var me = this; + var labels, i, ilen; + + me.ticks = ticks.map(function(tick) { + return tick.value; + }); + + 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(); + + // BACKWARD COMPAT: synchronize `_ticks` with labels (so potentially `this.ticks`) + for (i = 0, ilen = ticks.length; i < ilen; ++i) { + ticks[i].label = labels[i]; + } + + return labels; + }, + /** + * @private + */ + _getLabelSizes: function() { + var me = this; + var labelSizes = me._labelSizes; + + if (!labelSizes) { + me._labelSizes = labelSizes = computeLabelSizes(me.ctx, parseTickFontOptions(me.options.ticks), me.getTicks(), me.longestTextCache); + me.longestLabelWidth = labelSizes.widest.width; + } + + return labelSizes; + }, + + /** + * @private + */ + _parseValue: function(value) { + var start, end, min, max; + + if (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$1.noop, @@ -10698,44 +11986,37 @@ * The coordinate (0, 0) is at the upper-left corner of the canvas */ getPixelForTick: function(index) { var me = this; var offset = me.options.offset; - if (me.isHorizontal()) { - var innerWidth = me.width - (me.paddingLeft + me.paddingRight); - var tickWidth = innerWidth / Math.max((me._ticks.length - (offset ? 0 : 1)), 1); - var pixel = (tickWidth * index) + me.paddingLeft; + var numTicks = me._ticks.length; + var tickWidth = 1 / Math.max(numTicks - (offset ? 0 : 1), 1); - if (offset) { - pixel += tickWidth / 2; - } - - var finalVal = me.left + pixel; - finalVal += me.isFullWidth() ? me.margins.left : 0; - return finalVal; - } - var innerHeight = me.height - (me.paddingTop + me.paddingBottom); - return me.top + (index * (innerHeight / (me._ticks.length - 1))); + return index < 0 || index > numTicks - 1 + ? null + : me.getPixelForDecimal(index * tickWidth + (offset ? tickWidth / 2 : 0)); }, /** * 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; - if (me.isHorizontal()) { - var innerWidth = me.width - (me.paddingLeft + me.paddingRight); - var valueOffset = (innerWidth * decimal) + me.paddingLeft; - var finalVal = me.left + valueOffset; - finalVal += me.isFullWidth() ? me.margins.left : 0; - return finalVal; + if (me._reversePixels) { + decimal = 1 - decimal; } - return me.top + (decimal * me.height); + + return me._startPixel + decimal * me._length; }, + getDecimalForPixel: function(pixel) { + var decimal = (pixel - this._startPixel) / this._length; + return this._reversePixels ? 1 - decimal : decimal; + }, + /** * Returns the pixel for the minimum chart value * The coordinate (0, 0) is at the upper-left corner of the canvas */ getBasePixel: function() { @@ -10757,71 +12038,59 @@ * 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.minor; - var tickCount = ticks.length; - var skipRatio = false; - var maxTicks = optionTicks.maxTicksLimit; + var tickOpts = me.options.ticks; + var axisLength = me._length; + var ticksLimit = tickOpts.maxTicksLimit || axisLength / me._tickSize() + 1; + var majorIndices = tickOpts.major.enabled ? getMajorIndices(ticks) : []; + var numMajorIndices = majorIndices.length; + var first = majorIndices[0]; + var last = majorIndices[numMajorIndices - 1]; + var i, ilen, spacing, avgMajorSpacing; - // 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.paddingLeft + me.paddingRight) - : me.height - (me.paddingTop + me.PaddingBottom); - - var result = []; - var i, tick; - - if (ticksLength > axisLength) { - skipRatio = 1 + Math.floor(ticksLength / axisLength); + // If there are too many major ticks to display them all + if (numMajorIndices > ticksLimit) { + skipMajors(ticks, majorIndices, numMajorIndices / ticksLimit); + return nonSkipped(ticks); } - // 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)); - } + spacing = calculateSpacing(majorIndices, ticks, axisLength, ticksLimit); - 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; + if (numMajorIndices > 0) { + for (i = 0, ilen = numMajorIndices - 1; i < ilen; i++) { + skip(ticks, spacing, majorIndices[i], majorIndices[i + 1]); } - result.push(tick); + avgMajorSpacing = numMajorIndices > 1 ? (last - first) / (numMajorIndices - 1) : null; + skip(ticks, spacing, helpers$1.isNullOrUndef(avgMajorSpacing) ? 0 : first - avgMajorSpacing, first); + skip(ticks, spacing, last, helpers$1.isNullOrUndef(avgMajorSpacing) ? ticks.length : last + avgMajorSpacing); + return nonSkipped(ticks); } - return result; + skip(ticks, spacing); + return nonSkipped(ticks); }, /** * @private */ _tickSize: function() { var me = this; - var isHorizontal = me.isHorizontal(); - var optionTicks = me.options.ticks.minor; + var optionTicks = me.options.ticks; // Calculate space needed by label in axis direction. var rot = helpers$1.toRadians(me.labelRotation); var cos = Math.abs(Math.cos(rot)); var sin = Math.abs(Math.sin(rot)); + var labelSizes = me._getLabelSizes(); var padding = optionTicks.autoSkipPadding || 0; - var w = (me.longestLabelWidth + padding) || 0; + var w = labelSizes ? labelSizes.widest.width + padding : 0; + var h = labelSizes ? labelSizes.highest.height + padding : 0; - var tickFont = helpers$1.options._parseFont(optionTicks); - var h = (me._maxLabelLines * tickFont.lineHeight + padding) || 0; - // Calculate space needed for 1 tick in axis direction. - return isHorizontal + return me.isHorizontal() ? h * cos > w * sin ? w / cos : h / sin : h * sin < w * cos ? h / cos : w / sin; }, /** @@ -10849,412 +12118,532 @@ return false; }, /** - * Actually draw the scale on the canvas - * @param {object} chartArea - the area of the chart to draw full grid lines on + * @private */ - draw: function(chartArea) { + _computeGridLineItems: function(chartArea) { var me = this; - var options = me.options; - - if (!me._isVisible()) { - return; - } - var chart = me.chart; - var context = me.ctx; - var globalDefaults = core_defaults.global; - var defaultFontColor = globalDefaults.defaultFontColor; - var optionTicks = options.ticks.minor; - var optionMajorTicks = options.ticks.major || optionTicks; + var options = me.options; var gridLines = options.gridLines; - var scaleLabel = options.scaleLabel; var position = options.position; - - var isRotated = me.labelRotation !== 0; - var isMirrored = optionTicks.mirror; + var offsetGridLines = gridLines.offsetGridLines; var isHorizontal = me.isHorizontal(); + var ticks = me._ticksToDraw; + var ticksLength = ticks.length + (offsetGridLines ? 1 : 0); - var parseFont = helpers$1.options._parseFont; - var ticks = optionTicks.display && optionTicks.autoSkip ? me._autoSkip(me.getTicks()) : me.getTicks(); - var tickFontColor = valueOrDefault$9(optionTicks.fontColor, defaultFontColor); - var tickFont = parseFont(optionTicks); - var lineHeight = tickFont.lineHeight; - var majorTickFontColor = valueOrDefault$9(optionMajorTicks.fontColor, defaultFontColor); - var majorTickFont = parseFont(optionMajorTicks); - var tickPadding = optionTicks.padding; - var labelOffset = optionTicks.labelOffset; - - var tl = gridLines.drawTicks ? gridLines.tickMarkLength : 0; - - var scaleLabelFontColor = valueOrDefault$9(scaleLabel.fontColor, defaultFontColor); - var scaleLabelFont = parseFont(scaleLabel); - var scaleLabelPadding = helpers$1.options.toPadding(scaleLabel.padding); - var labelRotationRadians = helpers$1.toRadians(me.labelRotation); - - var itemsToDraw = []; - + var tl = getTickMarkLength(gridLines); + var items = []; var axisWidth = gridLines.drawBorder ? valueAtIndexOrDefault(gridLines.lineWidth, 0, 0) : 0; + var axisHalfWidth = axisWidth / 2; var alignPixel = helpers$1._alignPixel; - var borderValue, tickStart, tickEnd; + var alignBorderValue = function(pixel) { + return alignPixel(chart, pixel, axisWidth); + }; + var borderValue, i, tick, lineValue, alignedLineValue; + var tx1, ty1, tx2, ty2, x1, y1, x2, y2, lineWidth, lineColor, borderDash, borderDashOffset; if (position === 'top') { - borderValue = alignPixel(chart, me.bottom, axisWidth); - tickStart = me.bottom - tl; - tickEnd = borderValue - axisWidth / 2; + borderValue = alignBorderValue(me.bottom); + ty1 = me.bottom - tl; + ty2 = borderValue - axisHalfWidth; + y1 = alignBorderValue(chartArea.top) + axisHalfWidth; + y2 = chartArea.bottom; } else if (position === 'bottom') { - borderValue = alignPixel(chart, me.top, axisWidth); - tickStart = borderValue + axisWidth / 2; - tickEnd = me.top + tl; + borderValue = alignBorderValue(me.top); + y1 = chartArea.top; + y2 = alignBorderValue(chartArea.bottom) - axisHalfWidth; + ty1 = borderValue + axisHalfWidth; + ty2 = me.top + tl; } else if (position === 'left') { - borderValue = alignPixel(chart, me.right, axisWidth); - tickStart = me.right - tl; - tickEnd = borderValue - axisWidth / 2; + borderValue = alignBorderValue(me.right); + tx1 = me.right - tl; + tx2 = borderValue - axisHalfWidth; + x1 = alignBorderValue(chartArea.left) + axisHalfWidth; + x2 = chartArea.right; } else { - borderValue = alignPixel(chart, me.left, axisWidth); - tickStart = borderValue + axisWidth / 2; - tickEnd = me.left + tl; + borderValue = alignBorderValue(me.left); + x1 = chartArea.left; + x2 = alignBorderValue(chartArea.right) - axisHalfWidth; + tx1 = borderValue + axisHalfWidth; + tx2 = me.left + tl; } - var epsilon = 0.0000001; // 0.0000001 is margin in pixels for Accumulated error. + for (i = 0; i < ticksLength; ++i) { + tick = ticks[i] || {}; - helpers$1.each(ticks, function(tick, index) { // autoskipper skipped this tick (#4635) - if (helpers$1.isNullOrUndef(tick.label)) { - return; + if (isNullOrUndef(tick.label) && i < ticks.length) { + continue; } - var label = tick.label; - var lineWidth, lineColor, borderDash, borderDashOffset; - if (index === me.zeroLineIndex && options.offset === gridLines.offsetGridLines) { + if (i === me.zeroLineIndex && options.offset === 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); + lineWidth = valueAtIndexOrDefault(gridLines.lineWidth, i, 1); + lineColor = valueAtIndexOrDefault(gridLines.color, i, 'rgba(0,0,0,0.1)'); 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$1.isArray(label) ? label.length : 1; - var lineValue = getPixelForGridLine(me, index, gridLines.offsetGridLines); + lineValue = getPixelForGridLine(me, tick._index || i, offsetGridLines); - if (isHorizontal) { - var labelYOffset = tl + tickPadding; + // Skip if the pixel is out of the range + if (lineValue === undefined) { + continue; + } - if (lineValue < me.left - epsilon) { - lineColor = 'rgba(0,0,0,0)'; - } + alignedLineValue = alignPixel(chart, lineValue, lineWidth); - 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; - } + if (isHorizontal) { + tx1 = tx2 = x1 = x2 = alignedLineValue; } 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; - } + ty1 = ty2 = y1 = y2 = alignedLineValue; } - itemsToDraw.push({ + 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, + width: lineWidth, + color: lineColor, + borderDash: borderDash, + borderDashOffset: borderDashOffset, + }); + } + + items.ticksLength = ticksLength; + items.borderValue = borderValue; + + return items; + }, + + /** + * @private + */ + _computeLabelItems: function() { + var me = this; + var options = me.options; + var optionTicks = options.ticks; + var position = options.position; + var isMirrored = optionTicks.mirror; + var isHorizontal = me.isHorizontal(); + var ticks = me._ticksToDraw; + var fonts = parseTickFontOptions(optionTicks); + var tickPadding = optionTicks.padding; + var tl = getTickMarkLength(options.gridLines); + var rotation = -helpers$1.toRadians(me.labelRotation); + var items = []; + var i, ilen, tick, label, x, y, textAlign, pixel, font, lineHeight, lineCount, textOffset; + + if (position === 'top') { + y = me.bottom - tl - tickPadding; + textAlign = !rotation ? 'center' : 'left'; + } else if (position === 'bottom') { + y = me.top + tl + tickPadding; + textAlign = !rotation ? 'center' : 'right'; + } else if (position === 'left') { + x = me.right - (isMirrored ? 0 : tl) - tickPadding; + textAlign = isMirrored ? 'left' : 'right'; + } else { + x = me.left + (isMirrored ? 0 : tl) + tickPadding; + textAlign = isMirrored ? 'right' : 'left'; + } + + for (i = 0, ilen = ticks.length; i < ilen; ++i) { + tick = ticks[i]; + label = tick.label; + + // autoskipper skipped this tick (#4635) + if (isNullOrUndef(label)) { + continue; + } + + pixel = me.getPixelForTick(tick._index || i) + optionTicks.labelOffset; + font = tick.major ? fonts.major : fonts.minor; + lineHeight = font.lineHeight; + lineCount = isArray(label) ? label.length : 1; + + if (isHorizontal) { + x = pixel; + textOffset = position === 'top' + ? ((!rotation ? 0.5 : 1) - lineCount) * lineHeight + : (!rotation ? 0.5 : 0) * lineHeight; + } else { + y = pixel; + textOffset = (1 - lineCount) * lineHeight / 2; + } + + items.push({ + x: x, + y: y, + rotation: rotation, label: label, - major: tick.major, + font: font, textOffset: textOffset, textAlign: textAlign }); - }); + } - // Draw all of the tick labels, tick marks, and grid lines at the correct places - helpers$1.each(itemsToDraw, function(itemToDraw) { - var glWidth = itemToDraw.glWidth; - var glColor = itemToDraw.glColor; + return items; + }, - if (gridLines.display && glWidth && glColor) { - context.save(); - context.lineWidth = glWidth; - context.strokeStyle = glColor; - if (context.setLineDash) { - context.setLineDash(itemToDraw.glBorderDash); - context.lineDashOffset = itemToDraw.glBorderDashOffset; + /** + * @private + */ + _drawGrid: function(chartArea) { + var me = this; + var gridLines = me.options.gridLines; + + if (!gridLines.display) { + return; + } + + var ctx = me.ctx; + var chart = me.chart; + var alignPixel = helpers$1._alignPixel; + var axisWidth = gridLines.drawBorder ? valueAtIndexOrDefault(gridLines.lineWidth, 0, 0) : 0; + var items = me._gridLineItems || (me._gridLineItems = me._computeGridLineItems(chartArea)); + var width, color, i, ilen, item; + + for (i = 0, ilen = items.length; i < ilen; ++i) { + item = items[i]; + width = item.width; + color = item.color; + + if (width && color) { + ctx.save(); + ctx.lineWidth = width; + ctx.strokeStyle = color; + if (ctx.setLineDash) { + ctx.setLineDash(item.borderDash); + ctx.lineDashOffset = item.borderDashOffset; } - context.beginPath(); + ctx.beginPath(); if (gridLines.drawTicks) { - context.moveTo(itemToDraw.tx1, itemToDraw.ty1); - context.lineTo(itemToDraw.tx2, itemToDraw.ty2); + ctx.moveTo(item.tx1, item.ty1); + ctx.lineTo(item.tx2, item.ty2); } if (gridLines.drawOnChartArea) { - context.moveTo(itemToDraw.x1, itemToDraw.y1); - context.lineTo(itemToDraw.x2, itemToDraw.y2); + ctx.moveTo(item.x1, item.y1); + ctx.lineTo(item.x2, item.y2); } - context.stroke(); - context.restore(); + ctx.stroke(); + ctx.restore(); } - - if (optionTicks.display) { - // Make sure we draw text in the correct color and font - context.save(); - context.translate(itemToDraw.labelX, itemToDraw.labelY); - context.rotate(itemToDraw.rotation); - context.font = itemToDraw.major ? majorTickFont.string : tickFont.string; - context.fillStyle = itemToDraw.major ? majorTickFontColor : tickFontColor; - context.textBaseline = 'middle'; - context.textAlign = itemToDraw.textAlign; - - var label = itemToDraw.label; - var y = itemToDraw.textOffset; - if (helpers$1.isArray(label)) { - 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); - y += lineHeight; - } - } else { - context.fillText(label, 0, y); - } - context.restore(); - } - }); - - if (scaleLabel.display) { - // Draw the scale label - var scaleLabelX; - var scaleLabelY; - var rotation = 0; - var halfLineHeight = scaleLabelFont.lineHeight / 2; - - if (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; - } - - context.save(); - context.translate(scaleLabelX, scaleLabelY); - context.rotate(rotation); - context.textAlign = 'center'; - context.textBaseline = 'middle'; - context.fillStyle = scaleLabelFontColor; // render in correct colour - context.font = scaleLabelFont.string; - context.fillText(scaleLabel.labelString, 0, 0); - context.restore(); } if (axisWidth) { // Draw the line at the edge of the axis var firstLineWidth = axisWidth; - var lastLineWidth = valueAtIndexOrDefault(gridLines.lineWidth, ticks.length - 1, 0); + var lastLineWidth = valueAtIndexOrDefault(gridLines.lineWidth, items.ticksLength - 1, 1); + var borderValue = items.borderValue; var x1, x2, y1, y2; - if (isHorizontal) { + 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; } - context.lineWidth = axisWidth; - context.strokeStyle = valueAtIndexOrDefault(gridLines.color, 0); - context.beginPath(); - context.moveTo(x1, y1); - context.lineTo(x2, y2); - context.stroke(); + ctx.lineWidth = axisWidth; + ctx.strokeStyle = valueAtIndexOrDefault(gridLines.color, 0); + ctx.beginPath(); + ctx.moveTo(x1, y1); + ctx.lineTo(x2, y2); + ctx.stroke(); } + }, + + /** + * @private + */ + _drawLabels: function() { + var me = this; + var optionTicks = me.options.ticks; + + if (!optionTicks.display) { + return; + } + + var ctx = me.ctx; + var items = me._labelItems || (me._labelItems = me._computeLabelItems()); + var i, j, ilen, jlen, item, tickFont, label, y; + + for (i = 0, ilen = items.length; i < ilen; ++i) { + item = items[i]; + tickFont = item.font; + + // Make sure we draw text in the correct color and font + ctx.save(); + ctx.translate(item.x, item.y); + ctx.rotate(item.rotation); + ctx.font = tickFont.string; + ctx.fillStyle = tickFont.color; + ctx.textBaseline = 'middle'; + ctx.textAlign = item.textAlign; + + label = item.label; + y = item.textOffset; + if (isArray(label)) { + for (j = 0, jlen = label.length; j < jlen; ++j) { + // We just make sure the multiline element is a string here.. + ctx.fillText('' + label[j], 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$a(scaleLabel.fontColor, core_defaults.global.defaultFontColor); + var scaleLabelFont = helpers$1.options._parseFont(scaleLabel); + var scaleLabelPadding = helpers$1.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.width / 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.height / 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(); + me._drawLabels(); + }, + + /** + * @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); + } + }]; + }, + + /** + * @private + */ + _getMatchingVisibleMetas: function(type) { + var me = this; + var isHorizontal = me.isHorizontal(); + return me.chart._getSortedVisibleDatasetMetas() + .filter(function(meta) { + return (!type || meta.type === type) + && (isHorizontal ? meta.xAxisID === me.id : meta.yAxisID === me.id); + }); } }); +Scale.prototype._draw = Scale.prototype.draw; + +var core_scale = Scale; + +var isNullOrUndef$1 = helpers$1.isNullOrUndef; + var defaultConfig = { position: 'bottom' }; var scale_category = core_scale.extend({ - /** - * Internal function to get the correct labels. If data.xLabels or data.yLabels are defined, use those - * else fall back to data.labels - * @private - */ - getLabels: function() { - var data = this.chart.data; - return this.options.labels || (this.isHorizontal() ? data.xLabels : data.yLabels) || data.labels; - }, - determineDataLimits: function() { var me = this; - var labels = me.getLabels(); - me.minIndex = 0; - me.maxIndex = labels.length - 1; + var labels = me._getLabels(); + var ticksOpts = me.options.ticks; + var min = ticksOpts.min; + var max = ticksOpts.max; + var minIndex = 0; + var maxIndex = labels.length - 1; var findIndex; - if (me.options.ticks.min !== undefined) { + if (min !== undefined) { // user specified min value - findIndex = labels.indexOf(me.options.ticks.min); - me.minIndex = findIndex !== -1 ? findIndex : me.minIndex; + findIndex = labels.indexOf(min); + if (findIndex >= 0) { + minIndex = findIndex; + } } - if (me.options.ticks.max !== undefined) { + if (max !== undefined) { // user specified max value - findIndex = labels.indexOf(me.options.ticks.max); - me.maxIndex = findIndex !== -1 ? findIndex : me.maxIndex; + findIndex = labels.indexOf(max); + if (findIndex >= 0) { + maxIndex = findIndex; + } } - me.min = labels[me.minIndex]; - me.max = labels[me.maxIndex]; + me.minIndex = minIndex; + me.maxIndex = maxIndex; + me.min = labels[minIndex]; + me.max = labels[maxIndex]; }, buildTicks: function() { var me = this; - var labels = me.getLabels(); + var labels = me._getLabels(); + var minIndex = me.minIndex; + var maxIndex = me.maxIndex; + // If we are viewing some subset of labels, slice the original array - me.ticks = (me.minIndex === 0 && me.maxIndex === labels.length - 1) ? labels : labels.slice(me.minIndex, me.maxIndex + 1); + me.ticks = (minIndex === 0 && maxIndex === labels.length - 1) ? labels : labels.slice(minIndex, maxIndex + 1); }, getLabelForIndex: function(index, datasetIndex) { var me = this; var chart = me.chart; if (chart.getDatasetMeta(datasetIndex).controller._getValueScaleId() === me.id) { return me.getRightValue(chart.data.datasets[datasetIndex].data[index]); } - return me.ticks[index - me.minIndex]; + return me._getLabels()[index]; }, - // Used to get data value locations. Value can either be an index or a numerical value - getPixelForValue: function(value, index) { + _configure: function() { var me = this; var offset = me.options.offset; - // 1 is added because we need the length but we have the indexes - var offsetAmt = Math.max((me.maxIndex + 1 - me.minIndex - (offset ? 0 : 1)), 1); + var ticks = me.ticks; - // If value is a data object, then index is the index in the data array, - // not the index of the scale. We need to change that. - var valueCategory; - if (value !== undefined && value !== null) { - valueCategory = me.isHorizontal() ? value.x : value.y; + core_scale.prototype._configure.call(me); + + if (!me.isHorizontal()) { + // For backward compatibility, vertical category scale reverse is inverted. + me._reversePixels = !me._reversePixels; } - if (valueCategory !== undefined || (value !== undefined && isNaN(index))) { - var labels = me.getLabels(); - value = valueCategory || value; - var idx = labels.indexOf(value); - index = idx !== -1 ? idx : index; + + if (!ticks) { + return; } - if (me.isHorizontal()) { - var valueWidth = me.width / offsetAmt; - var widthOffset = (valueWidth * (index - me.minIndex)); + me._startValue = me.minIndex - (offset ? 0.5 : 0); + me._valueRange = Math.max(ticks.length - (offset ? 0 : 1), 1); + }, - if (offset) { - widthOffset += (valueWidth / 2); - } + // Used to get data value locations. Value can either be an index or a numerical value + getPixelForValue: function(value, index, datasetIndex) { + var me = this; + var valueCategory, labels, idx; - return me.left + widthOffset; + if (!isNullOrUndef$1(index) && !isNullOrUndef$1(datasetIndex)) { + value = me.chart.data.datasets[datasetIndex].data[index]; } - var valueHeight = me.height / offsetAmt; - var heightOffset = (valueHeight * (index - me.minIndex)); - if (offset) { - heightOffset += (valueHeight / 2); + // If value is a data object, then index is the index in the data array, + // not the index of the scale. We need to change that. + if (!isNullOrUndef$1(value)) { + valueCategory = me.isHorizontal() ? value.x : value.y; } - - return me.top + heightOffset; + if (valueCategory !== undefined || (value !== undefined && isNaN(index))) { + labels = me._getLabels(); + value = helpers$1.valueOrDefault(valueCategory, value); + idx = labels.indexOf(value); + index = idx !== -1 ? idx : index; + if (isNaN(index)) { + index = value; + } + } + return me.getPixelForDecimal((index - me._startValue) / me._valueRange); }, getPixelForTick: function(index) { - return this.getPixelForValue(this.ticks[index], index + this.minIndex, null); + var ticks = this.ticks; + return index < 0 || index > ticks.length - 1 + ? null + : this.getPixelForValue(ticks[index], index + this.minIndex); }, getValueForPixel: function(pixel) { var me = this; - var offset = me.options.offset; - var value; - var offsetAmt = Math.max((me._ticks.length - (offset ? 0 : 1)), 1); - var horz = me.isHorizontal(); - var valueDimension = (horz ? me.width : me.height) / offsetAmt; - - pixel -= horz ? me.left : me.top; - - if (offset) { - pixel -= (valueDimension / 2); - } - - if (pixel <= 0) { - value = 0; - } else { - value = Math.round(pixel / valueDimension); - } - - return value + me.minIndex; + var value = Math.round(me._startValue + me.getDecimalForPixel(pixel) * me._valueRange); + return Math.min(Math.max(value, 0), me.ticks.length - 1); }, getBasePixel: function() { return this.bottom; } @@ -11263,11 +12652,11 @@ // INTERNAL: static default options, registered in src/index.js var _defaults = defaultConfig; scale_category._defaults = _defaults; var noop = helpers$1.noop; -var isNullOrUndef = helpers$1.isNullOrUndef; +var isNullOrUndef$2 = helpers$1.isNullOrUndef; /** * Generate a set of linear ticks * @param generationOptions the options used to generate the ticks * @param dataRange the range of the data @@ -11291,21 +12680,21 @@ var spacing = helpers$1.niceNum((rmax - rmin) / maxNumSpaces / unit) * unit; var factor, niceMin, niceMax, numSpaces; // Beyond MIN_SPACING floating point numbers being to lose precision // such that we can't do the math necessary to generate ticks - if (spacing < MIN_SPACING && isNullOrUndef(min) && isNullOrUndef(max)) { + if (spacing < MIN_SPACING && isNullOrUndef$2(min) && isNullOrUndef$2(max)) { return [rmin, rmax]; } numSpaces = Math.ceil(rmax / spacing) - Math.floor(rmin / spacing); if (numSpaces > maxNumSpaces) { // If the calculated num of spaces exceeds maxNumSpaces, recalculate it spacing = helpers$1.niceNum(numSpaces * spacing / maxNumSpaces / unit) * unit; } - if (stepSize || isNullOrUndef(precision)) { + if (stepSize || isNullOrUndef$2(precision)) { // If a precision is not specified, calculate factor based on spacing factor = Math.pow(10, helpers$1._decimalPlaces(spacing)); } else { // If the user specified a precision, round to that number of decimal places factor = Math.pow(10, precision); @@ -11316,14 +12705,14 @@ niceMax = Math.ceil(rmax / spacing) * spacing; // If min, max and stepSize is set and they make an evenly spaced scale use it. if (stepSize) { // If very close to our whole number, use it. - if (!isNullOrUndef(min) && helpers$1.almostWhole(min / spacing, spacing / 1000)) { + if (!isNullOrUndef$2(min) && helpers$1.almostWhole(min / spacing, spacing / 1000)) { niceMin = min; } - if (!isNullOrUndef(max) && helpers$1.almostWhole(max / spacing, spacing / 1000)) { + if (!isNullOrUndef$2(max) && helpers$1.almostWhole(max / spacing, spacing / 1000)) { niceMax = max; } } numSpaces = (niceMax - niceMin) / spacing; @@ -11334,15 +12723,15 @@ numSpaces = Math.ceil(numSpaces); } niceMin = Math.round(niceMin * factor) / factor; niceMax = Math.round(niceMax * factor) / factor; - ticks.push(isNullOrUndef(min) ? niceMin : min); + ticks.push(isNullOrUndef$2(min) ? niceMin : min); for (var j = 1; j < numSpaces; ++j) { ticks.push(Math.round((niceMin + j * spacing) * factor) / factor); } - ticks.push(isNullOrUndef(max) ? niceMax : max); + ticks.push(isNullOrUndef$2(max) ? niceMax : max); return ticks; } var scale_linearbase = core_scale.extend({ @@ -11490,137 +12879,146 @@ var me = this; me.ticksAsNumbers = me.ticks.slice(); me.zeroLineIndex = me.ticks.indexOf(0); core_scale.prototype.convertTicksToLabels.call(me); + }, + + _configure: function() { + var me = this; + var ticks = me.getTicks(); + var start = me.min; + var end = me.max; + var offset; + + core_scale.prototype._configure.call(me); + + if (me.options.offset && ticks.length) { + offset = (end - start) / Math.max(ticks.length - 1, 1) / 2; + start -= offset; + end += offset; + } + me._startValue = start; + me._endValue = end; + me._valueRange = end - start; } }); var defaultConfig$1 = { position: 'left', ticks: { callback: core_ticks.formatters.linear } }; -var scale_linear = scale_linearbase.extend({ - determineDataLimits: function() { - var me = this; - var opts = me.options; - var chart = me.chart; - var data = chart.data; - var datasets = data.datasets; - var isHorizontal = me.isHorizontal(); - var DEFAULT_MIN = 0; - var DEFAULT_MAX = 1; +var DEFAULT_MIN = 0; +var DEFAULT_MAX = 1; - function IDMatches(meta) { - return isHorizontal ? meta.xAxisID === me.id : meta.yAxisID === me.id; - } +function getOrCreateStack(stacks, stacked, meta) { + var key = [ + meta.type, + // we have a separate stack for stack=undefined datasets when the opts.stacked is undefined + stacked === undefined && meta.stack === undefined ? meta.index : '', + meta.stack + ].join('.'); - // First Calculate the range - me.min = null; - me.max = null; + if (stacks[key] === undefined) { + stacks[key] = { + pos: [], + neg: [] + }; + } - var hasStacks = opts.stacked; - if (hasStacks === undefined) { - helpers$1.each(datasets, function(dataset, datasetIndex) { - if (hasStacks) { - return; - } + return stacks[key]; +} - var meta = chart.getDatasetMeta(datasetIndex); - if (chart.isDatasetVisible(datasetIndex) && IDMatches(meta) && - meta.stack !== undefined) { - hasStacks = true; - } - }); +function stackData(scale, stacks, meta, data) { + var opts = scale.options; + var stacked = opts.stacked; + var stack = getOrCreateStack(stacks, stacked, meta); + var pos = stack.pos; + var neg = stack.neg; + var ilen = data.length; + var i, value; + + for (i = 0; i < ilen; ++i) { + value = scale._parseValue(data[i]); + if (isNaN(value.min) || isNaN(value.max) || meta.data[i].hidden) { + continue; } - if (opts.stacked || hasStacks) { - var valuesPerStack = {}; + pos[i] = pos[i] || 0; + neg[i] = neg[i] || 0; - helpers$1.each(datasets, function(dataset, datasetIndex) { - var meta = chart.getDatasetMeta(datasetIndex); - var key = [ - meta.type, - // we have a separate stack for stack=undefined datasets when the opts.stacked is undefined - ((opts.stacked === undefined && meta.stack === undefined) ? datasetIndex : ''), - meta.stack - ].join('.'); + if (opts.relativePoints) { + pos[i] = 100; + } else if (value.min < 0 || value.max < 0) { + neg[i] += value.min; + } else { + pos[i] += value.max; + } + } +} - if (valuesPerStack[key] === undefined) { - valuesPerStack[key] = { - positiveValues: [], - negativeValues: [] - }; - } +function updateMinMax(scale, meta, data) { + var ilen = data.length; + var i, value; - // Store these per type - var positiveValues = valuesPerStack[key].positiveValues; - var negativeValues = valuesPerStack[key].negativeValues; + for (i = 0; i < ilen; ++i) { + value = scale._parseValue(data[i]); + if (isNaN(value.min) || isNaN(value.max) || meta.data[i].hidden) { + continue; + } - if (chart.isDatasetVisible(datasetIndex) && IDMatches(meta)) { - helpers$1.each(dataset.data, function(rawValue, index) { - var value = +me.getRightValue(rawValue); - if (isNaN(value) || meta.data[index].hidden) { - return; - } + scale.min = Math.min(scale.min, value.min); + scale.max = Math.max(scale.max, value.max); + } +} - positiveValues[index] = positiveValues[index] || 0; - negativeValues[index] = negativeValues[index] || 0; +var scale_linear = scale_linearbase.extend({ + determineDataLimits: function() { + var me = this; + var opts = me.options; + var chart = me.chart; + var datasets = chart.data.datasets; + var metasets = me._getMatchingVisibleMetas(); + var hasStacks = opts.stacked; + var stacks = {}; + var ilen = metasets.length; + var i, meta, data, values; - if (opts.relativePoints) { - positiveValues[index] = 100; - } else if (value < 0) { - negativeValues[index] += value; - } else { - positiveValues[index] += value; - } - }); - } - }); + me.min = Number.POSITIVE_INFINITY; + me.max = Number.NEGATIVE_INFINITY; - helpers$1.each(valuesPerStack, function(valuesForType) { - var values = valuesForType.positiveValues.concat(valuesForType.negativeValues); - var minVal = helpers$1.min(values); - var maxVal = helpers$1.max(values); - me.min = me.min === null ? minVal : Math.min(me.min, minVal); - me.max = me.max === null ? maxVal : Math.max(me.max, maxVal); - }); + if (hasStacks === undefined) { + for (i = 0; !hasStacks && i < ilen; ++i) { + meta = metasets[i]; + hasStacks = meta.stack !== undefined; + } + } - } else { - helpers$1.each(datasets, function(dataset, datasetIndex) { - var meta = chart.getDatasetMeta(datasetIndex); - if (chart.isDatasetVisible(datasetIndex) && IDMatches(meta)) { - helpers$1.each(dataset.data, function(rawValue, index) { - var value = +me.getRightValue(rawValue); - if (isNaN(value) || meta.data[index].hidden) { - return; - } - - if (me.min === null) { - me.min = value; - } else if (value < me.min) { - me.min = value; - } - - if (me.max === null) { - me.max = value; - } else if (value > me.max) { - me.max = value; - } - }); - } - }); + for (i = 0; i < ilen; ++i) { + meta = metasets[i]; + data = datasets[meta.index].data; + if (hasStacks) { + stackData(me, stacks, meta, data); + } else { + updateMinMax(me, meta, data); + } } - me.min = isFinite(me.min) && !isNaN(me.min) ? me.min : DEFAULT_MIN; - me.max = isFinite(me.max) && !isNaN(me.max) ? me.max : DEFAULT_MAX; + helpers$1.each(stacks, function(stackValues) { + values = stackValues.pos.concat(stackValues.neg); + me.min = Math.min(me.min, helpers$1.min(values)); + me.max = Math.max(me.max, helpers$1.max(values)); + }); + me.min = helpers$1.isFinite(me.min) && !isNaN(me.min) ? me.min : DEFAULT_MIN; + me.max = helpers$1.isFinite(me.max) && !isNaN(me.max) ? me.max : DEFAULT_MAX; + // Common base implementation to handle ticks.min, ticks.max, ticks.beginAtZero - this.handleTickRangeOptions(); + me.handleTickRangeOptions(); }, // Returns the maximum number of ticks based on the scale dimension _computeTickLimit: function() { var me = this; @@ -11640,74 +13038,62 @@ this.ticks.reverse(); } }, getLabelForIndex: function(index, datasetIndex) { - return +this.getRightValue(this.chart.data.datasets[datasetIndex].data[index]); + return this._getScaleLabel(this.chart.data.datasets[datasetIndex].data[index]); }, // Utils getPixelForValue: function(value) { - // This must be called after fit has been run so that - // this.left, this.top, this.right, and this.bottom have been defined var me = this; - var start = me.start; - - var rightValue = +me.getRightValue(value); - var pixel; - var range = me.end - start; - - if (me.isHorizontal()) { - pixel = me.left + (me.width / range * (rightValue - start)); - } else { - pixel = me.bottom - (me.height / range * (rightValue - start)); - } - return pixel; + return me.getPixelForDecimal((+me.getRightValue(value) - me._startValue) / me._valueRange); }, getValueForPixel: function(pixel) { - var me = this; - var isHorizontal = me.isHorizontal(); - var innerDimension = isHorizontal ? me.width : me.height; - var offset = (isHorizontal ? pixel - me.left : me.bottom - pixel) / innerDimension; - return me.start + ((me.end - me.start) * offset); + return this._startValue + this.getDecimalForPixel(pixel) * this._valueRange; }, getPixelForTick: function(index) { - return this.getPixelForValue(this.ticksAsNumbers[index]); + var ticks = this.ticksAsNumbers; + if (index < 0 || index > ticks.length - 1) { + return null; + } + return this.getPixelForValue(ticks[index]); } }); // INTERNAL: static default options, registered in src/index.js var _defaults$1 = defaultConfig$1; scale_linear._defaults = _defaults$1; -var valueOrDefault$a = helpers$1.valueOrDefault; +var valueOrDefault$b = helpers$1.valueOrDefault; +var log10 = helpers$1.math.log10; /** * Generate a set of logarithmic ticks * @param generationOptions the options used to generate the ticks * @param dataRange the range of the data * @returns {number[]} array of tick values */ function generateTicks$1(generationOptions, dataRange) { var ticks = []; - var tickVal = valueOrDefault$a(generationOptions.min, Math.pow(10, Math.floor(helpers$1.log10(dataRange.min)))); + var tickVal = valueOrDefault$b(generationOptions.min, Math.pow(10, Math.floor(log10(dataRange.min)))); - var endExp = Math.floor(helpers$1.log10(dataRange.max)); + var endExp = Math.floor(log10(dataRange.max)); var endSignificand = Math.ceil(dataRange.max / Math.pow(10, endExp)); var exp, significand; if (tickVal === 0) { - exp = Math.floor(helpers$1.log10(dataRange.minNotZero)); + exp = Math.floor(log10(dataRange.minNotZero)); significand = Math.floor(dataRange.minNotZero / Math.pow(10, exp)); ticks.push(tickVal); tickVal = significand * Math.pow(10, exp); } else { - exp = Math.floor(helpers$1.log10(tickVal)); + exp = Math.floor(log10(tickVal)); significand = Math.floor(tickVal / Math.pow(10, exp)); } var precision = exp < 0 ? Math.pow(10, Math.abs(exp)) : 1; do { @@ -11721,11 +13107,11 @@ } tickVal = Math.round(significand * Math.pow(10, exp) * precision) / precision; } while (exp < endExp || (exp === endExp && significand < endSignificand)); - var lastTick = valueOrDefault$a(generationOptions.max, tickVal); + var lastTick = valueOrDefault$b(generationOptions.max, tickVal); ticks.push(lastTick); return ticks; } @@ -11746,42 +13132,39 @@ var scale_logarithmic = core_scale.extend({ determineDataLimits: function() { var me = this; var opts = me.options; var chart = me.chart; - var data = chart.data; - var datasets = data.datasets; + var datasets = chart.data.datasets; var isHorizontal = me.isHorizontal(); function IDMatches(meta) { return isHorizontal ? meta.xAxisID === me.id : meta.yAxisID === me.id; } + var datasetIndex, meta, value, data, i, ilen; // Calculate Range - me.min = null; - me.max = null; - me.minNotZero = null; + me.min = Number.POSITIVE_INFINITY; + me.max = Number.NEGATIVE_INFINITY; + me.minNotZero = Number.POSITIVE_INFINITY; var hasStacks = opts.stacked; if (hasStacks === undefined) { - helpers$1.each(datasets, function(dataset, datasetIndex) { - if (hasStacks) { - return; - } - - var meta = chart.getDatasetMeta(datasetIndex); + for (datasetIndex = 0; datasetIndex < datasets.length; datasetIndex++) { + meta = chart.getDatasetMeta(datasetIndex); if (chart.isDatasetVisible(datasetIndex) && IDMatches(meta) && meta.stack !== undefined) { hasStacks = true; + break; } - }); + } } if (opts.stacked || hasStacks) { var valuesPerStack = {}; - helpers$1.each(datasets, function(dataset, datasetIndex) { - var meta = chart.getDatasetMeta(datasetIndex); + for (datasetIndex = 0; datasetIndex < datasets.length; datasetIndex++) { + meta = chart.getDatasetMeta(datasetIndex); var key = [ meta.type, // we have a separate stack for stack=undefined datasets when the opts.stacked is undefined ((opts.stacked === undefined && meta.stack === undefined) ? datasetIndex : ''), meta.stack @@ -11790,63 +13173,60 @@ if (chart.isDatasetVisible(datasetIndex) && IDMatches(meta)) { if (valuesPerStack[key] === undefined) { valuesPerStack[key] = []; } - helpers$1.each(dataset.data, function(rawValue, index) { + data = datasets[datasetIndex].data; + for (i = 0, ilen = data.length; i < ilen; i++) { var values = valuesPerStack[key]; - var value = +me.getRightValue(rawValue); + value = me._parseValue(data[i]); // invalid, hidden and negative values are ignored - if (isNaN(value) || meta.data[index].hidden || value < 0) { - return; + if (isNaN(value.min) || isNaN(value.max) || meta.data[i].hidden || value.min < 0 || value.max < 0) { + continue; } - values[index] = values[index] || 0; - values[index] += value; - }); + values[i] = values[i] || 0; + values[i] += value.max; + } } - }); + } helpers$1.each(valuesPerStack, function(valuesForType) { if (valuesForType.length > 0) { var minVal = helpers$1.min(valuesForType); var maxVal = helpers$1.max(valuesForType); - me.min = me.min === null ? minVal : Math.min(me.min, minVal); - me.max = me.max === null ? maxVal : Math.max(me.max, maxVal); + me.min = Math.min(me.min, minVal); + me.max = Math.max(me.max, maxVal); } }); } else { - helpers$1.each(datasets, function(dataset, datasetIndex) { - var meta = chart.getDatasetMeta(datasetIndex); + for (datasetIndex = 0; datasetIndex < datasets.length; datasetIndex++) { + meta = chart.getDatasetMeta(datasetIndex); if (chart.isDatasetVisible(datasetIndex) && IDMatches(meta)) { - helpers$1.each(dataset.data, function(rawValue, index) { - var value = +me.getRightValue(rawValue); + data = datasets[datasetIndex].data; + for (i = 0, ilen = data.length; i < ilen; i++) { + value = me._parseValue(data[i]); // invalid, hidden and negative values are ignored - if (isNaN(value) || meta.data[index].hidden || value < 0) { - return; + if (isNaN(value.min) || isNaN(value.max) || meta.data[i].hidden || value.min < 0 || value.max < 0) { + continue; } - if (me.min === null) { - me.min = value; - } else if (value < me.min) { - me.min = value; - } + me.min = Math.min(value.min, me.min); + me.max = Math.max(value.max, me.max); - if (me.max === null) { - me.max = value; - } else if (value > me.max) { - me.max = value; + if (value.min !== 0) { + me.minNotZero = Math.min(value.min, me.minNotZero); } - - if (value !== 0 && (me.minNotZero === null || value < me.minNotZero)) { - me.minNotZero = value; - } - }); + } } - }); + } } + me.min = helpers$1.isFinite(me.min) ? me.min : null; + me.max = helpers$1.isFinite(me.max) ? me.max : null; + me.minNotZero = helpers$1.isFinite(me.minNotZero) ? me.minNotZero : null; + // Common base implementation to handle ticks.min, ticks.max this.handleTickRangeOptions(); }, handleTickRangeOptions: function() { @@ -11858,30 +13238,30 @@ me.min = nonNegativeOrDefault(tickOpts.min, me.min); me.max = nonNegativeOrDefault(tickOpts.max, me.max); if (me.min === me.max) { if (me.min !== 0 && me.min !== null) { - me.min = Math.pow(10, Math.floor(helpers$1.log10(me.min)) - 1); - me.max = Math.pow(10, Math.floor(helpers$1.log10(me.max)) + 1); + me.min = Math.pow(10, Math.floor(log10(me.min)) - 1); + me.max = Math.pow(10, Math.floor(log10(me.max)) + 1); } else { me.min = DEFAULT_MIN; me.max = DEFAULT_MAX; } } if (me.min === null) { - me.min = Math.pow(10, Math.floor(helpers$1.log10(me.max)) - 1); + me.min = Math.pow(10, Math.floor(log10(me.max)) - 1); } if (me.max === null) { me.max = me.min !== 0 - ? Math.pow(10, Math.floor(helpers$1.log10(me.min)) + 1) + ? Math.pow(10, Math.floor(log10(me.min)) + 1) : DEFAULT_MAX; } if (me.minNotZero === null) { if (me.min > 0) { me.minNotZero = me.min; } else if (me.max < 1) { - me.minNotZero = Math.pow(10, Math.floor(helpers$1.log10(me.max))); + me.minNotZero = Math.pow(10, Math.floor(log10(me.max))); } else { me.minNotZero = DEFAULT_MIN; } } }, @@ -11921,126 +13301,90 @@ core_scale.prototype.convertTicksToLabels.call(this); }, // Get the correct tooltip label getLabelForIndex: function(index, datasetIndex) { - return +this.getRightValue(this.chart.data.datasets[datasetIndex].data[index]); + return this._getScaleLabel(this.chart.data.datasets[datasetIndex].data[index]); }, getPixelForTick: function(index) { - return this.getPixelForValue(this.tickValues[index]); + var ticks = this.tickValues; + if (index < 0 || index > ticks.length - 1) { + return null; + } + return this.getPixelForValue(ticks[index]); }, /** * Returns the value of the first tick. * @param {number} value - The minimum not zero value. * @return {number} The first tick value. * @private */ _getFirstTickValue: function(value) { - var exp = Math.floor(helpers$1.log10(value)); + var exp = Math.floor(log10(value)); var significand = Math.floor(value / Math.pow(10, exp)); return significand * Math.pow(10, exp); }, - getPixelForValue: function(value) { + _configure: function() { var me = this; - var tickOpts = me.options.ticks; - var reverse = tickOpts.reverse; - var log10 = helpers$1.log10; - var firstTickValue = me._getFirstTickValue(me.minNotZero); + var start = me.min; var offset = 0; - var innerDimension, pixel, start, end, sign; - value = +me.getRightValue(value); - if (reverse) { - start = me.end; - end = me.start; - sign = -1; - } else { - start = me.start; - end = me.end; - sign = 1; + core_scale.prototype._configure.call(me); + + if (start === 0) { + start = me._getFirstTickValue(me.minNotZero); + offset = valueOrDefault$b(me.options.ticks.fontSize, core_defaults.global.defaultFontSize) / me._length; } - if (me.isHorizontal()) { - innerDimension = me.width; - pixel = reverse ? me.right : me.left; - } else { - innerDimension = me.height; - sign *= -1; // invert, since the upper-left corner of the canvas is at pixel (0, 0) - pixel = reverse ? me.top : me.bottom; - } - if (value !== start) { - if (start === 0) { // include zero tick - offset = valueOrDefault$a(tickOpts.fontSize, core_defaults.global.defaultFontSize); - innerDimension -= offset; - start = firstTickValue; - } - if (value !== 0) { - offset += innerDimension / (log10(end) - log10(start)) * (log10(value) - log10(start)); - } - pixel += sign * offset; - } - return pixel; + + me._startValue = log10(start); + me._valueOffset = offset; + me._valueRange = (log10(me.max) - log10(start)) / (1 - offset); }, - getValueForPixel: function(pixel) { + getPixelForValue: function(value) { var me = this; - var tickOpts = me.options.ticks; - var reverse = tickOpts.reverse; - var log10 = helpers$1.log10; - var firstTickValue = me._getFirstTickValue(me.minNotZero); - var innerDimension, start, end, value; + var decimal = 0; - if (reverse) { - start = me.end; - end = me.start; - } else { - start = me.start; - end = me.end; + value = +me.getRightValue(value); + + if (value > me.min && value > 0) { + decimal = (log10(value) - me._startValue) / me._valueRange + me._valueOffset; } - if (me.isHorizontal()) { - innerDimension = me.width; - value = reverse ? me.right - pixel : pixel - me.left; - } else { - innerDimension = me.height; - value = reverse ? pixel - me.top : me.bottom - pixel; - } - if (value !== start) { - if (start === 0) { // include zero tick - var offset = valueOrDefault$a(tickOpts.fontSize, core_defaults.global.defaultFontSize); - value -= offset; - innerDimension -= offset; - start = firstTickValue; - } - value *= log10(end) - log10(start); - value /= innerDimension; - value = Math.pow(10, log10(start) + value); - } - return value; + return me.getPixelForDecimal(decimal); + }, + + getValueForPixel: function(pixel) { + var me = this; + var decimal = me.getDecimalForPixel(pixel); + return decimal === 0 && me.min === 0 + ? 0 + : Math.pow(10, me._startValue + (decimal - me._valueOffset) * me._valueRange); } }); // INTERNAL: static default options, registered in src/index.js var _defaults$2 = defaultConfig$2; scale_logarithmic._defaults = _defaults$2; -var valueOrDefault$b = helpers$1.valueOrDefault; +var valueOrDefault$c = helpers$1.valueOrDefault; var valueAtIndexOrDefault$1 = helpers$1.valueAtIndexOrDefault; -var resolve$7 = helpers$1.options.resolve; +var resolve$4 = helpers$1.options.resolve; var defaultConfig$3 = { display: true, // Boolean - Whether to animate scaling the chart from the centre animate: true, position: 'chartArea', angleLines: { display: true, - color: 'rgba(0, 0, 0, 0.1)', + color: 'rgba(0,0,0,0.1)', lineWidth: 1, borderDash: [], borderDashOffset: 0.0 }, @@ -12077,20 +13421,15 @@ return label; } } }; -function getValueCount(scale) { - var opts = scale.options; - return opts.angleLines.display || opts.pointLabels.display ? scale.chart.data.labels.length : 0; -} - function getTickBackdropHeight(opts) { var tickOpts = opts.ticks; if (tickOpts.display && opts.display) { - return valueOrDefault$b(tickOpts.fontSize, core_defaults.global.defaultFontSize) + tickOpts.backdropPaddingY * 2; + return valueOrDefault$c(tickOpts.fontSize, core_defaults.global.defaultFontSize) + tickOpts.backdropPaddingY * 2; } return 0; } function measureLabelSize(ctx, lineHeight, label) { @@ -12171,14 +13510,14 @@ var i, textSize, pointPosition; scale.ctx.font = plFont.string; scale._pointLabelSizes = []; - var valueCount = getValueCount(scale); + var valueCount = scale.chart.data.labels.length; for (i = 0; i < valueCount; i++) { pointPosition = scale.getPointPosition(i, scale.drawingArea + 5); - textSize = measureLabelSize(scale.ctx, plFont.lineHeight, scale.pointLabels[i] || ''); + textSize = measureLabelSize(scale.ctx, plFont.lineHeight, scale.pointLabels[i]); scale._pointLabelSizes[i] = textSize; // Add quarter circle to make degree 0 mean top of circle var angleRadians = scale.getIndexAngle(i); var angle = helpers$1.toDegrees(angleRadians) % 360; @@ -12242,65 +13581,42 @@ } function drawPointLabels(scale) { var ctx = scale.ctx; var opts = scale.options; - var angleLineOpts = opts.angleLines; - var gridLineOpts = opts.gridLines; var pointLabelOpts = opts.pointLabels; - var lineWidth = valueOrDefault$b(angleLineOpts.lineWidth, gridLineOpts.lineWidth); - var lineColor = valueOrDefault$b(angleLineOpts.color, gridLineOpts.color); var tickBackdropHeight = getTickBackdropHeight(opts); - - ctx.save(); - ctx.lineWidth = lineWidth; - ctx.strokeStyle = lineColor; - if (ctx.setLineDash) { - ctx.setLineDash(resolve$7([angleLineOpts.borderDash, gridLineOpts.borderDash, []])); - ctx.lineDashOffset = resolve$7([angleLineOpts.borderDashOffset, gridLineOpts.borderDashOffset, 0.0]); - } - var outerDistance = scale.getDistanceFromCenterForValue(opts.ticks.reverse ? scale.min : scale.max); - - // Point Label Font var plFont = helpers$1.options._parseFont(pointLabelOpts); + ctx.save(); + ctx.font = plFont.string; ctx.textBaseline = 'middle'; - for (var i = getValueCount(scale) - 1; i >= 0; i--) { - if (angleLineOpts.display && lineWidth && lineColor) { - var outerPosition = scale.getPointPosition(i, outerDistance); - ctx.beginPath(); - ctx.moveTo(scale.xCenter, scale.yCenter); - ctx.lineTo(outerPosition.x, outerPosition.y); - ctx.stroke(); - } + for (var i = scale.chart.data.labels.length - 1; i >= 0; i--) { + // Extra pixels out for some label spacing + var extra = (i === 0 ? tickBackdropHeight / 2 : 0); + var pointLabelPosition = scale.getPointPosition(i, outerDistance + extra + 5); - if (pointLabelOpts.display) { - // 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 = valueAtIndexOrDefault$1(pointLabelOpts.fontColor, i, core_defaults.global.defaultFontColor); + ctx.fillStyle = pointLabelFontColor; - // Keep this in loop since we may support array properties here - var pointLabelFontColor = valueAtIndexOrDefault$1(pointLabelOpts.fontColor, i, core_defaults.global.defaultFontColor); - ctx.fillStyle = pointLabelFontColor; - - var angleRadians = scale.getIndexAngle(i); - var angle = helpers$1.toDegrees(angleRadians); - ctx.textAlign = getTextAlignForAngle(angle); - adjustPointPositionForLabelHeight(angle, scale._pointLabelSizes[i], pointLabelPosition); - fillText(ctx, scale.pointLabels[i] || '', pointLabelPosition, plFont.lineHeight); - } + var angleRadians = scale.getIndexAngle(i); + var angle = helpers$1.toDegrees(angleRadians); + ctx.textAlign = getTextAlignForAngle(angle); + adjustPointPositionForLabelHeight(angle, scale._pointLabelSizes[i], pointLabelPosition); + fillText(ctx, scale.pointLabels[i], pointLabelPosition, plFont.lineHeight); } ctx.restore(); } function drawRadiusLine(scale, gridLineOpts, radius, index) { var ctx = scale.ctx; var circular = gridLineOpts.circular; - var valueCount = getValueCount(scale); + var valueCount = scale.chart.data.labels.length; var lineColor = valueAtIndexOrDefault$1(gridLineOpts.color, index - 1); var lineWidth = valueAtIndexOrDefault$1(gridLineOpts.lineWidth, index - 1); var pointPosition; if ((!circular && !valueCount) || !lineColor || !lineWidth) { @@ -12389,11 +13705,14 @@ var me = this; scale_linearbase.prototype.convertTicksToLabels.call(me); // Point labels - me.pointLabels = me.chart.data.labels.map(me.options.pointLabels.callback, me); + me.pointLabels = me.chart.data.labels.map(function() { + var label = helpers$1.callback(me.options.pointLabels.callback, arguments, me); + return label || label === 0 ? label : ''; + }); }, getLabelForIndex: function(index, datasetIndex) { return +this.getRightValue(this.chart.data.datasets[datasetIndex].data[index]); }, @@ -12441,26 +13760,26 @@ me.xCenter = Math.floor(((maxLeft + maxRight) / 2) + me.left); me.yCenter = Math.floor(((maxTop + maxBottom) / 2) + me.top + me.paddingTop); }, getIndexAngle: function(index) { - var angleMultiplier = (Math.PI * 2) / getValueCount(this); - var startAngle = this.chart.options && this.chart.options.startAngle ? - this.chart.options.startAngle : - 0; + var chart = this.chart; + var angleMultiplier = 360 / chart.data.labels.length; + var options = chart.options || {}; + var startAngle = options.startAngle || 0; - var startAngleRadians = startAngle * Math.PI * 2 / 360; - // Start from the top instead of right, so remove a quarter of the circle - return index * angleMultiplier + startAngleRadians; + var angle = (index * angleMultiplier + startAngle) % 360; + + return (angle < 0 ? angle + 360 : angle) * Math.PI * 2 / 360; }, getDistanceFromCenterForValue: function(value) { var me = this; - if (value === null) { - return 0; // null always in center + if (helpers$1.isNullOrUndef(value)) { + return NaN; } // Take into account half font size + the yPadding of the top value var scalingFactor = me.drawingArea / (me.max - me.min); if (me.options.ticks.reverse) { @@ -12480,128 +13799,179 @@ getPointPositionForValue: function(index, value) { return this.getPointPosition(index, this.getDistanceFromCenterForValue(value)); }, - getBasePosition: function() { + getBasePosition: function(index) { var me = this; var min = me.min; var max = me.max; - return me.getPointPositionForValue(0, + return me.getPointPositionForValue(index || 0, me.beginAtZero ? 0 : min < 0 && max < 0 ? max : min > 0 && max > 0 ? min : 0); }, - draw: function() { + /** + * @private + */ + _drawGrid: function() { var me = this; + var ctx = me.ctx; var opts = me.options; var gridLineOpts = opts.gridLines; + var angleLineOpts = opts.angleLines; + var lineWidth = valueOrDefault$c(angleLineOpts.lineWidth, gridLineOpts.lineWidth); + var lineColor = valueOrDefault$c(angleLineOpts.color, gridLineOpts.color); + var i, offset, position; + + if (opts.pointLabels.display) { + drawPointLabels(me); + } + + if (gridLineOpts.display) { + helpers$1.each(me.ticks, function(label, index) { + if (index !== 0) { + offset = me.getDistanceFromCenterForValue(me.ticksAsNumbers[index]); + drawRadiusLine(me, gridLineOpts, offset, index); + } + }); + } + + if (angleLineOpts.display && lineWidth && lineColor) { + ctx.save(); + ctx.lineWidth = lineWidth; + ctx.strokeStyle = lineColor; + if (ctx.setLineDash) { + ctx.setLineDash(resolve$4([angleLineOpts.borderDash, gridLineOpts.borderDash, []])); + ctx.lineDashOffset = resolve$4([angleLineOpts.borderDashOffset, gridLineOpts.borderDashOffset, 0.0]); + } + + for (i = me.chart.data.labels.length - 1; i >= 0; i--) { + offset = me.getDistanceFromCenterForValue(opts.ticks.reverse ? me.min : me.max); + position = me.getPointPosition(i, offset); + ctx.beginPath(); + ctx.moveTo(me.xCenter, me.yCenter); + ctx.lineTo(position.x, position.y); + ctx.stroke(); + } + + ctx.restore(); + } + }, + + /** + * @private + */ + _drawLabels: function() { + var me = this; + var ctx = me.ctx; + var opts = me.options; var tickOpts = opts.ticks; - if (opts.display) { - var ctx = me.ctx; - var startAngle = this.getIndexAngle(0); - var tickFont = helpers$1.options._parseFont(tickOpts); + if (!tickOpts.display) { + return; + } - if (opts.angleLines.display || opts.pointLabels.display) { - drawPointLabels(me); + var startAngle = me.getIndexAngle(0); + var tickFont = helpers$1.options._parseFont(tickOpts); + var tickFontColor = valueOrDefault$c(tickOpts.fontColor, core_defaults.global.defaultFontColor); + var offset, width; + + ctx.save(); + ctx.font = tickFont.string; + ctx.translate(me.xCenter, me.yCenter); + ctx.rotate(startAngle); + ctx.textAlign = 'center'; + ctx.textBaseline = 'middle'; + + helpers$1.each(me.ticks, function(label, index) { + if (index === 0 && !tickOpts.reverse) { + return; } - helpers$1.each(me.ticks, function(label, index) { - // Don't draw a centre value (if it is minimum) - if (index > 0 || tickOpts.reverse) { - var yCenterOffset = me.getDistanceFromCenterForValue(me.ticksAsNumbers[index]); + offset = me.getDistanceFromCenterForValue(me.ticksAsNumbers[index]); - // Draw circular lines around the scale - if (gridLineOpts.display && index !== 0) { - drawRadiusLine(me, gridLineOpts, yCenterOffset, index); - } + if (tickOpts.showLabelBackdrop) { + width = ctx.measureText(label).width; + ctx.fillStyle = tickOpts.backdropColor; - if (tickOpts.display) { - var tickFontColor = valueOrDefault$b(tickOpts.fontColor, core_defaults.global.defaultFontColor); - ctx.font = tickFont.string; + ctx.fillRect( + -width / 2 - tickOpts.backdropPaddingX, + -offset - tickFont.size / 2 - tickOpts.backdropPaddingY, + width + tickOpts.backdropPaddingX * 2, + tickFont.size + tickOpts.backdropPaddingY * 2 + ); + } - ctx.save(); - ctx.translate(me.xCenter, me.yCenter); - ctx.rotate(startAngle); + ctx.fillStyle = tickFontColor; + ctx.fillText(label, 0, -offset); + }); - if (tickOpts.showLabelBackdrop) { - var labelWidth = ctx.measureText(label).width; - ctx.fillStyle = tickOpts.backdropColor; - ctx.fillRect( - -labelWidth / 2 - tickOpts.backdropPaddingX, - -yCenterOffset - tickFont.size / 2 - tickOpts.backdropPaddingY, - labelWidth + tickOpts.backdropPaddingX * 2, - tickFont.size + tickOpts.backdropPaddingY * 2 - ); - } + ctx.restore(); + }, - ctx.textAlign = 'center'; - ctx.textBaseline = 'middle'; - ctx.fillStyle = tickFontColor; - ctx.fillText(label, 0, -yCenterOffset); - ctx.restore(); - } - } - }); - } - } + /** + * @private + */ + _drawTitle: helpers$1.noop }); // INTERNAL: static default options, registered in src/index.js var _defaults$3 = defaultConfig$3; scale_radialLinear._defaults = _defaults$3; -var valueOrDefault$c = helpers$1.valueOrDefault; +var deprecated$1 = helpers$1._deprecated; +var resolve$5 = helpers$1.options.resolve; +var valueOrDefault$d = helpers$1.valueOrDefault; // Integer constants are from the ES6 spec. var MIN_INTEGER = Number.MIN_SAFE_INTEGER || -9007199254740991; var MAX_INTEGER = Number.MAX_SAFE_INTEGER || 9007199254740991; var INTERVALS = { millisecond: { common: true, size: 1, - steps: [1, 2, 5, 10, 20, 50, 100, 250, 500] + steps: 1000 }, second: { common: true, size: 1000, - steps: [1, 2, 5, 10, 15, 30] + steps: 60 }, minute: { common: true, size: 60000, - steps: [1, 2, 5, 10, 15, 30] + steps: 60 }, hour: { common: true, size: 3600000, - steps: [1, 2, 3, 6, 12] + steps: 24 }, day: { common: true, size: 86400000, - steps: [1, 2, 5] + steps: 30 }, week: { common: false, size: 604800000, - steps: [1, 2, 3, 4] + steps: 4 }, month: { common: true, size: 2.628e9, - steps: [1, 2, 3] + steps: 12 }, quarter: { common: false, size: 7.884e9, - steps: [1, 2, 3, 4] + steps: 4 }, year: { common: true, size: 3.154e10 } @@ -12627,10 +13997,18 @@ } return out; } +function getMin(options) { + return helpers$1.valueOrDefault(options.time.min, options.ticks.min); +} + +function getMax(options) { + return helpers$1.valueOrDefault(options.time.max, options.ticks.max); +} + /** * Returns an array of {time, pos} objects used to interpolate a specific `time` or position * (`pos`) on the scale, by searching entries before and after the requested value. `pos` is * a decimal between 0 and 1: 0 being the start of the scale (left or top) and 1 the other * extremity (left + width or top + height). Note that it would be more optimized to directly @@ -12779,44 +14157,19 @@ return value; } /** - * Returns the number of unit to skip to be able to display up to `capacity` number of ticks - * in `unit` for the given `min` / `max` range and respecting the interval steps constraints. - */ -function determineStepSize(min, max, unit, capacity) { - var range = max - min; - var interval = INTERVALS[unit]; - var milliseconds = interval.size; - var steps = interval.steps; - var i, ilen, factor; - - if (!steps) { - return Math.ceil(range / (capacity * milliseconds)); - } - - for (i = 0, ilen = steps.length; i < ilen; ++i) { - factor = steps[i]; - if (Math.ceil(range / (milliseconds * factor)) <= capacity) { - break; - } - } - - return factor; -} - -/** * Figures out what unit results in an appropriate number of auto-generated ticks */ function determineUnitForAutoTicks(minUnit, min, max, capacity) { var ilen = UNITS.length; var i, interval, factor; for (i = UNITS.indexOf(minUnit); i < ilen - 1; ++i) { interval = INTERVALS[UNITS[i]]; - factor = interval.steps ? interval.steps[interval.steps.length - 1] : MAX_INTEGER; + factor = interval.steps ? interval.steps : MAX_INTEGER; if (interval.common && Math.ceil((max - min) / (factor * interval.size)) <= capacity) { return UNITS[i]; } } @@ -12825,17 +14178,16 @@ } /** * Figures out what unit to format a set of ticks with */ -function determineUnitForFormatting(scale, ticks, minUnit, min, max) { - var ilen = UNITS.length; +function determineUnitForFormatting(scale, numTicks, minUnit, min, max) { var i, unit; - for (i = ilen - 1; i >= UNITS.indexOf(minUnit); i--) { + for (i = UNITS.length - 1; i >= UNITS.indexOf(minUnit); i--) { unit = UNITS[i]; - if (INTERVALS[unit].common && scale._adapter.diff(max, min, unit) >= ticks.length) { + if (INTERVALS[unit].common && scale._adapter.diff(max, min, unit) >= numTicks - 1) { return unit; } } return UNITS[minUnit ? UNITS.indexOf(minUnit) : 0]; @@ -12849,64 +14201,46 @@ } } /** * Generates a maximum of `capacity` timestamps between min and max, rounded to the - * `minor` unit, aligned on the `major` unit and using the given scale time `options`. + * `minor` unit using the given scale time `options`. * Important: this method can return ticks outside the min and max range, it's the * responsibility of the calling code to clamp values if needed. */ function generate(scale, min, max, capacity) { var adapter = scale._adapter; var options = scale.options; var timeOpts = options.time; var minor = timeOpts.unit || determineUnitForAutoTicks(timeOpts.minUnit, min, max, capacity); - var major = determineMajorUnit(minor); - var stepSize = valueOrDefault$c(timeOpts.stepSize, timeOpts.unitStepSize); + var stepSize = resolve$5([timeOpts.stepSize, timeOpts.unitStepSize, 1]); var weekday = minor === 'week' ? timeOpts.isoWeekday : false; - var majorTicksEnabled = options.ticks.major.enabled; - var interval = INTERVALS[minor]; var first = min; - var last = max; var ticks = []; var time; - if (!stepSize) { - stepSize = determineStepSize(min, max, minor, capacity); - } - // For 'week' unit, handle the first day of week option if (weekday) { first = +adapter.startOf(first, 'isoWeek', weekday); - last = +adapter.startOf(last, 'isoWeek', weekday); } - // Align first/last ticks on unit + // Align first ticks on unit first = +adapter.startOf(first, weekday ? 'day' : minor); - last = +adapter.startOf(last, weekday ? 'day' : minor); - // Make sure that the last tick include max - if (last < max) { - last = +adapter.add(last, 1, minor); + // Prevent browser from freezing in case user options request millions of milliseconds + if (adapter.diff(max, min, minor) > 100000 * stepSize) { + throw min + ' and ' + max + ' are too far apart with stepSize of ' + stepSize + ' ' + minor; } - time = first; - - if (majorTicksEnabled && major && !weekday && !timeOpts.round) { - // Align the first tick on the previous `minor` unit aligned on the `major` unit: - // we first aligned time on the previous `major` unit then add the number of full - // stepSize there is between first and the previous major time. - time = +adapter.startOf(time, major); - time = +adapter.add(time, ~~((first - time) / (interval.size * stepSize)) * stepSize, minor); + for (time = first; time < max; time = +adapter.add(time, stepSize, minor)) { + ticks.push(time); } - for (; time < last; time = +adapter.add(time, stepSize, minor)) { - ticks.push(+time); + if (time === max || options.bounds === 'ticks') { + ticks.push(time); } - ticks.push(+time); - return ticks; } /** * Returns the start and end offsets from edges in the form of {start, end} @@ -12918,46 +14252,61 @@ var start = 0; var end = 0; var first, last; if (options.offset && ticks.length) { - if (!options.time.min) { - first = interpolate$1(table, 'time', ticks[0], 'pos'); - if (ticks.length === 1) { - start = 1 - first; - } else { - start = (interpolate$1(table, 'time', ticks[1], 'pos') - first) / 2; - } + first = interpolate$1(table, 'time', ticks[0], 'pos'); + if (ticks.length === 1) { + start = 1 - first; + } else { + start = (interpolate$1(table, 'time', ticks[1], 'pos') - first) / 2; } - if (!options.time.max) { - last = interpolate$1(table, 'time', ticks[ticks.length - 1], 'pos'); - if (ticks.length === 1) { - end = last; - } else { - end = (last - interpolate$1(table, 'time', ticks[ticks.length - 2], 'pos')) / 2; - } + last = interpolate$1(table, 'time', ticks[ticks.length - 1], 'pos'); + if (ticks.length === 1) { + end = last; + } else { + end = (last - interpolate$1(table, 'time', ticks[ticks.length - 2], 'pos')) / 2; } } - return {start: start, end: end}; + return {start: start, end: end, factor: 1 / (start + 1 + end)}; } +function setMajorTicks(scale, ticks, map, majorUnit) { + var adapter = scale._adapter; + var first = +adapter.startOf(ticks[0].value, majorUnit); + var last = ticks[ticks.length - 1].value; + var major, index; + + for (major = first; major <= last; major = +adapter.add(major, 1, majorUnit)) { + index = map[major]; + if (index >= 0) { + ticks[index].major = true; + } + } + return ticks; +} + function ticksFromTimestamps(scale, values, majorUnit) { var ticks = []; - var i, ilen, value, major; + var map = {}; + var ilen = values.length; + var i, value; - for (i = 0, ilen = values.length; i < ilen; ++i) { + for (i = 0; i < ilen; ++i) { value = values[i]; - major = majorUnit ? value === +scale._adapter.startOf(value, majorUnit) : false; + map[value] = i; ticks.push({ value: value, - major: major + major: false }); } - return ticks; + // We set the major ticks separately from the above loop because calling startOf for every tick + // is expensive when there is a large number of ticks + return (ilen === 0 || !majorUnit) ? ticks : setMajorTicks(scale, ticks, map, majorUnit); } var defaultConfig$4 = { position: 'bottom', @@ -12980,11 +14329,10 @@ bounds: 'data', adapters: {}, time: { parser: false, // false == a pattern string from https://momentjs.com/docs/#/parsing/string-format/ or a custom callback that converts its argument to a moment - format: false, // DEPRECATED false == date objects, moment object, callback or a pattern string from https://momentjs.com/docs/#/parsing/string-format/ unit: false, // false == automatic or override with week, month, year, etc. round: false, // none, or override with week, month, year, etc. displayFormat: false, // DEPRECATED isoWeekday: false, // override week start day - see https://momentjs.com/docs/#/get-set/iso-weekday/ minUnit: 'millisecond', @@ -13020,13 +14368,13 @@ var options = me.options; var time = options.time || (options.time = {}); var adapter = me._adapter = new core_adapters._date(options.adapters.date); // DEPRECATIONS: output a message only one time per update - if (time.format) { - console.warn('options.time.format is deprecated and replaced by options.time.parser.'); - } + deprecated$1('time scale', time.format, 'time.format', 'time.parser'); + deprecated$1('time scale', time.min, 'time.min', 'ticks.min'); + deprecated$1('time scale', time.max, 'time.max', 'ticks.max'); // Backward compatibility: before introducing adapter, `displayFormats` was // supposed to contain *all* unit/string pairs but this can't be resolved // when loading the scale (adapters are loaded afterward), so let's populate // missing formats on update @@ -13047,26 +14395,24 @@ determineDataLimits: function() { var me = this; var chart = me.chart; var adapter = me._adapter; - var timeOpts = me.options.time; - var unit = timeOpts.unit || 'day'; + var options = me.options; + var unit = options.time.unit || 'day'; var min = MAX_INTEGER; var max = MIN_INTEGER; var timestamps = []; var datasets = []; var labels = []; - var i, j, ilen, jlen, data, timestamp; - var dataLabels = chart.data.labels || []; + var i, j, ilen, jlen, data, timestamp, labelsAdded; + var dataLabels = me._getLabels(); - // Convert labels to timestamps for (i = 0, ilen = dataLabels.length; i < ilen; ++i) { labels.push(parse(me, dataLabels[i])); } - // Convert data to timestamps for (i = 0, ilen = (chart.data.datasets || []).length; i < ilen; ++i) { if (chart.isDatasetVisible(i)) { data = chart.data.datasets[i].data; // Let's consider that all data have the same format. @@ -13077,46 +14423,44 @@ timestamp = parse(me, data[j]); timestamps.push(timestamp); datasets[i][j] = timestamp; } } else { - for (j = 0, jlen = labels.length; j < jlen; ++j) { - timestamps.push(labels[j]); - } datasets[i] = labels.slice(0); + if (!labelsAdded) { + timestamps = timestamps.concat(labels); + labelsAdded = true; + } } } else { datasets[i] = []; } } if (labels.length) { - // Sort labels **after** data have been converted - labels = arrayUnique(labels).sort(sorter); min = Math.min(min, labels[0]); max = Math.max(max, labels[labels.length - 1]); } if (timestamps.length) { - timestamps = arrayUnique(timestamps).sort(sorter); + timestamps = ilen > 1 ? arrayUnique(timestamps).sort(sorter) : timestamps.sort(sorter); min = Math.min(min, timestamps[0]); max = Math.max(max, timestamps[timestamps.length - 1]); } - min = parse(me, timeOpts.min) || min; - max = parse(me, timeOpts.max) || max; + min = parse(me, getMin(options)) || min; + max = parse(me, getMax(options)) || max; // In case there is no valid min/max, set limits based on unit time option min = min === MAX_INTEGER ? +adapter.startOf(Date.now(), unit) : min; max = max === MIN_INTEGER ? +adapter.endOf(Date.now(), unit) + 1 : max; // Make sure that max is strictly higher than min (required by the lookup table) me.min = Math.min(min, max); me.max = Math.max(min + 1, max); // PRIVATE - me._horizontal = me.isHorizontal(); me._table = []; me._timestamps = { data: timestamps, datasets: datasets, labels: labels @@ -13126,35 +14470,35 @@ buildTicks: function() { var me = this; var min = me.min; var max = me.max; var options = me.options; + var tickOpts = options.ticks; var timeOpts = options.time; - var timestamps = []; + var timestamps = me._timestamps; var ticks = []; + var capacity = me.getLabelCapacity(min); + var source = tickOpts.source; + var distribution = options.distribution; var i, ilen, timestamp; - switch (options.ticks.source) { - case 'data': - timestamps = me._timestamps.data; - break; - case 'labels': - timestamps = me._timestamps.labels; - break; - case 'auto': - default: - timestamps = generate(me, min, max, me.getLabelCapacity(min), options); + if (source === 'data' || (source === 'auto' && distribution === 'series')) { + timestamps = timestamps.data; + } else if (source === 'labels') { + timestamps = timestamps.labels; + } else { + timestamps = generate(me, min, max, capacity); } if (options.bounds === 'ticks' && timestamps.length) { min = timestamps[0]; max = timestamps[timestamps.length - 1]; } // Enforce limits with user min/max options - min = parse(me, timeOpts.min) || min; - max = parse(me, timeOpts.max) || max; + min = parse(me, getMin(options)) || min; + max = parse(me, getMax(options)) || max; // Remove ticks outside the min/max range for (i = 0, ilen = timestamps.length; i < ilen; ++i) { timestamp = timestamps[i]; if (timestamp >= min && timestamp <= max) { @@ -13164,16 +14508,21 @@ me.min = min; me.max = max; // PRIVATE - me._unit = timeOpts.unit || determineUnitForFormatting(me, ticks, timeOpts.minUnit, me.min, me.max); - me._majorUnit = determineMajorUnit(me._unit); - me._table = buildLookupTable(me._timestamps.data, min, max, options.distribution); + // determineUnitForFormatting relies on the number of ticks so we don't use it when + // autoSkip is enabled because we don't yet know what the final number of ticks will be + me._unit = timeOpts.unit || (tickOpts.autoSkip + ? determineUnitForAutoTicks(timeOpts.minUnit, me.min, me.max, capacity) + : determineUnitForFormatting(me, ticks.length, timeOpts.minUnit, me.min, me.max)); + me._majorUnit = !tickOpts.major.enabled || me._unit === 'year' ? undefined + : determineMajorUnit(me._unit); + me._table = buildLookupTable(me._timestamps.data, min, max, distribution); me._offsets = computeOffsets(me._table, ticks, min, max, options); - if (options.ticks.reverse) { + if (tickOpts.reverse) { ticks.reverse(); } return ticksFromTimestamps(me, ticks, me._majorUnit); }, @@ -13208,16 +14557,21 @@ var options = me.options; var formats = options.time.displayFormats; var minorFormat = formats[me._unit]; var majorUnit = me._majorUnit; var majorFormat = formats[majorUnit]; - var majorTime = +adapter.startOf(time, majorUnit); - var majorTickOpts = options.ticks.major; - var major = majorTickOpts.enabled && majorUnit && majorFormat && time === majorTime; + var tick = ticks[index]; + var tickOpts = options.ticks; + var major = majorUnit && majorFormat && tick && tick.major; var label = adapter.format(time, format ? format : major ? majorFormat : minorFormat); - var tickOpts = major ? majorTickOpts : options.ticks.minor; - var formatter = valueOrDefault$c(tickOpts.callback, tickOpts.userCallback); + var nestedTickOpts = major ? tickOpts.major : tickOpts.minor; + var formatter = resolve$5([ + nestedTickOpts.callback, + nestedTickOpts.userCallback, + tickOpts.callback, + tickOpts.userCallback + ]); return formatter ? formatter(label, index, ticks) : label; }, convertTicksToLabels: function(ticks) { @@ -13234,17 +14588,13 @@ /** * @private */ getPixelForOffset: function(time) { var me = this; - var isReverse = me.options.ticks.reverse; - var size = me._horizontal ? me.width : me.height; - var start = me._horizontal ? isReverse ? me.right : me.left : isReverse ? me.bottom : me.top; + var offsets = me._offsets; var pos = interpolate$1(me._table, 'time', time, 'pos'); - var offset = size * (me._offsets.start + pos) / (me._offsets.start + 1 + me._offsets.end); - - return isReverse ? start - offset : start + offset; + return me.getPixelForDecimal((offsets.start + pos) * offsets.factor); }, getPixelForValue: function(value, index, datasetIndex) { var me = this; var time = null; @@ -13269,48 +14619,62 @@ null; }, getValueForPixel: function(pixel) { var me = this; - var size = me._horizontal ? me.width : me.height; - var start = me._horizontal ? me.left : me.top; - var pos = (size ? (pixel - start) / size : 0) * (me._offsets.start + 1 + me._offsets.start) - me._offsets.end; + var offsets = me._offsets; + var pos = me.getDecimalForPixel(pixel) / offsets.factor - offsets.end; var time = interpolate$1(me._table, 'pos', pos, 'time'); // DEPRECATION, we should return time directly return me._adapter._create(time); }, /** - * Crude approximation of what the label width might be * @private */ - getLabelWidth: function(label) { + _getLabelSize: function(label) { var me = this; var ticksOpts = me.options.ticks; var tickLabelWidth = me.ctx.measureText(label).width; - var angle = helpers$1.toRadians(ticksOpts.maxRotation); + var angle = helpers$1.toRadians(me.isHorizontal() ? ticksOpts.maxRotation : ticksOpts.minRotation); var cosRotation = Math.cos(angle); var sinRotation = Math.sin(angle); - var tickFontSize = valueOrDefault$c(ticksOpts.fontSize, core_defaults.global.defaultFontSize); + var tickFontSize = valueOrDefault$d(ticksOpts.fontSize, core_defaults.global.defaultFontSize); - return (tickLabelWidth * cosRotation) + (tickFontSize * sinRotation); + return { + w: (tickLabelWidth * cosRotation) + (tickFontSize * sinRotation), + h: (tickLabelWidth * sinRotation) + (tickFontSize * cosRotation) + }; }, /** + * Crude approximation of what the label width might be * @private */ + getLabelWidth: function(label) { + return this._getLabelSize(label).w; + }, + + /** + * @private + */ getLabelCapacity: function(exampleTime) { var me = this; + var timeOpts = me.options.time; + var displayFormats = timeOpts.displayFormats; // pick the longest format (milliseconds) for guestimation - var format = me.options.time.displayFormats.millisecond; - var exampleLabel = me.tickFormatFunction(exampleTime, 0, [], format); - var tickLabelWidth = me.getLabelWidth(exampleLabel); - var innerWidth = me.isHorizontal() ? me.width : me.height; - var capacity = Math.floor(innerWidth / tickLabelWidth); + var format = displayFormats[timeOpts.unit] || displayFormats.millisecond; + var exampleLabel = me.tickFormatFunction(exampleTime, 0, ticksFromTimestamps(me, [exampleTime], me._majorUnit), format); + var size = me._getLabelSize(exampleLabel); + var capacity = Math.floor(me.isHorizontal() ? me.width / size.w : me.height / size.h); + if (me.options.offset) { + capacity--; + } + return capacity > 0 ? capacity : 1; } }); // INTERNAL: static default options, registered in src/index.js @@ -13325,11 +14689,11 @@ time: scale_time }; var moment = createCommonjsModule(function (module, exports) { (function (global, factory) { - module.exports = factory(); + module.exports = factory() ; }(commonjsGlobal, (function () { var hookCallback; function hooks () { return hookCallback.apply(null, arguments); @@ -17961,11 +19325,11 @@ add: function(time, amount, unit) { return moment(time).add(amount, unit).valueOf(); }, diff: function(max, min, unit) { - return moment.duration(moment(max).diff(moment(min))).as(unit); + return moment(max).diff(moment(min), unit); }, startOf: function(time, unit, weekday) { time = moment(time); if (unit === 'isoWeek') { @@ -18016,10 +19380,16 @@ boundary: function(source) { var boundary = source.boundary; var x = boundary ? boundary.x : null; var y = boundary ? boundary.y : null; + if (helpers$1.isArray(boundary)) { + return function(point, i) { + return boundary[i]; + }; + } + return function(point) { return { x: x === null ? point.x : x, y: y === null ? point.y : y, }; @@ -18075,11 +19445,11 @@ default: return false; } } -function computeBoundary(source) { +function computeLinearBoundary(source) { var model = source.el._model || {}; var scale = source.el._scale || {}; var fill = source.fill; var target = null; var horizontal; @@ -18096,12 +19466,10 @@ target = model.scaleBottom === undefined ? scale.bottom : model.scaleBottom; } else if (fill === 'end') { target = model.scaleTop === undefined ? scale.top : model.scaleTop; } else if (model.scaleZero !== undefined) { target = model.scaleZero; - } else if (scale.getBasePosition) { - target = scale.getBasePosition(); } else if (scale.getBasePixel) { target = scale.getBasePixel(); } if (target !== undefined && target !== null) { @@ -18119,10 +19487,48 @@ } return null; } +function computeCircularBoundary(source) { + var scale = source.el._scale; + var options = scale.options; + var length = scale.chart.data.labels.length; + var fill = source.fill; + var target = []; + var start, end, center, i, point; + + if (!length) { + return null; + } + + start = options.ticks.reverse ? scale.max : scale.min; + end = options.ticks.reverse ? scale.min : scale.max; + center = scale.getPointPositionForValue(0, start); + for (i = 0; i < length; ++i) { + point = fill === 'start' || fill === 'end' + ? scale.getPointPositionForValue(i, fill === 'start' ? start : end) + : scale.getBasePosition(i); + if (options.gridLines.circular) { + point.cx = center.x; + point.cy = center.y; + point.angle = scale.getIndexAngle(i) - Math.PI / 2; + } + target.push(point); + } + return target; +} + +function computeBoundary(source) { + var scale = source.el._scale || {}; + + if (scale.getPointPositionForValue) { + return computeCircularBoundary(source); + } + return computeLinearBoundary(source); +} + function resolveTarget(sources, index, propagate) { var source = sources[index]; var fill = source.fill; var visited = [index]; var target; @@ -18170,11 +19576,11 @@ function isDrawable(point) { return point && !point.skip; } function drawArea(ctx, curve0, curve1, len0, len1) { - var i; + var i, cx, cy, r; if (!len0 || !len1) { return; } @@ -18182,10 +19588,20 @@ ctx.moveTo(curve0[0].x, curve0[0].y); for (i = 1; i < len0; ++i) { helpers$1.canvas.lineTo(ctx, curve0[i - 1], curve0[i]); } + if (curve1[0].angle !== undefined) { + cx = curve1[0].cx; + cy = curve1[0].cy; + r = Math.sqrt(Math.pow(curve1[0].x - cx, 2) + Math.pow(curve1[0].y - cy, 2)); + for (i = len1 - 1; i > 0; --i) { + ctx.arc(cx, cy, r, curve1[i].angle, curve1[i - 1].angle, true); + } + return; + } + // joining the two area curves ctx.lineTo(curve1[len1 - 1].x, curve1[len1 - 1].y); // building opposite area curve (reverse) for (i = len1 - 1; i > 0; --i) { @@ -18198,21 +19614,26 @@ var span = view.spanGaps; var curve0 = []; var curve1 = []; var len0 = 0; var len1 = 0; - var i, ilen, index, p0, p1, d0, d1; + var i, ilen, index, p0, p1, d0, d1, loopOffset; ctx.beginPath(); - for (i = 0, ilen = (count + !!loop); i < ilen; ++i) { + for (i = 0, ilen = count; i < ilen; ++i) { index = i % count; p0 = points[index]._view; p1 = mapper(p0, index, view); d0 = isDrawable(p0); d1 = isDrawable(p1); + if (loop && loopOffset === undefined && d0) { + loopOffset = i + 1; + ilen = count + loopOffset; + } + if (d0 && d1) { len0 = curve0.push(p0); len1 = curve1.push(p1); } else if (len0 && len1) { if (!span) { @@ -18275,38 +19696,46 @@ source.boundary = computeBoundary(source); source.mapper = createMapper(source); } }, - beforeDatasetDraw: function(chart, args) { - var meta = args.meta.$filler; - if (!meta) { - return; - } - + beforeDatasetsDraw: function(chart) { + var metasets = chart._getSortedVisibleDatasetMetas(); var ctx = chart.ctx; - var el = meta.el; - var view = el._view; - var points = el._children || []; - var mapper = meta.mapper; - var color = view.backgroundColor || core_defaults.global.defaultColor; + var meta, i, el, view, points, mapper, color; - if (mapper && color && points.length) { - helpers$1.canvas.clipArea(ctx, chart.chartArea); - doFill(ctx, points, mapper, view, color, el._loop); - helpers$1.canvas.unclipArea(ctx); + for (i = metasets.length - 1; i >= 0; --i) { + meta = metasets[i].$filler; + + if (!meta || !meta.visible) { + continue; + } + + el = meta.el; + view = el._view; + points = el._children || []; + mapper = meta.mapper; + color = view.backgroundColor || core_defaults.global.defaultColor; + + if (mapper && color && points.length) { + helpers$1.canvas.clipArea(ctx, chart.chartArea); + doFill(ctx, points, mapper, view, color, el._loop); + helpers$1.canvas.unclipArea(ctx); + } } } }; +var getRtlHelper$1 = helpers$1.rtl.getRtlAdapter; var noop$1 = helpers$1.noop; -var valueOrDefault$d = helpers$1.valueOrDefault; +var valueOrDefault$e = helpers$1.valueOrDefault; core_defaults._set('global', { legend: { display: true, position: 'top', + align: 'center', fullWidth: true, reverse: false, weight: 1000, // a callback that will handle @@ -18338,44 +19767,55 @@ // lineDash // lineDashOffset : // lineJoin : // lineWidth : generateLabels: function(chart) { - var data = chart.data; - return helpers$1.isArray(data.datasets) ? data.datasets.map(function(dataset, i) { + var datasets = chart.data.datasets; + var options = chart.options.legend || {}; + var usePointStyle = options.labels && options.labels.usePointStyle; + + return chart._getSortedDatasetMetas().map(function(meta) { + var style = meta.controller.getStyle(usePointStyle ? 0 : undefined); + return { - text: dataset.label, - fillStyle: (!helpers$1.isArray(dataset.backgroundColor) ? dataset.backgroundColor : dataset.backgroundColor[0]), - hidden: !chart.isDatasetVisible(i), - lineCap: dataset.borderCapStyle, - lineDash: dataset.borderDash, - lineDashOffset: dataset.borderDashOffset, - lineJoin: dataset.borderJoinStyle, - lineWidth: dataset.borderWidth, - strokeStyle: dataset.borderColor, - pointStyle: dataset.pointStyle, + text: datasets[meta.index].label, + fillStyle: style.backgroundColor, + hidden: !chart.isDatasetVisible(meta.index), + lineCap: style.borderCapStyle, + lineDash: style.borderDash, + lineDashOffset: style.borderDashOffset, + lineJoin: style.borderJoinStyle, + lineWidth: style.borderWidth, + strokeStyle: style.borderColor, + pointStyle: style.pointStyle, + rotation: style.rotation, // Below is extra data used for toggling the datasets - datasetIndex: i + datasetIndex: meta.index }; - }, this) : []; + }, this); } } }, legendCallback: function(chart) { - var text = []; - text.push('<ul class="' + chart.id + '-legend">'); - for (var i = 0; i < chart.data.datasets.length; i++) { - text.push('<li><span style="background-color:' + chart.data.datasets[i].backgroundColor + '"></span>'); - if (chart.data.datasets[i].label) { - text.push(chart.data.datasets[i].label); + var list = document.createElement('ul'); + var datasets = chart.data.datasets; + var i, ilen, listItem, listItemSpan; + + list.setAttribute('class', chart.id + '-legend'); + + for (i = 0, ilen = datasets.length; i < ilen; i++) { + listItem = list.appendChild(document.createElement('li')); + listItemSpan = listItem.appendChild(document.createElement('span')); + listItemSpan.style.backgroundColor = datasets[i].backgroundColor; + if (datasets[i].label) { + listItem.appendChild(document.createTextNode(datasets[i].label)); } - text.push('</li>'); } - text.push('</ul>'); - return text.join(''); + + return list.outerHTML; } }); /** * Helper function to get the box width based on the usePointStyle option @@ -18393,22 +19833,23 @@ * IMPORTANT: this class is exposed publicly as Chart.Legend, backward compatibility required! */ var Legend = core_element.extend({ initialize: function(config) { - helpers$1.extend(this, config); + var me = this; + helpers$1.extend(me, config); // Contains hit boxes for each dataset (in dataset order) - this.legendHitBoxes = []; + me.legendHitBoxes = []; /** * @private */ - this._hoveredItem = null; + me._hoveredItem = null; // Are we in doughnut mode which has a different data type - this.doughnutMode = false; + me.doughnutMode = false; }, // These methods are ordered by lifecycle. Utilities then follow. // Any function defined here is inherited by all legend types. // Any function can be extended by the legend type @@ -18527,83 +19968,86 @@ minSize.width = display ? 10 : 0; minSize.height = me.maxHeight; // fill all the height } // Increase sizes here - if (display) { - ctx.font = labelFont.string; + if (!display) { + me.width = minSize.width = me.height = minSize.height = 0; + return; + } + ctx.font = labelFont.string; - if (isHorizontal) { - // Labels + if (isHorizontal) { + // Labels - // Width of each line of legend boxes. Labels wrap onto multiple lines when there are too many to fit on one - var lineWidths = me.lineWidths = [0]; - var totalHeight = 0; + // Width of each line of legend boxes. Labels wrap onto multiple lines when there are too many to fit on one + var lineWidths = me.lineWidths = [0]; + var totalHeight = 0; - ctx.textAlign = 'left'; - ctx.textBaseline = 'top'; + ctx.textAlign = 'left'; + ctx.textBaseline = 'middle'; - helpers$1.each(me.legendItems, function(legendItem, i) { - var boxWidth = getBoxWidth(labelOpts, fontSize); - var width = boxWidth + (fontSize / 2) + ctx.measureText(legendItem.text).width; + helpers$1.each(me.legendItems, function(legendItem, i) { + var boxWidth = getBoxWidth(labelOpts, fontSize); + var width = boxWidth + (fontSize / 2) + ctx.measureText(legendItem.text).width; - if (i === 0 || lineWidths[lineWidths.length - 1] + width + labelOpts.padding > minSize.width) { - totalHeight += fontSize + labelOpts.padding; - lineWidths[lineWidths.length - (i > 0 ? 0 : 1)] = labelOpts.padding; - } + if (i === 0 || lineWidths[lineWidths.length - 1] + width + 2 * labelOpts.padding > minSize.width) { + totalHeight += fontSize + labelOpts.padding; + lineWidths[lineWidths.length - (i > 0 ? 0 : 1)] = 0; + } - // Store the hitbox width and height here. Final position will be updated in `draw` - hitboxes[i] = { - left: 0, - top: 0, - width: width, - height: fontSize - }; + // Store the hitbox width and height here. Final position will be updated in `draw` + hitboxes[i] = { + left: 0, + top: 0, + width: width, + height: fontSize + }; - lineWidths[lineWidths.length - 1] += width + labelOpts.padding; - }); + lineWidths[lineWidths.length - 1] += width + labelOpts.padding; + }); - minSize.height += totalHeight; + minSize.height += totalHeight; - } else { - var vPadding = labelOpts.padding; - var columnWidths = me.columnWidths = []; - var totalWidth = labelOpts.padding; - var currentColWidth = 0; - var currentColHeight = 0; - var itemHeight = fontSize + vPadding; + } else { + var vPadding = labelOpts.padding; + var columnWidths = me.columnWidths = []; + var columnHeights = me.columnHeights = []; + var totalWidth = labelOpts.padding; + var currentColWidth = 0; + var currentColHeight = 0; - helpers$1.each(me.legendItems, function(legendItem, i) { - var boxWidth = getBoxWidth(labelOpts, fontSize); - var itemWidth = boxWidth + (fontSize / 2) + ctx.measureText(legendItem.text).width; + helpers$1.each(me.legendItems, function(legendItem, i) { + var boxWidth = getBoxWidth(labelOpts, fontSize); + var itemWidth = boxWidth + (fontSize / 2) + ctx.measureText(legendItem.text).width; - // If too tall, go to new column - if (i > 0 && currentColHeight + itemHeight > minSize.height - vPadding) { - totalWidth += currentColWidth + labelOpts.padding; - columnWidths.push(currentColWidth); // previous column width + // If too tall, go to new column + if (i > 0 && currentColHeight + fontSize + 2 * vPadding > minSize.height) { + totalWidth += currentColWidth + labelOpts.padding; + columnWidths.push(currentColWidth); // previous column width + columnHeights.push(currentColHeight); + currentColWidth = 0; + currentColHeight = 0; + } - currentColWidth = 0; - currentColHeight = 0; - } + // Get max width + currentColWidth = Math.max(currentColWidth, itemWidth); + currentColHeight += fontSize + vPadding; - // Get max width - currentColWidth = Math.max(currentColWidth, itemWidth); - currentColHeight += itemHeight; + // Store the hitbox width and height here. Final position will be updated in `draw` + hitboxes[i] = { + left: 0, + top: 0, + width: itemWidth, + height: fontSize + }; + }); - // Store the hitbox width and height here. Final position will be updated in `draw` - hitboxes[i] = { - left: 0, - top: 0, - width: itemWidth, - height: fontSize - }; - }); - - totalWidth += currentColWidth; - columnWidths.push(currentColWidth); - minSize.width += totalWidth; - } + totalWidth += currentColWidth; + columnWidths.push(currentColWidth); + columnHeights.push(currentColHeight); + minSize.width += totalWidth; } me.width = minSize.width; me.height = minSize.height; }, @@ -18620,143 +20064,167 @@ var opts = me.options; var labelOpts = opts.labels; var globalDefaults = core_defaults.global; var defaultColor = globalDefaults.defaultColor; var lineDefault = globalDefaults.elements.line; + var legendHeight = me.height; + var columnHeights = me.columnHeights; var legendWidth = me.width; var lineWidths = me.lineWidths; - if (opts.display) { - var ctx = me.ctx; - var fontColor = valueOrDefault$d(labelOpts.fontColor, globalDefaults.defaultFontColor); - var labelFont = helpers$1.options._parseFont(labelOpts); - var fontSize = labelFont.size; - var cursor; + if (!opts.display) { + return; + } - // Canvas setup - ctx.textAlign = 'left'; - ctx.textBaseline = 'middle'; - ctx.lineWidth = 0.5; - ctx.strokeStyle = fontColor; // for strikethrough effect - ctx.fillStyle = fontColor; // render in correct colour - ctx.font = labelFont.string; + var rtlHelper = getRtlHelper$1(opts.rtl, me.left, me.minSize.width); + var ctx = me.ctx; + var fontColor = valueOrDefault$e(labelOpts.fontColor, globalDefaults.defaultFontColor); + var labelFont = helpers$1.options._parseFont(labelOpts); + var fontSize = labelFont.size; + var cursor; - var boxWidth = getBoxWidth(labelOpts, fontSize); - var hitboxes = me.legendHitBoxes; + // Canvas setup + ctx.textAlign = rtlHelper.textAlign('left'); + ctx.textBaseline = 'middle'; + ctx.lineWidth = 0.5; + ctx.strokeStyle = fontColor; // for strikethrough effect + ctx.fillStyle = fontColor; // render in correct colour + ctx.font = labelFont.string; - // current position - var drawLegendBox = function(x, y, legendItem) { - if (isNaN(boxWidth) || boxWidth <= 0) { - return; - } + var boxWidth = getBoxWidth(labelOpts, fontSize); + var hitboxes = me.legendHitBoxes; - // Set the ctx for the box - ctx.save(); + // current position + var drawLegendBox = function(x, y, legendItem) { + if (isNaN(boxWidth) || boxWidth <= 0) { + return; + } - var lineWidth = valueOrDefault$d(legendItem.lineWidth, lineDefault.borderWidth); - ctx.fillStyle = valueOrDefault$d(legendItem.fillStyle, defaultColor); - ctx.lineCap = valueOrDefault$d(legendItem.lineCap, lineDefault.borderCapStyle); - ctx.lineDashOffset = valueOrDefault$d(legendItem.lineDashOffset, lineDefault.borderDashOffset); - ctx.lineJoin = valueOrDefault$d(legendItem.lineJoin, lineDefault.borderJoinStyle); - ctx.lineWidth = lineWidth; - ctx.strokeStyle = valueOrDefault$d(legendItem.strokeStyle, defaultColor); + // Set the ctx for the box + ctx.save(); - if (ctx.setLineDash) { - // IE 9 and 10 do not support line dash - ctx.setLineDash(valueOrDefault$d(legendItem.lineDash, lineDefault.borderDash)); - } + var lineWidth = valueOrDefault$e(legendItem.lineWidth, lineDefault.borderWidth); + ctx.fillStyle = valueOrDefault$e(legendItem.fillStyle, defaultColor); + ctx.lineCap = valueOrDefault$e(legendItem.lineCap, lineDefault.borderCapStyle); + ctx.lineDashOffset = valueOrDefault$e(legendItem.lineDashOffset, lineDefault.borderDashOffset); + ctx.lineJoin = valueOrDefault$e(legendItem.lineJoin, lineDefault.borderJoinStyle); + ctx.lineWidth = lineWidth; + ctx.strokeStyle = valueOrDefault$e(legendItem.strokeStyle, defaultColor); - if (opts.labels && opts.labels.usePointStyle) { - // Recalculate x and y for drawPoint() because its expecting - // x and y to be center of figure (instead of top left) - var radius = boxWidth * Math.SQRT2 / 2; - var centerX = x + boxWidth / 2; - var centerY = y + fontSize / 2; + if (ctx.setLineDash) { + // IE 9 and 10 do not support line dash + ctx.setLineDash(valueOrDefault$e(legendItem.lineDash, lineDefault.borderDash)); + } - // Draw pointStyle as legend symbol - helpers$1.canvas.drawPoint(ctx, legendItem.pointStyle, radius, centerX, centerY); - } else { - // Draw box as legend symbol - if (lineWidth !== 0) { - ctx.strokeRect(x, y, boxWidth, fontSize); - } - ctx.fillRect(x, y, boxWidth, fontSize); + if (labelOpts && labelOpts.usePointStyle) { + // Recalculate x and y for drawPoint() because its expecting + // x and y to be center of figure (instead of top left) + var radius = boxWidth * Math.SQRT2 / 2; + var centerX = rtlHelper.xPlus(x, boxWidth / 2); + var centerY = y + fontSize / 2; + + // Draw pointStyle as legend symbol + helpers$1.canvas.drawPoint(ctx, legendItem.pointStyle, radius, centerX, centerY, legendItem.rotation); + } else { + // Draw box as legend symbol + ctx.fillRect(rtlHelper.leftForLtr(x, boxWidth), y, boxWidth, fontSize); + if (lineWidth !== 0) { + ctx.strokeRect(rtlHelper.leftForLtr(x, boxWidth), y, boxWidth, fontSize); } + } - ctx.restore(); - }; - var fillText = function(x, y, legendItem, textWidth) { - var halfFontSize = fontSize / 2; - var xLeft = boxWidth + halfFontSize + x; - var yMiddle = y + halfFontSize; + ctx.restore(); + }; - ctx.fillText(legendItem.text, xLeft, yMiddle); + var fillText = function(x, y, legendItem, textWidth) { + var halfFontSize = fontSize / 2; + var xLeft = rtlHelper.xPlus(x, boxWidth + halfFontSize); + var yMiddle = y + halfFontSize; - if (legendItem.hidden) { - // Strikethrough the text if hidden - ctx.beginPath(); - ctx.lineWidth = 2; - ctx.moveTo(xLeft, yMiddle); - ctx.lineTo(xLeft + textWidth, yMiddle); - ctx.stroke(); - } - }; + ctx.fillText(legendItem.text, xLeft, yMiddle); - // Horizontal - var isHorizontal = me.isHorizontal(); - if (isHorizontal) { - cursor = { - x: me.left + ((legendWidth - lineWidths[0]) / 2) + labelOpts.padding, - y: me.top + labelOpts.padding, - line: 0 - }; - } else { - cursor = { - x: me.left + labelOpts.padding, - y: me.top + labelOpts.padding, - line: 0 - }; + if (legendItem.hidden) { + // Strikethrough the text if hidden + ctx.beginPath(); + ctx.lineWidth = 2; + ctx.moveTo(xLeft, yMiddle); + ctx.lineTo(rtlHelper.xPlus(xLeft, textWidth), yMiddle); + ctx.stroke(); } + }; - var itemHeight = fontSize + labelOpts.padding; - helpers$1.each(me.legendItems, function(legendItem, i) { - var textWidth = ctx.measureText(legendItem.text).width; - var width = boxWidth + (fontSize / 2) + textWidth; - var x = cursor.x; - var y = cursor.y; + var alignmentOffset = function(dimension, blockSize) { + switch (opts.align) { + case 'start': + return labelOpts.padding; + case 'end': + return dimension - blockSize; + default: // center + return (dimension - blockSize + labelOpts.padding) / 2; + } + }; - // Use (me.left + me.minSize.width) and (me.top + me.minSize.height) - // instead of me.right and me.bottom because me.width and me.height - // may have been changed since me.minSize was calculated - if (isHorizontal) { - if (i > 0 && x + width + labelOpts.padding > me.left + me.minSize.width) { - y = cursor.y += itemHeight; - cursor.line++; - x = cursor.x = me.left + ((legendWidth - lineWidths[cursor.line]) / 2) + labelOpts.padding; - } - } else if (i > 0 && y + itemHeight > me.top + me.minSize.height) { - x = cursor.x = x + me.columnWidths[cursor.line] + labelOpts.padding; - y = cursor.y = me.top + labelOpts.padding; + // Horizontal + var isHorizontal = me.isHorizontal(); + if (isHorizontal) { + cursor = { + x: me.left + alignmentOffset(legendWidth, lineWidths[0]), + y: me.top + labelOpts.padding, + line: 0 + }; + } else { + cursor = { + x: me.left + labelOpts.padding, + y: me.top + alignmentOffset(legendHeight, columnHeights[0]), + line: 0 + }; + } + + helpers$1.rtl.overrideTextDirection(me.ctx, opts.textDirection); + + var itemHeight = fontSize + labelOpts.padding; + helpers$1.each(me.legendItems, function(legendItem, i) { + var textWidth = ctx.measureText(legendItem.text).width; + var width = boxWidth + (fontSize / 2) + textWidth; + var x = cursor.x; + var y = cursor.y; + + rtlHelper.setWidth(me.minSize.width); + + // Use (me.left + me.minSize.width) and (me.top + me.minSize.height) + // instead of me.right and me.bottom because me.width and me.height + // may have been changed since me.minSize was calculated + if (isHorizontal) { + if (i > 0 && x + width + labelOpts.padding > me.left + me.minSize.width) { + y = cursor.y += itemHeight; cursor.line++; + x = cursor.x = me.left + alignmentOffset(legendWidth, lineWidths[cursor.line]); } + } else if (i > 0 && y + itemHeight > me.top + me.minSize.height) { + x = cursor.x = x + me.columnWidths[cursor.line] + labelOpts.padding; + cursor.line++; + y = cursor.y = me.top + alignmentOffset(legendHeight, columnHeights[cursor.line]); + } - drawLegendBox(x, y, legendItem); + var realX = rtlHelper.x(x); - hitboxes[i].left = x; - hitboxes[i].top = y; + drawLegendBox(realX, y, legendItem); - // Fill the actual label - fillText(x, y, legendItem, textWidth); + hitboxes[i].left = rtlHelper.leftForLtr(realX, hitboxes[i].width); + hitboxes[i].top = y; - if (isHorizontal) { - cursor.x += width + labelOpts.padding; - } else { - cursor.y += itemHeight; - } + // Fill the actual label + fillText(realX, y, legendItem, textWidth); - }); - } + if (isHorizontal) { + cursor.x += width + labelOpts.padding; + } else { + cursor.y += itemHeight; + } + }); + + helpers$1.rtl.restoreTextDirection(me.ctx, opts.textDirection); }, /** * @private */ @@ -18990,27 +20458,24 @@ beforeFit: noop$2, fit: function() { var me = this; var opts = me.options; - var display = opts.display; - var minSize = me.minSize; - var lineCount = helpers$1.isArray(opts.text) ? opts.text.length : 1; - var fontOpts = helpers$1.options._parseFont(opts); - var textSize = display ? (lineCount * fontOpts.lineHeight) + (opts.padding * 2) : 0; + var minSize = me.minSize = {}; + var isHorizontal = me.isHorizontal(); + var lineCount, textSize; - if (me.isHorizontal()) { - minSize.width = me.maxWidth; // fill all the width - minSize.height = textSize; - } else { - minSize.width = textSize; - minSize.height = me.maxHeight; // fill all the height + if (!opts.display) { + me.width = minSize.width = me.height = minSize.height = 0; + return; } - me.width = minSize.width; - me.height = minSize.height; + lineCount = helpers$1.isArray(opts.text) ? opts.text.length : 1; + textSize = lineCount * helpers$1.options._parseFont(opts).lineHeight + opts.padding * 2; + me.width = minSize.width = isHorizontal ? me.maxWidth : textSize; + me.height = minSize.height = isHorizontal ? textSize : me.maxHeight; }, afterFit: noop$2, // Shared Methods isHorizontal: function() { @@ -19022,55 +20487,57 @@ draw: function() { var me = this; var ctx = me.ctx; var opts = me.options; - if (opts.display) { - var fontOpts = helpers$1.options._parseFont(opts); - var lineHeight = fontOpts.lineHeight; - var offset = lineHeight / 2 + opts.padding; - var rotation = 0; - var top = me.top; - var left = me.left; - var bottom = me.bottom; - var right = me.right; - var maxWidth, titleX, titleY; + if (!opts.display) { + return; + } - ctx.fillStyle = helpers$1.valueOrDefault(opts.fontColor, core_defaults.global.defaultFontColor); // render in correct colour - ctx.font = fontOpts.string; + var fontOpts = helpers$1.options._parseFont(opts); + var lineHeight = fontOpts.lineHeight; + var offset = lineHeight / 2 + opts.padding; + var rotation = 0; + var top = me.top; + var left = me.left; + var bottom = me.bottom; + var right = me.right; + var maxWidth, titleX, titleY; - // Horizontal - if (me.isHorizontal()) { - titleX = left + ((right - left) / 2); // midpoint of the width - titleY = top + offset; - maxWidth = right - left; - } else { - titleX = opts.position === 'left' ? left + offset : right - offset; - titleY = top + ((bottom - top) / 2); - maxWidth = bottom - top; - rotation = Math.PI * (opts.position === 'left' ? -0.5 : 0.5); - } + ctx.fillStyle = helpers$1.valueOrDefault(opts.fontColor, core_defaults.global.defaultFontColor); // render in correct colour + ctx.font = fontOpts.string; - ctx.save(); - ctx.translate(titleX, titleY); - ctx.rotate(rotation); - ctx.textAlign = 'center'; - ctx.textBaseline = 'middle'; + // Horizontal + if (me.isHorizontal()) { + titleX = left + ((right - left) / 2); // midpoint of the width + titleY = top + offset; + maxWidth = right - left; + } else { + titleX = opts.position === 'left' ? left + offset : right - offset; + titleY = top + ((bottom - top) / 2); + maxWidth = bottom - top; + rotation = Math.PI * (opts.position === 'left' ? -0.5 : 0.5); + } - var text = opts.text; - if (helpers$1.isArray(text)) { - var y = 0; - for (var i = 0; i < text.length; ++i) { - ctx.fillText(text[i], 0, y, maxWidth); - y += lineHeight; - } - } else { - ctx.fillText(text, 0, 0, maxWidth); - } + ctx.save(); + ctx.translate(titleX, titleY); + ctx.rotate(rotation); + ctx.textAlign = 'center'; + ctx.textBaseline = 'middle'; - ctx.restore(); + var text = opts.text; + if (helpers$1.isArray(text)) { + var y = 0; + for (var i = 0; i < text.length; ++i) { + ctx.fillText(text[i], 0, y, maxWidth); + y += lineHeight; + } + } else { + ctx.fillText(text, 0, 0, maxWidth); } + + ctx.restore(); } }); function createNewTitleBlockAndAttach(chart, titleOpts) { var title = new Title({ @@ -19138,10 +20605,10 @@ core_controller.helpers = helpers$1; // @todo dispatch these helpers into appropriated helpers/helpers.* file and write unit tests! -core_helpers(core_controller); +core_helpers(); core_controller._adapters = core_adapters; core_controller.Animation = core_animation; core_controller.animationService = core_animations; core_controller.controllers = controllers;