-
Notifications
You must be signed in to change notification settings - Fork 6
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Enable gradient styles on the legend items (#29)
* Enable gradient styles on the legend items * manages colors also at data index leven * changes check on array length * improves some parts of code * adds JSDoc to expose functions * fixes lint error * changes name of variable * adds test cases * Update src/colors.js Co-authored-by: Jukka Kurkela <[email protected]> * apply some reviews * apply some reviews 2 * adds additional tests and check if legend can be changed * fixes code smells * adds alpha interpolation * adds test cases to radial linear axis and on hover border color * changes image in README * reduces dimension of image * sets the precise dimension of image * Update src/index.js Co-authored-by: Jukka Kurkela <[email protected]> * Update src/index.js Co-authored-by: Jukka Kurkela <[email protected]> * Update src/index.js Co-authored-by: Jukka Kurkela <[email protected]> Co-authored-by: Jukka Kurkela <[email protected]>
- Loading branch information
1 parent
a57e1c6
commit e3e82eb
Showing
42 changed files
with
1,077 additions
and
56 deletions.
There are no files selected for viewing
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,57 @@ | ||
import {color} from 'chart.js/helpers'; | ||
import {getGradientData, getPixelStop} from './helpers'; | ||
|
||
// 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 | ||
*/ | ||
export 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); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,89 @@ | ||
import {Chart} from 'chart.js'; | ||
import {isNumber} from 'chart.js/helpers'; | ||
|
||
export 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} | ||
*/ | ||
export 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 | ||
*/ | ||
export 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 | ||
*/ | ||
export 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 | ||
*/ | ||
export 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 | ||
*/ | ||
export 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}; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,82 @@ | ||
import {defined} from 'chart.js/helpers'; | ||
import {getInterpolatedColorByValue} from './colors'; | ||
import {areaIsValid, createGradient, applyColors, getGradientData} from './helpers'; | ||
|
||
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 | ||
*/ | ||
export 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); | ||
} | ||
} | ||
} |
Oops, something went wrong.