From: Jukka Kurkela Date: Mon, 6 Jul 2020 21:38:04 +0000 (+0300) Subject: Generic registry for controllers, scales, elements and plugins (#7435) X-Git-Tag: v3.0.0-beta.2~56 X-Git-Url: http://git.ipfire.org/?a=commitdiff_plain;h=6bd5ad5518042a841f8f378b5a1ee29fd7ad4249;p=thirdparty%2FChart.js.git Generic registry for controllers, scales, elements and plugins (#7435) * Generic registry for controllers, scales, elements and plugins * Remove references to scale service --- diff --git a/docs/docs/axes/index.md b/docs/docs/axes/index.md index 6466d327a..d9f830ce7 100644 --- a/docs/docs/axes/index.md +++ b/docs/docs/axes/index.md @@ -45,14 +45,12 @@ There are a number of config callbacks that can be used to change parameters in ### Updating Axis Defaults -The default configuration for a scale can be easily changed using the scale service. All you need to do is to pass in a partial configuration that will be merged with the current scale default configuration to form the new default. +The default configuration for a scale can be easily changed. All you need to do is set the new options to `Chart.defaults.scales[type]`. For example, to set the minimum value of 0 for all linear scales, you would do the following. Any linear scales created after this time would now have a minimum of 0. ```javascript -Chart.scaleService.updateScaleDefaults('linear', { - min: 0 -}); +Chart.defaults.scales.linear.min = 0; ``` ## Creating New Axes diff --git a/docs/docs/developers/axes.md b/docs/docs/developers/axes.md index d6adadbe4..e4cd873c1 100644 --- a/docs/docs/developers/axes.md +++ b/docs/docs/developers/axes.md @@ -17,7 +17,7 @@ MyScale.defaults = defaultConfigObject; Once you have created your scale class, you need to register it with the global chart object so that it can be used. A default config for the scale may be provided when registering the constructor. The first parameter to the register function is a string key that is used later to identify which scale type to use for a chart. ```javascript -Chart.scaleService.registerScale(MyScale); +Chart.register(MyScale); ``` To use the new scale, simply pass in the string key to the config when creating a chart. diff --git a/docs/docs/developers/charts.md b/docs/docs/developers/charts.md index f68bebe47..14c0cf1c8 100644 --- a/docs/docs/developers/charts.md +++ b/docs/docs/developers/charts.md @@ -49,6 +49,7 @@ Dataset controllers must implement the following interface. ``` The following methods may optionally be overridden by derived dataset controllers. + ```javascript { // Initializes the controller @@ -79,12 +80,6 @@ The built in controller types are: For example, to derive a new chart type that extends from a bubble chart, you would do the following. ```javascript -// Sets the default config for 'derivedBubble' to be the same as the bubble defaults. -// We look for the defaults by doing Chart.defaults[chartType] -// It looks like a bug exists when the defaults don't exist -Chart.defaults.derivedBubble = Chart.defaults.bubble; - -// I think the recommend using Chart.controllers.bubble.extend({ extensions here }); class Custom extends Chart.controllers.bubble { draw() { // Call super method first @@ -103,10 +98,11 @@ class Custom extends Chart.controllers.bubble { ctx.restore(); } }); +Custom.id = 'derivedBubble'; +Custom.defaults = Chart.defaults.bubble; -// Stores the controller so that the chart initialization routine can look it up with -// Chart.controllers[type] -Chart.controllers.derivedBubble = Custom; +// Stores the controller so that the chart initialization routine can look it up +Chart.register(Custom); // Now we can create and use our new chart type new Chart(ctx, { diff --git a/src/controllers/controller.bar.js b/src/controllers/controller.bar.js index 5afb5b4ed..360812029 100644 --- a/src/controllers/controller.bar.js +++ b/src/controllers/controller.bar.js @@ -1,41 +1,8 @@ import DatasetController from '../core/core.datasetController'; -import defaults from '../core/core.defaults'; -import {Rectangle} from '../elements/index'; import {clipArea, unclipArea} from '../helpers/helpers.canvas'; import {isArray, isNullOrUndef, valueOrDefault, resolveObjectKey} from '../helpers/helpers.core'; import {_limitValue, sign} from '../helpers/helpers.math'; -defaults.set('bar', { - hover: { - mode: 'index' - }, - - datasets: { - categoryPercentage: 0.8, - barPercentage: 0.9, - animation: { - numbers: { - type: 'number', - properties: ['x', 'y', 'base', 'width', 'height'] - } - } - }, - - scales: { - _index_: { - type: 'category', - offset: true, - gridLines: { - offsetGridLines: true - } - }, - _value_: { - type: 'linear', - beginAtZero: true, - } - } -}); - /** * Computes the "optimal" sample size to maintain bars equally sized while preventing overlap. * @private @@ -507,16 +474,51 @@ export default class BarController extends DatasetController { } -BarController.prototype.dataElementType = Rectangle; - -BarController.prototype.dataElementOptions = [ - 'backgroundColor', - 'borderColor', - 'borderSkipped', - 'borderWidth', - 'barPercentage', - 'barThickness', - 'categoryPercentage', - 'maxBarThickness', - 'minBarLength' -]; +BarController.id = 'bar'; + +/** + * @type {any} + */ +BarController.defaults = { + datasetElementType: false, + dataElementType: 'rectangle', + dataElementOptions: [ + 'backgroundColor', + 'borderColor', + 'borderSkipped', + 'borderWidth', + 'barPercentage', + 'barThickness', + 'categoryPercentage', + 'maxBarThickness', + 'minBarLength' + ], + hover: { + mode: 'index' + }, + + datasets: { + categoryPercentage: 0.8, + barPercentage: 0.9, + animation: { + numbers: { + type: 'number', + properties: ['x', 'y', 'base', 'width', 'height'] + } + } + }, + + scales: { + _index_: { + type: 'category', + offset: true, + gridLines: { + offsetGridLines: true + } + }, + _value_: { + type: 'linear', + beginAtZero: true, + } + } +}; diff --git a/src/controllers/controller.bubble.js b/src/controllers/controller.bubble.js index 0fd32fec3..3dd89e9f1 100644 --- a/src/controllers/controller.bubble.js +++ b/src/controllers/controller.bubble.js @@ -1,34 +1,7 @@ import DatasetController from '../core/core.datasetController'; -import defaults from '../core/core.defaults'; -import {Point} from '../elements/index'; import {resolve} from '../helpers/helpers.options'; import {resolveObjectKey} from '../helpers/helpers.core'; -defaults.set('bubble', { - animation: { - numbers: { - properties: ['x', 'y', 'borderWidth', 'radius'] - } - }, - scales: { - x: { - type: 'linear' - }, - y: { - type: 'linear' - } - }, - - tooltips: { - callbacks: { - title() { - // Title doesn't make sense for scatter since we format the data as a point - return ''; - } - } - } -}); - export default class BubbleController extends DatasetController { /** @@ -165,14 +138,42 @@ export default class BubbleController extends DatasetController { } } -BubbleController.prototype.dataElementType = Point; - -BubbleController.prototype.dataElementOptions = [ - 'backgroundColor', - 'borderColor', - 'borderWidth', - 'hitRadius', - 'radius', - 'pointStyle', - 'rotation' -]; +BubbleController.id = 'bubble'; + +/** + * @type {any} + */ +BubbleController.defaults = { + datasetElementType: false, + dataElementType: 'point', + dataElementOptions: [ + 'backgroundColor', + 'borderColor', + 'borderWidth', + 'hitRadius', + 'radius', + 'pointStyle', + 'rotation' + ], + animation: { + numbers: { + properties: ['x', 'y', 'borderWidth', 'radius'] + } + }, + scales: { + x: { + type: 'linear' + }, + y: { + type: 'linear' + } + }, + tooltips: { + callbacks: { + title() { + // Title doesn't make sense for scatter since we format the data as a point + return ''; + } + } + } +}; diff --git a/src/controllers/controller.doughnut.js b/src/controllers/controller.doughnut.js index a7c13124d..892c9ae3e 100644 --- a/src/controllers/controller.doughnut.js +++ b/src/controllers/controller.doughnut.js @@ -1,6 +1,4 @@ import DatasetController from '../core/core.datasetController'; -import defaults from '../core/core.defaults'; -import {Arc} from '../elements/index'; import {isArray, valueOrDefault} from '../helpers/helpers.core'; /** @@ -11,83 +9,6 @@ const PI = Math.PI; const DOUBLE_PI = PI * 2; const HALF_PI = PI / 2; -defaults.set('doughnut', { - animation: { - numbers: { - type: 'number', - properties: ['circumference', 'endAngle', 'innerRadius', 'outerRadius', 'startAngle', 'x', 'y'] - }, - // Boolean - Whether we animate the rotation of the Doughnut - animateRotate: true, - // Boolean - Whether we animate scaling the Doughnut from the centre - animateScale: false - }, - aspectRatio: 1, - legend: { - labels: { - generateLabels(chart) { - const data = chart.data; - if (data.labels.length && data.datasets.length) { - return data.labels.map((label, i) => { - const meta = chart.getDatasetMeta(0); - const style = meta.controller.getStyle(i); - - return { - text: label, - fillStyle: style.backgroundColor, - strokeStyle: style.borderColor, - lineWidth: style.borderWidth, - hidden: !chart.getDataVisibility(i), - - // Extra data used for toggling the correct item - index: i - }; - }); - } - return []; - } - }, - - onClick(e, legendItem, legend) { - legend.chart.toggleDataVisibility(legendItem.index); - legend.chart.update(); - } - }, - - // The percentage of the chart that we cut out of the middle. - cutoutPercentage: 50, - - // The rotation of the chart, where the first data arc begins. - rotation: -HALF_PI, - - // The total circumference of the chart. - circumference: DOUBLE_PI, - - // Need to override these to give a nice default - tooltips: { - callbacks: { - title() { - return ''; - }, - label(tooltipItem, data) { - let dataLabel = data.labels[tooltipItem.index]; - const value = ': ' + data.datasets[tooltipItem.datasetIndex].data[tooltipItem.index]; - - if (isArray(dataLabel)) { - // show value on first line of multiline label - // need to clone because we are changing the value - dataLabel = dataLabel.slice(); - dataLabel[0] += value; - } else { - dataLabel += value; - } - - return dataLabel; - } - } - } -}); - function getRatioAndOffset(rotation, circumference, cutout) { let ratioX = 1; let ratioY = 1; @@ -330,14 +251,95 @@ export default class DoughnutController extends DatasetController { } } -DoughnutController.prototype.dataElementType = Arc; - -DoughnutController.prototype.dataElementOptions = [ - 'backgroundColor', - 'borderColor', - 'borderWidth', - 'borderAlign', - 'hoverBackgroundColor', - 'hoverBorderColor', - 'hoverBorderWidth', -]; +DoughnutController.id = 'doughnut'; + +/** + * @type {any} + */ +DoughnutController.defaults = { + datasetElementType: false, + dataElementType: 'arc', + dataElementOptions: [ + 'backgroundColor', + 'borderColor', + 'borderWidth', + 'borderAlign', + 'hoverBackgroundColor', + 'hoverBorderColor', + 'hoverBorderWidth', + ], + animation: { + numbers: { + type: 'number', + properties: ['circumference', 'endAngle', 'innerRadius', 'outerRadius', 'startAngle', 'x', 'y'] + }, + // Boolean - Whether we animate the rotation of the Doughnut + animateRotate: true, + // Boolean - Whether we animate scaling the Doughnut from the centre + animateScale: false + }, + aspectRatio: 1, + legend: { + labels: { + generateLabels(chart) { + const data = chart.data; + if (data.labels.length && data.datasets.length) { + return data.labels.map((label, i) => { + const meta = chart.getDatasetMeta(0); + const style = meta.controller.getStyle(i); + + return { + text: label, + fillStyle: style.backgroundColor, + strokeStyle: style.borderColor, + lineWidth: style.borderWidth, + hidden: !chart.getDataVisibility(i), + + // Extra data used for toggling the correct item + index: i + }; + }); + } + return []; + } + }, + + onClick(e, legendItem, legend) { + legend.chart.toggleDataVisibility(legendItem.index); + legend.chart.update(); + } + }, + + // The percentage of the chart that we cut out of the middle. + cutoutPercentage: 50, + + // The rotation of the chart, where the first data arc begins. + rotation: -HALF_PI, + + // The total circumference of the chart. + circumference: DOUBLE_PI, + + // Need to override these to give a nice default + tooltips: { + callbacks: { + title() { + return ''; + }, + label(tooltipItem, data) { + let dataLabel = data.labels[tooltipItem.index]; + const value = ': ' + data.datasets[tooltipItem.datasetIndex].data[tooltipItem.index]; + + if (isArray(dataLabel)) { + // show value on first line of multiline label + // need to clone because we are changing the value + dataLabel = dataLabel.slice(); + dataLabel[0] += value; + } else { + dataLabel += value; + } + + return dataLabel; + } + } + } +}; diff --git a/src/controllers/controller.line.js b/src/controllers/controller.line.js index e9d31189d..873dc281f 100644 --- a/src/controllers/controller.line.js +++ b/src/controllers/controller.line.js @@ -1,28 +1,8 @@ import DatasetController from '../core/core.datasetController'; -import defaults from '../core/core.defaults'; -import {Line, Point} from '../elements/index'; import {valueOrDefault} from '../helpers/helpers.core'; import {isNumber} from '../helpers/helpers.math'; import {resolve} from '../helpers/helpers.options'; -defaults.set('line', { - showLines: true, - spanGaps: false, - - hover: { - mode: 'index' - }, - - scales: { - _index_: { - type: 'category', - }, - _value_: { - type: 'linear', - }, - } -}); - export default class LineController extends DatasetController { constructor(chart, datasetIndex) { @@ -158,34 +138,55 @@ export default class LineController extends DatasetController { } } -LineController.prototype.datasetElementType = Line; - -LineController.prototype.dataElementType = Point; - -LineController.prototype.datasetElementOptions = [ - 'backgroundColor', - 'borderCapStyle', - 'borderColor', - 'borderDash', - 'borderDashOffset', - 'borderJoinStyle', - 'borderWidth', - 'capBezierPoints', - 'cubicInterpolationMode', - 'fill' -]; - -LineController.prototype.dataElementOptions = { - backgroundColor: 'pointBackgroundColor', - borderColor: 'pointBorderColor', - borderWidth: 'pointBorderWidth', - hitRadius: 'pointHitRadius', - hoverHitRadius: 'pointHitRadius', - hoverBackgroundColor: 'pointHoverBackgroundColor', - hoverBorderColor: 'pointHoverBorderColor', - hoverBorderWidth: 'pointHoverBorderWidth', - hoverRadius: 'pointHoverRadius', - pointStyle: 'pointStyle', - radius: 'pointRadius', - rotation: 'pointRotation' +LineController.id = 'line'; + +/** + * @type {any} + */ +LineController.defaults = { + datasetElementType: 'line', + datasetElementOptions: [ + 'backgroundColor', + 'borderCapStyle', + 'borderColor', + 'borderDash', + 'borderDashOffset', + 'borderJoinStyle', + 'borderWidth', + 'capBezierPoints', + 'cubicInterpolationMode', + 'fill' + ], + + dataElementType: 'point', + dataElementOptions: { + backgroundColor: 'pointBackgroundColor', + borderColor: 'pointBorderColor', + borderWidth: 'pointBorderWidth', + hitRadius: 'pointHitRadius', + hoverHitRadius: 'pointHitRadius', + hoverBackgroundColor: 'pointHoverBackgroundColor', + hoverBorderColor: 'pointHoverBorderColor', + hoverBorderWidth: 'pointHoverBorderWidth', + hoverRadius: 'pointHoverRadius', + pointStyle: 'pointStyle', + radius: 'pointRadius', + rotation: 'pointRotation' + }, + + showLines: true, + spanGaps: false, + + hover: { + mode: 'index' + }, + + scales: { + _index_: { + type: 'category', + }, + _value_: { + type: 'linear', + }, + } }; diff --git a/src/controllers/controller.pie.js b/src/controllers/controller.pie.js index 09acd50b5..de95a69ef 100644 --- a/src/controllers/controller.pie.js +++ b/src/controllers/controller.pie.js @@ -1,11 +1,15 @@ import DoughnutController from './controller.doughnut'; -import defaults from '../core/core.defaults'; -import {clone} from '../helpers/helpers.core'; - -defaults.set('pie', clone(defaults.doughnut)); -defaults.set('pie', { - cutoutPercentage: 0 -}); // Pie charts are Doughnut chart with different defaults -export default DoughnutController; +export default class PieController extends DoughnutController { + +} + +PieController.id = 'pie'; + +/** + * @type {any} + */ +PieController.defaults = { + cutoutPercentage: 0 +}; diff --git a/src/controllers/controller.polarArea.js b/src/controllers/controller.polarArea.js index 2a2cda994..5006eaef3 100644 --- a/src/controllers/controller.polarArea.js +++ b/src/controllers/controller.polarArea.js @@ -1,83 +1,7 @@ import DatasetController from '../core/core.datasetController'; -import defaults from '../core/core.defaults'; -import {Arc} from '../elements/index'; import {toRadians} from '../helpers/helpers.math'; import {resolve} from '../helpers/helpers.options'; -defaults.set('polarArea', { - animation: { - numbers: { - type: 'number', - properties: ['x', 'y', 'startAngle', 'endAngle', 'innerRadius', 'outerRadius'] - }, - animateRotate: true, - animateScale: true - }, - aspectRatio: 1, - datasets: { - indexAxis: 'r' - }, - scales: { - r: { - type: 'radialLinear', - angleLines: { - display: false - }, - beginAtZero: true, - gridLines: { - circular: true - }, - pointLabels: { - display: false - } - } - }, - - startAngle: 0, - legend: { - labels: { - generateLabels(chart) { - const data = chart.data; - if (data.labels.length && data.datasets.length) { - return data.labels.map((label, i) => { - const meta = chart.getDatasetMeta(0); - const style = meta.controller.getStyle(i); - - return { - text: label, - fillStyle: style.backgroundColor, - strokeStyle: style.borderColor, - lineWidth: style.borderWidth, - hidden: !chart.getDataVisibility(i), - - // Extra data used for toggling the correct item - index: i - }; - }); - } - return []; - } - }, - - onClick(e, legendItem, legend) { - legend.chart.toggleDataVisibility(legendItem.index); - legend.chart.update(); - } - }, - - // Need to override these to give a nice default - tooltips: { - callbacks: { - title() { - return ''; - }, - label(item, data) { - return data.labels[item.index] + ': ' + item.value; - } - } - } -}); - function getStartAngleRadians(deg) { // radialLinear scale draws angleLines using startAngle. 0 is expected to be at top. // Here we adjust to standard unit circle used in drawing, where 0 is at right. @@ -211,14 +135,92 @@ export default class PolarAreaController extends DatasetController { } } -PolarAreaController.prototype.dataElementType = Arc; - -PolarAreaController.prototype.dataElementOptions = [ - 'backgroundColor', - 'borderColor', - 'borderWidth', - 'borderAlign', - 'hoverBackgroundColor', - 'hoverBorderColor', - 'hoverBorderWidth' -]; +PolarAreaController.id = 'polarArea'; + +/** + * @type {any} + */ +PolarAreaController.defaults = { + dataElementType: 'arc', + dataElementOptions: [ + 'backgroundColor', + 'borderColor', + 'borderWidth', + 'borderAlign', + 'hoverBackgroundColor', + 'hoverBorderColor', + 'hoverBorderWidth' + ], + + animation: { + numbers: { + type: 'number', + properties: ['x', 'y', 'startAngle', 'endAngle', 'innerRadius', 'outerRadius'] + }, + animateRotate: true, + animateScale: true + }, + aspectRatio: 1, + datasets: { + indexAxis: 'r' + }, + scales: { + r: { + type: 'radialLinear', + angleLines: { + display: false + }, + beginAtZero: true, + gridLines: { + circular: true + }, + pointLabels: { + display: false + } + } + }, + + startAngle: 0, + legend: { + labels: { + generateLabels(chart) { + const data = chart.data; + if (data.labels.length && data.datasets.length) { + return data.labels.map((label, i) => { + const meta = chart.getDatasetMeta(0); + const style = meta.controller.getStyle(i); + + return { + text: label, + fillStyle: style.backgroundColor, + strokeStyle: style.borderColor, + lineWidth: style.borderWidth, + hidden: !chart.getDataVisibility(i), + + // Extra data used for toggling the correct item + index: i + }; + }); + } + return []; + } + }, + + onClick(e, legendItem, legend) { + legend.chart.toggleDataVisibility(legendItem.index); + legend.chart.update(); + } + }, + + // Need to override these to give a nice default + tooltips: { + callbacks: { + title() { + return ''; + }, + label(item, data) { + return data.labels[item.index] + ': ' + item.value; + } + } + } +}; diff --git a/src/controllers/controller.radar.js b/src/controllers/controller.radar.js index 185b14db3..b7dca8f6a 100644 --- a/src/controllers/controller.radar.js +++ b/src/controllers/controller.radar.js @@ -1,27 +1,6 @@ import DatasetController from '../core/core.datasetController'; -import defaults from '../core/core.defaults'; -import {Line, Point} from '../elements/index'; import {valueOrDefault} from '../helpers/helpers.core'; -defaults.set('radar', { - aspectRatio: 1, - spanGaps: false, - scales: { - r: { - type: 'radialLinear', - } - }, - datasets: { - indexAxis: 'r' - }, - elements: { - line: { - fill: 'start', - tension: 0 // no bezier in radar - } - } -}); - export default class RadarController extends DatasetController { /** @@ -104,31 +83,53 @@ export default class RadarController extends DatasetController { } } -RadarController.prototype.datasetElementType = Line; - -RadarController.prototype.dataElementType = Point; - -RadarController.prototype.datasetElementOptions = [ - 'backgroundColor', - 'borderColor', - 'borderCapStyle', - 'borderDash', - 'borderDashOffset', - 'borderJoinStyle', - 'borderWidth', - 'fill' -]; - -RadarController.prototype.dataElementOptions = { - backgroundColor: 'pointBackgroundColor', - borderColor: 'pointBorderColor', - borderWidth: 'pointBorderWidth', - hitRadius: 'pointHitRadius', - hoverBackgroundColor: 'pointHoverBackgroundColor', - hoverBorderColor: 'pointHoverBorderColor', - hoverBorderWidth: 'pointHoverBorderWidth', - hoverRadius: 'pointHoverRadius', - pointStyle: 'pointStyle', - radius: 'pointRadius', - rotation: 'pointRotation' +RadarController.id = 'radar'; + +/** + * @type {any} + */ +RadarController.defaults = { + datasetElementType: 'line', + datasetElementOptions: [ + 'backgroundColor', + 'borderColor', + 'borderCapStyle', + 'borderDash', + 'borderDashOffset', + 'borderJoinStyle', + 'borderWidth', + 'fill' + ], + + dataElementType: 'point', + dataElementOptions: { + backgroundColor: 'pointBackgroundColor', + borderColor: 'pointBorderColor', + borderWidth: 'pointBorderWidth', + hitRadius: 'pointHitRadius', + hoverBackgroundColor: 'pointHoverBackgroundColor', + hoverBorderColor: 'pointHoverBorderColor', + hoverBorderWidth: 'pointHoverBorderWidth', + hoverRadius: 'pointHoverRadius', + pointStyle: 'pointStyle', + radius: 'pointRadius', + rotation: 'pointRotation' + }, + + aspectRatio: 1, + spanGaps: false, + scales: { + r: { + type: 'radialLinear', + } + }, + datasets: { + indexAxis: 'r' + }, + elements: { + line: { + fill: 'start', + tension: 0 // no bezier in radar + } + } }; diff --git a/src/controllers/controller.scatter.js b/src/controllers/controller.scatter.js index 8acbac025..75165a9d4 100644 --- a/src/controllers/controller.scatter.js +++ b/src/controllers/controller.scatter.js @@ -1,7 +1,15 @@ import LineController from './controller.line'; -import defaults from '../core/core.defaults'; -defaults.set('scatter', { +export default class ScatterController extends LineController { + +} + +ScatterController.id = 'scatter'; + +/** + * @type {any} + */ +ScatterController.defaults = { scales: { x: { type: 'linear' @@ -25,7 +33,4 @@ defaults.set('scatter', { } } } -}); - -// Scatter charts use line controllers -export default LineController; +}; diff --git a/src/controllers/index.js b/src/controllers/index.js index b853df696..8c0129b36 100644 --- a/src/controllers/index.js +++ b/src/controllers/index.js @@ -1,8 +1,8 @@ -export {default as bar} from './controller.bar'; -export {default as bubble} from './controller.bubble'; -export {default as doughnut} from './controller.doughnut'; -export {default as line} from './controller.line'; -export {default as polarArea} from './controller.polarArea'; -export {default as pie} from './controller.pie'; -export {default as radar} from './controller.radar'; -export {default as scatter} from './controller.scatter'; +export {default as BarController} from './controller.bar'; +export {default as BubbleController} from './controller.bubble'; +export {default as DoughnutController} from './controller.doughnut'; +export {default as LineController} from './controller.line'; +export {default as PolarAreaController} from './controller.polarArea'; +export {default as PieController} from './controller.pie'; +export {default as RadarController} from './controller.radar'; +export {default as ScatterController} from './controller.scatter'; diff --git a/src/core/core.controller.js b/src/core/core.controller.js index f35ae23f2..e36a514a4 100644 --- a/src/core/core.controller.js +++ b/src/core/core.controller.js @@ -1,12 +1,11 @@ /* eslint-disable import/no-namespace, import/namespace */ import animator from './core.animator'; -import * as controllers from '../controllers'; import defaults from './core.defaults'; import Interaction from './core.interaction'; import layouts from './core.layouts'; import {BasicPlatform, DomPlatform} from '../platform'; import plugins from './core.plugins'; -import scaleService from './core.scaleService'; +import registry from './core.registry'; import {getMaximumWidth, getMaximumHeight, retinaScale} from '../helpers/helpers.dom'; import {mergeIf, merge, _merger, each, callback as callCallback, uid, valueOrDefault, _elementsEqual} from '../helpers/helpers.core'; import {clear as canvasClear, clipArea, unclipArea, _isPointInArea} from '../helpers/helpers.canvas'; @@ -80,7 +79,7 @@ function mergeScaleConfig(config, options) { // apply scale defaults, if not overridden by dataset defaults Object.keys(scales).forEach(key => { const scale = scales[key]; - mergeIf(scale, scaleService.getScaleDefaults(scale.type)); + mergeIf(scale, [defaults.scales[scale.type], defaults.scale]); }); return scales; @@ -442,10 +441,7 @@ class Chart { if (id in scales && scales[id].type === scaleType) { scale = scales[id]; } else { - const scaleClass = scaleService.getScaleConstructor(scaleType); - if (!scaleClass) { - return; - } + const scaleClass = registry.getScale(scaleType); scale = new scaleClass({ id, type: scaleType, @@ -473,7 +469,13 @@ class Chart { me.scales = scales; - scaleService.addScalesToLayout(this); + each(scales, (scale) => { + // Set ILayoutItem parameters for backwards compatibility + scale.fullWidth = scale.options.fullWidth; + scale.position = scale.options.position; + scale.weight = scale.options.weight; + layouts.addBox(me, scale); + }); } /** @@ -537,11 +539,14 @@ class Chart { meta.controller.updateIndex(i); meta.controller.linkScales(); } else { - const ControllerClass = controllers[meta.type]; - if (ControllerClass === undefined) { - throw new Error('"' + meta.type + '" is not a chart type.'); - } - + const controllerDefaults = defaults[type]; + const ControllerClass = registry.getController(type); + Object.assign(ControllerClass.prototype, { + dataElementType: registry.getElement(controllerDefaults.dataElementType), + datasetElementType: controllerDefaults.datasetElementType && registry.getElement(controllerDefaults.datasetElementType), + dataElementOptions: controllerDefaults.dataElementOptions, + datasetElementOptions: controllerDefaults.datasetElementOptions + }); meta.controller = new ControllerClass(me, i); newControllers.push(meta.controller); } @@ -1171,4 +1176,6 @@ Chart.version = version; */ Chart.instances = {}; +Chart.registry = registry; + export default Chart; diff --git a/src/core/core.datasetController.js b/src/core/core.datasetController.js index 255f54cb3..57c431e6a 100644 --- a/src/core/core.datasetController.js +++ b/src/core/core.datasetController.js @@ -686,7 +686,6 @@ export default class DatasetController { datasetIndex: this.index, active }; - } /** @@ -696,7 +695,7 @@ export default class DatasetController { resolveDatasetElementOptions(active) { return this._resolveOptions(this.datasetElementOptions, { active, - type: this.datasetElementType._type + type: this.datasetElementType.id }); } @@ -718,7 +717,7 @@ export default class DatasetController { index, active, info, - type: me.dataElementType._type + type: me.dataElementType.id }); if (info.cacheable) { @@ -975,6 +974,11 @@ export default class DatasetController { } } +/** + * @type {any} + */ +DatasetController.defaults = {}; + /** * Element type used to generate a meta dataset (e.g. Chart.element.Line). */ diff --git a/src/core/core.defaults.js b/src/core/core.defaults.js index 652ee34c5..9cd435a76 100644 --- a/src/core/core.defaults.js +++ b/src/core/core.defaults.js @@ -1,4 +1,4 @@ -import {merge, isArray, valueOrDefault} from '../helpers/helpers.core'; +import {merge, valueOrDefault} from '../helpers/helpers.core'; /** * @param {object} node @@ -58,6 +58,8 @@ export class Defaults { this.tooltips = undefined; this.doughnut = undefined; this._routes = {}; + this.scales = undefined; + this.controllers = undefined; } /** * @param {string} scope @@ -67,52 +69,48 @@ export class Defaults { return merge(getScope(this, scope), values); } + get(scope) { + return getScope(this, scope); + } + /** * Routes the named defaults to fallback to another scope/name. * This routing is useful when those target values, like defaults.color, are changed runtime. * If the values would be copied, the runtime change would not take effect. By routing, the * fallback is evaluated at each access, so its always up to date. * - * Examples: + * Example: * * defaults.route('elements.arc', 'backgroundColor', '', 'color') * - reads the backgroundColor from defaults.color when undefined locally * - * defaults.route('elements.line', ['backgroundColor', 'borderColor'], '', 'color') - * - reads the backgroundColor and borderColor from defaults.color when undefined locally - * - * defaults.route('elements.customLine', ['borderWidth', 'tension'], 'elements.line', ['borderWidth', 'tension']) - * - reads the borderWidth and tension from elements.line when those are not defined in elements.customLine - * * @param {string} scope Scope this route applies to. - * @param {string[]} names Names of the properties that should be routed to different namespace when not defined here. - * @param {string} targetScope The namespace where those properties should be routed to. Empty string ('') is the root of defaults. - * @param {string|string[]} targetNames The target name/names in the target scope the properties should be routed to. + * @param {string} name Property name that should be routed to different namespace when not defined here. + * @param {string} targetScope The namespace where those properties should be routed to. + * Empty string ('') is the root of defaults. + * @param {string} targetName The target name in the target scope the property should be routed to. */ - route(scope, names, targetScope, targetNames) { + route(scope, name, targetScope, targetName) { const scopeObject = getScope(this, scope); const targetScopeObject = getScope(this, targetScope); - const targetNamesIsArray = isArray(targetNames); - names.forEach((name, index) => { - const privateName = '_' + name; - const targetName = targetNamesIsArray ? targetNames[index] : targetNames; - Object.defineProperties(scopeObject, { - // A private property is defined to hold the actual value, when this property is set in its scope (set in the setter) - [privateName]: { - writable: true + const privateName = '_' + name; + + Object.defineProperties(scopeObject, { + // A private property is defined to hold the actual value, when this property is set in its scope (set in the setter) + [privateName]: { + writable: true + }, + // The actual property is defined as getter/setter so we can do the routing when value is not locally set. + [name]: { + enumerable: true, + get() { + // @ts-ignore + return valueOrDefault(this[privateName], targetScopeObject[targetName]); }, - // The actual property is defined as getter/setter so we can do the routing when value is not locally set. - [name]: { - enumerable: true, - get() { - // @ts-ignore - return valueOrDefault(this[privateName], targetScopeObject[targetName]); - }, - set(value) { - this[privateName] = value; - } + set(value) { + this[privateName] = value; } - }); + } }); } } diff --git a/src/core/core.element.js b/src/core/core.element.js index 039ef657b..b0927ce99 100644 --- a/src/core/core.element.js +++ b/src/core/core.element.js @@ -42,3 +42,13 @@ export default class Element { return ret; } } + +/** + * @type any + */ +Element.defaults = {}; + +/** + * @type any + */ +Element.defaultRoutes = undefined; diff --git a/src/core/core.registry.js b/src/core/core.registry.js new file mode 100644 index 000000000..9b685bca4 --- /dev/null +++ b/src/core/core.registry.js @@ -0,0 +1,154 @@ +import DatasetController from './core.datasetController'; +import Element from './core.element'; +import Scale from './core.scale'; +import TypedRegistry from './core.typedRegistry'; +import {each, callback as call} from '../helpers/helpers.core'; + +/** + * Please use the module's default export which provides a singleton instance + * Note: class is exported for typedoc + */ +export class Registry { + constructor() { + this.controllers = new TypedRegistry(DatasetController, ''); + this.elements = new TypedRegistry(Element, 'elements'); + this.plugins = new TypedRegistry(Object, 'plugins'); + this.scales = new TypedRegistry(Scale, 'scales'); + // Order is important, Scale has Element in prototype chain, + // so Scales must be before Elements. Plugins are a fallback, so not listed here. + this._typedRegistries = [this.controllers, this.scales, this.elements]; + } + + /** + * @param {...any} args + */ + add(...args) { + this._registerEach(args); + } + + /** + * @param {...typeof DatasetController} args + */ + addControllers(...args) { + this._registerEach(args, this.controllers); + } + + /** + * @param {...typeof Element} args + */ + addElements(...args) { + this._registerEach(args, this.elements); + } + + /** + * @param {...any} args + */ + addPlugins(...args) { + this._registerEach(args, this.plugins); + } + + /** + * @param {...typeof Scale} args + */ + addScales(...args) { + this._registerEach(args, this.scales); + } + + /** + * @param {string} id + * @returns {typeof DatasetController} + */ + getController(id) { + return this._get(id, this.controllers, 'controller'); + } + + /** + * @param {string} id + * @returns {typeof Element} + */ + getElement(id) { + return this._get(id, this.elements, 'element'); + } + + /** + * @param {string} id + * @returns {object} + */ + getPlugin(id) { + return this._get(id, this.plugins, 'plugin'); + } + + /** + * @param {string} id + * @returns {typeof Scale} + */ + getScale(id) { + return this._get(id, this.scales, 'scale'); + } + + /** + * @private + */ + _registerEach(args, typedRegistry) { + const me = this; + [...args].forEach(arg => { + const reg = typedRegistry || me._getRegistryForType(arg); + if (reg.isForType(arg)) { + me._registerComponent(reg, arg); + } else { + // Handle loopable args + // Use case: + // import * as plugins from './plugins'; + // Chart.register(plugins); + each(arg, item => { + // If there are mixed types in the loopable, make sure those are + // registered in correct registry + // Use case: (treemap exporting controller, elements etc) + // import * as treemap from 'chartjs-chart-treemap'; + // Chart.register(treemap); + + const itemReg = typedRegistry || me._getRegistryForType(item); + me._registerComponent(itemReg, item); + }); + } + }); + } + + /** + * @private + */ + _registerComponent(registry, component) { + call(component.beforeRegister, [], component); + registry.register(component); + call(component.afterRegister, [], component); + } + + /** + * @private + */ + _getRegistryForType(type) { + for (let i = 0; i < this._typedRegistries.length; i++) { + const reg = this._typedRegistries[i]; + if (reg.isForType(type)) { + return reg; + } + } + // plugins is the fallback registry + return this.plugins; + } + + /** + * @private + */ + _get(id, typedRegistry, type) { + const item = typedRegistry.get(id); + if (item === undefined) { + throw new Error('"' + id + '" is not a registered ' + type + '.'); + } + return item; + } + +} + +// singleton instance +export default new Registry(); diff --git a/src/core/core.scaleService.js b/src/core/core.scaleService.js deleted file mode 100644 index 00308dada..000000000 --- a/src/core/core.scaleService.js +++ /dev/null @@ -1,43 +0,0 @@ -import defaults from './core.defaults'; -import {clone, each, merge} from '../helpers/helpers.core'; -import layouts from './core.layouts'; - -export default { - // Scale registration object. Extensions can register new scale types (such as log or DB scales) and then - // use the new chart options to grab the correct scale - constructors: {}, - // Use a registration function so that we can move to an ES6 map when we no longer need to support - // old browsers - - // Scale config defaults - defaults: {}, - registerScale(scaleConstructor) { - const me = this; - const type = scaleConstructor.id; - me.constructors[type] = scaleConstructor; - me.defaults[type] = clone(scaleConstructor.defaults); - }, - getScaleConstructor(type) { - return Object.prototype.hasOwnProperty.call(this.constructors, type) ? this.constructors[type] : undefined; - }, - getScaleDefaults(type) { - // Return the scale defaults merged with the global settings so that we always use the latest ones - return Object.prototype.hasOwnProperty.call(this.defaults, type) ? merge({}, [defaults.scale, this.defaults[type]]) : {}; - }, - updateScaleDefaults(type, additions) { - const me = this; - if (Object.prototype.hasOwnProperty.call(me.defaults, type)) { - me.defaults[type] = Object.assign(me.defaults[type], additions); - } - }, - addScalesToLayout(chart) { - // Adds each scale to the chart.boxes array to be sized accordingly - each(chart.scales, (scale) => { - // Set ILayoutItem parameters for backwards compatibility - scale.fullWidth = scale.options.fullWidth; - scale.position = scale.options.position; - scale.weight = scale.options.weight; - layouts.addBox(chart, scale); - }); - } -}; diff --git a/src/core/core.typedRegistry.js b/src/core/core.typedRegistry.js new file mode 100644 index 000000000..e84126ba5 --- /dev/null +++ b/src/core/core.typedRegistry.js @@ -0,0 +1,104 @@ +import defaults from './core.defaults'; +import {valueOrDefault} from '../helpers/helpers.core'; + +/** + * @typedef {{id: string, defaults: any, defaultRoutes: any}} IChartComponent + */ + +export default class TypedRegistry { + constructor(type, scope) { + this.type = type; + this.scope = scope; + this.items = Object.create(null); + } + + isForType(type) { + return Object.prototype.isPrototypeOf.call(this.type, type); + } + + /** + * @param {IChartComponent} item + * @param {string} [scopeOverride] + * @returns {string} The scope where items defaults were registered to. + */ + register(item, scopeOverride) { + const proto = Object.getPrototypeOf(item); + let parentScope; + + if (isIChartComponent(proto)) { + // Make sure the parent is registered and note the scope where its defaults are. + parentScope = this.register(proto); + } + + const items = this.items; + const id = item.id; + const baseScope = valueOrDefault(scopeOverride, this.scope); + const scope = baseScope ? baseScope + '.' + id : id; + + if (!id) { + throw new Error('class does not have id: ' + Object.getPrototypeOf(item)); + } + + if (id in items) { + // already registered + return scope; + } + + items[id] = item; + registerDefaults(item, scope, parentScope); + + return scope; + } + + /** + * @param {string} id + * @returns {object?} + */ + get(id) { + return this.items[id]; + } + + /** + * @param {IChartComponent} item + */ + unregister(item) { + const items = this.items; + const id = item.id; + + if (id in items) { + delete items[id]; + } + + if (id in defaults[this.scope]) { + delete defaults[this.scope][id]; + } else if (id in defaults) { + delete defaults[id]; + } + } +} + +function registerDefaults(item, scope, parentScope) { + // Inherit the parent's defaults + const itemDefaults = parentScope + ? Object.assign({}, defaults.get(parentScope), item.defaults) + : item.defaults; + + defaults.set(scope, itemDefaults); + + if (item.defaultRoutes) { + routeDefaults(scope, item.defaultRoutes); + } +} + +function routeDefaults(scope, routes) { + Object.keys(routes).forEach(property => { + const parts = routes[property].split('.'); + const targetName = parts.pop(); + const targetScope = parts.join('.'); + defaults.route(scope, property, targetScope, targetName); + }); +} + +function isIChartComponent(proto) { + return 'id' in proto && 'defaults' in proto; +} diff --git a/src/core/index.js b/src/core/index.js index 6f91d53e8..af0b30963 100644 --- a/src/core/index.js +++ b/src/core/index.js @@ -9,6 +9,6 @@ export {default as Element} from './core.element'; export {default as Interaction} from './core.interaction'; export {default as layouts} from './core.layouts'; export {default as plugins} from './core.plugins'; +export {default as registry} from './core.registry'; export {default as Scale} from './core.scale'; -export {default as ScaleService} from './core.scaleService'; export {default as Ticks} from './core.ticks'; diff --git a/src/elements/element.arc.js b/src/elements/element.arc.js index 243524468..a69415a03 100644 --- a/src/elements/element.arc.js +++ b/src/elements/element.arc.js @@ -1,16 +1,7 @@ -import defaults from '../core/core.defaults'; import Element from '../core/core.element'; import {_angleBetween, getAngleFromPoint} from '../helpers/helpers.math'; -const TAU = Math.PI * 2; - -const scope = 'elements.arc'; -defaults.set(scope, { - borderAlign: 'center', - borderColor: '#fff', - borderWidth: 2 -}); -defaults.route(scope, ['backgroundColor'], '', ['color']); +const TAU = Math.PI * 2; function clipArc(ctx, model) { const {startAngle, endAngle, pixelMargin, x, y} = model; @@ -108,7 +99,7 @@ function drawBorder(ctx, element, model) { ctx.stroke(); } -class Arc extends Element { +export default class Arc extends Element { constructor(cfg) { super(); @@ -207,6 +198,20 @@ class Arc extends Element { } } -Arc._type = 'arc'; +Arc.id = 'arc'; -export default Arc; +/** + * @type {any} + */ +Arc.defaults = { + borderAlign: 'center', + borderColor: '#fff', + borderWidth: 2 +}; + +/** + * @type {any} + */ +Arc.defaultRoutes = { + backgroundColor: 'color' +}; diff --git a/src/elements/element.line.js b/src/elements/element.line.js index db1cae385..46b5ae6e8 100644 --- a/src/elements/element.line.js +++ b/src/elements/element.line.js @@ -1,4 +1,3 @@ -import defaults from '../core/core.defaults'; import Element from '../core/core.element'; import {_bezierInterpolation, _pointInLine, _steppedInterpolation} from '../helpers/helpers.interpolation'; import {_computeSegments, _boundSegments} from '../helpers/helpers.segment'; @@ -9,20 +8,6 @@ import {_updateBezierControlPoints} from '../helpers/helpers.curve'; * @typedef { import("./element.point").default } Point */ -const scope = 'elements.line'; -defaults.set(scope, { - borderCapStyle: 'butt', - borderDash: [], - borderDashOffset: 0, - borderJoinStyle: 'miter', - borderWidth: 3, - capBezierPoints: true, - fill: true, - tension: 0 -}); - -defaults.route(scope, ['backgroundColor', 'borderColor'], '', 'color'); - function setStyle(ctx, vm) { ctx.lineCap = vm.borderCapStyle; ctx.setLineDash(vm.borderDash); @@ -199,7 +184,7 @@ function _getInterpolationMethod(options) { return _pointInLine; } -class Line extends Element { +export default class Line extends Element { constructor(cfg) { super(); @@ -359,6 +344,26 @@ class Line extends Element { } } -Line._type = 'line'; +Line.id = 'line'; -export default Line; +/** + * @type {any} + */ +Line.defaults = { + borderCapStyle: 'butt', + borderDash: [], + borderDashOffset: 0, + borderJoinStyle: 'miter', + borderWidth: 3, + capBezierPoints: true, + fill: true, + tension: 0 +}; + +/** + * @type {any} + */ +Line.defaultRoutes = { + backgroundColor: 'color', + borderColor: 'color' +}; diff --git a/src/elements/element.point.js b/src/elements/element.point.js index 93e4948bb..52b38645f 100644 --- a/src/elements/element.point.js +++ b/src/elements/element.point.js @@ -1,20 +1,7 @@ -import defaults from '../core/core.defaults'; import Element from '../core/core.element'; import {_isPointInArea, drawPoint} from '../helpers/helpers.canvas'; -const scope = 'elements.point'; -defaults.set(scope, { - borderWidth: 1, - hitRadius: 1, - hoverBorderWidth: 1, - hoverRadius: 4, - pointStyle: 'circle', - radius: 3 -}); - -defaults.route(scope, ['backgroundColor', 'borderColor'], '', 'color'); - -class Point extends Element { +export default class Point extends Element { constructor(cfg) { super(); @@ -82,6 +69,24 @@ class Point extends Element { } } -Point._type = 'point'; +Point.id = 'point'; -export default Point; +/** + * @type {any} + */ +Point.defaults = { + borderWidth: 1, + hitRadius: 1, + hoverBorderWidth: 1, + hoverRadius: 4, + pointStyle: 'circle', + radius: 3 +}; + +/** + * @type {any} + */ +Point.defaultRoutes = { + backgroundColor: 'color', + borderColor: 'color' +}; diff --git a/src/elements/element.rectangle.js b/src/elements/element.rectangle.js index 8dfa2833d..2363a87ff 100644 --- a/src/elements/element.rectangle.js +++ b/src/elements/element.rectangle.js @@ -1,15 +1,6 @@ -import defaults from '../core/core.defaults'; import Element from '../core/core.element'; import {isObject} from '../helpers/helpers.core'; -const scope = 'elements.rectangle'; -defaults.set(scope, { - borderSkipped: 'start', - borderWidth: 0 -}); - -defaults.route(scope, ['backgroundColor', 'borderColor'], '', 'color'); - /** * Helper function to get the bounds of the bar regardless of the orientation * @param {Rectangle} bar the bar @@ -131,7 +122,7 @@ function inRange(bar, x, y, useFinalPosition) { && (skipY || y >= bounds.top && y <= bounds.bottom); } -class Rectangle extends Element { +export default class Rectangle extends Element { constructor(cfg) { super(); @@ -193,6 +184,20 @@ class Rectangle extends Element { } } -Rectangle._type = 'rectangle'; +Rectangle.id = 'rectangle'; -export default Rectangle; +/** + * @type {any} + */ +Rectangle.defaults = { + borderSkipped: 'start', + borderWidth: 0 +}; + +/** + * @type {any} + */ +Rectangle.defaultRoutes = { + backgroundColor: 'color', + borderColor: 'color' +}; diff --git a/src/index.js b/src/index.js index 4ee572ae7..3b2cd389c 100644 --- a/src/index.js +++ b/src/index.js @@ -17,18 +17,25 @@ import Element from './core/core.element'; import * as elements from './elements/index'; import Interaction from './core/core.interaction'; import layouts from './core/core.layouts'; -import * as platforms from './platform'; +import * as platforms from './platform/index'; +import * as plugins from './plugins'; import pluginsCore from './core/core.plugins'; +import registry from './core/core.registry'; import Scale from './core/core.scale'; -import scaleService from './core/core.scaleService'; +import * as scales from './scales'; import Ticks from './core/core.ticks'; +Chart.register = (...items) => registry.add(...items); + +// Register built-ins +Chart.register(controllers, scales, elements, plugins); + Chart.helpers = helpers; Chart._adapters = _adapters; Chart.Animation = Animation; Chart.animator = animator; Chart.animationService = animationService; -Chart.controllers = controllers; +Chart.controllers = registry.controllers.items; Chart.DatasetController = DatasetController; Chart.defaults = defaults; Chart.Element = Element; @@ -37,16 +44,10 @@ Chart.Interaction = Interaction; Chart.layouts = layouts; Chart.platforms = platforms; Chart.plugins = pluginsCore; +Chart.registry = registry; Chart.Scale = Scale; -Chart.scaleService = scaleService; Chart.Ticks = Ticks; -// Register built-in scales -import * as scales from './scales'; -Object.keys(scales).forEach(key => Chart.scaleService.registerScale(scales[key])); - -// Loading built-in plugins -import * as plugins from './plugins'; for (const k in plugins) { if (Object.prototype.hasOwnProperty.call(plugins, k)) { Chart.plugins.register(plugins[k]); diff --git a/src/scales/scale.category.js b/src/scales/scale.category.js index 3386aa1e0..988bcc0cd 100644 --- a/src/scales/scale.category.js +++ b/src/scales/scale.category.js @@ -1,9 +1,6 @@ import Scale from '../core/core.scale'; -const defaultConfig = { -}; - -class CategoryScale extends Scale { +export default class CategoryScale extends Scale { constructor(cfg) { super(cfg); @@ -108,7 +105,7 @@ class CategoryScale extends Scale { CategoryScale.id = 'category'; -// INTERNAL: default options, registered in src/index.js -CategoryScale.defaults = defaultConfig; - -export default CategoryScale; +/** + * @type {any} + */ +CategoryScale.defaults = {}; diff --git a/src/scales/scale.linear.js b/src/scales/scale.linear.js index c7f8e88ec..bd3fc5af6 100644 --- a/src/scales/scale.linear.js +++ b/src/scales/scale.linear.js @@ -2,13 +2,7 @@ import {isFinite, valueOrDefault} from '../helpers/helpers.core'; import LinearScaleBase from './scale.linearbase'; import Ticks from '../core/core.ticks'; -const defaultConfig = { - ticks: { - callback: Ticks.formatters.numeric - } -}; - -class LinearScale extends LinearScaleBase { +export default class LinearScale extends LinearScaleBase { determineDataLimits() { const me = this; @@ -54,7 +48,11 @@ class LinearScale extends LinearScaleBase { LinearScale.id = 'linear'; -// INTERNAL: default options, registered in src/index.js -LinearScale.defaults = defaultConfig; - -export default LinearScale; +/** + * @type {any} + */ +LinearScale.defaults = { + ticks: { + callback: Ticks.formatters.numeric + } +}; diff --git a/src/scales/scale.logarithmic.js b/src/scales/scale.logarithmic.js index e0cce4182..98942f9b6 100644 --- a/src/scales/scale.logarithmic.js +++ b/src/scales/scale.logarithmic.js @@ -47,17 +47,7 @@ function generateTicks(generationOptions, dataRange) { return ticks; } -const defaultConfig = { - // label settings - ticks: { - callback: Ticks.formatters.logarithmic, - major: { - enabled: true - } - } -}; - -class LogarithmicScale extends Scale { +export default class LogarithmicScale extends Scale { constructor(cfg) { super(cfg); @@ -184,7 +174,14 @@ class LogarithmicScale extends Scale { LogarithmicScale.id = 'logarithmic'; -// INTERNAL: default options, registered in src/index.js -LogarithmicScale.defaults = defaultConfig; - -export default LogarithmicScale; +/** + * @type {any} + */ +LogarithmicScale.defaults = { + ticks: { + callback: Ticks.formatters.logarithmic, + major: { + enabled: true + } + } +}; diff --git a/src/scales/scale.radialLinear.js b/src/scales/scale.radialLinear.js index 888c5c570..fa1f7caa4 100644 --- a/src/scales/scale.radialLinear.js +++ b/src/scales/scale.radialLinear.js @@ -6,59 +6,6 @@ import Ticks from '../core/core.ticks'; import {valueOrDefault, isArray, isFinite, callback as callCallback, isNullOrUndef} from '../helpers/helpers.core'; import {toFont, resolve} from '../helpers/helpers.options'; - -const defaultConfig = { - display: true, - - // Boolean - Whether to animate scaling the chart from the centre - animate: true, - position: 'chartArea', - - angleLines: { - display: true, - color: 'rgba(0,0,0,0.1)', - lineWidth: 1, - borderDash: [], - borderDashOffset: 0.0 - }, - - gridLines: { - circular: false - }, - - // label settings - ticks: { - // Boolean - Show a backdrop to the scale label - showLabelBackdrop: true, - - // String - The colour of the label backdrop - backdropColor: 'rgba(255,255,255,0.75)', - - // Number - The backdrop padding above & below the label in pixels - backdropPaddingY: 2, - - // Number - The backdrop padding to the side of the label in pixels - backdropPaddingX: 2, - - callback: Ticks.formatters.numeric - }, - - pointLabels: { - // Boolean - if true, show point labels - display: true, - - // Number - Point label font size in pixels - font: { - size: 10 - }, - - // Function - Used to convert point labels - callback(label) { - return label; - } - } -}; - function getTickBackdropHeight(opts) { const tickOpts = opts.ticks; @@ -306,7 +253,7 @@ function numberOrZero(param) { return isNumber(param) ? param : 0; } -class RadialLinearScale extends LinearScaleBase { +export default class RadialLinearScale extends LinearScaleBase { constructor(cfg) { super(cfg); @@ -593,7 +540,57 @@ class RadialLinearScale extends LinearScaleBase { RadialLinearScale.id = 'radialLinear'; -// INTERNAL: default options, registered in src/index.js -RadialLinearScale.defaults = defaultConfig; +/** + * @type {any} + */ +RadialLinearScale.defaults = { + display: true, + + // Boolean - Whether to animate scaling the chart from the centre + animate: true, + position: 'chartArea', + + angleLines: { + display: true, + color: 'rgba(0,0,0,0.1)', + lineWidth: 1, + borderDash: [], + borderDashOffset: 0.0 + }, -export default RadialLinearScale; + gridLines: { + circular: false + }, + + // label settings + ticks: { + // Boolean - Show a backdrop to the scale label + showLabelBackdrop: true, + + // String - The colour of the label backdrop + backdropColor: 'rgba(255,255,255,0.75)', + + // Number - The backdrop padding above & below the label in pixels + backdropPaddingY: 2, + + // Number - The backdrop padding to the side of the label in pixels + backdropPaddingX: 2, + + callback: Ticks.formatters.numeric + }, + + pointLabels: { + // Boolean - if true, show point labels + display: true, + + // Number - Point label font size in pixels + font: { + size: 10 + }, + + // Function - Used to convert point labels + callback(label) { + return label; + } + } +}; diff --git a/src/scales/scale.time.js b/src/scales/scale.time.js index 61e2491b7..888f424c5 100644 --- a/src/scales/scale.time.js +++ b/src/scales/scale.time.js @@ -198,44 +198,7 @@ function ticksFromTimestamps(scale, values, majorUnit) { return (ilen === 0 || !majorUnit) ? ticks : setMajorTicks(scale, ticks, map, majorUnit); } -const defaultConfig = { - - /** - * Scale boundary strategy (bypassed by min/max time options) - * - `data`: make sure data are fully visible, ticks outside are removed - * - `ticks`: make sure ticks are fully visible, data outside are truncated - * @see https://github.com/chartjs/Chart.js/pull/4556 - * @since 2.7.0 - */ - bounds: 'data', - - adapters: {}, - time: { - parser: false, // false == a pattern string from or a custom callback that converts its argument to a timestamp - unit: false, // false == automatic or override with week, month, year, etc. - round: false, // none, or override with week, month, year, etc. - isoWeekday: false, // override week start day - minUnit: 'millisecond', - displayFormats: {} - }, - ticks: { - /** - * Ticks generation input values: - * - 'auto': generates "optimal" ticks based on scale size and time options. - * - 'data': generates ticks from data (including labels from data {t|x|y} objects). - * - 'labels': generates ticks from user given `data.labels` values ONLY. - * @see https://github.com/chartjs/Chart.js/pull/4507 - * @since 2.7.0 - */ - source: 'auto', - - major: { - enabled: false - } - } -}; - -class TimeScale extends Scale { +export default class TimeScale extends Scale { /** * @param {object} props @@ -644,7 +607,41 @@ class TimeScale extends Scale { TimeScale.id = 'time'; -// INTERNAL: default options, registered in src/index.js -TimeScale.defaults = defaultConfig; +/** + * @type {any} + */ +TimeScale.defaults = { + /** + * Scale boundary strategy (bypassed by min/max time options) + * - `data`: make sure data are fully visible, ticks outside are removed + * - `ticks`: make sure ticks are fully visible, data outside are truncated + * @see https://github.com/chartjs/Chart.js/pull/4556 + * @since 2.7.0 + */ + bounds: 'data', -export default TimeScale; + adapters: {}, + time: { + parser: false, // false == a pattern string from or a custom callback that converts its argument to a timestamp + unit: false, // false == automatic or override with week, month, year, etc. + round: false, // none, or override with week, month, year, etc. + isoWeekday: false, // override week start day + minUnit: 'millisecond', + displayFormats: {} + }, + ticks: { + /** + * Ticks generation input values: + * - 'auto': generates "optimal" ticks based on scale size and time options. + * - 'data': generates ticks from data (including labels from data {t|x|y} objects). + * - 'labels': generates ticks from user given `data.labels` values ONLY. + * @see https://github.com/chartjs/Chart.js/pull/4507 + * @since 2.7.0 + */ + source: 'auto', + + major: { + enabled: false + } + } +}; diff --git a/src/scales/scale.timeseries.js b/src/scales/scale.timeseries.js index 1476040eb..a1cce266f 100644 --- a/src/scales/scale.timeseries.js +++ b/src/scales/scale.timeseries.js @@ -191,7 +191,9 @@ class TimeSeriesScale extends TimeScale { TimeSeriesScale.id = 'timeseries'; -// INTERNAL: default options, registered in src/index.js +/** + * @type {any} + */ TimeSeriesScale.defaults = TimeScale.defaults; export default TimeSeriesScale; diff --git a/test/specs/controller.doughnut.tests.js b/test/specs/controller.doughnut.tests.js index dbf9f0ce3..32c992dcd 100644 --- a/test/specs/controller.doughnut.tests.js +++ b/test/specs/controller.doughnut.tests.js @@ -3,7 +3,7 @@ describe('Chart.controllers.doughnut', function() { it('should be registered as dataset controller', function() { expect(typeof Chart.controllers.doughnut).toBe('function'); - expect(Chart.controllers.doughnut).toBe(Chart.controllers.pie); + expect(typeof Chart.controllers.pie).toBe('function'); }); it('should be constructed', function() { diff --git a/test/specs/core.controller.tests.js b/test/specs/core.controller.tests.js index ed94068a7..e733d6a6b 100644 --- a/test/specs/core.controller.tests.js +++ b/test/specs/core.controller.tests.js @@ -163,7 +163,7 @@ describe('Chart', function() { } }); } - expect(createChart).toThrow(new Error('"area" is not a chart type.')); + expect(createChart).toThrow(new Error('"area" is not a registered controller.')); }); }); @@ -175,7 +175,7 @@ describe('Chart', function() { _jasmineCheckC: 'c0' }); - Chart.helpers.merge(Chart.scaleService.defaults.logarithmic, { + Chart.helpers.merge(Chart.defaults.scales.logarithmic, { _jasmineCheckB: 'b1', _jasmineCheckC: 'c1', }); @@ -185,8 +185,8 @@ describe('Chart', function() { delete Chart.defaults.scale._jasmineCheckA; delete Chart.defaults.scale._jasmineCheckB; delete Chart.defaults.scale._jasmineCheckC; - delete Chart.scaleService.defaults.logarithmic._jasmineCheckB; - delete Chart.scaleService.defaults.logarithmic._jasmineCheckC; + delete Chart.defaults.scales.logarithmic._jasmineCheckB; + delete Chart.defaults.scales.logarithmic._jasmineCheckC; }); it('should default to "category" for x scales and "linear" for y scales', function() { @@ -298,8 +298,8 @@ describe('Chart', function() { expect(Chart.defaults.line._jasmineCheck).not.toBeDefined(); expect(Chart.defaults._jasmineCheck).not.toBeDefined(); - expect(Chart.scaleService.defaults.linear._jasmineCheck).not.toBeDefined(); - expect(Chart.scaleService.defaults.category._jasmineCheck).not.toBeDefined(); + expect(Chart.defaults.scales.linear._jasmineCheck).not.toBeDefined(); + expect(Chart.defaults.scales.category._jasmineCheck).not.toBeDefined(); }); }); diff --git a/test/specs/core.scale.tests.js b/test/specs/core.scale.tests.js index 556fea585..2062eacee 100644 --- a/test/specs/core.scale.tests.js +++ b/test/specs/core.scale.tests.js @@ -462,7 +462,7 @@ describe('Core.scale', function() { } CustomScale.id = 'customScale'; CustomScale.defaults = {}; - Chart.scaleService.registerScale(CustomScale); + Chart.register(CustomScale); var chart = window.acquireChart({ type: 'line', diff --git a/test/specs/core.scaleService.tests.js b/test/specs/core.scaleService.tests.js deleted file mode 100644 index ecf5b21a8..000000000 --- a/test/specs/core.scaleService.tests.js +++ /dev/null @@ -1,30 +0,0 @@ -// Tests of the scale service -describe('Test the scale service', function() { - - it('should update scale defaults', function() { - var type = 'my_test_type'; - var Constructor = function() { - this.initialized = true; - }; - Constructor.id = type; - Constructor.defaults = { - testProp: true - }; - Chart.scaleService.registerScale(Constructor); - - // Should equal defaults but not be an identical object - expect(Chart.scaleService.getScaleDefaults(type)).toEqual(jasmine.objectContaining({ - testProp: true - })); - - Chart.scaleService.updateScaleDefaults(type, { - testProp: 'red', - newProp: 42 - }); - - expect(Chart.scaleService.getScaleDefaults(type)).toEqual(jasmine.objectContaining({ - testProp: 'red', - newProp: 42 - })); - }); -}); diff --git a/test/specs/global.namespace.tests.js b/test/specs/global.namespace.tests.js index cb4675fc6..8e39d5574 100644 --- a/test/specs/global.namespace.tests.js +++ b/test/specs/global.namespace.tests.js @@ -17,8 +17,8 @@ describe('Chart namespace', function() { expect(Chart.platforms.BasicPlatform instanceof Function).toBeTruthy(); expect(Chart.platforms.DomPlatform instanceof Function).toBeTruthy(); + expect(Chart.registry instanceof Object).toBeTruthy(); expect(Chart.Scale instanceof Object).toBeTruthy(); - expect(Chart.scaleService instanceof Object).toBeTruthy(); expect(Chart.Ticks instanceof Object).toBeTruthy(); }); }); diff --git a/test/specs/scale.category.tests.js b/test/specs/scale.category.tests.js index 0e17194ba..5c998dc4f 100644 --- a/test/specs/scale.category.tests.js +++ b/test/specs/scale.category.tests.js @@ -11,52 +11,15 @@ function getValues(scale) { describe('Category scale tests', function() { describe('auto', jasmine.fixture.specs('scale.category')); - it('Should register the constructor with the scale service', function() { - var Constructor = Chart.scaleService.getScaleConstructor('category'); + it('Should register the constructor with the registry', function() { + var Constructor = Chart.registry.getScale('category'); expect(Constructor).not.toBe(undefined); expect(typeof Constructor).toBe('function'); }); it('Should have the correct default config', function() { - var defaultConfig = Chart.scaleService.getScaleDefaults('category'); - expect(defaultConfig).toEqual({ - display: true, - reverse: false, - beginAtZero: false, - - gridLines: { - color: 'rgba(0,0,0,0.1)', - drawBorder: true, - drawOnChartArea: true, - drawTicks: true, // draw ticks extending towards the label - tickMarkLength: 10, - lineWidth: 1, - offsetGridLines: false, - display: true, - borderDash: [], - borderDashOffset: 0.0 - }, - offset: false, - scaleLabel: Chart.defaults.scale.scaleLabel, - ticks: { - minRotation: 0, - maxRotation: 50, - mirror: false, - padding: 0, - display: true, - callback: defaultConfig.ticks.callback, // make this nicer, then check explicitly below - autoSkip: true, - autoSkipPadding: 0, - labelOffset: 0, - minor: {}, - major: {}, - lineWidth: 0, - strokeStyle: '', - } - }); - - // Is this actually a function - expect(defaultConfig.ticks.callback).toEqual(jasmine.any(Function)); + var defaultConfig = Chart.defaults.scales.category; + expect(defaultConfig).toEqual({}); }); @@ -71,9 +34,9 @@ describe('Category scale tests', function() { xLabels: ['tick1', 'tick2', 'tick3', 'tick4', 'tick5'] }; - var config = Chart.helpers.clone(Chart.scaleService.getScaleDefaults('category')); + var config = Chart.helpers.clone(Chart.defaults.scales.category); config.position = 'bottom'; - var Constructor = Chart.scaleService.getScaleConstructor('category'); + var Constructor = Chart.registry.getScale('category'); var scale = new Constructor({ ctx: {}, chart: { @@ -99,9 +62,9 @@ describe('Category scale tests', function() { yLabels: ['tick1', 'tick2', 'tick3', 'tick4', 'tick5'] }; - var config = Chart.helpers.clone(Chart.scaleService.getScaleDefaults('category')); + var config = Chart.helpers.clone(Chart.defaults.scales.category); config.position = 'left'; // y axis - var Constructor = Chart.scaleService.getScaleConstructor('category'); + var Constructor = Chart.registry.getScale('category'); var scale = new Constructor({ ctx: {}, chart: { diff --git a/test/specs/scale.linear.tests.js b/test/specs/scale.linear.tests.js index 0d1eec7cd..36014eabf 100644 --- a/test/specs/scale.linear.tests.js +++ b/test/specs/scale.linear.tests.js @@ -5,48 +5,14 @@ function getLabels(scale) { describe('Linear Scale', function() { describe('auto', jasmine.fixture.specs('scale.linear')); - it('Should register the constructor with the scale service', function() { - var Constructor = Chart.scaleService.getScaleConstructor('linear'); + it('Should register the constructor with the registry', function() { + var Constructor = Chart.registry.getScale('linear'); expect(Constructor).not.toBe(undefined); expect(typeof Constructor).toBe('function'); }); it('Should have the correct default config', function() { - var defaultConfig = Chart.scaleService.getScaleDefaults('linear'); - expect(defaultConfig).toEqual({ - display: true, - gridLines: { - color: 'rgba(0,0,0,0.1)', - drawBorder: true, - drawOnChartArea: true, - drawTicks: true, // draw ticks extending towards the label - tickMarkLength: 10, - lineWidth: 1, - offsetGridLines: false, - display: true, - borderDash: [], - borderDashOffset: 0.0 - }, - offset: false, - reverse: false, - beginAtZero: false, - scaleLabel: Chart.defaults.scale.scaleLabel, - ticks: { - minRotation: 0, - maxRotation: 50, - mirror: false, - padding: 0, - display: true, - callback: defaultConfig.ticks.callback, // make this work nicer, then check below - autoSkip: true, - autoSkipPadding: 0, - labelOffset: 0, - minor: {}, - major: {}, - lineWidth: 0, - strokeStyle: '', - } - }); + var defaultConfig = Chart.defaults.scales.linear; expect(defaultConfig.ticks.callback).toEqual(jasmine.any(Function)); }); diff --git a/test/specs/scale.logarithmic.tests.js b/test/specs/scale.logarithmic.tests.js index c1c98d819..19a616ab2 100644 --- a/test/specs/scale.logarithmic.tests.js +++ b/test/specs/scale.logarithmic.tests.js @@ -3,49 +3,21 @@ function getLabels(scale) { } describe('Logarithmic Scale tests', function() { - it('should register the constructor with the scale service', function() { - var Constructor = Chart.scaleService.getScaleConstructor('logarithmic'); + it('should register', function() { + var Constructor = Chart.registry.getScale('logarithmic'); expect(Constructor).not.toBe(undefined); expect(typeof Constructor).toBe('function'); }); it('should have the correct default config', function() { - var defaultConfig = Chart.scaleService.getScaleDefaults('logarithmic'); + var defaultConfig = Chart.defaults.scales.logarithmic; expect(defaultConfig).toEqual({ - display: true, - gridLines: { - color: 'rgba(0,0,0,0.1)', - drawBorder: true, - drawOnChartArea: true, - drawTicks: true, - tickMarkLength: 10, - lineWidth: 1, - offsetGridLines: false, - display: true, - borderDash: [], - borderDashOffset: 0.0 - }, - offset: false, - reverse: false, - beginAtZero: false, - scaleLabel: Chart.defaults.scale.scaleLabel, ticks: { - minRotation: 0, - maxRotation: 50, - mirror: false, - padding: 0, - display: true, - callback: defaultConfig.ticks.callback, // make this nicer, then check explicitly below - autoSkip: true, - autoSkipPadding: 0, - labelOffset: 0, - minor: {}, - lineWidth: 0, - strokeStyle: '', + callback: Chart.Ticks.formatters.logarithmic, major: { enabled: true - }, - }, + } + } }); // Is this actually a function diff --git a/test/specs/scale.radialLinear.tests.js b/test/specs/scale.radialLinear.tests.js index 1e2268652..dd0846d71 100644 --- a/test/specs/scale.radialLinear.tests.js +++ b/test/specs/scale.radialLinear.tests.js @@ -6,15 +6,19 @@ function getLabels(scale) { describe('Test the radial linear scale', function() { describe('auto', jasmine.fixture.specs('scale.radialLinear')); - it('Should register the constructor with the scale service', function() { - var Constructor = Chart.scaleService.getScaleConstructor('radialLinear'); + it('Should register the constructor with the registry', function() { + var Constructor = Chart.registry.getScale('radialLinear'); expect(Constructor).not.toBe(undefined); expect(typeof Constructor).toBe('function'); }); it('Should have the correct default config', function() { - var defaultConfig = Chart.scaleService.getScaleDefaults('radialLinear'); + var defaultConfig = Chart.defaults.scales.radialLinear; expect(defaultConfig).toEqual({ + display: true, + animate: true, + position: 'chartArea', + angleLines: { display: true, color: 'rgba(0,0,0,0.1)', @@ -22,52 +26,26 @@ describe('Test the radial linear scale', function() { borderDash: [], borderDashOffset: 0.0 }, - animate: true, - display: true, + gridLines: { - circular: false, - color: 'rgba(0,0,0,0.1)', - drawBorder: true, - drawOnChartArea: true, - drawTicks: true, - tickMarkLength: 10, - lineWidth: 1, - offsetGridLines: false, - display: true, - borderDash: [], - borderDashOffset: 0.0 - }, - pointLabels: { - display: true, - font: { - size: 10, - }, - callback: defaultConfig.pointLabels.callback, // make this nicer, then check explicitly below + circular: false }, - position: 'chartArea', - offset: false, - reverse: false, - beginAtZero: false, - scaleLabel: Chart.defaults.scale.scaleLabel, + ticks: { + showLabelBackdrop: true, backdropColor: 'rgba(255,255,255,0.75)', backdropPaddingY: 2, backdropPaddingX: 2, - minRotation: 0, - maxRotation: 50, - mirror: false, - padding: 0, - showLabelBackdrop: true, - display: true, - callback: defaultConfig.ticks.callback, // make this nicer, then check explicitly below - autoSkip: true, - autoSkipPadding: 0, - labelOffset: 0, - minor: {}, - major: {}, - lineWidth: 0, - strokeStyle: '', + callback: defaultConfig.ticks.callback }, + + pointLabels: { + display: true, + font: { + size: 10 + }, + callback: defaultConfig.pointLabels.callback + } }); // Is this actually a function diff --git a/test/specs/scale.time.tests.js b/test/specs/scale.time.tests.js index 3300c19fa..db7fb9c39 100644 --- a/test/specs/scale.time.tests.js +++ b/test/specs/scale.time.tests.js @@ -52,64 +52,32 @@ describe('Time scale tests', function() { expect(window.moment).not.toBe(undefined); }); - it('should register the constructor with the scale service', function() { - var Constructor = Chart.scaleService.getScaleConstructor('time'); + it('should register the constructor with the registry', function() { + var Constructor = Chart.registry.getScale('time'); expect(Constructor).not.toBe(undefined); expect(typeof Constructor).toBe('function'); }); it('should have the correct default config', function() { - var defaultConfig = Chart.scaleService.getScaleDefaults('time'); + var defaultConfig = Chart.defaults.scales.time; expect(defaultConfig).toEqual({ - display: true, - gridLines: { - color: 'rgba(0,0,0,0.1)', - drawBorder: true, - drawOnChartArea: true, - drawTicks: true, - tickMarkLength: 10, - lineWidth: 1, - offsetGridLines: false, - display: true, - borderDash: [], - borderDashOffset: 0.0 - }, - offset: false, - reverse: false, - beginAtZero: false, - scaleLabel: Chart.defaults.scale.scaleLabel, bounds: 'data', adapters: {}, + time: { + parser: false, // false == a pattern string from or a custom callback that converts its argument to a timestamp + unit: false, // false == automatic or override with week, month, year, etc. + round: false, // none, or override with week, month, year, etc. + isoWeekday: false, // override week start day + minUnit: 'millisecond', + displayFormats: {} + }, ticks: { - minRotation: 0, - maxRotation: 50, - mirror: false, source: 'auto', - padding: 0, - display: true, - callback: defaultConfig.ticks.callback, // make this nicer, then check explicitly below, - autoSkip: true, - autoSkipPadding: 0, - labelOffset: 0, - minor: {}, major: { enabled: false - }, - lineWidth: 0, - strokeStyle: '', - }, - time: { - parser: false, - unit: false, - round: false, - isoWeekday: false, - minUnit: 'millisecond', - displayFormats: {} + } } }); - - // Is this actually a function - expect(defaultConfig.ticks.callback).toEqual(jasmine.any(Function)); }); it('should correctly determine the unit', function() { @@ -153,7 +121,7 @@ describe('Time scale tests', function() { var config; beforeEach(function() { - config = Chart.helpers.clone(Chart.scaleService.getScaleDefaults('time')); + config = Chart.helpers.clone(Chart.defaults.scales.time); config.ticks.source = 'labels'; config.time.unit = 'day'; }); @@ -202,7 +170,7 @@ describe('Time scale tests', function() { unit: 'week', isoWeekday: 3 // Wednesday } - }, Chart.scaleService.getScaleDefaults('time')); + }, Chart.defaults.scales.time); var scale = createScale(mockData, config); var ticks = getLabels(scale);