@mozaic-ds/chart 0.1.0-beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +16 -0
- package/dist/mozaic-chart.js +9858 -0
- package/dist/mozaic-chart.umd.cjs +22 -0
- package/dist/style.css +1 -0
- package/dist/vite.svg +1 -0
- package/package.json +79 -0
- package/src/assets/base.css +2 -0
- package/src/components/bar/BarChart.stories.ts +298 -0
- package/src/components/bar/BarChart.vue +247 -0
- package/src/components/doughnut/DoughnutChart.stories.ts +80 -0
- package/src/components/doughnut/DoughnutChart.vue +208 -0
- package/src/components/line/LineChart.stories.ts +60 -0
- package/src/components/line/LineChart.vue +245 -0
- package/src/components/radar/RadarChart.stories.ts +346 -0
- package/src/components/radar/RadarChart.vue +265 -0
- package/src/main.ts +6 -0
- package/src/services/BarChartFunctions.ts +126 -0
- package/src/services/ChartsCommonLegend.ts +366 -0
- package/src/services/DoughnutChartFunctions.ts +89 -0
- package/src/services/FormatUtilities.ts +30 -0
- package/src/services/GenericTooltipService.ts +305 -0
- package/src/services/PatternFunctions.ts +25 -0
- package/src/services/RadarChartFunctions.ts +70 -0
- package/src/services/patterns/ChartDesign.ts +27 -0
- package/src/services/patterns/patternCircles.ts +82 -0
- package/src/services/patterns/patternDashedDiagonals.ts +66 -0
- package/src/services/patterns/patternDiagonals.ts +101 -0
- package/src/services/patterns/patternSquares.ts +76 -0
- package/src/services/patterns/patternVerticalLines.ts +61 -0
- package/src/services/patterns/patternZigzag.ts +88 -0
- package/src/types/BarData.ts +11 -0
- package/src/types/ButtonName.ts +7 -0
- package/src/types/Chart.ts +10 -0
- package/src/types/DoughnutData.ts +6 -0
- package/src/types/GenericData.ts +11 -0
- package/src/types/LineChart.ts +5 -0
- package/src/types/RadarData.ts +36 -0
- package/src/types/TooltipChartType.ts +7 -0
- package/src/vite-env.d.ts +7 -0
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { reactive, ref } from 'vue';
|
|
2
|
+
import type { Ref } from 'vue';
|
|
3
|
+
import {
|
|
4
|
+
getHtmlLegendPlugin
|
|
5
|
+
} from './ChartsCommonLegend';
|
|
6
|
+
import PatternFunctions from './PatternFunctions';
|
|
7
|
+
import ChartDesign from './patterns/ChartDesign';
|
|
8
|
+
|
|
9
|
+
const { getPatternIndexWithShift } = PatternFunctions();
|
|
10
|
+
const {
|
|
11
|
+
patternsColors,
|
|
12
|
+
patternsColorsLowerOpacity,
|
|
13
|
+
patternsStandardList
|
|
14
|
+
} = ChartDesign();
|
|
15
|
+
|
|
16
|
+
interface Dataset {
|
|
17
|
+
data: any,
|
|
18
|
+
label: any,
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export default function () {
|
|
22
|
+
const borderWidth = ref(3);
|
|
23
|
+
const barChartRef = ref(null as any);
|
|
24
|
+
const onHoverIndex: { dataSetIndex: number, columnIndex: number } = reactive({
|
|
25
|
+
dataSetIndex: -1,
|
|
26
|
+
columnIndex: -1
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
function privateGetHtmlLegendPlugin(legendContainer: Ref, selectMode: Ref<boolean>) {
|
|
31
|
+
return getHtmlLegendPlugin(legendContainer, selectMode, onHoverIndex);
|
|
32
|
+
}
|
|
33
|
+
// Hack to force the chart to reload on Hover
|
|
34
|
+
function reloadChart () {
|
|
35
|
+
borderWidth.value = 4;
|
|
36
|
+
borderWidth.value = 3;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function getStackedDatasets (datasets: Dataset[], stackDatasets: boolean, patternShifting?: number) {
|
|
40
|
+
return datasets.map((dataset, index) => {
|
|
41
|
+
return {
|
|
42
|
+
borderColor: function (context: any) {
|
|
43
|
+
return getBorderColor(index, context.index, patternShifting);
|
|
44
|
+
},
|
|
45
|
+
backgroundColor: function (context: any) {
|
|
46
|
+
return getPattern(index, context.index, patternShifting);
|
|
47
|
+
},
|
|
48
|
+
borderWidth: borderWidth.value,
|
|
49
|
+
data: dataset.data,
|
|
50
|
+
label: dataset.label,
|
|
51
|
+
stack: `Stack ${stackDatasets ? '0' : index}`
|
|
52
|
+
};
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function getDatasets (firstDataSet: Dataset, secondDataSet: Dataset) {
|
|
57
|
+
const getStacked = getStackedDatasets([firstDataSet, secondDataSet], false);
|
|
58
|
+
return getStacked;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function getBorderColor (dataSetIndex: number, contextIndex: number, patternShifting?: number) {
|
|
62
|
+
const index = getPatternIndexWithShift(dataSetIndex, patternShifting);
|
|
63
|
+
if (displayFullOpacity(dataSetIndex, contextIndex)) {
|
|
64
|
+
return patternsColors[index];
|
|
65
|
+
} else {
|
|
66
|
+
return patternsColorsLowerOpacity[index];
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function getPattern (dataSetIndex: number, contextIndex: number, patternShifting?: number) {
|
|
71
|
+
const index = getPatternIndexWithShift(dataSetIndex, patternShifting);
|
|
72
|
+
if (displayFullOpacity(dataSetIndex, contextIndex)) {
|
|
73
|
+
return patternsStandardList[index](false);
|
|
74
|
+
} else {
|
|
75
|
+
return patternsStandardList[index](true);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function nothingHovered (): boolean {
|
|
80
|
+
return onHoverIndex.dataSetIndex < 0;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function columnHovered (dataSetIndex: number, contextIndex: number): boolean {
|
|
84
|
+
return onHoverIndex.dataSetIndex === dataSetIndex && onHoverIndex.columnIndex === contextIndex;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function legendHovered (dataSetIndex: number): boolean {
|
|
88
|
+
return onHoverIndex.dataSetIndex === dataSetIndex && onHoverIndex.columnIndex < 0;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function displayFullOpacity (dataSetIndex: number, contextIndex: number): boolean {
|
|
92
|
+
return nothingHovered() ||
|
|
93
|
+
columnHovered(dataSetIndex, contextIndex) ||
|
|
94
|
+
legendHovered(dataSetIndex);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function resetOnHoverIndex () {
|
|
98
|
+
onHoverIndex.dataSetIndex = -1;
|
|
99
|
+
onHoverIndex.columnIndex = -1;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function getOnHoverOptions () {
|
|
103
|
+
return (_ignore: unknown, activeElements: Array<{ index: number, datasetIndex: number }>) => {
|
|
104
|
+
if (activeElements[0] !== undefined) {
|
|
105
|
+
onHoverIndex.dataSetIndex = activeElements[0].datasetIndex;
|
|
106
|
+
onHoverIndex.columnIndex = activeElements[0].index;
|
|
107
|
+
} else {
|
|
108
|
+
resetOnHoverIndex();
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return {
|
|
114
|
+
onHoverIndex,
|
|
115
|
+
reloadChart,
|
|
116
|
+
getDatasets,
|
|
117
|
+
getStackedDatasets,
|
|
118
|
+
getOnHoverOptions,
|
|
119
|
+
getBorderColor,
|
|
120
|
+
getPattern,
|
|
121
|
+
privateGetHtmlLegendPlugin,
|
|
122
|
+
getPatternIndexWithShift,
|
|
123
|
+
barChartRef,
|
|
124
|
+
borderWidth,
|
|
125
|
+
};
|
|
126
|
+
}
|
|
@@ -0,0 +1,366 @@
|
|
|
1
|
+
import type { Ref } from 'vue';
|
|
2
|
+
import type { HTMLLegendPlugin } from "../types/Chart";
|
|
3
|
+
import type { ChartOptions } from 'chart.js';
|
|
4
|
+
import ChartDesign from './patterns/ChartDesign';
|
|
5
|
+
import PatternFunctions from './PatternFunctions';
|
|
6
|
+
import { formatValueAndRate } from './FormatUtilities';
|
|
7
|
+
import QuestionMarkSvg from '@mozaic-ds/icons/svg/Navigation_Notification_Question_24px.svg';
|
|
8
|
+
|
|
9
|
+
const {
|
|
10
|
+
patternsColors,
|
|
11
|
+
patternsStandardList
|
|
12
|
+
} = ChartDesign();
|
|
13
|
+
const { getPatternCanvas } = PatternFunctions();
|
|
14
|
+
|
|
15
|
+
export const LEGEND_FONT_SIZE = 14;
|
|
16
|
+
export const LEGEND_LABEL_LEFT_MARGIN = '6px';
|
|
17
|
+
export const LEGEND_BOX_SIZE = 18;
|
|
18
|
+
export const LEGEND_BOX_POINT_SIZE = 6;
|
|
19
|
+
|
|
20
|
+
export interface Chart {
|
|
21
|
+
update (): void;
|
|
22
|
+
|
|
23
|
+
toggleDataVisibility (datasetIndex: number): void;
|
|
24
|
+
|
|
25
|
+
isDatasetVisible (datasetIndex: number): boolean;
|
|
26
|
+
|
|
27
|
+
getDataVisibility(index: number): boolean;
|
|
28
|
+
|
|
29
|
+
setDatasetVisibility (datasetIndex: number, visible: boolean): void;
|
|
30
|
+
|
|
31
|
+
plugins: HTMLLegendPlugin,
|
|
32
|
+
|
|
33
|
+
options: ChartOptions<'radar'> | ChartOptions<'doughnut'> | ChartOptions<'bar'> | ChartOptions<'line'>,
|
|
34
|
+
|
|
35
|
+
config: { type?: string, data: { labels: string[] ,datasets: unknown[], data?: unknown[] } },
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface ChartItem {
|
|
39
|
+
fontColor: string,
|
|
40
|
+
hidden: boolean,
|
|
41
|
+
text: string,
|
|
42
|
+
fillStyle: string,
|
|
43
|
+
strokeStyle: string,
|
|
44
|
+
lineWidth: number
|
|
45
|
+
datasetIndex: number,
|
|
46
|
+
index: number
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function getHtmlLegendPlugin(legendContainer: Ref, selectMode: Ref<boolean>, onHoverIndex: any, maxValueToDisplay?: number, chartData?: any): HTMLLegendPlugin[] {
|
|
50
|
+
return [{
|
|
51
|
+
id: 'htmlLegend',
|
|
52
|
+
afterUpdate (chart: any) {
|
|
53
|
+
const isDoughnut: boolean = chart.config.type === 'doughnut';
|
|
54
|
+
const flexDirection = isDoughnut ? 'column' : 'row';
|
|
55
|
+
const ul: HTMLLIElement = getOrCreateLegendList(legendContainer, flexDirection);
|
|
56
|
+
ul.style.margin = '1.375rem 1.0625rem';
|
|
57
|
+
while (ul.firstChild) {
|
|
58
|
+
ul.firstChild.remove();
|
|
59
|
+
}
|
|
60
|
+
const items: ChartItem[] = chart.options.plugins.legend.labels.generateLabels(chart);
|
|
61
|
+
items.forEach((item: ChartItem): void => {
|
|
62
|
+
const isDoughnut: boolean = chart.config.type === 'doughnut';
|
|
63
|
+
const index: number = isDoughnut ? item.index : item.datasetIndex;
|
|
64
|
+
const li: HTMLElement = createHtmlLegendListElement(chart, selectMode, index);
|
|
65
|
+
if (isDoughnut) {
|
|
66
|
+
const isOthersElement: boolean = index + 1 === maxValueToDisplay ? true : false;
|
|
67
|
+
li.style.marginTop = '12px';
|
|
68
|
+
if (isOthersElement) {
|
|
69
|
+
li.style.position = 'relative';
|
|
70
|
+
}
|
|
71
|
+
} else {
|
|
72
|
+
li.style.marginRight = '12px'
|
|
73
|
+
}
|
|
74
|
+
li.style.width = 'max-content';
|
|
75
|
+
li.style.cursor = 'pointer';
|
|
76
|
+
let liContent: HTMLElement;
|
|
77
|
+
if (!selectMode.value) {
|
|
78
|
+
liContent = createLegendElementWithPatterns(item, chart, onHoverIndex);
|
|
79
|
+
} else {
|
|
80
|
+
liContent = createLegendElementWithCheckbox(chart, item, selectMode, onHoverIndex);
|
|
81
|
+
}
|
|
82
|
+
li.appendChild(liContent);
|
|
83
|
+
li.appendChild(createHtmlLegendItemText(item));
|
|
84
|
+
if (isDoughnut && maxValueToDisplay && hasOthersTooltipToDisplay(chartData, maxValueToDisplay, index)) {
|
|
85
|
+
li.appendChild(createTooltipAndItsIcon(chartData, maxValueToDisplay));
|
|
86
|
+
}
|
|
87
|
+
ul.appendChild(li);
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
}];
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function hasOthersTooltipToDisplay (doughnutData: any, maxValueToDisplay: number, index: number) {
|
|
94
|
+
return doughnutData.data.length > maxValueToDisplay && index === maxValueToDisplay - 1;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function createTooltipAndItsIcon (doughnutData: any, maxValueToDisplay: number): HTMLDivElement {
|
|
98
|
+
const iconTopWrapper = document.createElement('div');
|
|
99
|
+
const iconWrapper = document.createElement('div');
|
|
100
|
+
const icon = document.createElement('img');
|
|
101
|
+
iconTopWrapper.style.position = 'absolute';
|
|
102
|
+
iconTopWrapper.style.right = '-32px';
|
|
103
|
+
icon.src = QuestionMarkSvg;
|
|
104
|
+
icon.style.top = '0';
|
|
105
|
+
icon.style.width = '1.5rem';
|
|
106
|
+
icon.style.filter = 'invert(38%) sepia(19%) saturate(18%) hue-rotate(337deg) brightness(97%) contrast(85%)';
|
|
107
|
+
iconWrapper.style.position = 'relative';
|
|
108
|
+
iconWrapper.style.display = 'flex';
|
|
109
|
+
const tooltip = createLegendOthersTooltip(doughnutData, maxValueToDisplay);
|
|
110
|
+
icon.onmouseover = () => {
|
|
111
|
+
(iconWrapper.firstElementChild as HTMLElement).style.visibility = 'visible';
|
|
112
|
+
};
|
|
113
|
+
iconTopWrapper.appendChild(iconWrapper);
|
|
114
|
+
iconWrapper.appendChild(tooltip);
|
|
115
|
+
iconWrapper.appendChild(icon);
|
|
116
|
+
return iconTopWrapper;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function createLegendOthersTooltip (doughnutData: any, maxValueToDisplay: number) {
|
|
120
|
+
const tooltip = document.createElement('div');
|
|
121
|
+
tooltip.style.visibility= 'hidden';
|
|
122
|
+
tooltip.style.position = 'absolute';
|
|
123
|
+
tooltip.style.zIndex = '10';
|
|
124
|
+
tooltip.style.width = '350px';
|
|
125
|
+
tooltip.style.bottom = '100%';
|
|
126
|
+
tooltip.style.left = '50%';
|
|
127
|
+
tooltip.style.marginLeft = '-150px';
|
|
128
|
+
tooltip.style.background = '#FFFFFF';
|
|
129
|
+
tooltip.style.boxShadow = '0px 1px 5px rgba(0, 0, 0, 0.2)';
|
|
130
|
+
tooltip.style.borderRadius = '0.5rem';
|
|
131
|
+
tooltip.style.fontSize = '14px';
|
|
132
|
+
tooltip.style.overflow = 'hidden';
|
|
133
|
+
addOthersTooltipLines(doughnutData, maxValueToDisplay, tooltip);
|
|
134
|
+
return tooltip;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function addOthersTooltipLines (doughnutData: any, maxValueToDisplay: number, tooltip: HTMLDivElement) {
|
|
138
|
+
const startIndex = maxValueToDisplay - 1;
|
|
139
|
+
doughnutData.data.slice(startIndex).forEach((_ignore: any, index: number) => {
|
|
140
|
+
const dataIndex = startIndex + index;
|
|
141
|
+
const textWrapper = document.createElement('div');
|
|
142
|
+
textWrapper.style.display = 'flex';
|
|
143
|
+
textWrapper.style.justifyContent = 'space-between';
|
|
144
|
+
textWrapper.style.padding = '0.5rem';
|
|
145
|
+
textWrapper.style.border = '1px solid #CCCCCC';
|
|
146
|
+
const label = document.createElement('span');
|
|
147
|
+
label.appendChild(document.createTextNode(doughnutData.labels[dataIndex]));
|
|
148
|
+
const value = document.createElement('span');
|
|
149
|
+
value.appendChild(document.createTextNode(formatValueAndRate(doughnutData, dataIndex)));
|
|
150
|
+
textWrapper.appendChild(label);
|
|
151
|
+
textWrapper.appendChild(value);
|
|
152
|
+
tooltip.appendChild(textWrapper);
|
|
153
|
+
}
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export function createLegendElementWithPatterns
|
|
158
|
+
(item: ChartItem, chart: Chart, onHoverIndex: any | null)
|
|
159
|
+
: HTMLElement {
|
|
160
|
+
const isDoughnut: boolean = chart.config.type === 'doughnut';
|
|
161
|
+
const index: number = isDoughnut ? item.index : item.datasetIndex;
|
|
162
|
+
const img: HTMLImageElement = new Image();
|
|
163
|
+
const boxSpan: HTMLElement = createHtmlLegendLine(item, chart.config.type);
|
|
164
|
+
const pattern: CanvasPattern = patternsStandardList[index](false);
|
|
165
|
+
const patternCanvas: HTMLCanvasElement = getPatternCanvas(pattern);
|
|
166
|
+
img.src = patternCanvas.toDataURL();
|
|
167
|
+
boxSpan.style.background = `url(${img.src})`;
|
|
168
|
+
boxSpan.style.backgroundSize = 'cover';
|
|
169
|
+
boxSpan.style.borderColor = patternsColors[index];
|
|
170
|
+
boxSpan.style.borderWidth = '2px';
|
|
171
|
+
|
|
172
|
+
boxSpan.onmouseover = ():void => {
|
|
173
|
+
isDoughnut ? onHoverIndex.value = index : onHoverIndex.dataSetIndex = index;
|
|
174
|
+
};
|
|
175
|
+
boxSpan.onmouseleave = (): void => {
|
|
176
|
+
isDoughnut ? onHoverIndex.value = null : onHoverIndex.dataSetIndex = null;
|
|
177
|
+
};
|
|
178
|
+
return boxSpan;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export function createLegendElementWithCheckbox
|
|
182
|
+
(chart: Chart, item: ChartItem, selectMode: Ref<boolean>, onHoverIndex: any | null): HTMLElement {
|
|
183
|
+
const isDoughnut: boolean = chart.config.type === 'doughnut';
|
|
184
|
+
const index: number = isDoughnut ? item.index : item.datasetIndex;
|
|
185
|
+
const checkbox: HTMLElement = createLegendCheckbox(chart, item);
|
|
186
|
+
const labels = chart.config.data.labels;
|
|
187
|
+
const allCheckBoxesVisible: boolean =
|
|
188
|
+
labels.every((label: string, index: number) => chart.getDataVisibility(index));
|
|
189
|
+
if (allCheckBoxesVisible) {
|
|
190
|
+
if (isDoughnut) {
|
|
191
|
+
onHoverIndex.value = -1;
|
|
192
|
+
selectMode.value = false;
|
|
193
|
+
} else {
|
|
194
|
+
onHoverIndex.dataSetIndex = -1;
|
|
195
|
+
}
|
|
196
|
+
return checkbox;
|
|
197
|
+
}
|
|
198
|
+
checkbox.onmouseover = ():void => {
|
|
199
|
+
isDoughnut ? onHoverIndex.value = index : onHoverIndex.dataSetIndex = index;
|
|
200
|
+
};
|
|
201
|
+
checkbox.onmouseleave = (): void => {
|
|
202
|
+
isDoughnut ? onHoverIndex.value = null : onHoverIndex.dataSetIndex = null;
|
|
203
|
+
};
|
|
204
|
+
return checkbox;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
export function createHtmlLegendItemText (item: ChartItem) {
|
|
208
|
+
const textContainer = document.createElement('p');
|
|
209
|
+
textContainer.style.color = item.fontColor;
|
|
210
|
+
textContainer.style.fontSize = `${LEGEND_FONT_SIZE}px`;
|
|
211
|
+
textContainer.style.margin = '0';
|
|
212
|
+
textContainer.style.padding = '0';
|
|
213
|
+
|
|
214
|
+
const text = document.createTextNode(item.text);
|
|
215
|
+
textContainer.appendChild(text);
|
|
216
|
+
return textContainer;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
export function createHtmlLegendLine (item: ChartItem, type: string | undefined) {
|
|
220
|
+
const boxSpan = document.createElement('div');
|
|
221
|
+
if (type !== 'doughnut') {
|
|
222
|
+
boxSpan.style.background = item.fillStyle;
|
|
223
|
+
boxSpan.style.borderColor = item.strokeStyle;
|
|
224
|
+
boxSpan.style.borderWidth = item.lineWidth + 'px';
|
|
225
|
+
}
|
|
226
|
+
boxSpan.style.borderRadius = '5px';
|
|
227
|
+
boxSpan.style.borderStyle = 'solid';
|
|
228
|
+
boxSpan.style.display = 'flex';
|
|
229
|
+
boxSpan.style.justifyContent = 'center';
|
|
230
|
+
boxSpan.style.alignItems = 'center';
|
|
231
|
+
boxSpan.style.width = LEGEND_BOX_SIZE + 'px';
|
|
232
|
+
boxSpan.style.marginRight = LEGEND_LABEL_LEFT_MARGIN;
|
|
233
|
+
boxSpan.style.height = LEGEND_BOX_SIZE + 'px';
|
|
234
|
+
return boxSpan;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
export function createHtmlLegendDatasetSquare (item: ChartItem) {
|
|
238
|
+
const divPoint = document.createElement('div');
|
|
239
|
+
divPoint.style.height = LEGEND_BOX_POINT_SIZE + 'px';
|
|
240
|
+
divPoint.style.width = LEGEND_BOX_POINT_SIZE + 'px';
|
|
241
|
+
divPoint.style.background = 'white';
|
|
242
|
+
divPoint.style.borderStyle = 'solid';
|
|
243
|
+
divPoint.style.borderColor = item.strokeStyle;
|
|
244
|
+
divPoint.style.borderWidth = item.lineWidth + 'px';
|
|
245
|
+
return divPoint;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
export function createHtmlLegendListElement
|
|
249
|
+
(chart: Chart, selectMode: Ref, elementIndex: number) {
|
|
250
|
+
const li: HTMLElement = document.createElement('li');
|
|
251
|
+
li.style.alignItems = 'center';
|
|
252
|
+
li.style.cursor = selectMode.value ? '' : 'pointer';
|
|
253
|
+
li.style.display = 'flex';
|
|
254
|
+
li.style.flexDirection = 'row';
|
|
255
|
+
li.setAttribute('data-test-id', `legend-item-${elementIndex}`);
|
|
256
|
+
li.onclick = () => {
|
|
257
|
+
if (!selectMode.value) {
|
|
258
|
+
hideAllButThis(chart, elementIndex, selectMode);
|
|
259
|
+
chart.update();
|
|
260
|
+
} else {
|
|
261
|
+
switchItemVisibility(chart, elementIndex, selectMode);
|
|
262
|
+
}
|
|
263
|
+
};
|
|
264
|
+
return li;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
export function addCheckboxStyle (isDataSetVisible: boolean, item: ChartItem, checkbox: Element) {
|
|
268
|
+
let backgroundColor = '#fff';
|
|
269
|
+
let borderColor = '#666';
|
|
270
|
+
if (isDataSetVisible) {
|
|
271
|
+
backgroundColor = item.strokeStyle;
|
|
272
|
+
borderColor = item.strokeStyle;
|
|
273
|
+
}
|
|
274
|
+
checkbox.setAttribute('checked', '' + isDataSetVisible);
|
|
275
|
+
checkbox.setAttribute('class', 'mc-checkbox__input');
|
|
276
|
+
checkbox.setAttribute('style', `background-color: ${backgroundColor};
|
|
277
|
+
width: ${LEGEND_BOX_SIZE + 4 + 'px'};
|
|
278
|
+
height: ${LEGEND_BOX_SIZE + 4 + 'px'};
|
|
279
|
+
margin-right: ${LEGEND_LABEL_LEFT_MARGIN};
|
|
280
|
+
border-color: ${borderColor};`);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
export function createLegendCheckbox (chart: Chart, item: ChartItem) {
|
|
284
|
+
const isDoughnut: boolean = chart.config.type === 'doughnut';
|
|
285
|
+
const index: number = isDoughnut ? item.index : item.datasetIndex;
|
|
286
|
+
const checkbox = document.createElement('input');
|
|
287
|
+
checkbox.setAttribute('type', 'checkbox');
|
|
288
|
+
checkbox.setAttribute('data-test-id', `legend-checkbox-${index}`);
|
|
289
|
+
const isDataSetVisible = isDoughnut ? chart.getDataVisibility(index) : chart.isDatasetVisible(index);
|
|
290
|
+
addCheckboxStyle(isDataSetVisible, item, checkbox);
|
|
291
|
+
return checkbox;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function isMonoDataSetChart (chart: Chart) {
|
|
295
|
+
const { type } = chart.config;
|
|
296
|
+
return type === 'pie' || type === 'doughnut';
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function getChartsData (chart: any) {
|
|
300
|
+
let dataSets: unknown[] = chart.config.data.datasets;
|
|
301
|
+
if (isMonoDataSetChart(chart)) {
|
|
302
|
+
dataSets = chart.config.data.datasets[0].data;
|
|
303
|
+
}
|
|
304
|
+
return dataSets;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
export function hideAllButThis (chart: Chart, elementIndex: number, selectMode: Ref) {
|
|
308
|
+
if (!selectMode.value) {
|
|
309
|
+
const dataSets: unknown[] = getChartsData(chart);
|
|
310
|
+
selectMode.value = true;
|
|
311
|
+
dataSets.forEach((_data, index) => {
|
|
312
|
+
if (index !== elementIndex) {
|
|
313
|
+
switchItemVisibility(chart, index);
|
|
314
|
+
}
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
function allDataVisible (chart: Chart): boolean {
|
|
320
|
+
let allVisible = true;
|
|
321
|
+
const chartsData: unknown[] = getChartsData(chart);
|
|
322
|
+
chartsData.forEach((_data, dataIndex) => {
|
|
323
|
+
allVisible = allVisible && chart.isDatasetVisible(dataIndex);
|
|
324
|
+
});
|
|
325
|
+
return allVisible;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
export function switchItemVisibility (chart: Chart, elementIndex: number, selectMode?: Ref) {
|
|
329
|
+
if (isMonoDataSetChart(chart)) {
|
|
330
|
+
chart.toggleDataVisibility(elementIndex);
|
|
331
|
+
} else {
|
|
332
|
+
chart.setDatasetVisibility(elementIndex, !chart.isDatasetVisible(elementIndex));
|
|
333
|
+
}
|
|
334
|
+
if (selectMode && allDataVisible(chart)) {
|
|
335
|
+
selectMode.value = false;
|
|
336
|
+
}
|
|
337
|
+
chart.update();
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
export function createLegendElementWithSquareArea (item: ChartItem) {
|
|
342
|
+
const liContent = createHtmlLegendLine(item, '');
|
|
343
|
+
const divPoint = createHtmlLegendDatasetSquare(item);
|
|
344
|
+
const index = item.index || item.datasetIndex;
|
|
345
|
+
if (index === 0) {
|
|
346
|
+
divPoint.style.borderRadius = '25px';
|
|
347
|
+
} else {
|
|
348
|
+
divPoint.style.transform = 'rotate(45deg)';
|
|
349
|
+
}
|
|
350
|
+
liContent.appendChild(divPoint);
|
|
351
|
+
return liContent;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
export function getOrCreateLegendList (legendContainer: Ref, flexDirection: string) {
|
|
356
|
+
let listContainer = legendContainer.value?.querySelector('ul');
|
|
357
|
+
if (!listContainer) {
|
|
358
|
+
listContainer = document.createElement('ul');
|
|
359
|
+
listContainer.style.display = 'flex';
|
|
360
|
+
listContainer.style.flexDirection = flexDirection;
|
|
361
|
+
listContainer.style.margin = '0';
|
|
362
|
+
listContainer.style.padding = '0';
|
|
363
|
+
legendContainer.value?.appendChild(listContainer);
|
|
364
|
+
}
|
|
365
|
+
return listContainer;
|
|
366
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { ref } from 'vue';
|
|
2
|
+
import type { Ref } from 'vue';
|
|
3
|
+
import {
|
|
4
|
+
getHtmlLegendPlugin
|
|
5
|
+
} from '../services/ChartsCommonLegend';
|
|
6
|
+
import { formatWithThousandsSeprators } from '../services/FormatUtilities';
|
|
7
|
+
|
|
8
|
+
import ChartDesign from './patterns/ChartDesign';
|
|
9
|
+
|
|
10
|
+
const {
|
|
11
|
+
patternsStandardList
|
|
12
|
+
} = ChartDesign();
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
export default function () {
|
|
16
|
+
const doughnutRef: Ref = ref(null);
|
|
17
|
+
const onHoverIndex: Ref<number | null> = ref(null);
|
|
18
|
+
const backgroundColor: Ref<CanvasPattern[] | null> = ref(null);
|
|
19
|
+
|
|
20
|
+
function privateGetHtmlLegendPlugin(legendContainer: Ref, selectMode: Ref<boolean>, maxValueToDisplay: number, doughnutData: any) {
|
|
21
|
+
return getHtmlLegendPlugin(legendContainer, selectMode, onHoverIndex, maxValueToDisplay, doughnutData);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function getBackgroundColor(): void {
|
|
25
|
+
if (onHoverIndex.value !== null) {
|
|
26
|
+
backgroundColor.value = patternsStandardList
|
|
27
|
+
.map((pattern, index) => onHoverIndex.value === index ? pattern(false) : pattern(true));
|
|
28
|
+
} else {
|
|
29
|
+
backgroundColor.value = patternsStandardList
|
|
30
|
+
.map((pattern) => pattern(false));
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function getOnHoverOptions () {
|
|
35
|
+
return (_ignore: unknown, activeElements: Array<any>): void => {
|
|
36
|
+
if (activeElements[0] !== undefined) {
|
|
37
|
+
onHoverIndex.value = activeElements[0].element.$context.index;
|
|
38
|
+
} else {
|
|
39
|
+
onHoverIndex.value = null;
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const getFormatedText = (str: string) => {
|
|
45
|
+
return str.charAt(0).toUpperCase() + str.slice(1).toLowerCase();
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
function getDoughnutLabels(labels: string[], data: any[], maxValues: number) {
|
|
49
|
+
let truncatedLabels = labels.slice(0);
|
|
50
|
+
let truncatedData = data.slice(0);
|
|
51
|
+
if (labels.length > maxValues){
|
|
52
|
+
truncatedData = groupDataAfterNthValue(data, maxValues);
|
|
53
|
+
truncatedLabels = truncatedLabels.slice(0, maxValues - 1);
|
|
54
|
+
truncatedLabels.push('others');
|
|
55
|
+
}
|
|
56
|
+
return truncatedLabels.map((label: string, index: number) => `${getFormatedText(label)} (${formatWithThousandsSeprators(truncatedData[index].rate as number)} %)`);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function groupDataAfterNthValue (data: any, maxValues: number): any[] {
|
|
60
|
+
if (maxValues < 1) {
|
|
61
|
+
return data;
|
|
62
|
+
}
|
|
63
|
+
let truncatedData = data.slice(0);
|
|
64
|
+
if (data.length > maxValues) {
|
|
65
|
+
truncatedData = truncatedData.slice(0, maxValues);
|
|
66
|
+
truncatedData[maxValues - 1] = data.slice(maxValues).reduce((result: any, current: any) => {
|
|
67
|
+
result.rate += current.rate;
|
|
68
|
+
result.value += current.value;
|
|
69
|
+
return result;
|
|
70
|
+
}, {
|
|
71
|
+
rate: data[maxValues - 1].rate,
|
|
72
|
+
value: data[maxValues - 1].value
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
return truncatedData;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return {
|
|
79
|
+
onHoverIndex,
|
|
80
|
+
privateGetHtmlLegendPlugin,
|
|
81
|
+
getOnHoverOptions,
|
|
82
|
+
groupDataAfterNthValue,
|
|
83
|
+
getDoughnutLabels,
|
|
84
|
+
getBackgroundColor,
|
|
85
|
+
getFormatedText,
|
|
86
|
+
backgroundColor,
|
|
87
|
+
doughnutRef
|
|
88
|
+
};
|
|
89
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export function formatTicks (val: number, unit?: string | null): string {
|
|
2
|
+
const fixedValue = parseInt(val.toFixed());
|
|
3
|
+
return `${new Intl.NumberFormat().format(fixedValue)}${unit ? ' ' + unit : ''}`;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export function formatWithThousandsSeprators (value: number) {
|
|
7
|
+
return Math.abs(Number(value)) >= 1.0e+6
|
|
8
|
+
? new Intl.NumberFormat().format(parseFloat((Number(value) / 1.0e+6).toFixed(2))) + ' M'
|
|
9
|
+
: Math.abs(Number(value)) >= 1.0e+3
|
|
10
|
+
? new Intl.NumberFormat().format(parseFloat((Number(value) / 1.0e+3).toFixed(2))) + ' K'
|
|
11
|
+
: new Intl.NumberFormat().format(parseFloat((Number(value)).toFixed(2)));
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function numberWithThousandSeparators (value: string, unit?: string) {
|
|
15
|
+
const x = parseFloat(value).toFixed(2);
|
|
16
|
+
const formattedValue = x.replace(/\B(?=(\d{3}){1,5}(?!\d))/g, ' ');
|
|
17
|
+
return unit ? formattedValue + ' ' + unit : formattedValue;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
export function getPatternIndexWithShift (dataSetIndex: number, patternShifting?: number) {
|
|
22
|
+
return patternShifting
|
|
23
|
+
? (dataSetIndex + patternShifting) % 6
|
|
24
|
+
: dataSetIndex;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function formatValueAndRate (doughnutData: any, dataIndex: number) {
|
|
28
|
+
const textValue = `${formatWithThousandsSeprators(doughnutData.data[dataIndex].value)} (${formatWithThousandsSeprators(doughnutData.data[dataIndex].rate)}%)`;
|
|
29
|
+
return textValue;
|
|
30
|
+
}
|