Skip to content

Commit 3810ff0

Browse files
authored
Merge pull request #2615 from Brett-S-OWB/history-chart-legend-category-clean
Koala - History chart legend categories for mobile & large legends
2 parents 8d8fd39 + 57e439b commit 3810ff0

7 files changed

Lines changed: 347 additions & 123 deletions

File tree

packages/modules/web_themes/koala/source/src/components/charts/historyChart/HistoryChart.vue

Lines changed: 9 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
/>
1010
</div>
1111
<HistoryChartLegend
12-
v-if="legendDisplay && legendLarge"
12+
v-if="legendDisplay"
1313
:chart="chartRef?.chart || null"
1414
class="legend-wrapper q-mt-sm"
1515
/>
@@ -31,10 +31,6 @@ import {
3131
TimeScale,
3232
Tooltip,
3333
Filler,
34-
ChartEvent,
35-
LegendItem,
36-
LegendElement,
37-
ChartTypeRegistry,
3834
ChartDataset,
3935
ChartType,
4036
} from 'chart.js';
@@ -69,10 +65,6 @@ const props = defineProps<{
6965
7066
const chartRef = ref<ChartComponentRef | null>(null);
7167
72-
const legendLarge = computed(() =>
73-
lineChartData?.value?.datasets.length > 15 ? true : false,
74-
);
75-
7668
const applyHiddenDatasetsToChart = <TType extends ChartType, TData>(
7769
chart: Chart<TType, TData>,
7870
): void => {
@@ -125,6 +117,7 @@ const chartRange = computed(
125117
const chargePointDatasets = computed(() =>
126118
chargePointIds.value.map((cpId) => ({
127119
label: `${chargePointNames.value(cpId)}`,
120+
category: 'chargepoint',
128121
unit: 'kW',
129122
borderColor: '#4766b5',
130123
backgroundColor: 'rgba(71, 102, 181, 0.2)',
@@ -148,6 +141,7 @@ const vehicleDatasets = computed(() =>
148141
if (selectedData.value.some((item) => socKey in item)) {
149142
return {
150143
label: `${vehicle.name} SoC`,
144+
category: 'vehicle',
151145
unit: '%',
152146
borderColor: '#9F8AFF',
153147
borderWidth: 2,
@@ -175,6 +169,7 @@ const lineChartData = computed(() => {
175169
datasets: [
176170
{
177171
label: gridMeterName.value,
172+
category: 'component',
178173
unit: 'kW',
179174
borderColor: '#a33c42',
180175
backgroundColor: 'rgba(239,182,188, 0.2)',
@@ -191,6 +186,7 @@ const lineChartData = computed(() => {
191186
},
192187
{
193188
label: 'Hausverbrauch',
189+
category: 'component',
194190
unit: 'kW',
195191
borderColor: '#949aa1',
196192
backgroundColor: 'rgba(148, 154, 161, 0.2)',
@@ -207,6 +203,7 @@ const lineChartData = computed(() => {
207203
},
208204
{
209205
label: 'PV ges.',
206+
category: 'component',
210207
unit: 'kW',
211208
borderColor: 'green',
212209
backgroundColor: 'rgba(144, 238, 144, 0.2)',
@@ -223,6 +220,7 @@ const lineChartData = computed(() => {
223220
},
224221
{
225222
label: 'Speicher ges.',
223+
category: 'component',
226224
unit: 'kW',
227225
borderColor: '#b5a647',
228226
backgroundColor: 'rgba(181, 166, 71, 0.2)',
@@ -239,6 +237,7 @@ const lineChartData = computed(() => {
239237
},
240238
{
241239
label: 'Speicher SoC',
240+
category: 'component',
242241
unit: '%',
243242
borderColor: '#FFB96E',
244243
borderWidth: 2,
@@ -265,33 +264,7 @@ const chartOptions = computed<ChartOptions<'line'>>(() => ({
265264
animation: false,
266265
plugins: {
267266
legend: {
268-
display: !legendLarge.value && legendDisplay.value,
269-
fullSize: true,
270-
align: 'center' as const,
271-
position: 'bottom' as const,
272-
labels: {
273-
boxWidth: 19,
274-
boxHeight: 0.1,
275-
},
276-
onClick: (
277-
e: ChartEvent,
278-
legendItem: LegendItem,
279-
legend: LegendElement<keyof ChartTypeRegistry>,
280-
) => {
281-
const index = legendItem.datasetIndex!;
282-
const chartInstance = legend.chart;
283-
const datasetName = legendItem.text;
284-
285-
// Toggle visibility using the store
286-
localDataStore.toggleDataset(datasetName);
287-
288-
// Update chart visibility
289-
if (localDataStore.isDatasetHidden(datasetName)) {
290-
chartInstance.hide(index);
291-
} else {
292-
chartInstance.show(index);
293-
}
294-
},
267+
display: false,
295268
},
296269
tooltip: {
297270
mode: 'index' as const,

packages/modules/web_themes/koala/source/src/components/charts/historyChart/HistoryChartLegend.vue

Lines changed: 94 additions & 86 deletions
Original file line numberDiff line numberDiff line change
@@ -1,70 +1,119 @@
11
<template>
2-
<q-scroll-area
3-
v-if="chart"
4-
:thumb-style="thumbStyle"
5-
:bar-style="barStyle"
6-
class="custom-legend-container"
7-
>
8-
<q-list dense class="q-pa-none">
9-
<div class="row wrap q-pa-none items-center justify-center">
10-
<q-item
11-
v-for="(dataset, index) in legendItems"
12-
:key="dataset.text || index"
13-
clickable
14-
dense
15-
class="q-py-none"
16-
:class="{ 'legend-item-hidden': dataset.hidden }"
17-
@click="toggleDataset(dataset.text, dataset.datasetIndex)"
18-
>
19-
<q-item-section avatar class="q-pr-none">
20-
<div
21-
class="legend-color-box q-mr-sm"
22-
:style="{ backgroundColor: getItemColor(dataset) }"
23-
></div>
24-
</q-item-section>
25-
<q-item-section>
26-
<q-item-label class="text-caption">{{ dataset.text }}</q-item-label>
27-
</q-item-section>
28-
</q-item>
29-
</div>
30-
</q-list>
31-
</q-scroll-area>
2+
<!-- On smaller screens (<md) always show categories -->
3+
<HistoryChartLegendCategoriesGroup
4+
v-if="$q.screen.lt.md"
5+
:categorizedLegendItems="categorizedLegendItems"
6+
:toggleDataset="toggleDataset"
7+
:getItemColor="getItemColor"
8+
:getItemLineType="getItemLineType"
9+
/>
10+
11+
<!-- On larger screens: show standard legend if legend not large; otherwise show categories -->
12+
<HistoryChartLegendStandard
13+
v-else-if="chart && !$q.screen.lt.sm && !legendLarge"
14+
:items="legendItems"
15+
:toggleDataset="toggleDataset"
16+
:getItemColor="getItemColor"
17+
:getItemLineType="getItemLineType"
18+
/>
19+
20+
<HistoryChartLegendCategoriesGroup
21+
v-else
22+
:categorizedLegendItems="categorizedLegendItems"
23+
:toggleDataset="toggleDataset"
24+
:getItemColor="getItemColor"
25+
:getItemLineType="getItemLineType"
26+
/>
3227
</template>
3328

3429
<script setup lang="ts">
35-
import { ref, watch } from 'vue';
30+
import { ref, watch, computed, nextTick } from 'vue';
3631
import { useLocalDataStore } from 'src/stores/localData-store';
37-
import { Chart, LegendItem } from 'chart.js';
32+
import { Chart, ChartDataset, LegendItem } from 'chart.js';
33+
import type {
34+
Category,
35+
CategorizedDataset,
36+
LegendItemWithCategory,
37+
} from './history-chart-model';
38+
import { useMqttStore } from 'src/stores/mqtt-store';
39+
import { useQuasar } from 'quasar';
40+
import HistoryChartLegendCategoriesGroup from './HistoryChartLegendCategoriesGroup.vue';
41+
import HistoryChartLegendStandard from './HistoryChartLegendStandard.vue';
42+
43+
const mqttStore = useMqttStore();
44+
const $q = useQuasar();
3845
3946
const props = defineProps<{
4047
chart: Chart | null;
4148
}>();
4249
4350
const localDataStore = useLocalDataStore();
44-
const legendItems = ref<LegendItem[]>([]);
51+
const legendItems = ref<LegendItemWithCategory[]>([]);
52+
53+
const legendLarge = computed(() => {
54+
return legendItems.value.length > 20;
55+
});
4556
4657
const updateLegendItems = () => {
4758
if (!props.chart) return;
4859
const items =
4960
props.chart.options.plugins?.legend?.labels?.generateLabels?.(
5061
props.chart,
5162
) || [];
52-
53-
items.forEach((item) => {
63+
(items as LegendItemWithCategory[]).forEach((item) => {
5464
if (item.text && localDataStore.isDatasetHidden(item.text)) {
5565
item.hidden = true;
5666
}
67+
// Inject the category from the dataset
68+
const dataset = props.chart?.data.datasets[
69+
item.datasetIndex!
70+
] as unknown as CategorizedDataset;
71+
item.category = dataset.category;
5772
});
58-
legendItems.value = items;
73+
legendItems.value = items as LegendItemWithCategory[];
5974
};
6075
61-
const getItemColor = (item: LegendItem) => {
62-
if (!props.chart || item.datasetIndex === undefined) return '#ccc';
76+
const categorizedLegendItems = computed(() => {
77+
const categories: Record<Category, LegendItemWithCategory[]> = {
78+
chargepoint: [],
79+
vehicle: [],
80+
battery: [],
81+
component: [],
82+
};
83+
for (const item of legendItems.value) {
84+
const category = item.category;
85+
if (category && categories[category]) {
86+
categories[category].push(item);
87+
} else {
88+
categories.component.push(item);
89+
}
90+
}
91+
// Sort each category's items alphabetically
92+
Object.keys(categories).forEach((key) => {
93+
categories[key as Category].sort((a, b) =>
94+
(a.text || '').localeCompare(b.text || '', undefined, { numeric: true }),
95+
);
96+
});
97+
return categories;
98+
});
6399
100+
const getItemColor = (item: LegendItem): string => {
101+
if (!props.chart || item.datasetIndex === undefined) return '#ccc';
64102
const dataset = props.chart.data.datasets[item.datasetIndex];
65103
return (dataset.borderColor as string) || '#ccc';
66104
};
67105
106+
const getItemLineType = (item: LegendItem) => {
107+
if (!props.chart || item.datasetIndex === undefined) return;
108+
const dataset = props.chart.data.datasets[
109+
item.datasetIndex
110+
] as ChartDataset<'line'>;
111+
const borderDash = dataset.borderDash;
112+
return Array.isArray(borderDash) && borderDash.length > 0
113+
? 'dashed'
114+
: 'solid';
115+
};
116+
68117
const toggleDataset = (datasetName?: string, datasetIndex?: number) => {
69118
if (!props.chart || !datasetName || datasetIndex === undefined) return;
70119
localDataStore.toggleDataset(datasetName);
@@ -96,52 +145,11 @@ watch(
96145
{ immediate: true },
97146
);
98147
99-
const thumbStyle = {
100-
borderRadius: '5px',
101-
backgroundColor: 'var(--q-primary)',
102-
width: '6px',
103-
opacity: '1',
104-
};
105-
106-
const barStyle = {
107-
borderRadius: '5px',
108-
backgroundColor: 'var(--q-secondary)',
109-
width: '6px',
110-
opacity: '1',
111-
};
148+
watch(
149+
() => mqttStore.vehicleList,
150+
async () => {
151+
await nextTick();
152+
updateLegendItems();
153+
},
154+
);
112155
</script>
113-
114-
<style scoped>
115-
.custom-legend-container {
116-
margin-bottom: 5px;
117-
height: 70px;
118-
border-radius: 5px;
119-
text-align: left;
120-
width: 100%;
121-
}
122-
123-
.legend-color-box {
124-
display: inline-block;
125-
width: 20px;
126-
height: 3px;
127-
}
128-
129-
.legend-item-hidden {
130-
opacity: 0.6 !important;
131-
text-decoration: line-through !important;
132-
}
133-
134-
/* Override the avatar section min-width */
135-
:deep(.q-item__section--avatar) {
136-
min-width: 5px !important;
137-
padding-right: 0px !important;
138-
}
139-
140-
/* For very small screens */
141-
@media (max-width: 576px) {
142-
.legend-color-box {
143-
width: 10px;
144-
height: 2px;
145-
}
146-
}
147-
</style>
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
<template>
2+
<div class="row justify-center items-center">
3+
<HistoryChartLegendCategory
4+
:label="'Komponenten'"
5+
:items="categorizedLegendItems.component"
6+
:toggleDataset="toggleDataset"
7+
:getItemColor="getItemColor"
8+
:getItemLineType="getItemLineType"
9+
menuAnchor="bottom right"
10+
menuSelf="top right"
11+
/>
12+
13+
<HistoryChartLegendCategory
14+
:label="'Ladepunkte'"
15+
:items="categorizedLegendItems.chargepoint"
16+
:toggleDataset="toggleDataset"
17+
:getItemColor="getItemColor"
18+
:getItemLineType="getItemLineType"
19+
menuAnchor="bottom middle"
20+
menuSelf="top middle"
21+
menuFormat="q-mx-lg"
22+
/>
23+
24+
<HistoryChartLegendCategory
25+
:label="'Fahrzeuge'"
26+
:items="categorizedLegendItems.vehicle"
27+
:toggleDataset="toggleDataset"
28+
:getItemColor="getItemColor"
29+
:getItemLineType="getItemLineType"
30+
menuAnchor="bottom left"
31+
menuSelf="top left"
32+
/>
33+
</div>
34+
</template>
35+
36+
<script setup lang="ts">
37+
import HistoryChartLegendCategory from './HistoryChartLegendCategory.vue';
38+
import type { LegendItem } from 'chart.js';
39+
import type { Category } from './history-chart-model';
40+
41+
defineProps<{
42+
categorizedLegendItems: Record<Category, LegendItem[]>;
43+
toggleDataset: (datasetName: string, datasetIndex: number) => void;
44+
getItemColor: (dataset: LegendItem) => string;
45+
getItemLineType: (dataset: LegendItem) => string | undefined;
46+
}>();
47+
</script>

0 commit comments

Comments
 (0)