UNPKG

chartjs-plugin-gradient

Version:
349 lines (313 loc) 10.7 kB
/*! * chartjs-plugin-gradient v0.6.1 * https://github.com/kurkle/chartjs-plugin-gradient#readme * (c) 2022 Jukka Kurkela * Released under the MIT License */ import { isNumber, color, defined } from 'chart.js/helpers'; import { Chart } from 'chart.js'; const isChartV3 = Chart.version; const parse = isChartV3 ? (scale, value) => scale.parse(value) : (scale, value) => value; function scaleValue(scale, value) { const normValue = isNumber(value) ? parseFloat(value) : parse(scale, value); return scale.getPixelForValue(normValue); } /** * @typedef { import("chart.js").Chart } Chart * @typedef { import("chart.js").Scale } Scale */ /** * check if the area is consistent * @param {Object} area - area to check * @returns {boolean} */ const areaIsValid = (area) => area && area.right > area.left && area.bottom > area.top; /** * Create a canvas gradient * @param {CanvasRenderingContext2D} ctx - chart canvas context * @param {string} axis - axis type of scale * @param {Object} area - scale instance * @returns {CanvasGradient} created gradient */ function createGradient(ctx, axis, area) { if (axis === 'r') { return ctx.createRadialGradient(area.xCenter, area.yCenter, 0, area.xCenter, area.yCenter, area.drawingArea); } if (axis === 'y') { return ctx.createLinearGradient(0, area.bottom, 0, area.top); } return ctx.createLinearGradient(area.left, 0, area.right, 0); } /** * Add color stop to a gradient * @param {CanvasGradient} gradient - gradient instance * @param {Array} colors - all colors to add */ function applyColors(gradient, colors) { colors.forEach(function(item) { gradient.addColorStop( item.stop, item.color.rgbString() ); }); } /** * Get the gradient plugin configuration from the state for a specific dataset option * @param {Object} state - state of the plugin * @param {{key: string, legendItemKey: string}} keyOption - option of the dataset where the gradient is applied * @param {number} datasetIndex - dataset index * @returns {Object|undefined} gradient plugin configuration from the state for a specific dataset option */ function getGradientData(state, keyOption, datasetIndex) { if (state.options.has(keyOption.key)) { const option = state.options.get(keyOption.key); const gradientData = option.filter((el) => el.datasetIndex === datasetIndex); if (gradientData.length) { return gradientData[0]; } } } /** * Get the pixel and its percentage on the scale, used for color stop in the gradient, for the passed value * @param {Scale} scale - scale used by dataset * @param {string|number} value - value to search * @returns {{pixel: number, stop: number}} the pixel and its percentage on the scale, used for color stop in the gradient */ function getPixelStop(scale, value) { if (scale.type === 'radialLinear') { const distance = scale.getDistanceFromCenterForValue(value); return {pixel: distance, stop: distance / scale.drawingArea}; } const reverse = scale.options.reverse; const pixel = scaleValue(scale, value); const stop = scale.getDecimalForPixel(pixel); return {pixel, stop: reverse ? 1 - stop : stop}; } // IEC 61966-2-1:1999 const toRGBs = (l) => l <= 0.0031308 ? l * 12.92 : Math.pow(l, 1.0 / 2.4) * 1.055 - 0.055; // IEC 61966-2-1:1999 const fromRGBs = (srgb) => srgb <= 0.04045 ? srgb / 12.92 : Math.pow((srgb + 0.055) / 1.055, 2.4); function interpolate(percent, startColor, endColor) { const start = startColor.color.rgb; const startR = fromRGBs(start.r / 255); const startG = fromRGBs(start.g / 255); const startB = fromRGBs(start.b / 255); const end = endColor.color.rgb; const endR = fromRGBs(end.r / 255); const endG = fromRGBs(end.g / 255); const endB = fromRGBs(end.b / 255); return color({ r: Math.round(toRGBs(startR + percent * (endR - startR)) * 255), g: Math.round(toRGBs(startG + percent * (endG - startG)) * 255), b: Math.round(toRGBs(startB + percent * (endB - startB)) * 255), a: start.a + percent * Math.abs(end.a - start.a) }); } /** * Calculate a color from gradient stop color by a value of the dataset. * @param {Object} state - state of the plugin * @param {{key: string, legendItemKey: string}} keyOption - option of the dataset where the gradient is applied * @param {number} datasetIndex - dataset index * @param {number} value - value used for searching the color * @returns {Object} calculated color */ function getInterpolatedColorByValue(state, keyOption, datasetIndex, value) { const data = getGradientData(state, keyOption, datasetIndex); if (!data || !data.stopColors.length) { return; } const {stop: percent} = getPixelStop(data.scale, value); let startColor, endColor; for (const stopColor of data.stopColors) { if (stopColor.stop === percent) { return stopColor.color; } if (stopColor.stop < percent) { startColor = stopColor; } else if (stopColor.stop > percent && !endColor) { endColor = stopColor; } } if (!endColor) { return startColor; } return interpolate(percent, startColor, endColor); } const legendOptions = [ {key: 'backgroundColor', legendItemKey: 'fillStyle'}, {key: 'borderColor', legendItemKey: 'strokeStyle'}]; const legendBoxHeight = (chart, options) => options.labels && options.labels.font && defined(options.labels.font.size) ? options.labels.font.size : chart.options.font.size; function setLegendItem(state, ctx, keyOption, item, area) { const data = getGradientData(state, keyOption, item.datasetIndex); if (!data || !data.stopColors.length) { return; } const value = createGradient(ctx, data.axis, area); applyColors(value, data.stopColors); item[keyOption.legendItemKey] = value; } function buildArea(hitBox, {boxWidth, boxHeight}) { return { top: hitBox.top, left: hitBox.left, bottom: hitBox.top + boxHeight, right: hitBox.left + boxWidth, xCenter: hitBox.left + boxWidth / 2, yCenter: hitBox.top + boxHeight / 2, drawingArea: Math.max(boxWidth, boxHeight) / 2 }; } function applyGradientToLegendByDatasetIndex(chart, state, item, boxSize) { const hitBox = chart.legend.legendHitBoxes[item.datasetIndex]; const area = buildArea(hitBox, boxSize); if (areaIsValid(area)) { legendOptions.forEach(function(keyOption) { setLegendItem(state, chart.ctx, keyOption, item, area); }); } } function applyGradientToLegendByDataIndex(chart, state, dataset, datasetIndex) { for (const item of chart.legend.legendItems) { legendOptions.forEach(function(keyOption) { const value = dataset.data[item.index]; const c = getInterpolatedColorByValue(state, keyOption, datasetIndex, value); if (c && c.valid) { item[keyOption.legendItemKey] = c.rgbString(); } }); } } /** * @typedef { import("chart.js").Chart } Chart */ /** * Udpate the legend items, applying the gradients * @param {Chart} chart - chart instance * @param {Object} state - state of the plugin */ function updateLegendItems(chart, state) { const legend = chart.legend; const options = legend.options; const boxHeight = options.labels.boxHeight ? options.labels.boxHeight : legendBoxHeight(chart, options); const boxWidth = options.labels.boxWidth; const datasets = chart.data.datasets; for (let i = 0; i < datasets.length; i++) { const item = legend.legendItems[i]; if (item.datasetIndex === i) { applyGradientToLegendByDatasetIndex(chart, state, item, {boxWidth, boxHeight}); } else { applyGradientToLegendByDataIndex(chart, state, datasets[i], i); } } } const chartStates = new Map(); const getScale = isChartV3 ? (meta, axis) => meta[axis + 'Scale'] : (meta, axis) => meta.controller['_' + axis + 'Scale']; function addColors(scale, colors, stopColors) { for (const value of Object.keys(colors)) { const {pixel, stop} = getPixelStop(scale, value); if (isFinite(pixel) && isFinite(stop)) { const colorStop = color(colors[value]); if (colorStop && colorStop.valid) { stopColors.push({ stop: Math.max(0, Math.min(1, stop)), color: colorStop }); } } } stopColors.sort((a, b) => a.stop - b.stop); } function setValue(meta, dataset, key, value) { dataset[key] = value; if (!meta.dataset) { return; } if (meta.dataset.options) { meta.dataset.options[key] = value; } else { meta.dataset[key] = value; } } function getStateOptions(state, meta, key, datasetIndex) { let stateOptions = state.options.get(key); if (!stateOptions) { stateOptions = []; state.options.set(key, stateOptions); } else if (!meta.hidden) { stateOptions = stateOptions.filter((el) => el.datasetIndex !== datasetIndex); state.options.set(key, stateOptions); } return stateOptions; } function updateDataset(chart, state, gradient, dataset, datasetIndex) { const ctx = chart.ctx; const meta = chart.getDatasetMeta(datasetIndex); if (meta.hidden) { return; } for (const [key, options] of Object.entries(gradient)) { const {axis, colors} = options; if (!colors) { continue; } const scale = getScale(meta, axis); if (!scale) { console.warn(`Scale not found for '${axis}'-axis in datasets[${datasetIndex}] of chart id ${chart.id}, skipping.`); continue; } const stateOptions = getStateOptions(state, meta, key, datasetIndex); const option = { datasetIndex, axis, scale, stopColors: [] }; stateOptions.push(option); const value = createGradient(ctx, axis, scale); addColors(scale, colors, option.stopColors); if (option.stopColors.length) { applyColors(value, option.stopColors); setValue(meta, dataset, key, value); } } } var index = { id: 'gradient', beforeInit(chart) { const state = {}; state.options = new Map(); chartStates.set(chart, state); }, beforeDatasetsUpdate(chart) { const area = chart.chartArea; if (!areaIsValid(area)) { return; } const state = chartStates.get(chart); const datasets = chart.data.datasets; for (let i = 0; i < datasets.length; i++) { const dataset = datasets[i]; const gradient = dataset.gradient; if (gradient) { updateDataset(chart, state, gradient, dataset, i); } } }, afterUpdate(chart) { const state = chartStates.get(chart); if (chart.legend && chart.legend.options.display !== false && isChartV3) { updateLegendItems(chart, state); } }, destroy(chart) { chartStates.delete(chart); } }; export { index as default };