diff --git a/packages/frontend/src/components/MkChart.vue b/packages/frontend/src/components/MkChart.vue index c0562f02e351324bafffcee427b8c20d03e1e761..ea28cfa794bd7f5d6d6788b96cc937d92856c8ed 100644 --- a/packages/frontend/src/components/MkChart.vue +++ b/packages/frontend/src/components/MkChart.vue @@ -1,6 +1,7 @@ <template> <div class="cbbedffa"> <canvas ref="chartEl"></canvas> + <MkChartLegend ref="legendEl" style="margin-top: 8px;"/> <div v-if="fetching" class="fetching"> <MkLoading/> </div> @@ -24,6 +25,8 @@ import { chartVLine } from '@/scripts/chart-vline'; import { alpha } from '@/scripts/color'; import date from '@/filters/date'; import { initChart } from '@/scripts/init-chart'; +import { chartLegend } from '@/scripts/chart-legend'; +import MkChartLegend from '@/components/MkChartLegend.vue'; initChart(); @@ -67,6 +70,8 @@ const props = defineProps({ }, }); +let legendEl = $shallowRef<InstanceType<typeof MkChartLegend>>(); + const sum = (...arr) => arr.reduce((r, a) => r.map((b, i) => a[i] + b)); const negate = arr => arr.map(x => -x); @@ -220,11 +225,7 @@ const render = () => { }, plugins: { legend: { - display: props.detailed, - position: 'bottom', - labels: { - boxWidth: 16, - }, + display: false, }, tooltip: { enabled: false, @@ -264,7 +265,7 @@ const render = () => { gradient, }, }, - plugins: [chartVLine(vLineColor)], + plugins: [chartVLine(vLineColor), ...(props.detailed ? [chartLegend(legendEl)] : [])], }); }; diff --git a/packages/frontend/src/components/MkChartLegend.vue b/packages/frontend/src/components/MkChartLegend.vue new file mode 100644 index 0000000000000000000000000000000000000000..f33f7537238a848f13eb42e2dcd5dfb75cdc09f1 --- /dev/null +++ b/packages/frontend/src/components/MkChartLegend.vue @@ -0,0 +1,75 @@ +<template> +<div :class="$style.root"> + <button v-for="item in items" class="_button item" :class="{ disabled: item.hidden }" @click="onClick(item)"> + <span class="box" :style="{ background: chart.config.type === 'line' ? item.strokeStyle?.toString() : item.fillStyle?.toString() }"></span> + {{ item.text }} + </button> +</div> +</template> + +<script lang="ts" setup> +import { onMounted, ref, shallowRef, watch, PropType, onUnmounted } from 'vue'; +import { Chart, LegendItem } from 'chart.js'; + +const props = defineProps({ +}); + +let chart = $shallowRef<Chart>(); +let items = $shallowRef<LegendItem[]>([]); + +function update(_chart: Chart, _items: LegendItem[]) { + chart = _chart, + items = _items; +} + +function onClick(item: LegendItem) { + if (chart == null) return; + const { type } = chart.config; + if (type === 'pie' || type === 'doughnut') { + // Pie and doughnut charts only have a single dataset and visibility is per item + chart.toggleDataVisibility(item.index); + } else { + chart.setDatasetVisibility(item.datasetIndex, !chart.isDatasetVisible(item.datasetIndex)); + } + chart.update(); +} + +defineExpose({ + update, +}); +</script> + +<style lang="scss" module> +.root { + display: flex; + flex-wrap: wrap; + justify-content: center; + gap: 8px; + + &:global { + > .item { + font-size: 85%; + padding: 4px 12px 4px 8px; + border: solid 1px var(--divider); + border-radius: 999px; + + &:hover { + border-color: var(--inputBorderHover); + } + + &.disabled { + text-decoration: line-through; + opacity: 0.6; + } + + > .box { + display: inline-block; + width: 12px; + height: 12px; + border-radius: 100%; + vertical-align: -10%; + } + } + } +} +</style> diff --git a/packages/frontend/src/pages/user/activity.pv.vue b/packages/frontend/src/pages/user/activity.pv.vue index 2d83d1ddc35d66617263bc808df4ded006f89275..7715b666730c329ee32167e3f8ef34d3438268b3 100644 --- a/packages/frontend/src/pages/user/activity.pv.vue +++ b/packages/frontend/src/pages/user/activity.pv.vue @@ -3,6 +3,7 @@ <MkLoading v-if="fetching"/> <div v-show="!fetching" :class="$style.root" class="_panel"> <canvas ref="chartEl"></canvas> + <MkChartLegend ref="legendEl" style="margin-top: 8px;"/> </div> </div> </template> @@ -20,6 +21,8 @@ import { useChartTooltip } from '@/scripts/use-chart-tooltip'; import { chartVLine } from '@/scripts/chart-vline'; import { alpha } from '@/scripts/color'; import { initChart } from '@/scripts/init-chart'; +import { chartLegend } from '@/scripts/chart-legend'; +import MkChartLegend from '@/components/MkChartLegend.vue'; initChart(); @@ -28,6 +31,7 @@ const props = defineProps<{ }>(); const chartEl = $shallowRef<HTMLCanvasElement>(null); +let legendEl = $shallowRef<InstanceType<typeof MkChartLegend>>(); const now = new Date(); let chartInstance: Chart = null; const chartLimit = 30; @@ -153,14 +157,7 @@ async function renderChart() { }, }, legend: { - display: true, - position: 'bottom', - padding: { - left: 0, - right: 0, - top: 8, - bottom: 0, - }, + display: false, }, tooltip: { enabled: false, @@ -173,7 +170,7 @@ async function renderChart() { gradient, }, }, - plugins: [chartVLine(vLineColor)], + plugins: [chartVLine(vLineColor), chartLegend(legendEl)], }); fetching = false; diff --git a/packages/frontend/src/scripts/chart-legend.ts b/packages/frontend/src/scripts/chart-legend.ts new file mode 100644 index 0000000000000000000000000000000000000000..6a5370cc8732db13a21cbcf5a22f407b3f0efa2b --- /dev/null +++ b/packages/frontend/src/scripts/chart-legend.ts @@ -0,0 +1,12 @@ +import { Plugin } from 'chart.js'; +import MkChartLegend from '@/components/MkChartLegend.vue'; + +export const chartLegend = (legend: InstanceType<typeof MkChartLegend>) => ({ + id: 'htmlLegend', + afterUpdate(chart, args, options) { + // Reuse the built-in legendItems generator + const items = chart.options.plugins.legend.labels.generateLabels(chart); + + legend.update(chart, items); + }, +}) as Plugin; diff --git a/packages/frontend/src/scripts/chart-vline.ts b/packages/frontend/src/scripts/chart-vline.ts index 10021583e054db09688a03fc7e80788df9efdeab..f3214438348581c0f1a95712db030fbe9c1c8bbb 100644 --- a/packages/frontend/src/scripts/chart-vline.ts +++ b/packages/frontend/src/scripts/chart-vline.ts @@ -1,3 +1,5 @@ +import { Plugin } from 'chart.js'; + export const chartVLine = (vLineColor: string) => ({ id: 'vLine', beforeDraw(chart, args, options) { @@ -18,4 +20,4 @@ export const chartVLine = (vLineColor: string) => ({ ctx.restore(); } }, -}); +}) as Plugin;