diff options
-rw-r--r-- | .gitignore | 2 | ||||
-rw-r--r-- | assets/css/main.css | 5 | ||||
-rwxr-xr-x | assets/js/chart.js | 7419 | ||||
-rwxr-xr-x | assets/js/main.js | 202 | ||||
-rwxr-xr-x | assets/php/getData.php | 7 | ||||
-rwxr-xr-x | index.html | 9 |
6 files changed, 4501 insertions, 3143 deletions
@@ -1 +1 @@ - .idea +.idea diff --git a/assets/css/main.css b/assets/css/main.css index 805975f..a022802 100644 --- a/assets/css/main.css +++ b/assets/css/main.css @@ -10,6 +10,11 @@ html, body { text-rendering: optimizeLegibility !important; } +canvas { + margin: 8px auto 0; + width: 100%; +} + .loading, .stats { display: none; } diff --git a/assets/js/chart.js b/assets/js/chart.js index d3c7232..c7c25bf 100755 --- a/assets/js/chart.js +++ b/assets/js/chart.js @@ -1,5 +1,5 @@ /*! - * Chart.js v2.8.0 + * Chart.js v2.9.3 * https://www.chartjs.org * (c) 2019 Chart.js Contributors * Released under the MIT License @@ -7,825 +7,1271 @@ (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 : {}; + + 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; + } - var conversions = { - rgb2hsl: rgb2hsl, - rgb2hsv: rgb2hsv, - rgb2hwb: rgb2hwb, - rgb2cmyk: rgb2cmyk, - rgb2keyword: rgb2keyword, - rgb2xyz: rgb2xyz, - rgb2lab: rgb2lab, - rgb2lch: rgb2lch, - - hsl2rgb: hsl2rgb, - hsl2hsv: hsl2hsv, - hsl2hwb: hsl2hwb, - hsl2cmyk: hsl2cmyk, - hsl2keyword: hsl2keyword, - - hsv2rgb: hsv2rgb, - hsv2hsl: hsv2hsl, - hsv2hwb: hsv2hwb, - hsv2cmyk: hsv2cmyk, - hsv2keyword: hsv2keyword, - - hwb2rgb: hwb2rgb, - hwb2hsl: hwb2hsl, - hwb2hsv: hwb2hsv, - hwb2cmyk: hwb2cmyk, - hwb2keyword: hwb2keyword, - - cmyk2rgb: cmyk2rgb, - cmyk2hsl: cmyk2hsl, - cmyk2hsv: cmyk2hsv, - cmyk2hwb: cmyk2hwb, - cmyk2keyword: cmyk2keyword, - - keyword2rgb: keyword2rgb, - keyword2hsl: keyword2hsl, - keyword2hsv: keyword2hsv, - keyword2hwb: keyword2hwb, - keyword2cmyk: keyword2cmyk, - keyword2lab: keyword2lab, - keyword2xyz: keyword2xyz, - - xyz2rgb: xyz2rgb, - xyz2lab: xyz2lab, - xyz2lch: xyz2lch, - - lab2xyz: lab2xyz, - lab2rgb: lab2rgb, - lab2lch: lab2lch, - - lch2lab: lch2lab, - lch2xyz: lch2xyz, - lch2rgb: lch2rgb + function getCjsExportFromNamespace(n) { + return n && n['default'] || n; + } + + 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] }; + var conversions = createCommonjsModule(function (module) { + /* MIT license */ - 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 (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; +// 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.) - h = Math.min(h * 60, 360); + var reverseKeywords = {}; + for (var key in colorName) { + if (colorName.hasOwnProperty(key)) { + reverseKeywords[colorName[key]] = key; + } + } - if (h < 0) - h += 360; + 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']} + }; - l = (min + max) / 2; +// 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); + } - if (max == min) - s = 0; - else if (l <= 0.5) - s = delta / (max + min); - else - s = delta / (2 - max - min); + if (!('labels' in convert[model])) { + throw new Error('missing channel labels property: ' + model); + } - return [h, s * 100, l * 100]; - } + if (convert[model].labels.length !== convert[model].channels) { + throw new Error('channel and label counts mismatch: ' + model); + } - 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; - - if (max == 0) - s = 0; - else - s = (delta / max * 1000) / 10; - - 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; - - h = Math.min(h * 60, 360); - - if (h < 0) - h += 360; - - v = ((max / 255) * 1000) / 10; - - return [h, s, v]; - } + 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}); + } + } - 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)); + 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; - return [h, w * 100, b * 100]; - } + 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; + } - function rgb2cmyk(rgb) { - var r = rgb[0] / 255, - g = rgb[1] / 255, - b = rgb[2] / 255, - c, m, y, 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]; - } + h = Math.min(h * 60, 360); - function rgb2keyword(rgb) { - return reverseKeywords[JSON.stringify(rgb)]; - } + if (h < 0) { + h += 360; + } - function rgb2xyz(rgb) { - var r = rgb[0] / 255, - g = rgb[1] / 255, - b = rgb[2] / 255; + l = (min + max) / 2; - // 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); + if (max === min) { + s = 0; + } else if (l <= 0.5) { + s = delta / (max + min); + } else { + s = delta / (2 - max - min); + } - 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); + return [h, s * 100, l * 100]; + }; - return [x * 100, y * 100, z * 100]; - } + convert.rgb.hsv = function (rgb) { + var rdif; + var gdif; + var bdif; + var h; + var s; + + 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; + }; - function rgb2lab(rgb) { - var xyz = rgb2xyz(rgb), - x = xyz[0], - y = xyz[1], - z = xyz[2], - l, a, b; + if (diff === 0) { + h = s = 0; + } else { + s = diff / v; + rdif = diffc(r); + gdif = diffc(g); + bdif = diffc(b); - x /= 95.047; - y /= 100; - z /= 108.883; + 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; + } + } - 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); + return [ + h * 360, + s * 100, + v * 100 + ]; + }; - l = (116 * y) - 16; - a = 500 * (x - y); - b = 200 * (y - z); + 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)); - return [l, a, b]; - } + b = 1 - 1 / 255 * Math.max(r, Math.max(g, b)); - function rgb2lch(args) { - return lab2lch(rgb2lab(args)); - } + return [h, w * 100, b * 100]; + }; - function hsl2rgb(hsl) { - var h = hsl[0] / 360, - s = hsl[1] / 100, - l = hsl[2] / 100, - t1, t2, t3, rgb, val; + 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]; + }; - if (s == 0) { - val = l * 255; - return [val, val, val]; + /** + * 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) + ); } - if (l < 0.5) - t2 = l * (1 + s); - else - t2 = l + s - l * s; - t1 = 2 * l - t2; - - rgb = [0, 0, 0]; - for (var i = 0; i < 3; i++) { - t3 = h + 1 / 3 * -(i - 1); - t3 < 0 && t3++; - t3 > 1 && t3--; - - 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; - - rgb[i] = val * 255; - } + convert.rgb.keyword = function (rgb) { + var reversed = reverseKeywords[rgb]; + if (reversed) { + return reversed; + } - return rgb; - } + var currentClosestDistance = Infinity; + var currentClosestKeyword; - function hsl2hsv(hsl) { - var h = hsl[0], - s = hsl[1] / 100, - l = hsl[2] / 100, - sv, v; + for (var keyword in colorName) { + if (colorName.hasOwnProperty(keyword)) { + var value = colorName[keyword]; - if (l === 0) { - // no need to do calc on black - // also avoids divide by 0 error - return [0, 0, 0]; - } + // Compute comparative distance + var distance = comparativeDistance(rgb, value); - l *= 2; - s *= (l <= 1) ? l : 2 - l; - v = (l + s) / 2; - sv = (2 * s) / (l + s); - return [h, sv * 100, v * 100]; - } + // Check if its less, if so set as closest + if (distance < currentClosestDistance) { + currentClosestDistance = distance; + currentClosestKeyword = keyword; + } + } + } - function hsl2hwb(args) { - return rgb2hwb(hsl2rgb(args)); - } + return currentClosestKeyword; + }; - function hsl2cmyk(args) { - return rgb2cmyk(hsl2rgb(args)); - } + convert.keyword.rgb = function (keyword) { + return colorName[keyword]; + }; - function hsl2keyword(args) { - return rgb2keyword(hsl2rgb(args)); - } + convert.rgb.xyz = function (rgb) { + var r = rgb[0] / 255; + var g = rgb[1] / 255; + var b = rgb[2] / 255; + // 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 hsv2rgb(hsv) { - var h = hsv[0] / 60, - s = hsv[1] / 100, - v = hsv[2] / 100, - hi = Math.floor(h) % 6; - - 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; - - 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]; - } - } + 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 hsv2hsl(hsv) { - var h = hsv[0], - s = hsv[1] / 100, - v = hsv[2] / 100, - sl, l; - - l = (2 - s) * v; - sl = s * v; - sl /= (l <= 1) ? l : 2 - l; - sl = sl || 0; - l /= 2; - return [h, sl * 100, l * 100]; - } + return [x * 100, y * 100, z * 100]; + }; - function hsv2hwb(args) { - return rgb2hwb(hsv2rgb(args)) - } + 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; - function hsv2cmyk(args) { - return rgb2cmyk(hsv2rgb(args)); - } + x /= 95.047; + y /= 100; + z /= 108.883; - function hsv2keyword(args) { - return rgb2keyword(hsv2rgb(args)); - } + 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); + + l = (116 * y) - 16; + a = 500 * (x - y); + b = 200 * (y - z); + + return [l, a, b]; + }; + + 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; + + if (s === 0) { + val = l * 255; + return [val, val, val]; + } + + if (l < 0.5) { + t2 = l * (1 + s); + } else { + t2 = l + s - l * s; + } + + t1 = 2 * l - t2; + + 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--; + } + + 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; + } + + rgb[i] = val * 255; + } + + return rgb; + }; + + 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; + + 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); + + return [h, sv * 100, v * 100]; + }; + + 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; + + 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; + + 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]; + } + }; + + 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; + + 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; - - // wh + bl cant be > 1 - if (ratio > 1) { - wh /= ratio; - bl /= ratio; - } + 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; - 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 + // wh + bl cant be > 1 + if (ratio > 1) { + wh /= ratio; + bl /= ratio; + } - 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; - } + i = Math.floor(6 * h); + v = 1 - bl; + f = 6 * h - i; - return [r * 255, g * 255, b * 255]; - } + if ((i & 0x01) !== 0) { + f = 1 - f; + } - function hwb2hsl(args) { - return rgb2hsl(hwb2rgb(args)); - } + n = wh + f * (v - wh); // linear interpolation - function hwb2hsv(args) { - return rgb2hsv(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 hwb2cmyk(args) { - return rgb2cmyk(hwb2rgb(args)); - } + return [r * 255, g * 255, b * 255]; + }; - function hwb2keyword(args) { - return rgb2keyword(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 cmyk2rgb(cmyk) { - var c = cmyk[0] / 100, - m = cmyk[1] / 100, - y = cmyk[2] / 100, - k = cmyk[3] / 100, - r, g, b; - - 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]; - } + 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 cmyk2hsl(args) { - return rgb2hsl(cmyk2rgb(args)); - } + return [r * 255, g * 255, b * 255]; + }; - function cmyk2hsv(args) { - return rgb2hsv(cmyk2rgb(args)); - } + 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 cmyk2hwb(args) { - return rgb2hwb(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 cmyk2keyword(args) { - return rgb2keyword(cmyk2rgb(args)); - } + // assume sRGB + r = r > 0.0031308 + ? ((1.055 * Math.pow(r, 1.0 / 2.4)) - 0.055) + : r * 12.92; + g = g > 0.0031308 + ? ((1.055 * Math.pow(g, 1.0 / 2.4)) - 0.055) + : g * 12.92; - function xyz2rgb(xyz) { - var x = xyz[0] / 100, - y = xyz[1] / 100, - z = xyz[2] / 100, - r, g, b; + b = b > 0.0031308 + ? ((1.055 * Math.pow(b, 1.0 / 2.4)) - 0.055) + : b * 12.92; - 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); + r = Math.min(Math.max(0, r), 1); + g = Math.min(Math.max(0, g), 1); + b = Math.min(Math.max(0, b), 1); - // assume sRGB - r = r > 0.0031308 ? ((1.055 * Math.pow(r, 1.0 / 2.4)) - 0.055) - : r = (r * 12.92); + return [r * 255, g * 255, b * 255]; + }; - g = g > 0.0031308 ? ((1.055 * Math.pow(g, 1.0 / 2.4)) - 0.055) - : g = (g * 12.92); + convert.xyz.lab = function (xyz) { + var x = xyz[0]; + var y = xyz[1]; + var z = xyz[2]; + var l; + var a; + var b; - b = b > 0.0031308 ? ((1.055 * Math.pow(b, 1.0 / 2.4)) - 0.055) - : b = (b * 12.92); + x /= 95.047; + y /= 100; + z /= 108.883; - r = Math.min(Math.max(0, r), 1); - g = Math.min(Math.max(0, g), 1); - b = Math.min(Math.max(0, b), 1); + 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); - return [r * 255, g * 255, b * 255]; - } + l = (116 * y) - 16; + a = 500 * (x - y); + b = 200 * (y - z); - function xyz2lab(xyz) { - var x = xyz[0], - y = xyz[1], - z = xyz[2], - l, a, b; + return [l, a, b]; + }; - x /= 95.047; - y /= 100; - z /= 108.883; + convert.lab.xyz = function (lab) { + var l = lab[0]; + var a = lab[1]; + var b = lab[2]; + var x; + var y; + var z; + + y = (l + 16) / 116; + x = a / 500 + y; + z = y - b / 200; + + 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 *= 95.047; + y *= 100; + z *= 108.883; + + return [x, y, z]; + }; - 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.lab.lch = function (lab) { + var l = lab[0]; + var a = lab[1]; + var b = lab[2]; + var hr; + var h; + var c; - l = (116 * y) - 16; - a = 500 * (x - y); - b = 200 * (y - z); + hr = Math.atan2(b, a); + h = hr * 360 / 2 / Math.PI; - return [l, a, b]; - } + if (h < 0) { + h += 360; + } - function xyz2lch(args) { - return lab2lch(xyz2lab(args)); - } + c = Math.sqrt(a * a + b * b); - function lab2xyz(lab) { - var l = lab[0], - a = lab[1], - b = lab[2], - x, y, z, y2; + return [l, c, h]; + }; - 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); - } + convert.lch.lab = function (lch) { + var l = lch[0]; + var c = lch[1]; + var h = lch[2]; + var a; + var b; + var hr; - x = x / 95.047 <= 0.008856 ? x = (95.047 * ((a / 500) + y2 - (16 / 116))) / 7.787 : 95.047 * Math.pow((a / 500) + y2, 3); + hr = h / 360 * 2 * Math.PI; + a = c * Math.cos(hr); + b = c * Math.sin(hr); - z = z / 108.883 <= 0.008859 ? z = (108.883 * (y2 - (b / 200) - (16 / 116))) / 7.787 : 108.883 * Math.pow(y2 - (b / 200), 3); + return [l, a, b]; + }; - return [x, y, z]; - } + 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 + + value = Math.round(value / 50); + + if (value === 0) { + return 30; + } + + var ansi = 30 + + ((Math.round(b / 255) << 2) + | (Math.round(g / 255) << 1) + | Math.round(r / 255)); + + if (value === 2) { + ansi += 60; + } + + return ansi; + }; + + 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]); + }; + + convert.rgb.ansi256 = function (args) { + var r = args[0]; + var g = args[1]; + var b = args[2]; + + // 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; + } + + if (r > 248) { + return 231; + } + + return Math.round(((r - 8) / 247) * 24) + 232; + } + + var ansi = 16 + + (36 * Math.round(r / 255 * 5)) + + (6 * Math.round(g / 255 * 5)) + + Math.round(b / 255 * 5); + + return ansi; + }; + + 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]; + }; + + convert.ansi256.rgb = function (args) { + // handle greyscale + if (args >= 232) { + var c = (args - 232) * 10 + 8; + return [c, c, c]; + } + + 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]; + }; + + 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 string = integer.toString(16).toUpperCase(); + return '000000'.substring(string.length) + string; + }; + + 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]; + } + + var colorString = match[0]; + + 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; + + return [r, g, b]; + }; + + 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; + + 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]; + }; + + 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]; + }; + + convert.hsv.hcg = function (hsv) { + var s = hsv[1] / 100; + var v = hsv[2] / 100; + + var c = s * v; + var f = 0; + + if (c < 1.0) { + f = (v - c) / (1 - c); + } + + return [hsv[0], c * 100, f * 100]; + }; + + 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; - function lab2lch(lab) { - var l = lab[0], - a = lab[1], - b = lab[2], - hr, h, c; + 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]; + }; - hr = Math.atan2(b, a); - h = hr * 360 / 2 / Math.PI; - if (h < 0) { - h += 360; + 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 + }; } - c = Math.sqrt(a * a + b * b); - return [l, c, h]; - } - function lab2rgb(args) { - return xyz2rgb(lab2xyz(args)); + return graph; } - function lch2lab(lch) { - var l = lch[0], - c = lch[1], - h = lch[2], - a, b, hr; +// https://en.wikipedia.org/wiki/Breadth-first_search + function deriveBFS(fromModel) { + var graph = buildGraph(); + var queue = [fromModel]; // unshift -> queue -> pop - hr = h / 360 * 2 * Math.PI; - a = c * Math.cos(hr); - b = c * Math.sin(hr); - return [l, a, b]; - } + graph[fromModel].distance = 0; - function lch2xyz(args) { - return lab2xyz(lch2lab(args)); - } + while (queue.length) { + var current = queue.pop(); + var adjacents = Object.keys(conversions[current]); - function lch2rgb(args) { - return lab2rgb(lch2lab(args)); - } + for (var len = adjacents.length, i = 0; i < len; i++) { + var adjacent = adjacents[i]; + var node = graph[adjacent]; - function keyword2rgb(keyword) { - return cssKeywords[keyword]; - } + if (node.distance === -1) { + node.distance = graph[current].distance + 1; + node.parent = current; + queue.unshift(adjacent); + } + } + } - function keyword2hsl(args) { - return rgb2hsl(keyword2rgb(args)); + return graph; } - function keyword2hsv(args) { - return rgb2hsv(keyword2rgb(args)); + function link(from, to) { + return function (args) { + return to(from(args)); + }; } - function keyword2hwb(args) { - return rgb2hwb(keyword2rgb(args)); - } + function wrapConversion(toModel, graph) { + var path = [graph[toModel].parent, toModel]; + var fn = conversions[graph[toModel].parent][toModel]; - function keyword2cmyk(args) { - return rgb2cmyk(keyword2rgb(args)); - } + 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; + } - function keyword2lab(args) { - return rgb2lab(keyword2rgb(args)); + fn.conversion = path; + return fn; } - function keyword2xyz(args) { - return rgb2xyz(keyword2rgb(args)); - } + var route = function (fromModel) { + var graph = deriveBFS(fromModel); + var conversion = {}; - 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] - }; + var models = Object.keys(graph); + for (var len = models.length, i = 0; i < len; i++) { + var toModel = models[i]; + var node = graph[toModel]; - var reverseKeywords = {}; - for (var key in cssKeywords) { - reverseKeywords[JSON.stringify(cssKeywords[key])] = key; - } + if (node.parent === null) { + // no possible conversion, or this node is the source model. + continue; + } + + conversion[toModel] = wrapConversion(toModel, graph); + } - var convert = function () { - return new Converter(); + return conversion; }; - 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); + var convert = {}; + + var models = Object.keys(conversions); + + function wrapRaw(fn) { + var wrappedFn = function (args) { + if (args === undefined || args === null) { + return args; } - })(func); - var pair = /(\w+)2(\w+)/.exec(func), - from = pair[1], - to = pair[2]; + if (arguments.length > 1) { + args = Array.prototype.slice.call(arguments); + } - // export rgb2hsl and ["rgb"]["hsl"] - convert[from] = convert[from] || {}; + return fn(args); + }; - convert[from][to] = convert[func] = (function (func) { - return function (arg) { - if (typeof arg == "number") - arg = Array.prototype.slice.call(arguments); + // preserve .conversion property if there is one + if ('conversion' in fn) { + wrappedFn.conversion = fn.conversion; + } - var val = conversions[func](arg); - if (typeof val == "string" || val === undefined) - return val; // keyword + return wrappedFn; + } - for (var i = 0; i < val.length; i++) - val[i] = Math.round(val[i]); - return val; + function wrapRounded(fn) { + var wrappedFn = function (args) { + if (args === undefined || args === null) { + return args; } - })(func); - } + if (arguments.length > 1) { + args = Array.prototype.slice.call(arguments); + } - /* Converter does lazy conversion and caching */ - var Converter = function () { - this.convs = {}; - }; + var result = fn(args); - /* 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); + // 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 this.setValues(space, values); - }; + return wrappedFn; + } - /* 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; - }; + models.forEach(function (fromModel) { + convert[fromModel] = {}; - /* 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); - - this.convs[space] = vals; - } - return vals; - }; + Object.defineProperty(convert[fromModel], 'channels', {value: conversions[fromModel].channels}); + Object.defineProperty(convert[fromModel], 'labels', {value: conversions[fromModel].labels}); - ["rgb", "hsl", "hsv", "cmyk", "keyword"].forEach(function (space) { - Converter.prototype[space] = function (vals) { - return this.routeSpace(space, arguments); - }; + 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], @@ -1044,7 +1490,7 @@ if (match[1] == "transparent") { return [0, 0, 0, 0]; } - rgb = colorName[match[1]]; + rgb = colorName$1[match[1]]; if (!rgb) { return; } @@ -1203,8 +1649,8 @@ //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 */ @@ -1968,14 +2414,12 @@ * @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]; + } + }); }, /** @@ -2001,6 +2445,13 @@ 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'); + } } }; @@ -2362,7 +2813,11 @@ 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; } } @@ -2554,6 +3009,8 @@ } }; +// 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', @@ -2612,8 +3069,6 @@ case '%': value /= 100; break; - default: - break; } return size * value; @@ -2678,9 +3133,12 @@ * 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) { @@ -2690,24 +3148,161 @@ } 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); @@ -2764,6 +3359,7 @@ }; helpers$1.extend(Element.prototype, { + _type: undefined, initialize: function () { this.hidden = false; @@ -2772,7 +3368,7 @@ 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; @@ -2786,7 +3382,7 @@ // No animation -> No Transition if (!model || ease === 1) { - me._view = model; + me._view = helpers$1.extend({}, model); me._start = null; return me; } @@ -2820,7 +3416,7 @@ 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 @@ -2831,7 +3427,7 @@ onAnimationComplete: null, // user specified callback to fire when the animation finishes }); - var core_animation = exports$2; + var core_animation = exports$3; // DEPRECATIONS @@ -2841,7 +3437,7 @@ * @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; } @@ -2853,7 +3449,7 @@ * @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; }, @@ -3071,12 +3667,42 @@ */ 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) { @@ -3086,13 +3712,16 @@ 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; } }, @@ -3137,7 +3766,7 @@ }, reset: function () { - this.update(true); + this._update(true); }, /** @@ -3213,6 +3842,31 @@ 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) { @@ -3245,6 +3899,127 @@ } }, + /** + * 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; @@ -3271,6 +4046,42 @@ /** * @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(); @@ -3336,6 +4147,8 @@ var core_datasetController = DatasetController; + var TAU = Math.PI * 2; + core_defaults._set('global', { elements: { arc: { @@ -3347,7 +4160,84 @@ } }); + 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; @@ -3369,13 +4259,13 @@ 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 @@ -3416,51 +4306,44 @@ 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(); @@ -3489,6 +4372,8 @@ }); var element_line = core_element.extend({ + _type: 'line', + draw: function () { var me = this; var vm = me._view; @@ -3498,11 +4383,27 @@ 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(); @@ -3522,35 +4423,34 @@ // 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) { - ctx.moveTo(currentVM.x, currentVM.y); - lastDrawnIndex = index; - } - } else { - previous = lastDrawnIndex === -1 ? previous : points[lastDrawnIndex]; + 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); - } else { - // Line to next point - helpers$1.canvas.lineTo(ctx, previous._view, current._view); - } - lastDrawnIndex = index; + 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, currentVM); } + lastDrawnIndex = index; } } + if (closePath) { + ctx.closePath(); + } + ctx.stroke(); ctx.restore(); } @@ -3587,6 +4487,8 @@ } 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; @@ -3769,6 +4671,8 @@ } var element_rectangle = core_element.extend({ + _type: 'rectangle', + draw: function () { var ctx = this._chart.ctx; var vm = this._view; @@ -3858,7 +4762,8 @@ 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: { @@ -3868,8 +4773,6 @@ scales: { xAxes: [{ type: 'category', - categoryPercentage: 0.8, - barPercentage: 0.9, offset: true, gridLines: { offsetGridLines: true @@ -3882,22 +4785,30 @@ } }); + 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; } @@ -3914,10 +4825,13 @@ 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. @@ -3973,15 +4887,37 @@ 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) { @@ -4000,7 +4936,7 @@ 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); @@ -4015,7 +4951,11 @@ 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(); }, @@ -4023,15 +4963,15 @@ /** * @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; @@ -4049,21 +4989,27 @@ */ _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; @@ -4101,28 +5047,18 @@ 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 }; }, @@ -4131,31 +5067,35 @@ * 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.index === datasetIndex) { + break; + } - if (imeta.bar && - imeta.stack === stack && - imeta.controller._getValueScaleId() === scale.id && - chart.isDatasetVisible(i)) { + 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; - ivalue = +scale.getRightValue(datasets[i].data[index]); - if ((value < 0 && ivalue < 0) || (value >= 0 && ivalue > 0)) { + if ((value.min < 0 && ivalue < 0) || (value.max >= 0 && ivalue > 0)) { start += ivalue; } } @@ -4163,12 +5103,12 @@ } 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; @@ -4186,9 +5126,8 @@ /** * @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); @@ -4196,7 +5135,7 @@ 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 { @@ -4219,7 +5158,8 @@ 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(); } } @@ -4230,46 +5170,25 @@ /** * @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 - }; - - 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); - } + 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); 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: { @@ -4311,6 +5230,22 @@ dataElementType: elements.Point, /** + * @private + */ + _dataElementOptions: [ + 'backgroundColor', + 'borderColor', + 'borderWidth', + 'hoverBackgroundColor', + 'hoverBorderColor', + 'hoverBorderWidth', + 'hoverRadius', + 'hitRadius', + 'pointStyle', + 'rotation' + ], + + /** * @protected */ update: function (reset) { @@ -4333,7 +5268,7 @@ 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; @@ -4376,25 +5311,22 @@ 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 = { @@ -4404,42 +5336,28 @@ 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: { @@ -4452,25 +5370,25 @@ 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: { @@ -4479,20 +5397,14 @@ 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 @@ -4524,10 +5436,10 @@ 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: { @@ -4560,6 +5472,19 @@ 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; @@ -4578,52 +5503,52 @@ 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(); @@ -4646,7 +5571,7 @@ 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 || {}; @@ -4712,7 +5637,7 @@ 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; }, @@ -4744,7 +5669,12 @@ 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; @@ -4770,51 +5700,9 @@ 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); - }, - - /** - * @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; + 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); }, /** @@ -4837,7 +5725,7 @@ * @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); }, /** @@ -4864,8 +5752,6 @@ yAxes: [{ type: 'category', position: 'left', - categoryPercentage: 0.8, - barPercentage: 0.9, offset: true, gridLines: { offsetGridLines: true @@ -4885,6 +5771,15 @@ } }); + core_defaults._set('global', { + datasets: { + horizontalBar: { + categoryPercentage: 0.8, + barPercentage: 0.9 + } + } + }); + var controller_horizontalBar = controller_bar.extend({ /** * @private @@ -4901,8 +5796,8 @@ } }); - 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', { @@ -4925,40 +5820,116 @@ } }); - 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(); } @@ -4985,12 +5956,12 @@ 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); @@ -5014,7 +5985,7 @@ 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 @@ -5024,91 +5995,21 @@ /** * @private */ - _resolvePointOptions: function (element, index) { - 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; - - // 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) { + _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; - 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] - ]); - } + var options = me.chart.options; + var lineOptions = options.elements.line; + var values = core_datasetController.prototype._resolveDatasetElementOptions.apply(me, arguments); // 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; }, @@ -5116,18 +6017,25 @@ 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 { @@ -5136,13 +6044,11 @@ } } - var rightValue = Number(yScale.getRightValue(value)); if (rightValue < 0) { return yScale.getPixelForValue(sumNeg + rightValue); } return yScale.getPixelForValue(sumPos + rightValue); } - return yScale.getPixelForValue(value); }, @@ -5207,18 +6113,19 @@ 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(); @@ -5247,14 +6154,14 @@ 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: { @@ -5281,25 +6188,25 @@ 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: { @@ -5308,20 +6215,14 @@ 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 @@ -5365,6 +6266,33 @@ 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(); @@ -5387,7 +6315,7 @@ } 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); } }, @@ -5493,48 +6421,6 @@ /** * @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; @@ -5553,7 +6439,7 @@ datasetIndex: me.index }; - return resolve$5([ + return resolve$3([ me.chart.options.elements.arc.angle, (2 * Math.PI) / count ], context, index); @@ -5568,40 +6454,85 @@ // 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 @@ -5611,7 +6542,7 @@ line._children = points; line._loop = true; // Model - line._model = me._resolveLineOptions(line); + line._model = me._resolveDatasetElementOptions(line); line.pivot(); @@ -5635,7 +6566,7 @@ 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; @@ -5658,7 +6589,7 @@ 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 @@ -5668,84 +6599,14 @@ /** * @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 - }; - - 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); + values.spanGaps = valueOrDefault$7(config.spanGaps, options.spanGaps); + values.tension = valueOrDefault$7(config.lineTension, options.elements.line.tension); return values; }, @@ -5757,6 +6618,13 @@ 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); } @@ -5790,10 +6658,10 @@ 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); } }); @@ -5815,8 +6683,6 @@ }] }, - showLines: false, - tooltips: { callbacks: { title: function () { @@ -5829,6 +6695,14 @@ } }); + core_defaults._set('global', { + datasets: { + scatter: { + showLine: false + } + } + }); + // Scatter charts use line controllers var controller_scatter = controller_line; @@ -5871,17 +6745,13 @@ * @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; - - for (i = 0, ilen = datasets.length; i < ilen; ++i) { - if (!chart.isDatasetVisible(i)) { - continue; - } + var metasets = chart._getSortedVisibleDatasetMetas(); + var metadata, i, j, ilen, jlen, element; - 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); } @@ -5966,15 +6836,12 @@ 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); } }); @@ -6155,55 +7022,194 @@ } }; + 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', { @@ -6255,6 +7261,14 @@ 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); }, @@ -6305,26 +7319,12 @@ 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'); - - // 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); + + var availableWidth = width - padding.width; + var availableHeight = height - padding.height; + var boxes = buildLayoutBoxes(chart.boxes); + var verticalBoxes = boxes.vertical; + var horizontalBoxes = boxes.horizontal; // Essentially we now have any number of boxes on each of the 4 sides. // Our canvas looks like the following. @@ -6352,211 +7352,57 @@ // | 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% - - // Step 2 - var verticalBoxWidth = (width - chartAreaWidth) / verticalBoxes.length; - - // Step 3 - // TODO re-limit horizontal axis height (this limit has affected only padding calculation since PR 1837) - // var horizontalBoxHeight = (height - chartAreaHeight) / horizontalBoxes.length; - - // 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, - }); - } - helpers$1.each(outerBoxes, getMinimumBoxSize); - - // If a horizontal box has padding, we move the left boxes over to avoid ugly charts (see issue #2478) - maxPadding = findMaxPadding(outerBoxes); - - // 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 - - // Function to fit a box - function fitBox(box) { - var minBoxSize = helpers$1.findNextWhere(minBoxSizes, function (minBox) { - return minBox.box === box; - }); - - 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); + 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); - // Set the Left and Right margins for the horizontal boxes - helpers$1.each(horizontalBoxes, fitBox); - addSizeByPosition(horizontalBoxes, outerBoxSizes); + setLayoutDims(verticalBoxes.concat(horizontalBoxes), params); - function finalFitVerticalBox(box) { - var minBoxSize = helpers$1.findNextWhere(minBoxSizes, function (minSize) { - return minSize.box === box; - }); + // First fit vertical boxes + fitBoxes(verticalBoxes, chartArea, params); - var scaleMargin = { - left: 0, - right: 0, - top: outerBoxSizes.top, - bottom: outerBoxSizes.bottom - }; - - if (minBoxSize) { - box.update(minBoxSize.width, maxChartAreaHeight, scaleMargin); - } + // Then fit horizontal boxes + if (fitBoxes(horizontalBoxes, chartArea, params)) { + // if the area changed, re-fit vertical boxes + fitBoxes(verticalBoxes, chartArea, params); } - // Let the left layout know the final margin - helpers$1.each(verticalBoxes, finalFitVerticalBox); + handleMaxPadding(chartArea); - // 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; - } - }); + // Finally place the boxes to correct coordinates + placeBoxes(boxes.leftAndTop, chartArea, params); - maxChartAreaHeight = newMaxChartAreaHeight; - maxChartAreaWidth = newMaxChartAreaWidth; - } - - // Step 7 - Position the boxes - var left = leftPadding + leftPaddingAddition; - var top = topPadding + topPaddingAddition; + // Move to opposite side of chart + chartArea.x += chartArea.w; + chartArea.y += chartArea.h; - 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; + placeBoxes(boxes.rightAndBottom, chartArea, params); - // 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); }); } }; @@ -6580,23 +7426,10 @@ 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'; @@ -6886,17 +7719,22 @@ } } - 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 = { @@ -6917,18 +7755,18 @@ _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); } }, @@ -6950,10 +7788,6 @@ // 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 @@ -6962,6 +7796,9 @@ // 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; } @@ -7319,7 +8156,8 @@ } }; - var valueOrDefault$7 = helpers$1.valueOrDefault; + var valueOrDefault$8 = helpers$1.valueOrDefault; + var getRtlHelper = helpers$1.rtl.getRtlAdapter; core_defaults._set('global', { tooltips: { @@ -7557,28 +8395,32 @@ 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, @@ -7805,7 +8647,7 @@ 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 = []; @@ -8063,25 +8905,28 @@ drawTitle: function (pt, vm, ctx) { var title = vm.title; + var length = title.length; + var titleFontSize, titleSpacing, i; + + if (length) { + var rtlHelper = getRtlHelper(vm.rtl, vm.x, vm.width); - if (title.length) { 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 } } @@ -8094,60 +8939,68 @@ 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; @@ -8159,21 +9012,27 @@ drawFooter: function (pt, vm, ctx) { var footer = vm.footer; + var length = footer.length; + var footerFontSize, i; + + if (length) { + var rtlHelper = getRtlHelper(vm.rtl, vm.x, vm.width); - if (footer.length) { 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; + } } }, @@ -8253,6 +9112,8 @@ // Draw Title, Body, and Footer pt.y += vm.yPadding; + helpers$1.rtl.overrideTextDirection(ctx, vm.textDirection); + // Titles this.drawTitle(pt, vm, ctx); @@ -8262,6 +9123,8 @@ // Footer this.drawFooter(pt, vm, ctx); + helpers$1.rtl.restoreTextDirection(ctx, vm.textDirection); + ctx.restore(); } }, @@ -8284,6 +9147,9 @@ me._active = []; } else { me._active = me._chart.getElementsAtEventForMode(e, options.mode, options); + if (options.reverse) { + me._active.reverse(); + } } // Remember Last Actives @@ -8313,10 +9179,10 @@ */ 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: {}, @@ -8357,7 +9223,7 @@ 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({}); @@ -8441,10 +9307,31 @@ 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; @@ -8473,6 +9360,7 @@ 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, @@ -8529,9 +9417,6 @@ 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 @@ -8598,11 +9483,15 @@ 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) { @@ -8646,7 +9535,7 @@ 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; @@ -8698,19 +9587,24 @@ 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]; @@ -8718,10 +9612,10 @@ 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; }, @@ -8747,6 +9641,7 @@ update: function (config) { var me = this; + var i, ilen; if (!config || typeof config !== 'object') { // backwards compatibility @@ -8773,9 +9668,9 @@ 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(); @@ -8799,6 +9694,8 @@ // 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, @@ -8824,6 +9721,20 @@ 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 @@ -8871,7 +9782,7 @@ return; } - meta.controller.update(); + meta.controller._update(); core_plugins.notify(me, 'afterDatasetUpdate', [args]); }, @@ -8888,7 +9799,7 @@ } 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) { @@ -8930,6 +9841,7 @@ draw: function (easingValue) { var me = this; + var i, layers; me.clear(); @@ -8947,12 +9859,21 @@ 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]); @@ -8974,22 +9895,48 @@ }, /** + * @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]); @@ -9000,12 +9947,11 @@ * 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 }; @@ -9085,7 +10031,9 @@ controller: null, hidden: null, // See isDatasetVisible() comment xAxisID: null, - yAxisID: null + yAxisID: null, + order: dataset.order || 0, + index: datasetIndex }; } @@ -9211,15 +10159,19 @@ }, 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'](); + } }, /** @@ -9435,7 +10387,7 @@ }; 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) { @@ -9464,19 +10416,6 @@ } 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); }; @@ -9918,25 +10857,30 @@ 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); @@ -9954,6 +10898,10 @@ } return longest; }; + + /** + * @deprecated + */ helpers$1.numberOfLabelLines = function (arrayOfThings) { var numberOfLines = 1; helpers$1.each(arrayOfThings, function (thing) { @@ -10151,7 +11099,9 @@ 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 @@ -10177,7 +11127,9 @@ } }; - 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', { @@ -10188,7 +11140,7 @@ // 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, @@ -10237,41 +11189,267 @@ } }); - 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); + } + }); + } + + /** + * 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; } - var core_scale = core_element.extend({ + 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 @@ -10296,40 +11474,45 @@ 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(); @@ -10344,9 +11527,14 @@ 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(); @@ -10374,49 +11562,81 @@ // 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]); }, @@ -10469,7 +11689,7 @@ 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) @@ -10497,40 +11717,40 @@ }, calculateTickRotation: function () { var me = this; - var context = me.ctx; - var tickOpts = me.options.ticks; - var labels = labelsFromTicks(me._ticks); - - // 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; - - var labelRotation = tickOpts.minRotation || 0; - - 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; - - // Allow 3 pixels x2 padding either side for label readability - var tickWidth = me.getPixelForTick(1) - me.getPixelForTick(0) - 6; + 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; + + if (!me._isVisible() || !tickOpts.display || minRotation >= maxRotation || numTicks <= 1 || !me.isHorizontal()) { + me.labelRotation = minRotation; + return; + } - // 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); + labelSizes = me._getLabelSizes(); + maxLabelWidth = labelSizes.widest.width; + maxLabelHeight = labelSizes.highest.height - labelSizes.highest.offset; - if (sinRotation * originalLabelWidth > me.maxHeight) { - // go back one step - labelRotation--; - break; - } + // 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); - 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; @@ -10552,111 +11772,99 @@ 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 - } - - // 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; - } + } else if (display) { + minSize.height = getTickMarkLength(gridLineOpts) + getScaleLabelHeight(scaleLabelOpts); } // Don't bother fitting the ticks if we are not showing the labels if (tickOpts.display && display) { - var 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; - - // Store max number of lines and widest label for _autoSkip - me._maxLabelLines = tallestLabelHeightInLines; - me.longestLabelWidth = largestTextWidth; + 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; 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; + } }, /** @@ -10666,10 +11874,10 @@ 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); } }, @@ -10679,22 +11887,24 @@ // 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()) { @@ -10710,6 +11920,85 @@ 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 @@ -10740,21 +12029,12 @@ 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; - - if (offset) { - pixel += tickWidth / 2; - } + var numTicks = me._ticks.length; + var tickWidth = 1 / Math.max(numTicks - (offset ? 0 : 1), 1); - 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)); }, /** @@ -10763,15 +12043,17 @@ */ 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; }, /** @@ -10799,44 +12081,34 @@ */ _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; - - // 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); - } + 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; - // 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)); + // If there are too many major ticks to display them all + if (numMajorIndices > ticksLimit) { + skipMajors(ticks, majorIndices, numMajorIndices / ticksLimit); + return nonSkipped(ticks); } - for (i = 0; i < tickCount; i++) { - tick = ticks[i]; + spacing = calculateSpacing(majorIndices, ticks, axisLength, ticksLimit); - 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); }, /** @@ -10844,22 +12116,20 @@ */ _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 tickFont = helpers$1.options._parseFont(optionTicks); - var h = (me._maxLabelLines * tickFont.lineHeight + padding) || 0; + var w = labelSizes ? labelSizes.widest.width + padding : 0; + var h = labelSizes ? labelSizes.highest.height + padding : 0; // Calculate space needed for 1 tick in axis direction. - return isHorizontal + return me.isHorizontal() ? h * cos > w * sin ? w / cos : h / sin : h * sin < w * cos ? h / cos : w / sin; }, @@ -10891,152 +12161,93 @@ }, /** - * 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, @@ -11045,114 +12256,146 @@ 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 }); - }); + } + + return items; + }, + + /** + * @private + */ + _drawGrid: function (chartArea) { + var me = this; + var gridLines = me.options.gridLines; + + if (!gridLines.display) { + return; + } - // 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; - - if (gridLines.display && glWidth && glColor) { - context.save(); - context.lineWidth = glWidth; - context.strokeStyle = glColor; - if (context.setLineDash) { - context.setLineDash(itemToDraw.glBorderDash); - context.lineDashOffset = itemToDraw.glBorderDashOffset; + 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); - } - - context.stroke(); - context.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); + ctx.moveTo(item.x1, item.y1); + ctx.lineTo(item.x2, item.y2); } - 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; + ctx.stroke(); + ctx.restore(); } - - 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; @@ -11162,59 +12405,214 @@ 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) { @@ -11225,74 +12623,66 @@ 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 () { @@ -11305,7 +12695,7 @@ 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 @@ -11333,7 +12723,7 @@ // 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]; } @@ -11343,7 +12733,7 @@ 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 { @@ -11358,10 +12748,10 @@ // 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; } } @@ -11376,11 +12766,11 @@ 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; } @@ -11532,6 +12922,25 @@ 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; } }); @@ -11542,123 +12951,113 @@ } }; - 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; - - function IDMatches(meta) { - return isHorizontal ? meta.xAxisID === me.id : meta.yAxisID === me.id; - } - - // First Calculate the range - me.min = null; - me.max = null; + var DEFAULT_MIN = 0; + var DEFAULT_MAX = 1; + + 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('.'); + + 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 = {}; - - 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 (valuesPerStack[key] === undefined) { - valuesPerStack[key] = { - positiveValues: [], - negativeValues: [] - }; - } + pos[i] = pos[i] || 0; + neg[i] = neg[i] || 0; - // Store these per type - var positiveValues = valuesPerStack[key].positiveValues; - var negativeValues = valuesPerStack[key].negativeValues; + if (opts.relativePoints) { + pos[i] = 100; + } else if (value.min < 0 || value.max < 0) { + neg[i] += value.min; + } else { + pos[i] += value.max; + } + } + } - 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; - } + function updateMinMax(scale, meta, data) { + var ilen = data.length; + var i, value; - positiveValues[index] = positiveValues[index] || 0; - negativeValues[index] = negativeValues[index] || 0; + 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.relativePoints) { - positiveValues[index] = 100; - } else if (value < 0) { - negativeValues[index] += value; - } else { - positiveValues[index] += value; - } - }); - } - }); + scale.min = Math.min(scale.min, value.min); + scale.max = Math.max(scale.max, value.max); + } + } - 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); - }); + 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; - } 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; - } + me.min = Number.POSITIVE_INFINITY; + me.max = Number.NEGATIVE_INFINITY; - if (me.min === null) { - me.min = value; - } else if (value < me.min) { - me.min = value; - } + if (hasStacks === undefined) { + for (i = 0; !hasStacks && i < ilen; ++i) { + meta = metasets[i]; + hasStacks = meta.stack !== undefined; + } + } - 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 @@ -11682,38 +13081,25 @@ }, 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]); } }); @@ -11721,7 +13107,8 @@ 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 @@ -11732,20 +13119,20 @@ 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; @@ -11763,7 +13150,7 @@ 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; @@ -11788,39 +13175,37 @@ 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 @@ -11833,59 +13218,56 @@ 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(); }, @@ -11901,26 +13283,26 @@ 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; } @@ -11964,11 +13346,15 @@ // 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]); }, /** @@ -11978,87 +13364,47 @@ * @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; - } - 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; + 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; } - 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; - } - 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); + value = +me.getRightValue(value); + + if (value > me.min && value > 0) { + decimal = (log10(value) - me._startValue) / me._valueRange + me._valueOffset; } - 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); } }); @@ -12066,9 +13412,9 @@ 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, @@ -12079,7 +13425,7 @@ angleLines: { display: true, - color: 'rgba(0, 0, 0, 0.1)', + color: 'rgba(0,0,0,0.1)', lineWidth: 1, borderDash: [], borderDashOffset: 0.0 @@ -12120,16 +13466,11 @@ } }; - 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; } @@ -12214,10 +13555,10 @@ 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 @@ -12285,53 +13626,30 @@ 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(); } @@ -12339,7 +13657,7 @@ 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; @@ -12432,7 +13750,10 @@ 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) { @@ -12484,22 +13805,22 @@ }, 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 startAngleRadians = startAngle * Math.PI * 2 / 360; + var chart = this.chart; + var angleMultiplier = 360 / chart.data.labels.length; + var options = chart.options || {}; + var startAngle = options.startAngle || 0; // 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 @@ -12523,79 +13844,130 @@ 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 tickOpts = opts.ticks; + 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.display) { - var ctx = me.ctx; - var startAngle = this.getIndexAngle(0); - var tickFont = helpers$1.options._parseFont(tickOpts); + if (opts.pointLabels.display) { + drawPointLabels(me); + } - if (opts.angleLines.display || 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]); } - 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]); + 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(); + } - // Draw circular lines around the scale - if (gridLineOpts.display && index !== 0) { - drawRadiusLine(me, gridLineOpts, yCenterOffset, index); - } + ctx.restore(); + } + }, - if (tickOpts.display) { - var tickFontColor = valueOrDefault$b(tickOpts.fontColor, core_defaults.global.defaultFontColor); - ctx.font = tickFont.string; - - ctx.save(); - ctx.translate(me.xCenter, me.yCenter); - ctx.rotate(startAngle); - - 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 - ); - } + /** + * @private + */ + _drawLabels: function () { + var me = this; + var ctx = me.ctx; + var opts = me.options; + var tickOpts = opts.ticks; - ctx.textAlign = 'center'; - ctx.textBaseline = 'middle'; - ctx.fillStyle = tickFontColor; - ctx.fillText(label, 0, -yCenterOffset); - ctx.restore(); - } - } - }); + if (!tickOpts.display) { + return; } - } + + 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; + } + + offset = me.getDistanceFromCenterForValue(me.ticksAsNumbers[index]); + + if (tickOpts.showLabelBackdrop) { + width = ctx.measureText(label).width; + ctx.fillStyle = tickOpts.backdropColor; + + ctx.fillRect( + -width / 2 - tickOpts.backdropPaddingX, + -offset - tickFont.size / 2 - tickOpts.backdropPaddingY, + width + tickOpts.backdropPaddingX * 2, + tickFont.size + tickOpts.backdropPaddingY * 2 + ); + } + + ctx.fillStyle = tickFontColor; + ctx.fillText(label, 0, -offset); + }); + + 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; @@ -12605,42 +13977,42 @@ 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, @@ -12670,6 +14042,14 @@ 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 @@ -12822,31 +14202,6 @@ } /** - * 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) { @@ -12855,7 +14210,7 @@ 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]; @@ -12868,13 +14223,12 @@ /** * 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; } } @@ -12892,7 +14246,7 @@ /** * 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. */ @@ -12901,51 +14255,33 @@ 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; } @@ -12961,42 +14297,57 @@ 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 = { @@ -13023,7 +14374,6 @@ 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 @@ -13063,9 +14413,9 @@ 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 @@ -13090,22 +14440,20 @@ 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; @@ -13120,10 +14468,11 @@ 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] = []; @@ -13131,20 +14480,18 @@ } 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; @@ -13155,7 +14502,6 @@ me.max = Math.max(min + 1, max); // PRIVATE - me._horizontal = me.isHorizontal(); me._table = []; me._timestamps = { data: timestamps, @@ -13169,21 +14515,21 @@ 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) { @@ -13192,8 +14538,8 @@ } // 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) { @@ -13207,12 +14553,17 @@ 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(); } @@ -13251,12 +14602,17 @@ 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; }, @@ -13277,13 +14633,9 @@ */ 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) { @@ -13312,9 +14664,8 @@ 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 @@ -13322,19 +14673,29 @@ }, /** - * 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; }, /** @@ -13342,13 +14703,18 @@ */ 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; } @@ -18046,7 +19412,7 @@ }, 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) { @@ -18101,6 +19467,12 @@ 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, @@ -18160,7 +19532,7 @@ } } - function computeBoundary(source) { + function computeLinearBoundary(source) { var model = source.el._model || {}; var scale = source.el._scale || {}; var fill = source.fill; @@ -18181,8 +19553,6 @@ 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(); } @@ -18204,6 +19574,44 @@ 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; @@ -18255,7 +19663,7 @@ } function drawArea(ctx, curve0, curve1, len0, len1) { - var i; + var i, cx, cy, r; if (!len0 || !len1) { return; @@ -18267,6 +19675,16 @@ 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); @@ -18283,17 +19701,22 @@ 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); @@ -18360,34 +19783,42 @@ } }, - 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; + + for (i = metasets.length - 1; i >= 0; --i) { + meta = metasets[i].$filler; + + if (!meta || !meta.visible) { + continue; + } - 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); + 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, @@ -18423,40 +19854,51 @@ // 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; } }); @@ -18478,18 +19920,19 @@ 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. @@ -18612,79 +20055,82 @@ } // 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; - - 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 - - currentColWidth = 0; - currentColHeight = 0; - } + } else { + var vPadding = labelOpts.padding; + var columnWidths = me.columnWidths = []; + var columnHeights = me.columnHeights = []; + var totalWidth = labelOpts.padding; + var currentColWidth = 0; + var currentColHeight = 0; - // Get max width - currentColWidth = Math.max(currentColWidth, itemWidth); - currentColHeight += itemHeight; + 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 + fontSize + 2 * vPadding > minSize.height) { + totalWidth += currentColWidth + labelOpts.padding; + columnWidths.push(currentColWidth); // previous column width + columnHeights.push(currentColHeight); + currentColWidth = 0; + currentColHeight = 0; + } - // Store the hitbox width and height here. Final position will be updated in `draw` - hitboxes[i] = { - left: 0, - top: 0, - width: itemWidth, - height: fontSize - }; - }); + // Get max width + currentColWidth = Math.max(currentColWidth, itemWidth); + currentColHeight += fontSize + vPadding; - totalWidth += currentColWidth; - columnWidths.push(currentColWidth); - minSize.width += totalWidth; - } + // 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); + columnHeights.push(currentColHeight); + minSize.width += totalWidth; } me.width = minSize.width; @@ -18705,139 +20151,163 @@ 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; + + // 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; + + var boxWidth = getBoxWidth(labelOpts, fontSize); + var hitboxes = me.legendHitBoxes; + + // 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$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); - // Set the ctx for the box - ctx.save(); + if (ctx.setLineDash) { + // IE 9 and 10 do not support line dash + ctx.setLineDash(valueOrDefault$e(legendItem.lineDash, lineDefault.borderDash)); + } - 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); + 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; - if (ctx.setLineDash) { - // IE 9 and 10 do not support line dash - ctx.setLineDash(valueOrDefault$d(legendItem.lineDash, lineDefault.borderDash)); + // 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); } + } - 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; + ctx.restore(); + }; - // 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); - } + var fillText = function (x, y, legendItem, textWidth) { + var halfFontSize = fontSize / 2; + var xLeft = rtlHelper.xPlus(x, boxWidth + halfFontSize); + var yMiddle = y + halfFontSize; - ctx.restore(); + ctx.fillText(legendItem.text, xLeft, yMiddle); + + 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 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; + } + }; + + // Horizontal + var isHorizontal = me.isHorizontal(); + if (isHorizontal) { + cursor = { + x: me.left + alignmentOffset(legendWidth, lineWidths[0]), + y: me.top + labelOpts.padding, + line: 0 }; - var fillText = function (x, y, legendItem, textWidth) { - var halfFontSize = fontSize / 2; - var xLeft = boxWidth + halfFontSize + x; - var yMiddle = y + halfFontSize; - - ctx.fillText(legendItem.text, xLeft, yMiddle); - - if (legendItem.hidden) { - // Strikethrough the text if hidden - ctx.beginPath(); - ctx.lineWidth = 2; - ctx.moveTo(xLeft, yMiddle); - ctx.lineTo(xLeft + textWidth, yMiddle); - ctx.stroke(); - } + } else { + cursor = { + x: me.left + labelOpts.padding, + y: me.top + alignmentOffset(legendHeight, columnHeights[0]), + line: 0 }; + } - // 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 - }; - } + 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; - - // 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; + 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); }, /** @@ -19075,23 +20545,20 @@ 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, @@ -19107,51 +20574,53 @@ 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; - - ctx.fillStyle = helpers$1.valueOrDefault(opts.fontColor, core_defaults.global.defaultFontColor); // render in correct colour - ctx.font = fontOpts.string; - - // 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); - } + if (!opts.display) { + return; + } - ctx.save(); - ctx.translate(titleX, titleY); - ctx.rotate(rotation); - ctx.textAlign = 'center'; - ctx.textBaseline = 'middle'; + 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; + + ctx.fillStyle = helpers$1.valueOrDefault(opts.fontColor, core_defaults.global.defaultFontColor); // render in correct colour + ctx.font = fontOpts.string; + + // 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'; + + 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; } - - ctx.restore(); + } else { + ctx.fillText(text, 0, 0, maxWidth); } + + ctx.restore(); } }); @@ -19223,7 +20692,7 @@ 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; diff --git a/assets/js/main.js b/assets/js/main.js index 76b9544..8a9dcf3 100755 --- a/assets/js/main.js +++ b/assets/js/main.js @@ -8,16 +8,16 @@ const cookie = document.querySelector("#cookie"); const cookieWrap = document.querySelector("#cookie_wrap"); const loading = document.querySelector("#loading"); const stats = document.querySelector("#stats"); -const heatMap = document.querySelector("#heatMap"); +const toggle = document.querySelector("#toggle"); cookie.addEventListener("keyup", e => { if (e.key === "Enter") { const request = new XMLHttpRequest(); request.onreadystatechange = () => { if (request.readyState === 4 && request.status === 200) { - analyze(request.responseText); loading.style.display = "none"; stats.style.display = "block"; + analyze(request.responseText); } else if (request.readyState === 4 && request.status !== 200) alert("Cookie is not valid!") }; @@ -30,139 +30,61 @@ cookie.addEventListener("keyup", e => { }); function analyze(data) { + const filtered = {}; data = JSON.parse(data).flat(1); - let totalWatchedSeconds = 0; - const hourObject = Array(24).fill(0); - const watchCountObject = {}; - - data.forEach(element => { - let title; - const seriesTitle = element.seriesTitle; - const movieTitle = element.title; - const watchDate = element.date; - const duration = element.duration; - // Generate watch time array (eg. 12am) - hourObject[(new Date(watchDate)).getHours()]++; - - if (seriesTitle !== undefined) title = seriesTitle; - else title = movieTitle; + // Push all titles with empty fields + data.forEach(node => filtered[node["seriesTitle"] ? node["seriesTitle"] : node["videoTitle"]] = { + duration: 0, + dates: [], + count: 0 + }); - if (watchCountObject[title] !== undefined) { - watchCountObject[title].date.push(new Date(watchDate)); - watchCountObject[title].watchTimeInSeconds += duration; - watchCountObject[title].watchTime = secondsToHours(watchCountObject[title].watchTimeInSeconds); - watchCountObject[title].count++; - totalWatchedSeconds += duration - } else { - watchCountObject[title] = { - date: [new Date(watchDate)], - watchTime: secondsToHours(duration), - watchTimeInSeconds: duration, - count: 1 - }; - totalWatchedSeconds += duration; - } + // Push duration, date and count + data.forEach(node => { + const obj = filtered[node["seriesTitle"] ? node["seriesTitle"] : node["videoTitle"]]; + obj.duration += node["duration"] / 60 / 60; // hours + obj.dates.push(new Date(node["date"])); + obj.count++; }); - renderTotalSpent(totalWatchedSeconds); - renderHourChart(hourObject); - renderTopChart(watchCountObject); - renderHeatMap(watchCountObject); - console.log(watchCountObject); -} + setSizes(); + drawTopTitles(filtered); -function renderTotalSpent(total) { - document.querySelector("#totalSpent").innerHTML = ` - Days: ${Math.floor(total / 60 / 60 / 24)}, - Hours: ${Math.floor(total / 60 / 60)}, - Minutes: ${Math.round(total / 60)}, - Seconds: ${total}` -} + toggle.onclick = () => drawTopTitles(filtered); -function renderHourChart(hourObject) { - const element = document - .getElementById("hourChart") - .getContext("2d"); + console.log(filtered); +} - new Chart(element, { - type: "line", - data: { - labels: [ - "12am", - "1am", - "2am", - "3am", - "4am", - "5am", - "6am", - "7am", - "8am", - "9am", - "10am", - "11am", - "12pm", - "1pm", - "2pm", - "3pm", - "4pm", - "5pm", - "6pm", - "7pm", - "8pm", - "9pm", - "10pm", - "11pm" - ], - datasets: [{ - label: "Average watch times", - borderColor: "rgb(255, 99, 132)", - cubicInterpolationMode: "monotone", - pointRadius: 0, - pointHitRadius: 15, - data: hourObject - }] - }, - options: { - scales: { - yAxes: [{ - ticks: { - display: false - } - }] - }, - legend: { - display: false - } - } - }); +function setSizes() { + const elements = document.getElementsByTagName("canvas"); + for (const elem of elements) { + elem.setAttribute("width", document.querySelector(".stats div").offsetWidth); + elem.setAttribute("height", window.innerHeight / 2 + 200); + } } -function renderTopChart(object) { - const sorted = Object.keys(object).sort((a, b) => { - return object[b].watchTimeInSeconds - object[a].watchTimeInSeconds - }); - const data = sorted.map(element => object[element].watchTimeInSeconds); - const labels = sorted.map(element => { - return element + " (" + Math.floor(object[element].watchTimeInSeconds / 60 / 60) + " hours)" - }); - const colorArray = Array.from({length: data.length}, () => - "#" + ((1 << 24) * Math.random() | 0).toString(16)); +let previous; - const element = document - .getElementById("topChart") - .getContext("2d"); +function drawTopTitles(data) { + // Toggle layout + toggle.setAttribute("data-current", toggle.getAttribute("data-current") === "bar" ? "pie" : "bar"); + if (previous) + previous.destroy(); - new Chart(element, { - type: 'doughnut', + const ctx = document.getElementById("topChart"); + previous = new Chart(ctx, { + type: toggle.getAttribute("data-current"), data: { + labels: Object.keys(data).sort((a, b) => data[b].duration - data[a].duration), datasets: [{ - data: data, - backgroundColor: colorArray - }], - labels: labels, + data: Object.keys(data).map(key => +data[key].duration.toFixed(2)).sort((a, b) => b - a), + backgroundColor: Array.from({length: Object.keys(data).length}, () => "#" + ((1 << 24) * Math.random() | 0).toString(16)) + }] }, options: { + responsive: false, + maintainAspectRatio: true, animation: { animateScale: true, animateRotate: true @@ -171,47 +93,5 @@ function renderTopChart(object) { display: false } } - }); -} - -function renderHeatMap(object) { - const allDates = Object.keys(object).map(element => object[element].date).flat(10) - .map(element => element.setHours(0, 0, 0, 0)); - const watchedPerWeek = [[], [], [], [], [], [], []]; - - for (let i = 0; i < 366; i++) { - const date = new Date(); - date.setDate(date.getDate() - i); - date.setHours(0, 0, 0, 0); - watchedPerWeek[date.getDay()].push(allDates.map(element => element === date.getTime()).filter(Boolean).length); - } - - const maxWatchedPerDay = Math.max.apply(Math, watchedPerWeek.flat(2)); - - watchedPerWeek.map((element, i) => { - watchedPerWeek[i].push(["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"][i]); - return watchedPerWeek[i].reverse(); - }); - - watchedPerWeek.forEach(element => { - const tableRow = document.createElement("tr"); - element.forEach(count => { - const tableData = document.createElement("td"); - tableData.style.backgroundColor = "rgba(255,13,0," + count / maxWatchedPerDay + ")"; - if (typeof count !== "number") tableData.appendChild(document.createTextNode(count)); - tableRow.appendChild(tableData); - - tableData.addEventListener("mouseover", () => { - document.querySelector("#information").innerText = `You've watched ${count} titles on that day!`; - }); - }); - - heatMap.appendChild(tableRow) }) } - -function secondsToHours(seconds) { - const date = new Date(null); - date.setSeconds(seconds); - return date.toISOString().substr(11, 8) -} diff --git a/assets/php/getData.php b/assets/php/getData.php index 22f0f26..9849752 100755 --- a/assets/php/getData.php +++ b/assets/php/getData.php @@ -5,15 +5,18 @@ * @copyright Marvin Borner 2018 */ +$debug = true; $cookie = $_POST['cookie']; -if (isset($cookie)) { +if ($debug) { + print_r(file_get_contents("debug.json")); +} else if (isset($cookie)) { $isLastPage = false; $currentPage = 0; $result = '['; while ($isLastPage === false) { - $ch = curl_init('https://www.netflix.com/api/shakti/ve8ded8cd/viewingactivity?pg=' . $currentPage . '&pgSize=100'); + $ch = curl_init('https://www.netflix.com/api/shakti/v52f427f5/viewingactivity?pg=' . $currentPage . '&pgSize=100'); curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); curl_setopt($ch, CURLOPT_HEADER, 0); curl_setopt($ch, CURLOPT_COOKIE, $cookie); @@ -28,14 +28,15 @@ <div class="stats" id="stats"> <h2>Netflix Statistics</h2> <div>Total watch time: <span id="totalSpent"></span></div> - <div> + <div class="statWrap"> <canvas id="hourChart"></canvas> </div> - <div> + <div class="statWrap"> <p>Most watched titles: </p> - <canvas id="topChart"></canvas> + <button data-current="bar" id="toggle">Toggle</button> + <canvas height="500" id="topChart" width="1500"></canvas> </div> - <div> + <div class="statWrap"> <p>Overview of the year:</p> <div id="information"></div> <table class="heatMap" id="heatMap"></table> |