|
1 | 1 | <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 | + /> |
32 | 27 | </template> |
33 | 28 |
|
34 | 29 | <script setup lang="ts"> |
35 | | -import { ref, watch } from 'vue'; |
| 30 | +import { ref, watch, computed, nextTick } from 'vue'; |
36 | 31 | 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(); |
38 | 45 |
|
39 | 46 | const props = defineProps<{ |
40 | 47 | chart: Chart | null; |
41 | 48 | }>(); |
42 | 49 |
|
43 | 50 | 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 | +}); |
45 | 56 |
|
46 | 57 | const updateLegendItems = () => { |
47 | 58 | if (!props.chart) return; |
48 | 59 | const items = |
49 | 60 | props.chart.options.plugins?.legend?.labels?.generateLabels?.( |
50 | 61 | props.chart, |
51 | 62 | ) || []; |
52 | | -
|
53 | | - items.forEach((item) => { |
| 63 | + (items as LegendItemWithCategory[]).forEach((item) => { |
54 | 64 | if (item.text && localDataStore.isDatasetHidden(item.text)) { |
55 | 65 | item.hidden = true; |
56 | 66 | } |
| 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; |
57 | 72 | }); |
58 | | - legendItems.value = items; |
| 73 | + legendItems.value = items as LegendItemWithCategory[]; |
59 | 74 | }; |
60 | 75 |
|
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 | +}); |
63 | 99 |
|
| 100 | +const getItemColor = (item: LegendItem): string => { |
| 101 | + if (!props.chart || item.datasetIndex === undefined) return '#ccc'; |
64 | 102 | const dataset = props.chart.data.datasets[item.datasetIndex]; |
65 | 103 | return (dataset.borderColor as string) || '#ccc'; |
66 | 104 | }; |
67 | 105 |
|
| 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 | +
|
68 | 117 | const toggleDataset = (datasetName?: string, datasetIndex?: number) => { |
69 | 118 | if (!props.chart || !datasetName || datasetIndex === undefined) return; |
70 | 119 | localDataStore.toggleDataset(datasetName); |
@@ -96,52 +145,11 @@ watch( |
96 | 145 | { immediate: true }, |
97 | 146 | ); |
98 | 147 |
|
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 | +); |
112 | 155 | </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> |
|
0 commit comments