import { ChartArea, CartesianScaleOptions, LinearScale, LinearScaleOptions, LogarithmicScale, LogarithmicScaleOptions, } from 'chart.js'; export interface ILegendScaleOptions extends CartesianScaleOptions { /** * whether to render a color legend * @default true */ display: boolean; /** * the property name that stores the value in the data elements * @default value */ property: string; legend: { /** * location of the legend on the chart area * @default bottom-right */ position: | 'left' | 'right' | 'top' | 'bottom' | 'top-left' | 'top-right' | 'top-right' | 'bottom-right' | 'bottom-left' | { x: number; y: number }; /** * alignment of the scale, e.g., `right` means that it is a vertical scale * with the ticks on the right side * @default right */ align: 'left' | 'right' | 'top' | 'bottom'; /** * length of the legend, i.e., for a horizontal scale the width * if a value < 1 is given, is it assume to be a ratio of the corresponding * chart area * @default 100 */ length: number; /** * how wide the scale is, i.e., for a horizontal scale the height * if a value < 1 is given, is it assume to be a ratio of the corresponding * chart area * @default 50 */ width: number; /** * how many pixels should be used for the color bar * @default 10 */ indicatorWidth: number; /** * margin pixels such that it doesn't stick to the edge of the chart * @default 8 */ margin: number | ChartArea; }; } export const baseDefaults = { position: 'chartArea', property: 'value', grid: { z: 1, drawOnChartArea: false, }, ticks: { z: 1, }, legend: { align: 'right', position: 'bottom-right', length: 100, width: 50, margin: 8, indicatorWidth: 10, }, }; interface IPositionOption { position?: string; } function computeLegendMargin(legend: ILegendScaleOptions['legend']): { left: number; top: number; right: number; bottom: number; } { const { indicatorWidth, align: pos, margin } = legend; const left = (typeof margin === 'number' ? margin : margin.left) + (pos === 'right' ? indicatorWidth : 0); const top = (typeof margin === 'number' ? margin : margin.top) + (pos === 'bottom' ? indicatorWidth : 0); const right = (typeof margin === 'number' ? margin : margin.right) + (pos === 'left' ? indicatorWidth : 0); const bottom = (typeof margin === 'number' ? margin : margin.bottom) + (pos === 'top' ? indicatorWidth : 0); return { left, top, right, bottom }; } function computeLegendPosition( chartArea: ChartArea, legend: ILegendScaleOptions['legend'], width: number, height: number, legendSize: { w: number; h: number } ): [number, number] { const { indicatorWidth, align: axisPos, position: pos } = legend; const isHor = axisPos === 'top' || axisPos === 'bottom'; const w = (axisPos === 'left' ? legendSize.w : width) + (isHor ? indicatorWidth : 0); const h = (axisPos === 'top' ? legendSize.h : height) + (!isHor ? indicatorWidth : 0); const margin = computeLegendMargin(legend); if (typeof pos === 'string') { switch (pos) { case 'top-left': return [margin.left, margin.top]; case 'top': return [(chartArea.right - w) / 2, margin.top]; case 'left': return [margin.left, (chartArea.bottom - h) / 2]; case 'top-right': return [chartArea.right - w - margin.right, margin.top]; case 'bottom-right': return [chartArea.right - w - margin.right, chartArea.bottom - h - margin.bottom]; case 'bottom': return [(chartArea.right - w) / 2, chartArea.bottom - h - margin.bottom]; case 'bottom-left': return [margin.left, chartArea.bottom - h - margin.bottom]; default: // right return [chartArea.right - w - margin.right, (chartArea.bottom - h) / 2]; } } return [pos.x, pos.y]; } export class LegendScale extends LinearScale { /** * @hidden */ legendSize: { w: number; h: number } = { w: 0, h: 0 }; /** * @hidden */ init(options: O): void { (options as unknown as IPositionOption).position = 'chartArea'; super.init(options); this.axis = 'r'; } /** * @hidden */ parse(raw: any, index: number): number { if (raw && typeof raw[this.options.property] === 'number') { return raw[this.options.property]; } return super.parse(raw, index) as number; } /** * @hidden */ isHorizontal(): boolean { return this.options.legend.align === 'top' || this.options.legend.align === 'bottom'; } protected _getNormalizedValue(v: number): number | null { if (v == null || Number.isNaN(v)) { return null; } return (v - (this as any)._startValue) / (this as any)._valueRange; } /** * @hidden */ update(maxWidth: number, maxHeight: number, margins: ChartArea): void { const ch = Math.min(maxHeight, this.bottom == null ? Number.POSITIVE_INFINITY : this.bottom); const cw = Math.min(maxWidth, this.right == null ? Number.POSITIVE_INFINITY : this.right); const l = this.options.legend; const isHor = this.isHorizontal(); const factor = (v: number, full: number) => (v < 1 ? full * v : v); const w = Math.min(cw, factor(isHor ? l.length : l.width, cw)) - (!isHor ? l.indicatorWidth : 0); const h = Math.min(ch, factor(!isHor ? l.length : l.width, ch)) - (isHor ? l.indicatorWidth : 0); this.legendSize = { w, h }; this.bottom = h; this.height = h; this.right = w; this.width = w; const bak = (this.options as IPositionOption).position; (this.options as IPositionOption).position = this.options.legend.align; const r = super.update(w, h, margins); (this.options as IPositionOption).position = bak; this.height = Math.min(h, this.height); this.width = Math.min(w, this.width); return r; } /** * @hidden */ _computeLabelArea(): void { return undefined; } /** * @hidden */ draw(chartArea: ChartArea): void { if (!(this as any)._isVisible()) { return; } const pos = computeLegendPosition(chartArea, this.options.legend, this.width, this.height, this.legendSize); /** @type {CanvasRenderingContext2D} */ const { ctx } = this; ctx.save(); ctx.translate(pos[0], pos[1]); const bak = (this.options as IPositionOption).position; (this.options as IPositionOption).position = this.options.legend.align; super.draw({ ...chartArea, bottom: this.height + 10, right: this.width }); (this.options as IPositionOption).position = bak; const { indicatorWidth } = this.options.legend; switch (this.options.legend.align) { case 'left': ctx.translate(this.legendSize.w, 0); break; case 'top': ctx.translate(0, this.legendSize.h); break; case 'bottom': ctx.translate(0, -indicatorWidth); break; default: ctx.translate(-indicatorWidth, 0); break; } this._drawIndicator(); ctx.restore(); } /** * @hidden */ protected _drawIndicator(): void { // hook } } export class LogarithmicLegendScale< O extends ILegendScaleOptions & LogarithmicScaleOptions, > extends LogarithmicScale { /** * @hidden */ legendSize: { w: number; h: number } = { w: 0, h: 0 }; /** * @hidden */ init(options: O): void { LegendScale.prototype.init.call(this, options); } /** * @hidden */ parse(raw: any, index: number): number { return LegendScale.prototype.parse.call(this, raw, index); } /** * @hidden */ isHorizontal(): boolean { return this.options.legend.align === 'top' || this.options.legend.align === 'bottom'; } protected _getNormalizedValue(v: number): number | null { if (v == null || Number.isNaN(v)) { return null; } return (Math.log10(v) - (this as any)._startValue) / (this as any)._valueRange; } /** * @hidden */ update(maxWidth: number, maxHeight: number, margins: ChartArea): void { return LegendScale.prototype.update.call(this, maxWidth, maxHeight, margins); } /** * @hidden */ _computeLabelArea(): void { return undefined; } /** * @hidden */ draw(chartArea: ChartArea): void { return LegendScale.prototype.draw.call(this, chartArea); } protected _drawIndicator(): void { // hook } }