@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
text/typescript
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';
}