UNPKG

@mozaic-ds/chart

Version:

This template should help get you started developing with Vue 3 and TypeScript in Vite. The template uses Vue 3 `<script setup>` SFCs, check out the [script setup docs](https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup) to learn more.

534 lines (496 loc) 15.9 kB
import type { Ref } from 'vue'; import type { HTMLLegendPlugin } from '../types/Chart'; import type { ChartOptions } from 'chart.js'; import PatternFunctions from './PatternFunctions'; import { formatValueAndRate } from './FormatUtilities'; import QuestionMarkSvg from '@mozaic-ds/icons/svg/Navigation_Notification_Question_24px.svg'; const { getPatternCanvas } = PatternFunctions(); export const LEGEND_FONT_SIZE = 14; export const LEGEND_LABEL_LEFT_MARGIN = '6px'; export const LEGEND_BOX_SIZE = '22px'; export const LEGEND_BOX_POINT_SIZE = 6; export const LEGEND_BOX_BORDER = '2px'; export interface Chart { update(): void; toggleDataVisibility(datasetIndex: number): void; isDatasetVisible(datasetIndex: number): boolean; getDataVisibility(index: number): boolean; setDatasetVisibility(datasetIndex: number, visible: boolean): void; plugins: HTMLLegendPlugin; options: | ChartOptions<'radar'> | ChartOptions<'doughnut'> | ChartOptions<'bar'> | ChartOptions<'line'>; config: { type?: string; data: { labels: string[]; datasets: unknown[]; data?: unknown[] }; }; } export interface ChartItem { fontColor: string; hidden: boolean; text: string; fillStyle: string; strokeStyle: string; lineWidth: number; datasetIndex: number; index: number; lineCap?: string; } export function getHtmlLegendPlugin( legendContainer: Ref, selectMode: Ref<boolean>, onHoverIndex: any, disableAccessibility: Ref<boolean>, patternsColors: Ref<string[]>, patternsList: Ref< (( hover: boolean, color: string, disableAccessibility: boolean ) => CanvasPattern)[] >, enableHoverFeature: Ref<boolean>, maxValueToDisplay?: number, chartData?: any ): HTMLLegendPlugin { return { id: 'htmlLegend', afterUpdate(chart: any) { const isDoughnut: boolean = chart.config.type === 'doughnut'; const flexDirection = isDoughnut ? 'column' : 'row'; const ul: HTMLLIElement = getOrCreateLegendList( legendContainer, flexDirection ); ul.style.margin = '1.375rem 1.0625rem'; while (ul.firstChild) { ul.firstChild.remove(); } const items: ChartItem[] = chart.options.plugins.legend.labels.generateLabels(chart); items.forEach((item: ChartItem): void => { const isDoughnut: boolean = chart.config.type === 'doughnut'; const index: number = isDoughnut ? item.index : item.datasetIndex; const li: HTMLElement = createHtmlLegendListElement( chart, selectMode, index ); if (isDoughnut) { const isOthersElement: boolean = index + 1 === maxValueToDisplay ? true : false; li.style.marginTop = '12px'; if (isOthersElement) { li.style.position = 'relative'; } } else { li.style.marginRight = '10px'; } li.style.width = 'max-content'; li.style.cursor = 'pointer'; let liContent: HTMLElement; if (!selectMode.value) { liContent = createLegendElementWithPatterns( item, chart, onHoverIndex, disableAccessibility.value, patternsColors.value, patternsList.value, enableHoverFeature.value ); } else { liContent = createLegendElementWithCheckbox( chart, item, selectMode, onHoverIndex, patternsColors.value, enableHoverFeature.value ); } liContent.style.boxSizing = 'border-box'; li.appendChild(liContent); li.appendChild(createHtmlLegendItemText(item)); if ( isDoughnut && maxValueToDisplay && hasOthersTooltipToDisplay(chartData, maxValueToDisplay, index) ) { li.appendChild(createTooltipAndItsIcon(chartData, maxValueToDisplay)); } ul.appendChild(li); }); } }; } export function hasOthersTooltipToDisplay( doughnutData: any, maxValueToDisplay: number, index: number ) { return ( doughnutData.data.length > maxValueToDisplay && index === maxValueToDisplay - 1 ); } export function createTooltipAndItsIcon( doughnutData: any, maxValueToDisplay: number ): HTMLDivElement { const iconTopWrapper = document.createElement('div'); const iconWrapper = document.createElement('div'); const icon = document.createElement('img'); iconTopWrapper.style.position = 'absolute'; iconTopWrapper.style.right = '-32px'; icon.src = QuestionMarkSvg; icon.style.top = '0'; icon.style.width = '1.5rem'; icon.style.filter = 'invert(38%) sepia(19%) saturate(18%) hue-rotate(337deg) brightness(97%) contrast(85%)'; iconWrapper.style.position = 'relative'; iconWrapper.style.display = 'flex'; const tooltip = createLegendOthersTooltip(doughnutData, maxValueToDisplay); icon.onmouseover = () => { (iconWrapper.firstElementChild as HTMLElement).style.visibility = 'visible'; }; icon.onmouseleave = () => { (iconWrapper.firstElementChild as HTMLElement).style.visibility = 'hidden'; }; iconTopWrapper.appendChild(iconWrapper); iconWrapper.appendChild(tooltip); iconWrapper.appendChild(icon); return iconTopWrapper; } function createLegendOthersTooltip( doughnutData: any, maxValueToDisplay: number ) { const tooltip = document.createElement('div'); tooltip.style.visibility = 'hidden'; tooltip.style.position = 'absolute'; tooltip.style.zIndex = '10'; tooltip.style.width = '350px'; tooltip.style.bottom = '100%'; tooltip.style.left = '50%'; tooltip.style.marginLeft = '-150px'; tooltip.style.background = '#FFFFFF'; tooltip.style.boxShadow = '0px 1px 5px rgba(0, 0, 0, 0.2)'; tooltip.style.borderRadius = '0.5rem'; tooltip.style.fontSize = '14px'; tooltip.style.overflow = 'hidden'; addOthersTooltipLines(doughnutData, maxValueToDisplay, tooltip); return tooltip; } function addOthersTooltipLines( doughnutData: any, maxValueToDisplay: number, tooltip: HTMLDivElement ) { const startIndex = maxValueToDisplay - 1; doughnutData.data.slice(startIndex).forEach((_ignore: any, index: number) => { const dataIndex = startIndex + index; const textWrapper = document.createElement('div'); textWrapper.style.display = 'flex'; textWrapper.style.justifyContent = 'space-between'; textWrapper.style.padding = '0.5rem'; textWrapper.style.border = '1px solid #CCCCCC'; const label = document.createElement('span'); label.appendChild(document.createTextNode(doughnutData.labels[dataIndex])); const value = document.createElement('span'); value.appendChild( document.createTextNode(formatValueAndRate(doughnutData, dataIndex)) ); textWrapper.appendChild(label); textWrapper.appendChild(value); tooltip.appendChild(textWrapper); }); } export function createLegendElementWithPatterns( item: ChartItem, chart: Chart, onHoverIndex: any | null, disableAccessibility: boolean, patternsColors: string[], patternsList: (( hover: boolean, color: string, disableAccessibility: boolean ) => CanvasPattern)[], enableHoverFeature: boolean ): HTMLElement { const isDoughnut: boolean = chart.config.type === 'doughnut'; const index: number = isDoughnut ? item.index : item.datasetIndex; const img: HTMLImageElement = new Image(); const boxSpan: HTMLElement = createHtmlLegendLine(item, chart.config.type); const pattern: CanvasPattern = patternsList[index]( false, patternsColors[index], disableAccessibility ); const patternCanvas: HTMLCanvasElement = getPatternCanvas(pattern); img.src = patternCanvas.toDataURL(); boxSpan.style.background = `url(${img.src})`; boxSpan.style.backgroundSize = 'cover'; boxSpan.style.borderColor = patternsColors[index]; boxSpan.style.borderWidth = LEGEND_BOX_BORDER; if (enableHoverFeature) { boxSpan.onmouseover = (): void => { isDoughnut ? (onHoverIndex.value = index) : (onHoverIndex.dataSetIndex = index); }; boxSpan.onmouseleave = (): void => { isDoughnut ? (onHoverIndex.value = null) : (onHoverIndex.dataSetIndex = -1); }; } return boxSpan; } export function createLegendElementWithCheckbox( chart: Chart, item: ChartItem, selectMode: Ref<boolean>, onHoverIndex: any | null, patternsColors: string[], enableHoverFeature: boolean ): HTMLElement { const isDoughnut: boolean = chart.config.type === 'doughnut'; const index: number = isDoughnut ? item.index : item.datasetIndex; const checkbox: HTMLElement = createLegendCheckbox( chart, item, patternsColors ); const labels = chart.config.data.labels; const allCheckBoxesVisible: boolean = labels.every((_, index: number) => chart.getDataVisibility(index) ); if (allCheckBoxesVisible) { if (isDoughnut) { selectMode.value = false; onHoverIndex.value = -1; } return checkbox; } if (enableHoverFeature) { checkbox.onmouseover = (): void => { isDoughnut ? (onHoverIndex.value = index) : (onHoverIndex.dataSetIndex = index); chart.update(); }; checkbox.onmouseleave = (): void => { isDoughnut ? (onHoverIndex.value = null) : (onHoverIndex.dataSetIndex = -1); chart.update(); }; } return checkbox; } export function createHtmlLegendItemText(item: ChartItem) { const textContainer = document.createElement('p'); textContainer.style.color = item.fontColor; textContainer.style.fontSize = `${LEGEND_FONT_SIZE}px`; textContainer.style.margin = '0'; textContainer.style.padding = '0'; const text = document.createTextNode(item.text); textContainer.appendChild(text); return textContainer; } export function createHtmlLegendLine( item: ChartItem, type: string | undefined ) { const boxSpan = document.createElement('div'); if (type !== 'doughnut') { boxSpan.style.background = 'rgba(0, 0, 0, 0.1)'; boxSpan.style.borderColor = item.strokeStyle; boxSpan.style.borderWidth = LEGEND_BOX_BORDER; } boxSpan.style.borderRadius = '5px'; boxSpan.style.borderStyle = 'solid'; boxSpan.style.display = 'flex'; boxSpan.style.justifyContent = 'center'; boxSpan.style.alignItems = 'center'; boxSpan.style.minWidth = LEGEND_BOX_SIZE; boxSpan.style.marginRight = LEGEND_LABEL_LEFT_MARGIN; boxSpan.style.minHeight = LEGEND_BOX_SIZE; return boxSpan; } export function createHtmlLegendDatasetSquare(item: ChartItem) { const divPoint = document.createElement('div'); divPoint.style.height = LEGEND_BOX_POINT_SIZE + 'px'; divPoint.style.width = LEGEND_BOX_POINT_SIZE + 'px'; divPoint.style.background = 'white'; divPoint.style.borderStyle = 'solid'; divPoint.style.borderColor = item.strokeStyle; divPoint.style.borderWidth = LEGEND_BOX_BORDER; return divPoint; } export function createHtmlLegendListElement( chart: Chart, selectMode: Ref, elementIndex: number ) { const li: HTMLElement = document.createElement('li'); li.style.alignItems = 'center'; li.style.cursor = selectMode.value ? '' : 'pointer'; li.style.display = 'flex'; li.style.flexDirection = 'row'; li.setAttribute('data-test-id', `legend-item-${elementIndex}`); li.onclick = () => { if (!selectMode.value) { hideAllButThis(chart, elementIndex, selectMode); chart.update(); } else { switchItemVisibility(chart, elementIndex, selectMode); } }; return li; } export function addCheckboxStyle( isDataSetVisible: boolean, item: ChartItem, checkbox: Element, patternColor: string ) { let backgroundColor = '#fff'; let borderColor = '#666'; if (isDataSetVisible) { //Default white for patterns chart backgroundColor = isDefaultWhiteColor(item.strokeStyle) ? patternColor : item.strokeStyle; borderColor = isDefaultWhiteColor(item.strokeStyle) ? patternColor : item.strokeStyle; checkbox.setAttribute('checked', '' + isDataSetVisible); } checkbox.setAttribute('class', 'mc-checkbox__input'); checkbox.setAttribute( 'style', `background-color: ${backgroundColor}; min-width: ${LEGEND_BOX_SIZE}; min-height: ${LEGEND_BOX_SIZE}; margin-right: ${LEGEND_LABEL_LEFT_MARGIN}; border-color: ${borderColor};` ); } export function createLegendCheckbox( chart: Chart, item: ChartItem, patternsColors: string[] ) { const isDoughnut: boolean = chart.config.type === 'doughnut'; const index: number = isDoughnut ? item.index : item.datasetIndex; const checkbox = document.createElement('input'); checkbox.setAttribute('type', 'checkbox'); checkbox.setAttribute('data-test-id', `legend-checkbox-${index}`); const isDataSetVisible = isChartDataVisible(chart, index); const patternColor = patternsColors ? patternsColors[index] : undefined; addCheckboxStyle(isDataSetVisible, item, checkbox, patternColor as string); return checkbox; } function isMonoDataSetChart(chart: Chart) { const { type } = chart.config; return type === 'pie' || type === 'doughnut'; } function getChartsData(chart: any) { let dataSets: unknown[] = chart.config.data.datasets; if (isMonoDataSetChart(chart)) { dataSets = chart.config.data.datasets[0].data; } return dataSets; } export function hideAllButThis( chart: Chart, elementIndex: number, selectMode: Ref ) { if (!selectMode.value) { const dataSets: unknown[] = getChartsData(chart); selectMode.value = true; dataSets.forEach((_data, index) => { if (index !== elementIndex) { switchItemVisibility(chart, index); } }); } } function allDataVisible(chart: Chart): boolean { let allVisible = true; const chartsData: unknown[] = getChartsData(chart); chartsData.forEach((_data, dataIndex) => { allVisible = allVisible && isChartDataVisible(chart, dataIndex); }); return allVisible; } function isChartDataVisible(chart: Chart, dataIndex: number): boolean { if (isMonoDataSetChart(chart)) { return chart.getDataVisibility(dataIndex); } else { return chart.isDatasetVisible(dataIndex); } } export function switchItemVisibility( chart: Chart, elementIndex: number, selectMode?: Ref ) { if (isMonoDataSetChart(chart)) { chart.toggleDataVisibility(elementIndex); } else { chart.setDatasetVisibility( elementIndex, !chart.isDatasetVisible(elementIndex) ); } if (selectMode && allDataVisible(chart)) { selectMode.value = false; } chart.update(); } export function createLegendElementWithSquareArea( item: ChartItem, mainSerieFirstDataset?: boolean ) { const liContent = createHtmlLegendLine(item, ''); const divPoint = createHtmlLegendDatasetSquare(item); const index = item.index || item.datasetIndex; divPoint.style.width = '10px'; divPoint.style.height = '10px'; if (index % 2 === 0) { mainSerieFirstDataset ? (divPoint.style.borderRadius = '25px') : (divPoint.style.transform = 'rotate(45deg)'); } else { mainSerieFirstDataset ? (divPoint.style.transform = 'rotate(45deg)') : (divPoint.style.borderRadius = '25px'); } liContent.appendChild(divPoint); return liContent; } export function getOrCreateLegendList( legendContainer: Ref, flexDirection: string ) { let listContainer = legendContainer.value?.querySelector('ul'); if (!listContainer) { listContainer = document.createElement('ul'); listContainer.style.display = 'flex'; listContainer.style.flexDirection = flexDirection; listContainer.style.margin = '0'; listContainer.style.padding = '0'; legendContainer.value?.appendChild(listContainer); } return listContainer; } function isDefaultWhiteColor(color: string) { return color === '#00000000'; }