import type { IDatePatternKeys } from '@upswot/core' import { formatNumber, getFormattedDateByLocale } from '@upswot/core' import type { ActiveElement, ChartConfiguration, ChartEvent } from 'chart.js' import { ArcElement, BarController, BarElement, CategoryScale, Chart, Legend, LineController, LineElement, LinearScale, PieController, PointElement, Title, Tooltip, } from 'chart.js' import type { EventContext } from 'chartjs-plugin-annotation' import annotationPlugin from 'chartjs-plugin-annotation' import type { PropertyValueMap } from 'lit' import { css, html } from 'lit' import { property, query, state } from 'lit/decorators.js' import { when } from 'lit/directives/when.js' import { localizeManager, localized } from '../../dependencies' import { customElementIfNotExist } from '../../utils/customElementIfNotExist' import { parseFontStyle } from '../../utils/parseFontStyle' import { UsBaseElement } from '../base/UsBaseElement' import '../loader/UsLoader' import { getBarDatasets, getDoughnutDatasets, getLineDatasets, getPieDatasets } from './datasets' import type { IAverageLineAnnotationOptions } from './model/IAverageAnnotation' import type { IChartData, IChartLinePointVariants, IChartProps, IChartStyles, IChartTypes, IChartValueFormat } from './model/IChartProps' import UsChart_css from './styles.pcss' Chart.register( ArcElement, BarElement, BarController, CategoryScale, annotationPlugin, LineController, LineElement, LinearScale, PointElement, PieController, Legend, Title, Tooltip, ) declare global { interface HTMLElementTagNameMap { 'us-chart': UsChart } } @customElementIfNotExist('us-chart') @localized() export class UsChart extends UsBaseElement implements IChartProps { static styles = [ UsBaseElement.styles, css([UsChart_css] as TemplateStringsArray | any), ] @property({ type: Array, attribute: 'data' }) data: IChartData[] | Array | number[] = [] @property({ type: Array, attribute: 'datasets' }) datasets!: any[] @property({ type: Boolean }) loading!: boolean @property({ type: String }) type!: IChartTypes @property({ type: String }) locale = 'en' @property({ type: Boolean }) isDarkTheme = false @property({ type: String }) dateFormat: IDatePatternKeys = 'MMM d' @property({ type: String }) tooltipDateFormat: IDatePatternKeys = 'PP' @property({ type: String }) tooltipTitle: String = '' @property({ type: String }) tooltipPercentLabelDivider: String = '-' @property({ type: String }) yAxisFormat: IChartValueFormat = 'default' @property({ type: String }) xAxisFormat: IChartValueFormat = 'date' @property({ type: String }) yAxisSymbol = '' @property({ type: String }) xAxisSymbol = '' @property({ type: String }) xAxisKey!: string @property({ type: String }) yAxisKey!: string @property({ type: String }) pointVariant: IChartLinePointVariants = 'default' @property({ type: String }) visiblePoint!: string @property({ type: Boolean }) stacked!: boolean @property({ type: Boolean }) legend!: boolean @property({ type: Boolean }) interaction!: boolean @property({ type: Boolean }) horizontal!: boolean @property({ type: Boolean }) yAxisMinMaxDeviation!: boolean @property({ type: Boolean }) doughnutTotal?: boolean @property({ type: Boolean }) tooltipAdditionalData!: boolean @property({ type: Boolean }) splittingDate!: boolean @property({ type: String }) doughnutTotalTitle?: string @property({ type: String }) valueFormat: IChartValueFormat = 'default' @property({ type: String }) valueSymbol = '' @property({ type: Array }) labels!: string[] @property({ type: Array }) plugins!: any[] @property({ type: Array }) datasetLabels!: string[] @property({ type: String }) chartTitle = '' @property({ type: Number }) yAxisTicksCount!: number @property({ type: Number }) xAxisTicksCount!: number @property({ type: Number }) barPercentage!: number @property({ type: Number }) minValue!: number | undefined @property({ type: Number }) maxValue!: number | undefined @property({ type: Number }) stepSize!: number | undefined @property({ type: Number }) averageAmount!: number @property({ type: Boolean }) clip = false @property({ type: Boolean }) xAxisBeginAtZero!: boolean @property({ type: Boolean }) yAxisBeginAtZero!: boolean @property({ type: Boolean }) isLastDateAsterisk = false @property({ type: Boolean }) isDisabled = false @state() myChart: any = null @state() isAxisChart!: boolean @state() isPercentChart!: boolean @state() styleVariables!: IChartStyles @state() innerInteractionCondition!: boolean @state() isMultiDataset!: boolean @query('#chart-anchor') chartAnchor!: HTMLCanvasElement pointRadius = 4 averageLineElement: EventContext | null = null disconnectedCallback() { this.disposeChart() super.disconnectedCallback() } updated(changedProperties: PropertyValueMap | Map) { if (changedProperties.has('type')) { this.isAxisChart = this.type === 'bar' || this.type === 'line' this.isPercentChart = this.type === 'pie' || this.type === 'doughnut' } if ((changedProperties.has('loading') && !this.loading) || changedProperties.has('locale') || changedProperties.has('isDarkTheme') || changedProperties.has('type') || changedProperties.has('data') || changedProperties.has('dateFormat') || changedProperties.has('datasets') || changedProperties.has('yAxisFormat') || changedProperties.has('yAxisSymbol') || changedProperties.has('yAxisTicksCount') || changedProperties.has('xAxisFormat') || changedProperties.has('xAxisSymbol') || changedProperties.has('xAxisTicksCount') || changedProperties.has('pointVariant') || changedProperties.has('visiblePointDate') || changedProperties.has('horizontal') || changedProperties.has('interaction') || changedProperties.has('stacked') || changedProperties.has('yAxisMinMaxDeviation') || changedProperties.has('valueFormat') || changedProperties.has('valueSymbol') || changedProperties.has('doughnutTotal') || changedProperties.has('doughnutTotalTitle') || changedProperties.has('legend') || changedProperties.has('labels') || changedProperties.has('plugins') || changedProperties.has('averageAmount') || changedProperties.has('tooltipAdditionalData') || changedProperties.has('barPercentage') || changedProperties.has('isLastDateAsterisk') || changedProperties.has('isDisabled') || changedProperties.has('chartTitle')) this.mainInit() } async mainInit() { await this.updateComplete this.disposeChart() this.initChart() this.updateChartData() } render() { return html`
${when(this.loading, () => html``, () => html``) }
` } initChart() { if (this.chartAnchor) this.myChart = new Chart(this.chartAnchor, this.getChartOption()) } generateStyleVariables() { const computedStyle = getComputedStyle(this.chartAnchor) this.styleVariables = { '--color-graph-purple': computedStyle.getPropertyValue('--color-graph-purple').trim(), '--color-graph-turquioise': computedStyle.getPropertyValue('--color-graph-turquioise').trim(), '--color-graph-yellow': computedStyle.getPropertyValue('--color-graph-yellow').trim(), '--color-graph-blue': computedStyle.getPropertyValue('--color-graph-blue').trim(), '--color-graph-magenta': computedStyle.getPropertyValue('--color-graph-magenta').trim(), '--color-neutral-0': computedStyle.getPropertyValue('--color-neutral-0').trim(), '--color-neutral-700': computedStyle.getPropertyValue('--color-neutral-700').trim(), '--color-text-900': computedStyle.getPropertyValue('--color-text-900').trim(), '--color-text-700': computedStyle.getPropertyValue('--color-text-700').trim(), '--color-text-1000': computedStyle.getPropertyValue('--color-text-1000').trim(), '--text-p-xs-light': computedStyle.getPropertyValue('--text-p-xs-light').trim(), '--text-p-s-light': computedStyle.getPropertyValue('--text-p-s-light').trim(), '--text-p-m-heavy': computedStyle.getPropertyValue('--text-p-m-heavy').trim(), '--text-headline-h2': computedStyle.getPropertyValue('--text-headline-h2').trim(), '--text-label-m': computedStyle.getPropertyValue('--text-label-m').trim(), '--color-neutral-300': computedStyle.getPropertyValue('--color-neutral-300').trim(), '--color-neutral-600': computedStyle.getPropertyValue('--color-neutral-600').trim(), '--color-danger-600': computedStyle.getPropertyValue('--color-danger-600').trim(), '--color-success-600': computedStyle.getPropertyValue('--color-success-600').trim(), '--space-inner-xl': computedStyle.getPropertyValue('--space-inner-xl').trim(), '--space-inner-m': computedStyle.getPropertyValue('--space-inner-m').trim(), '--space-inner-xs': computedStyle.getPropertyValue('--space-inner-xs').trim(), '--space-outer-s': computedStyle.getPropertyValue('--space-outer-s').trim(), } } getChartOption(): ChartConfiguration { this.generateStyleVariables() const styleVariables = this.styleVariables const settings: any = { type: this.type, data: { labels: [], datasets: [], }, options: { layout: { padding: { bottom: -parseInt(styleVariables['--space-inner-m']), }, }, onHover: (event: ChartEvent, elements: ActiveElement[]) => { this.hideAverageLabel(elements) if (elements[0]) this.myChart.canvas.style.cursor = 'pointer' else this.myChart.canvas.style.cursor = 'default' }, barPercentage: this.barPercentage, clip: this.clip, responsive: true, maintainAspectRatio: false, plugins: { legend: { display: false, }, tooltip: { enabled: true, displayColors: false, padding: 12, titleMarginBottom: 2, titleColor: this.styleVariables['--color-text-1000'], titleFont: this.tooltipTitle ? parseFontStyle(this.styleVariables['--text-label-m']) : parseFontStyle(this.styleVariables['--text-p-s-light']), bodyFont: this.tooltipTitle ? parseFontStyle(this.styleVariables['--text-p-s-light']) : parseFontStyle(this.styleVariables['--text-p-m-heavy']), bodyColor: this.tooltipTitle ? this.styleVariables['--color-neutral-300'] : this.styleVariables['--color-text-1000'], footerColor: this.styleVariables['--color-text-1000'], footerFont: parseFontStyle(this.styleVariables['--text-p-m-heavy']), backgroundColor: this.styleVariables['--color-neutral-0'], borderColor: this.styleVariables['--color-neutral-300'], borderWidth: 1, cornerRadius: 4, callbacks: { labelTextColor: (context) => { if (context.dataset.tooltipBodyColor) return context.dataset.tooltipBodyColor return this.styleVariables['--color-text-1000'] }, footer: (context) => { let afterLabel = '' if (context.length && context[0].parsed.y !== null && this.tooltipTitle) { afterLabel = `~${this.valueSymbol}${formatNumber(context[0].parsed.y || 0, { locale: this.locale, shortForm: true, })}` } return afterLabel }, }, filter: (context) => { return !(context.raw.y === null) }, }, title: { title: { display: false, }, }, }, }, plugins: this.plugins || [], } if (this.isAxisChart) { this.setScales(settings) if (this.horizontal) this.setHorizontalState(settings) if (this.type === 'line') this.setPointSettings(settings) if (this.yAxisMinMaxDeviation) this.setYAxisDeviation(settings) if (this.interaction) { if (this.type === 'bar') this.setBarInteraction(settings) if (this.type === 'line') this.setInteraction(settings) } if (this.stacked && this.type === 'bar') this.setStackedCharts(settings) this.setFormattedValues(settings, 'y') this.setFormattedValues(settings, 'x') if (this.averageAmount) this.setAverageLine(settings) } if (this.isPercentChart) { const func = this.getFormattedValueFunction(this.valueFormat, this.valueSymbol) this.setTooltipPercentLabel(settings, func) } if (this.legend) this.setLegend(settings) if (this.chartTitle) this.setTitle(settings) return settings } setLegend(settings: any) { settings.options.plugins.legend = { position: this.isPercentChart ? 'left' : 'bottom', align: 'start', display: true, labels: { boxWidth: 12, boxHeight: 12, padding: 16, usePointStyle: true, pointStyle: 'rectRounded', font: parseFontStyle(this.styleVariables['--text-p-s-light']), color: this.styleVariables['--color-text-900'], }, } } setTitle(settings: any) { settings.options.plugins.title = { display: true, text: this.chartTitle, color: this.styleVariables['--color-text-900'], position: 'top', align: 'center', font: parseFontStyle(this.styleVariables['--text-p-m-heavy']), fullSize: true, } } setScales(settings: any) { const styleVariables = this.styleVariables const type = this.type const ticksFont = parseFontStyle(styleVariables['--text-p-xs-light']) const padding = parseInt(styleVariables['--space-inner-m']) const gridColor = styleVariables['--color-neutral-300'] const ticksXColor = styleVariables['--color-text-900'] const ticksYColor = styleVariables['--color-text-700'] settings.options.scales = { x: { beginAtZero: type === 'bar' ? true : this.xAxisBeginAtZero, border: { display: type === 'line', }, grid: { color: gridColor, display: type === 'line', drawOnChartArea: true, drawTicks: false, }, ticks: { padding, color: ticksXColor, font: ticksFont, maxRotation: 0, count: this.xAxisTicksCount, }, afterFit(scaleInstance) { if (scaleInstance._labelSizes && type === 'line') scaleInstance.paddingRight = Math.ceil(scaleInstance._labelSizes.last.width) / 2 + (padding / 2) }, }, y: { beginAtZero: type === 'bar' ? true : this.yAxisBeginAtZero, border: { display: type === 'line', }, grid: { color: gridColor, display: true, drawTicks: true, }, position: 'left', ticks: { padding, color: ticksYColor, font: ticksFont, display: true, count: this.yAxisTicksCount, }, afterFit(scaleInstance) { scaleInstance.paddingTop = padding }, }, } } setYAxisDeviation(settings: any) { settings.options.scales.y.grace = '10%' } setInteraction(settings: any) { settings.options.interaction = { intersect: false, mode: 'index', } } setBarInteraction(settings: any) { settings.options.interaction = { intersect: false, mode: 'point', } } setStackedCharts(settings: any) { settings.options.scales.x.stacked = true settings.options.scales.y.stacked = true } setHorizontalState(settings: any) { settings.options.indexAxis = 'y' } getFormattedValueFunction(variant: IChartValueFormat, symbol: string) { let formatValue: any switch (variant) { case 'date': { formatValue = this.splittingDate ? (value: string, locale, dateFormat, isTooltip = false) => { return isTooltip ? getFormattedDateByLocale(value, locale, dateFormat) : getFormattedDateByLocale(value, locale, dateFormat).split(' ') } : getFormattedDateByLocale break } case 'currency': { formatValue = (value: number, _locale, _dateFormat, isTooltip = false) => { return formatNumber(value, { symbol, toFixedValue: 2, locale: this.locale, shortForm: !isTooltip, }) } break } case 'afterValue': { formatValue = (value: number | string, _locale, _dateFormat) => { if (typeof value === 'number') { return `${formatNumber(value, { toFixedValue: 2, locale: this.locale, shortForm: Math.abs(value) >= 1000, })} ${symbol}` } else { return `${value} ${symbol}` } } break } default: { formatValue = (value: number | string, _locale, _dateFormat, isTooltip = false) => { if (typeof value === 'number') { return formatNumber(value, { toFixedValue: 0, locale: this.locale, shortForm: !isTooltip && Math.abs(value) >= 1000, }) } else { return value } } break } } return formatValue } setFormattedValues(settings: any, axis: 'y' | 'x') { const ticksCount = axis === 'y' ? this.yAxisTicksCount : this.xAxisTicksCount const dateFormat = this.dateFormat const locale = this.locale const horizontal = this.horizontal const isLastDateAsterisk = this.isLastDateAsterisk const isValue = (horizontal && axis === 'x') || (!horizontal && axis === 'y') const formatValue = this.getFormattedValueFunction(this[`${axis}AxisFormat`], this[`${axis}AxisSymbol`]) if (ticksCount) { settings.options.scales[axis].ticks.callback = function (value, index, values) { const indexesSet = new Set() for (let i = 0; i < ticksCount; i++) { const index = Math.floor((i * (values.length - 1)) / (ticksCount - 1)) indexesSet.add(index) } const addAsteriskForLastDate = (isLastDateAsterisk && index === values.length - 1 && !isValue) ? ' *' : '' if (indexesSet.has(index)) { const label = isValue ? value : (this as any).getLabelForValue(Number(value)) return formatValue(label, locale, dateFormat) + addAsteriskForLastDate } return null } } else { settings.options.scales[axis].ticks.callback = function (value, index, values) { const addAsteriskForLastDate = (isLastDateAsterisk && index === values.length - 1 && !isValue) ? ' *' : '' const label = isValue ? value : (this as any).getLabelForValue(Number(value)) return formatValue(label, locale, dateFormat) + addAsteriskForLastDate } } if (isValue) this.setTooltipAxisLabel(settings, formatValue, horizontal) else this.setTooltipAxisTitle(settings, formatValue) if (this.tooltipAdditionalData) this.setTooltipAdditionalData(settings, formatValue) settings.options.scales.y.min = this.minValue settings.options.scales.y.max = this.maxValue settings.options.scales.y.ticks.stepSize = this.stepSize } setTooltipPercentLabel(settings, formatValue: any) { const locale = this.locale const dateFormat = this.tooltipDateFormat settings.options.plugins.tooltip.callbacks.label = (context) => { const total = context.dataset.data.reduce((a, b) => { return a + b }) const currentValue = context.parsed let percentage: number | string = ((currentValue / total) * 100) percentage = percentage.toFixed(Number.isInteger(percentage) ? 0 : 2) const formattedValue = formatValue(currentValue, locale, dateFormat) return `${percentage}% ${this.tooltipPercentLabelDivider} ${formattedValue}` } } setTooltipAxisTitle(settings: any, formatValue: any) { const locale = this.locale const dateFormat = this.tooltipDateFormat const isTooltip = true let titleContext: any = null if (this.tooltipTitle) { settings.options.plugins.tooltip.callbacks.title = (context) => { titleContext = context return this.tooltipTitle } settings.options.plugins.tooltip.callbacks.label = () => { if (titleContext?.length) return formatValue(titleContext[0].label, locale, dateFormat, isTooltip) return null } } else { settings.options.plugins.tooltip.callbacks.title = (context) => { if (context?.length) return formatValue(context[0].label, locale, dateFormat, isTooltip) return null } } } setTooltipAxisLabel(settings: any, formatValue: any, horizontal: boolean) { const locale = this.locale const dateFormat = this.tooltipDateFormat if (this.tooltipTitle) return settings.options.plugins.tooltip.callbacks.label = (context) => { let label = context.dataset.label || '' if (label) label += ': ' const value = horizontal ? context.parsed.x : context.parsed.y const isTooltip = true if (value !== null) label += formatValue(value, locale, dateFormat, isTooltip) return label } } setTooltipAdditionalData(settings: any, formatValue: any) { const locale = this.locale const dateFormat = 'MMM' settings.options.plugins.tooltip.callbacks.afterBody = (context) => { if (!context || !context[0] || !context[0].raw || !context[0].dataset) return '' const prevDataIndex = context[0].dataIndex - 1 const selectedMonth = new Date(context[0].raw?.x) const prevMonth = context[0].dataset.data[prevDataIndex]?.x const ifPrevMonthIsAbsent = selectedMonth.setMonth(selectedMonth.getMonth() - 1) const prevMonthDate = prevDataIndex === -1 ? ifPrevMonthIsAbsent : prevMonth const resultMonth = prevMonthDate ? formatValue(prevMonthDate, locale, dateFormat) : '-' return context[0].raw.growthRate ? `${context[0].raw.growthRate} ${localizeManager.to()} ${resultMonth}` : '' } } setAverageLine(settings: any) { settings.options.plugins.annotation = { annotations: { annotation: { type: 'line', borderColor: this.styleVariables['--color-neutral-600'], borderDash: [6, 6], borderWidth: 2, label: { display: (ctx) => { return ctx.hovered }, content: this.getAverageContent(), position: ctx => ctx.hoverPosition, color: this.styleVariables['--color-text-900'], backgroundColor: this.styleVariables['--color-neutral-300'], borderRadius: 28, }, scaleID: 'y', value: this.averageAmount, enter: (ctx, event) => { this.averageLineElement = ctx ctx.hoverPosition = `${((event.x || 0) - ctx.chart.chartArea.left) / (ctx.chart.chartArea.width) * 100}%` ctx.hovered = true ctx.chart.update() }, leave: (ctx) => { this.averageLineElement = null ctx.hovered = false ctx.chart.update() }, hovered: false, hoverPosition: '', } as IAverageLineAnnotationOptions, }, } } setPointSettings(settings: any) { if (this.type !== 'line') return const setDefaultRadius = () => { settings.options.elements = { point: { radius: this.pointRadius }, } } switch (this.pointVariant) { case 'last': { settings.options.elements = { point: { radius: (context: any) => { let maxIndex: number if (this.datasets?.length) { maxIndex = this.datasets[0].data?.length - 1 } else { maxIndex = this.isMultiDataset ? (this.data[0] as Array).length - 1 : this.data.length - 1 } if (context.dataIndex === maxIndex) return this.pointRadius else return 0 }, hitRadius: 7, }, } break } case 'specific': { const specificPointDate = this.visiblePoint if (!specificPointDate) { setDefaultRadius() break } settings.options.elements = { point: { radius: (context: any) => { const label = context.raw ? context.raw.x : '' if (!label?.length) return if (specificPointDate === label) return this.pointRadius else return 0 }, hitRadius: 7, }, } break } case 'hidden': { settings.options.elements = { point: { radius: 0, hitRadius: 7, }, } break } default: { setDefaultRadius() } } } generateData(data: IChartData[]) { const axisData = data.map((item) => { const baseObject = { x: this.xAxisKey ? item[this.xAxisKey] : Object.values(item)[this.horizontal ? 1 : 0], y: this.yAxisKey ? item[this.yAxisKey] : Object.values(item)[this.horizontal ? 0 : 1], } return item.growthRate ? { ...baseObject, growthRate: item.growthRate } : baseObject }) const simpleData = data return this.isAxisChart ? axisData : simpleData } generateDatasets(data: IChartData[] | Array | number[], datasets: any[], config: any) { if (datasets && datasets.length) { config.data.datasets = JSON.parse(JSON.stringify(datasets)) config.data.datasets.forEach((item) => { item.data = this.generateData(item.data) }) } else if (data) { this.isMultiDataset = !!data.length && Array.isArray(data[0]) let defaultDatasets: any[] = [] const isDatasetLabels = Boolean(this.datasetLabels?.length) if (this.type === 'line') defaultDatasets = getLineDatasets(this.styleVariables) if (this.type === 'bar') defaultDatasets = getBarDatasets(this.styleVariables, this.isDisabled) if (this.type === 'pie') defaultDatasets = getPieDatasets(this.styleVariables) if (this.type === 'doughnut') defaultDatasets = getDoughnutDatasets(this.styleVariables) if (!this.isMultiDataset) { const dataset = { ...defaultDatasets[0], data: this.generateData(data as IChartData[]), } if (isDatasetLabels) dataset.label = this.datasetLabels[0] config.data.datasets = [dataset] } else { data.forEach((item, index) => { const dataset = { ...defaultDatasets[index % defaultDatasets.length], data: this.generateData(item as IChartData[]), } if (isDatasetLabels) dataset.label = this.datasetLabels[index] config.data.datasets.push(dataset) }) } } } generateArbitraryLine(settings: any, startingPoint: string | undefined) { if (!startingPoint) return const arbitraryLine = { id: 'arbitraryLine', beforeDraw(chart, _, options) { const { ctx, chartArea: { top, height }, scales: { x } } = chart const xWidth = options.xWidth ctx.save() ctx.fillStyle = options.color ctx.fillRect(x.getPixelForValue(options.xPosition) - (xWidth / 2), top, xWidth, height) ctx.restore() }, } settings.options.plugins.arbitraryLine = { color: this.styleVariables['--color-neutral-600'], xWidth: 2, xPosition: startingPoint, } settings.plugins.push(arbitraryLine) } hideAverageLabel(elements: ActiveElement[]) { if (elements.length && this.averageLineElement) { this.averageLineElement.hovered = false this.myChart.update() } } getAverageContent(): string { const formattedAverageAmount = formatNumber(this.averageAmount, { locale: this.locale, shortForm: true, }) const yAxisValue = this.yAxisFormat === 'afterValue' ? `${formattedAverageAmount} ${this.yAxisSymbol}` : `${this.yAxisSymbol}${formattedAverageAmount}` return `${localizeManager.average()}: ${yAxisValue}` } generateDoughnutTotal(config: any) { const styleVars = this.styleVariables const parsedLabelFont = parseFontStyle(styleVars['--text-label-m']) const parsedValueFont = parseFontStyle(styleVars['--text-headline-h2']) const parsedLabelFontLineHeight = parsedLabelFont.lineHeight?.match(/\d/g).join('') || 20 const parsedValueFontLineHeight = parsedValueFont.lineHeight?.match(/\d/g).join('') || 44 const total = config.data.datasets[0].data.reduce((a, b) => a + b) const formattedTotalValue = this.getFormattedValueFunction(this.valueFormat, this.valueSymbol)(total, this.locale, this.dateFormat) const totalLabel = this.doughnutTotalTitle const generateTotalWithLabel = (ctx: any, xCoor, yCoor) => { ctx.font = styleVars['--text-label-m'] ctx.fillStyle = styleVars['--color-text-900'] ctx.textAlign = 'center' ctx.textBaseline = 'middle' ctx.fillText(totalLabel, xCoor, yCoor - (parsedValueFontLineHeight / 2)) ctx.font = styleVars['--text-headline-h2'] ctx.fillStyle = styleVars['--color-text-1000'] ctx.fillText(formattedTotalValue, xCoor, yCoor + (parsedLabelFontLineHeight / 2)) ctx.restore() } const generateTotalWithoutLabel = (ctx: any, xCoor, yCoor) => { ctx.textAlign = 'center' ctx.textBaseline = 'middle' ctx.font = styleVars['--text-headline-h2'] ctx.fillStyle = styleVars['--color-text-1000'] ctx.fillText(formattedTotalValue, xCoor, yCoor) ctx.restore() } const generateTotalLabels = totalLabel ? generateTotalWithLabel : generateTotalWithoutLabel const doughnutLabel = { id: 'doughnutLabel', beforeDatasetsDraw(chart) { const { ctx } = chart ctx.save() const xCoor = chart.getDatasetMeta(0).data[0].x const yCoor = chart.getDatasetMeta(0).data[0].y generateTotalLabels(ctx, xCoor, yCoor) }, } config.plugins.push(doughnutLabel) } generateEmptyAxisValues(settings: any) { settings.options.scales.y.min = 0 settings.options.scales.y.max = 100 settings.options.scales.y.ticks = { stepSize: 25, } } generateLabels(config: any) { if (this.labels && this.labels.length) { config.data.labels = this.labels this.moveLegendToBottomIfNeeds(config) } else if (this.isAxisChart) { const labelAxis = this.horizontal ? 'y' : 'x' config.data.labels = config.data.datasets[0].data.map((item: { x: any; y: any }) => item[labelAxis]) } } moveLegendToBottomIfNeeds(config: any) { if (!this.legend || !this.isPercentChart) return const labelCharactersLimit = 15 const longLabels = config.data.labels.some((label) => { if (typeof label === 'string') return label.length > labelCharactersLimit return false }) if (longLabels) config.options.plugins.legend.position = 'bottom' } setDatasetCustomColors(config) { config.data.datasets.forEach((dataset) => { for (const key in dataset) { if (this.styleVariables[dataset[key]]) dataset[key] = this.styleVariables[dataset[key]] } }) } postDataActions(config: any) { if (this.type === 'line' && this.pointVariant === 'specific' && !this.horizontal) this.generateArbitraryLine(config, this.visiblePoint) if (this.type === 'doughnut' && this.doughnutTotal) this.generateDoughnutTotal(config) if (this.datasets && this.datasets.length) this.setDatasetCustomColors(config) } updateChartData() { if (!this.myChart || !this.myChart.canvas) return this.disposeChart() if (!this.data?.length && !this.datasets?.length && this.isAxisChart) this.generateEmptyAxisValues(this.myChart) const ctx = this.myChart.ctx const config = this.myChart.config this.myChart.destroy() this.generateDatasets(this.data, this.datasets, config) this.generateLabels(config) this.postDataActions(config) this.myChart = new Chart(ctx, config) this.myChart.update() } disposeChart() { this.myChart && this.myChart.destroy() this.myChart = null } }