UNPKG

@sgratzl/chartjs-chart-boxplot

Version:

Chart.js module for charting boxplots and violin charts

975 lines (961 loc) 33.3 kB
/** * @sgratzl/chartjs-chart-boxplot * https://github.com/sgratzl/chartjs-chart-boxplot * * Copyright (c) 2019-2023 Samuel Gratzl <sam@sgratzl.com> */ 'use strict'; var chart_js = require('chart.js'); var helpers = require('chart.js/helpers'); var boxplots = require('@sgratzl/boxplots'); function whiskers(boxplot, arr, coef = 1.5) { const iqr = boxplot.q3 - boxplot.q1; const coefValid = typeof coef === 'number' && coef > 0; let whiskerMin = coefValid ? Math.max(boxplot.min, boxplot.q1 - coef * iqr) : boxplot.min; let whiskerMax = coefValid ? Math.min(boxplot.max, boxplot.q3 + coef * iqr) : boxplot.max; if (Array.isArray(arr)) { for (let i = 0; i < arr.length; i += 1) { const v = arr[i]; if (v >= whiskerMin) { whiskerMin = v; break; } } for (let i = arr.length - 1; i >= 0; i -= 1) { const v = arr[i]; if (v <= whiskerMax) { whiskerMax = v; break; } } } return { whiskerMin, whiskerMax, }; } const defaultStatsOptions = { coef: 1.5, quantiles: 7, whiskersMode: 'nearest', }; function determineQuantiles(q) { if (typeof q === 'function') { return q; } const lookup = { hinges: boxplots.quantilesHinges, fivenum: boxplots.quantilesFivenum, 7: boxplots.quantilesType7, quantiles: boxplots.quantilesType7, linear: boxplots.quantilesLinear, lower: boxplots.quantilesLower, higher: boxplots.quantilesHigher, nearest: boxplots.quantilesNearest, midpoint: boxplots.quantilesMidpoint, }; return lookup[q] || boxplots.quantilesType7; } function determineStatsOptions(options) { const coef = options == null || typeof options.coef !== 'number' ? defaultStatsOptions.coef : options.coef; const q = options == null || options.quantiles == null ? boxplots.quantilesType7 : options.quantiles; const quantiles = determineQuantiles(q); const whiskersMode = options == null || typeof options.whiskersMode !== 'string' ? defaultStatsOptions.whiskersMode : options.whiskersMode; return { coef, quantiles, whiskersMode, }; } function boxplotStats(arr, options) { const vs = typeof Float64Array !== 'undefined' && !(arr instanceof Float32Array || arr instanceof Float64Array) ? Float64Array.from(arr) : arr; const r = boxplots.boxplot(vs, determineStatsOptions(options)); return { items: Array.from(r.items), outliers: r.outlier, whiskerMax: r.whiskerHigh, whiskerMin: r.whiskerLow, max: r.max, median: r.median, mean: r.mean, min: r.min, q1: r.q1, q3: r.q3, }; } function computeSamples(min, max, points) { const range = max - min; const samples = []; const inc = range / points; for (let v = min; v <= max && inc > 0; v += inc) { samples.push(v); } if (samples[samples.length - 1] !== max) { samples.push(max); } return samples; } function violinStats(arr, options) { if (arr.length === 0) { return undefined; } const vs = typeof Float64Array !== 'undefined' && !(arr instanceof Float32Array || arr instanceof Float64Array) ? Float64Array.from(arr) : arr; const stats = boxplots.boxplot(vs, determineStatsOptions(options)); const samples = computeSamples(stats.min, stats.max, options.points); const coords = samples.map((v) => ({ v, estimate: stats.kde(v) })); const maxEstimate = coords.reduce((a, d) => Math.max(a, d.estimate), Number.NEGATIVE_INFINITY); return { max: stats.max, min: stats.min, mean: stats.mean, median: stats.median, q1: stats.q1, q3: stats.q3, items: Array.from(stats.items), coords, outliers: [], maxEstimate, }; } function asBoxPlotStats(value, options) { if (!value) { return undefined; } if (typeof value.median === 'number' && typeof value.q1 === 'number' && typeof value.q3 === 'number') { if (typeof value.whiskerMin === 'undefined') { const { coef } = determineStatsOptions(options); const { whiskerMin, whiskerMax } = whiskers(value, Array.isArray(value.items) ? value.items.slice().sort((a, b) => a - b) : null, coef); value.whiskerMin = whiskerMin; value.whiskerMax = whiskerMax; } return value; } if (!Array.isArray(value)) { return undefined; } return boxplotStats(value, options); } function asViolinStats(value, options) { if (!value) { return undefined; } if (typeof value.median === 'number' && Array.isArray(value.coords)) { return value; } if (!Array.isArray(value)) { return undefined; } return violinStats(value, options); } function rnd(seed = Date.now()) { let s = seed; return () => { s = (s * 9301 + 49297) % 233280; return s / 233280; }; } const baseDefaults$1 = { borderWidth: 1, outlierStyle: 'circle', outlierRadius: 2, outlierBorderWidth: 1, itemStyle: 'circle', itemRadius: 0, itemBorderWidth: 0, itemHitRadius: 0, meanStyle: 'circle', meanRadius: 3, meanBorderWidth: 1, hitPadding: 2, outlierHitRadius: 4, }; const baseRoutes = { outlierBackgroundColor: 'backgroundColor', outlierBorderColor: 'borderColor', itemBackgroundColor: 'backgroundColor', itemBorderColor: 'borderColor', meanBackgroundColor: 'backgroundColor', meanBorderColor: 'borderColor', }; const baseOptionKeys = (() => Object.keys(baseDefaults$1).concat(Object.keys(baseRoutes)))(); let StatsBase$1 = class StatsBase extends chart_js.Element { isVertical() { return !this.horizontal; } _drawItems(ctx) { const vert = this.isVertical(); const props = this.getProps(['x', 'y', 'items', 'width', 'height', 'outliers']); const { options } = this; if (options.itemRadius <= 0 || !props.items || props.items.length <= 0) { return; } ctx.save(); ctx.strokeStyle = options.itemBorderColor; ctx.fillStyle = options.itemBackgroundColor; ctx.lineWidth = options.itemBorderWidth; const random = rnd(this._datasetIndex * 1000 + this._index); const pointOptions = { pointStyle: options.itemStyle, radius: options.itemRadius, borderWidth: options.itemBorderWidth, }; const outliers = new Set(props.outliers || []); if (vert) { props.items.forEach((v) => { if (!outliers.has(v)) { helpers.drawPoint(ctx, pointOptions, props.x - props.width / 2 + random() * props.width, v); } }); } else { props.items.forEach((v) => { if (!outliers.has(v)) { helpers.drawPoint(ctx, pointOptions, v, props.y - props.height / 2 + random() * props.height); } }); } ctx.restore(); } _drawOutliers(ctx) { const vert = this.isVertical(); const props = this.getProps(['x', 'y', 'outliers']); const { options } = this; if (options.outlierRadius <= 0 || !props.outliers || props.outliers.length === 0) { return; } ctx.save(); ctx.fillStyle = options.outlierBackgroundColor; ctx.strokeStyle = options.outlierBorderColor; ctx.lineWidth = options.outlierBorderWidth; const pointOptions = { pointStyle: options.outlierStyle, radius: options.outlierRadius, borderWidth: options.outlierBorderWidth, }; if (vert) { props.outliers.forEach((v) => { helpers.drawPoint(ctx, pointOptions, props.x, v); }); } else { props.outliers.forEach((v) => { helpers.drawPoint(ctx, pointOptions, v, props.y); }); } ctx.restore(); } _drawMeanDot(ctx) { const vert = this.isVertical(); const props = this.getProps(['x', 'y', 'mean']); const { options } = this; if (options.meanRadius <= 0 || props.mean == null || Number.isNaN(props.mean)) { return; } ctx.save(); ctx.fillStyle = options.meanBackgroundColor; ctx.strokeStyle = options.meanBorderColor; ctx.lineWidth = options.meanBorderWidth; const pointOptions = { pointStyle: options.meanStyle, radius: options.meanRadius, borderWidth: options.meanBorderWidth, }; if (vert) { helpers.drawPoint(ctx, pointOptions, props.x, props.mean); } else { helpers.drawPoint(ctx, pointOptions, props.mean, props.y); } ctx.restore(); } _getBounds(_useFinalPosition) { return { left: 0, top: 0, right: 0, bottom: 0, }; } _getHitBounds(useFinalPosition) { const padding = this.options.hitPadding; const b = this._getBounds(useFinalPosition); return { left: b.left - padding, top: b.top - padding, right: b.right + padding, bottom: b.bottom + padding, }; } inRange(mouseX, mouseY, useFinalPosition) { if (Number.isNaN(this.x) && Number.isNaN(this.y)) { return false; } return (this._boxInRange(mouseX, mouseY, useFinalPosition) || this._outlierIndexInRange(mouseX, mouseY, useFinalPosition) != null || this._itemIndexInRange(mouseX, mouseY, useFinalPosition) != null); } inXRange(mouseX, useFinalPosition) { const bounds = this._getHitBounds(useFinalPosition); return mouseX >= bounds.left && mouseX <= bounds.right; } inYRange(mouseY, useFinalPosition) { const bounds = this._getHitBounds(useFinalPosition); return mouseY >= bounds.top && mouseY <= bounds.bottom; } _outlierIndexInRange(mouseX, mouseY, useFinalPosition) { const props = this.getProps(['x', 'y'], useFinalPosition); const hitRadius = this.options.outlierHitRadius; const outliers = this._getOutliers(useFinalPosition); const vertical = this.isVertical(); if ((vertical && Math.abs(mouseX - props.x) > hitRadius) || (!vertical && Math.abs(mouseY - props.y) > hitRadius)) { return null; } const toCompare = vertical ? mouseY : mouseX; for (let i = 0; i < outliers.length; i += 1) { if (Math.abs(outliers[i] - toCompare) <= hitRadius) { return vertical ? { index: i, x: props.x, y: outliers[i] } : { index: i, x: outliers[i], y: props.y }; } } return null; } _itemIndexInRange(mouseX, mouseY, useFinalPosition) { const hitRadius = this.options.itemHitRadius; if (hitRadius <= 0) { return null; } const props = this.getProps(['x', 'y', 'items', 'width', 'height', 'outliers'], useFinalPosition); const vert = this.isVertical(); const { options } = this; if (options.itemRadius <= 0 || !props.items || props.items.length <= 0) { return null; } const random = rnd(this._datasetIndex * 1000 + this._index); const outliers = new Set(props.outliers || []); if (vert) { for (let i = 0; i < props.items.length; i++) { const y = props.items[i]; if (!outliers.has(y)) { const x = props.x - props.width / 2 + random() * props.width; if (Math.abs(x - mouseX) <= hitRadius && Math.abs(y - mouseY) <= hitRadius) { return { index: i, x, y }; } } } } else { for (let i = 0; i < props.items.length; i++) { const x = props.items[i]; if (!outliers.has(x)) { const y = props.y - props.height / 2 + random() * props.height; if (Math.abs(x - mouseX) <= hitRadius && Math.abs(y - mouseY) <= hitRadius) { return { index: i, x, y }; } } } } return null; } _boxInRange(mouseX, mouseY, useFinalPosition) { const bounds = this._getHitBounds(useFinalPosition); return mouseX >= bounds.left && mouseX <= bounds.right && mouseY >= bounds.top && mouseY <= bounds.bottom; } getCenterPoint(useFinalPosition) { const props = this.getProps(['x', 'y'], useFinalPosition); return { x: props.x, y: props.y, }; } _getOutliers(useFinalPosition) { const props = this.getProps(['outliers'], useFinalPosition); return props.outliers || []; } tooltipPosition(eventPosition, tooltip) { if (!eventPosition || typeof eventPosition === 'boolean') { return this.getCenterPoint(); } if (tooltip) { delete tooltip._tooltipOutlier; delete tooltip._tooltipItem; } const info = this._outlierIndexInRange(eventPosition.x, eventPosition.y); if (info != null && tooltip) { tooltip._tooltipOutlier = { index: info.index, datasetIndex: this._datasetIndex, }; return { x: info.x, y: info.y, }; } const itemInfo = this._itemIndexInRange(eventPosition.x, eventPosition.y); if (itemInfo != null && tooltip) { tooltip._tooltipItem = { index: itemInfo.index, datasetIndex: this._datasetIndex, }; return { x: itemInfo.x, y: itemInfo.y, }; } return this.getCenterPoint(); } }; const boxOptionsKeys = baseOptionKeys.concat(['medianColor', 'lowerBackgroundColor']); class BoxAndWiskers extends StatsBase$1 { draw(ctx) { ctx.save(); ctx.fillStyle = this.options.backgroundColor; ctx.strokeStyle = this.options.borderColor; ctx.lineWidth = this.options.borderWidth; this._drawBoxPlot(ctx); this._drawOutliers(ctx); this._drawMeanDot(ctx); ctx.restore(); this._drawItems(ctx); } _drawBoxPlot(ctx) { if (this.isVertical()) { this._drawBoxPlotVertical(ctx); } else { this._drawBoxPlotHorizontal(ctx); } } _drawBoxPlotVertical(ctx) { const { options } = this; const props = this.getProps(['x', 'width', 'q1', 'q3', 'median', 'whiskerMin', 'whiskerMax']); const { x } = props; const { width } = props; const x0 = x - width / 2; if (props.q3 > props.q1) { ctx.fillRect(x0, props.q1, width, props.q3 - props.q1); } else { ctx.fillRect(x0, props.q3, width, props.q1 - props.q3); } ctx.save(); if (options.medianColor && options.medianColor !== 'transparent' && options.medianColor !== '#0000') { ctx.strokeStyle = options.medianColor; } ctx.beginPath(); ctx.moveTo(x0, props.median); ctx.lineTo(x0 + width, props.median); ctx.closePath(); ctx.stroke(); ctx.restore(); ctx.save(); if (options.lowerBackgroundColor && options.lowerBackgroundColor !== 'transparent' && options.lowerBackgroundColor !== '#0000') { ctx.fillStyle = options.lowerBackgroundColor; if (props.q3 > props.q1) { ctx.fillRect(x0, props.median, width, props.q3 - props.median); } else { ctx.fillRect(x0, props.median, width, props.q1 - props.median); } } ctx.restore(); if (props.q3 > props.q1) { ctx.strokeRect(x0, props.q1, width, props.q3 - props.q1); } else { ctx.strokeRect(x0, props.q3, width, props.q1 - props.q3); } ctx.beginPath(); ctx.moveTo(x0, props.whiskerMin); ctx.lineTo(x0 + width, props.whiskerMin); ctx.moveTo(x, props.whiskerMin); ctx.lineTo(x, props.q1); ctx.moveTo(x0, props.whiskerMax); ctx.lineTo(x0 + width, props.whiskerMax); ctx.moveTo(x, props.whiskerMax); ctx.lineTo(x, props.q3); ctx.closePath(); ctx.stroke(); } _drawBoxPlotHorizontal(ctx) { const { options } = this; const props = this.getProps(['y', 'height', 'q1', 'q3', 'median', 'whiskerMin', 'whiskerMax']); const { y } = props; const { height } = props; const y0 = y - height / 2; if (props.q3 > props.q1) { ctx.fillRect(props.q1, y0, props.q3 - props.q1, height); } else { ctx.fillRect(props.q3, y0, props.q1 - props.q3, height); } ctx.save(); if (options.medianColor && options.medianColor !== 'transparent') { ctx.strokeStyle = options.medianColor; } ctx.beginPath(); ctx.moveTo(props.median, y0); ctx.lineTo(props.median, y0 + height); ctx.closePath(); ctx.stroke(); ctx.restore(); ctx.save(); if (options.lowerBackgroundColor && options.lowerBackgroundColor !== 'transparent') { ctx.fillStyle = options.lowerBackgroundColor; if (props.q3 > props.q1) { ctx.fillRect(props.median, y0, props.q3 - props.median, height); } else { ctx.fillRect(props.median, y0, props.q1 - props.median, height); } } ctx.restore(); if (props.q3 > props.q1) { ctx.strokeRect(props.q1, y0, props.q3 - props.q1, height); } else { ctx.strokeRect(props.q3, y0, props.q1 - props.q3, height); } ctx.beginPath(); ctx.moveTo(props.whiskerMin, y0); ctx.lineTo(props.whiskerMin, y0 + height); ctx.moveTo(props.whiskerMin, y); ctx.lineTo(props.q1, y); ctx.moveTo(props.whiskerMax, y0); ctx.lineTo(props.whiskerMax, y0 + height); ctx.moveTo(props.whiskerMax, y); ctx.lineTo(props.q3, y); ctx.closePath(); ctx.stroke(); } _getBounds(useFinalPosition) { const vert = this.isVertical(); if (this.x == null) { return { left: 0, top: 0, right: 0, bottom: 0, }; } if (vert) { const { x, width, whiskerMax, whiskerMin } = this.getProps(['x', 'width', 'whiskerMin', 'whiskerMax'], useFinalPosition); const x0 = x - width / 2; return { left: x0, top: whiskerMax, right: x0 + width, bottom: whiskerMin, }; } const { y, height, whiskerMax, whiskerMin } = this.getProps(['y', 'height', 'whiskerMin', 'whiskerMax'], useFinalPosition); const y0 = y - height / 2; return { left: whiskerMin, top: y0, right: whiskerMax, bottom: y0 + height, }; } } BoxAndWiskers.id = 'boxandwhiskers'; BoxAndWiskers.defaults = { ...chart_js.BarElement.defaults, ...baseDefaults$1, medianColor: 'transparent', lowerBackgroundColor: 'transparent', }; BoxAndWiskers.defaultRoutes = { ...chart_js.BarElement.defaultRoutes, ...baseRoutes }; class Violin extends StatsBase$1 { draw(ctx) { ctx.save(); ctx.fillStyle = this.options.backgroundColor; ctx.strokeStyle = this.options.borderColor; ctx.lineWidth = this.options.borderWidth; const props = this.getProps(['x', 'y', 'median', 'width', 'height', 'min', 'max', 'coords', 'maxEstimate']); if (props.median != null) { helpers.drawPoint(ctx, { pointStyle: 'rectRot', radius: 5, borderWidth: this.options.borderWidth, }, props.x, props.y); } if (props.coords && props.coords.length > 0) { this._drawCoords(ctx, props); } this._drawOutliers(ctx); this._drawMeanDot(ctx); ctx.restore(); this._drawItems(ctx); } _drawCoords(ctx, props) { let maxEstimate; if (props.maxEstimate == null) { maxEstimate = props.coords.reduce((a, d) => Math.max(a, d.estimate), Number.NEGATIVE_INFINITY); } else { maxEstimate = props.maxEstimate; } ctx.beginPath(); if (this.isVertical()) { const { x, width } = props; const factor = width / 2 / maxEstimate; props.coords.forEach((c) => { ctx.lineTo(x - c.estimate * factor, c.v); }); props.coords .slice() .reverse() .forEach((c) => { ctx.lineTo(x + c.estimate * factor, c.v); }); } else { const { y, height } = props; const factor = height / 2 / maxEstimate; props.coords.forEach((c) => { ctx.lineTo(c.v, y - c.estimate * factor); }); props.coords .slice() .reverse() .forEach((c) => { ctx.lineTo(c.v, y + c.estimate * factor); }); } ctx.closePath(); ctx.stroke(); ctx.fill(); } _getBounds(useFinalPosition) { if (this.isVertical()) { const { x, width, min, max } = this.getProps(['x', 'width', 'min', 'max'], useFinalPosition); const x0 = x - width / 2; return { left: x0, top: max, right: x0 + width, bottom: min, }; } const { y, height, min, max } = this.getProps(['y', 'height', 'min', 'max'], useFinalPosition); const y0 = y - height / 2; return { left: min, top: y0, right: max, bottom: y0 + height, }; } } Violin.id = 'violin'; Violin.defaults = { ...chart_js.BarElement.defaults, ...baseDefaults$1 }; Violin.defaultRoutes = { ...chart_js.BarElement.defaultRoutes, ...baseRoutes }; const interpolators = { number(from, to, factor) { if (from === to) { return to; } if (from == null) { return to; } if (to == null) { return from; } return from + (to - from) * factor; }, }; function interpolateNumberArray(from, to, factor) { if (typeof from === 'number' && typeof to === 'number') { return interpolators.number(from, to, factor); } if (Array.isArray(from) && Array.isArray(to)) { return to.map((t, i) => interpolators.number(from[i], t, factor)); } return to; } function interpolateKdeCoords(from, to, factor) { if (Array.isArray(from) && Array.isArray(to)) { return to.map((t, i) => ({ v: interpolators.number(from[i] ? from[i].v : null, t.v, factor), estimate: interpolators.number(from[i] ? from[i].estimate : null, t.estimate, factor), })); } return to; } function patchInHoveredOutlier(item) { const value = item.formattedValue; const that = this; if (value && that._tooltipOutlier != null && item.datasetIndex === that._tooltipOutlier.datasetIndex) { value.hoveredOutlierIndex = that._tooltipOutlier.index; } if (value && that._tooltipItem != null && item.datasetIndex === that._tooltipItem.datasetIndex) { value.hoveredItemIndex = that._tooltipItem.index; } } function outlierPositioner(items, eventPosition) { if (!items.length) { return false; } let x = 0; let y = 0; let count = 0; for (let i = 0; i < items.length; i += 1) { const el = items[i].element; if (el && el.hasValue()) { const pos = el.tooltipPosition(eventPosition, this); x += pos.x; y += pos.y; count += 1; } } return { x: x / count, y: y / count, }; } outlierPositioner.id = 'average'; outlierPositioner.register = () => { chart_js.Tooltip.positioners.average = outlierPositioner; return outlierPositioner; }; function baseDefaults(keys) { const colorKeys = ['borderColor', 'backgroundColor'].concat(keys.filter((c) => c.endsWith('Color'))); return { animations: { numberArray: { fn: interpolateNumberArray, properties: ['outliers', 'items'], }, colors: { type: 'color', properties: colorKeys, }, }, transitions: { show: { animations: { colors: { type: 'color', properties: colorKeys, from: 'transparent', }, }, }, hide: { animations: { colors: { type: 'color', properties: colorKeys, to: 'transparent', }, }, }, }, minStats: 'min', maxStats: 'max', ...defaultStatsOptions, }; } function defaultOverrides() { return { plugins: { tooltip: { position: outlierPositioner.register().id, callbacks: { beforeLabel: patchInHoveredOutlier, }, }, }, }; } class StatsBase extends chart_js.BarController { _transformStats(target, source, mapper) { for (const key of ['min', 'max', 'median', 'q3', 'q1', 'mean']) { const v = source[key]; if (typeof v === 'number') { target[key] = mapper(v); } } for (const key of ['outliers', 'items']) { if (Array.isArray(source[key])) { target[key] = source[key].map(mapper); } } } getMinMax(scale, canStack) { const bak = scale.axis; const config = this.options; scale.axis = config.minStats; const { min } = super.getMinMax(scale, canStack); scale.axis = config.maxStats; const { max } = super.getMinMax(scale, canStack); scale.axis = bak; return { min, max }; } parsePrimitiveData(meta, data, start, count) { const vScale = meta.vScale; const iScale = meta.iScale; const labels = iScale.getLabels(); const r = []; for (let i = 0; i < count; i += 1) { const index = i + start; const parsed = {}; parsed[iScale.axis] = iScale.parse(labels[index], index); const stats = this._parseStats(data == null ? null : data[index], this.options); if (stats) { Object.assign(parsed, stats); parsed[vScale.axis] = stats.median; } r.push(parsed); } return r; } parseArrayData(meta, data, start, count) { return this.parsePrimitiveData(meta, data, start, count); } parseObjectData(meta, data, start, count) { return this.parsePrimitiveData(meta, data, start, count); } getLabelAndValue(index) { const r = super.getLabelAndValue(index); const { vScale } = this._cachedMeta; const parsed = this.getParsed(index); if (!vScale || !parsed || r.value === 'NaN') { return r; } r.value = { raw: parsed, hoveredOutlierIndex: -1, hoveredItemIndex: -1, }; this._transformStats(r.value, parsed, (v) => vScale.getLabelForValue(v)); const s = this._toStringStats(r.value.raw); r.value.toString = function toString() { if (this.hoveredOutlierIndex >= 0) { return `(outlier: ${this.outliers[this.hoveredOutlierIndex]})`; } if (this.hoveredItemIndex >= 0) { return `(item: ${this.items[this.hoveredItemIndex]})`; } return s; }; return r; } _toStringStats(b) { const f = (v) => (v == null ? 'NaN' : helpers.formatNumber(v, this.chart.options.locale, {})); return `(min: ${f(b.min)}, 25% quantile: ${f(b.q1)}, median: ${f(b.median)}, mean: ${f(b.mean)}, 75% quantile: ${f(b.q3)}, max: ${f(b.max)})`; } updateElement(rectangle, index, properties, mode) { const reset = mode === 'reset'; const scale = this._cachedMeta.vScale; const parsed = this.getParsed(index); const base = scale.getBasePixel(); properties._datasetIndex = this.index; properties._index = index; this._transformStats(properties, parsed, (v) => (reset ? base : scale.getPixelForValue(v, index))); super.updateElement(rectangle, index, properties, mode); } } function patchController(type, config, controller, elements = [], scales = []) { chart_js.registry.addControllers(controller); if (Array.isArray(elements)) { chart_js.registry.addElements(...elements); } else { chart_js.registry.addElements(elements); } if (Array.isArray(scales)) { chart_js.registry.addScales(...scales); } else { chart_js.registry.addScales(scales); } const c = config; c.type = type; return c; } class BoxPlotController extends StatsBase { _parseStats(value, config) { return asBoxPlotStats(value, config); } _transformStats(target, source, mapper) { super._transformStats(target, source, mapper); for (const key of ['whiskerMin', 'whiskerMax']) { target[key] = mapper(source[key]); } } } BoxPlotController.id = 'boxplot'; BoxPlotController.defaults = helpers.merge({}, [ chart_js.BarController.defaults, baseDefaults(boxOptionsKeys), { animations: { numbers: { type: 'number', properties: chart_js.BarController.defaults.animations.numbers.properties.concat(['q1', 'q3', 'min', 'max', 'median', 'whiskerMin', 'whiskerMax', 'mean'], boxOptionsKeys.filter((c) => !c.endsWith('Color'))), }, }, dataElementType: BoxAndWiskers.id, }, ]); BoxPlotController.overrides = helpers.merge({}, [chart_js.BarController.overrides, defaultOverrides()]); class BoxPlotChart extends chart_js.Chart { constructor(item, config) { super(item, patchController('boxplot', config, BoxPlotController, BoxAndWiskers, [chart_js.LinearScale, chart_js.CategoryScale])); } } BoxPlotChart.id = BoxPlotController.id; class ViolinController extends StatsBase { _parseStats(value, config) { return asViolinStats(value, config); } _transformStats(target, source, mapper) { super._transformStats(target, source, mapper); target.maxEstimate = source.maxEstimate; if (Array.isArray(source.coords)) { target.coords = source.coords.map((c) => ({ ...c, v: mapper(c.v) })); } } } ViolinController.id = 'violin'; ViolinController.defaults = helpers.merge({}, [ chart_js.BarController.defaults, baseDefaults(baseOptionKeys), { points: 100, animations: { numbers: { type: 'number', properties: chart_js.BarController.defaults.animations.numbers.properties.concat(['q1', 'q3', 'min', 'max', 'median', 'maxEstimate'], baseOptionKeys.filter((c) => !c.endsWith('Color'))), }, kdeCoords: { fn: interpolateKdeCoords, properties: ['coords'], }, }, dataElementType: Violin.id, }, ]); ViolinController.overrides = helpers.merge({}, [chart_js.BarController.overrides, defaultOverrides()]); class ViolinChart extends chart_js.Chart { constructor(item, config) { super(item, patchController('violin', config, ViolinController, Violin, [chart_js.LinearScale, chart_js.CategoryScale])); } } ViolinChart.id = ViolinController.id; exports.BoxAndWiskers = BoxAndWiskers; exports.BoxPlotChart = BoxPlotChart; exports.BoxPlotController = BoxPlotController; exports.StatsBase = StatsBase$1; exports.Violin = Violin; exports.ViolinChart = ViolinChart; exports.ViolinController = ViolinController; //# sourceMappingURL=index.cjs.map