mirror of
https://github.com/amir20/dozzle.git
synced 2026-06-23 04:10:12 +00:00
ab6c93a679
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
138 lines
3.6 KiB
Vue
138 lines
3.6 KiB
Vue
<template>
|
|
<div ref="chartContainer" class="flex items-end gap-[2px]" @mousemove="onContainerHover">
|
|
<div
|
|
v-for="(bar, i) in downsampledBars"
|
|
:key="i"
|
|
class="bar min-h-px flex-1 rounded-t-sm"
|
|
:class="barClass"
|
|
:style="{ '--height': `${maxValue > 0 ? (bar.percent / maxValue) * 100 : 0}%` }"
|
|
></div>
|
|
</div>
|
|
</template>
|
|
|
|
<style scoped>
|
|
.bar {
|
|
height: var(--height);
|
|
will-change: height;
|
|
contain: layout;
|
|
}
|
|
</style>
|
|
|
|
<script setup lang="ts">
|
|
export interface BarDataPoint {
|
|
percent: number;
|
|
value: number;
|
|
}
|
|
|
|
const { chartData, barClass = "" } = defineProps<{
|
|
chartData: BarDataPoint[];
|
|
barClass?: string;
|
|
}>();
|
|
|
|
const hoverValue = defineEmit<[value: number]>();
|
|
|
|
const chartContainer = ref<HTMLElement | null>(null);
|
|
const { width } = useElementSize(chartContainer);
|
|
|
|
const BAR_WIDTH = 3;
|
|
const GAP = 2;
|
|
|
|
const availableBars = computed(() => Math.floor(width.value / (BAR_WIDTH + GAP)));
|
|
const bucketSize = computed(() => Math.ceil(chartData.length / availableBars.value));
|
|
|
|
const downsampledBars = ref<BarDataPoint[]>([]);
|
|
const maxValue = computed(() => {
|
|
const dataMax = Math.max(0, ...downsampledBars.value.map((b) => b.percent));
|
|
return Math.max(dataMax * 1.25, 1);
|
|
});
|
|
// Full recalculate when width/bucket size changes
|
|
watch([availableBars, bucketSize], () => {
|
|
recalculate();
|
|
changeCounter.value = 0;
|
|
});
|
|
|
|
// On data changes, only update the last bar unless a new bucket boundary is crossed.
|
|
// A wholesale replacement of the series (e.g. switching containers) is not detected
|
|
// here; the parent owns that and must call the exposed recalculate() on switch.
|
|
const changeCounter = ref(0);
|
|
let initialized = false;
|
|
watch(
|
|
() => chartData.at(-1),
|
|
() => {
|
|
if (!initialized) {
|
|
initialized = true;
|
|
recalculate();
|
|
return;
|
|
}
|
|
changeCounter.value++;
|
|
if (changeCounter.value >= bucketSize.value) {
|
|
recalculate();
|
|
changeCounter.value = 0;
|
|
} else {
|
|
updateLastBar();
|
|
}
|
|
},
|
|
);
|
|
|
|
defineExpose({ recalculate });
|
|
|
|
function averageBucket(bucket: BarDataPoint[]): BarDataPoint {
|
|
const percent = bucket.reduce((sum, d) => sum + d.percent, 0) / bucket.length;
|
|
const value = bucket.reduce((sum, d) => sum + d.value, 0) / bucket.length;
|
|
return { percent, value };
|
|
}
|
|
|
|
function recalculate() {
|
|
if (availableBars.value === 0) return;
|
|
|
|
if (chartData.length <= availableBars.value) {
|
|
downsampledBars.value = [...chartData];
|
|
return;
|
|
}
|
|
|
|
const size = bucketSize.value;
|
|
const result: BarDataPoint[] = [];
|
|
const numBuckets = Math.ceil(chartData.length / size);
|
|
|
|
for (let i = 0; i < numBuckets; i++) {
|
|
const start = i * size;
|
|
const end = Math.min(start + size, chartData.length);
|
|
result.push(averageBucket(chartData.slice(start, end)));
|
|
}
|
|
|
|
downsampledBars.value = result.slice(-availableBars.value);
|
|
}
|
|
|
|
function updateLastBar() {
|
|
if (downsampledBars.value.length === 0) return;
|
|
|
|
const size = bucketSize.value;
|
|
const lastBucketStart = (Math.ceil(chartData.length / size) - 1) * size;
|
|
const bucket = chartData.slice(lastBucketStart);
|
|
|
|
downsampledBars.value[downsampledBars.value.length - 1] = averageBucket(bucket);
|
|
}
|
|
|
|
function onContainerHover(event: MouseEvent) {
|
|
if (!chartContainer.value) return;
|
|
|
|
const bars = chartContainer.value.children;
|
|
if (bars.length === 0) return;
|
|
|
|
const mouseX = event.clientX;
|
|
let index = 0;
|
|
|
|
// Find the bar whose column contains the mouse x position
|
|
for (let i = 0; i < bars.length; i++) {
|
|
const rect = bars[i].getBoundingClientRect();
|
|
if (mouseX >= rect.left) {
|
|
index = i;
|
|
} else {
|
|
break;
|
|
}
|
|
}
|
|
|
|
hoverValue(downsampledBars.value[index].value);
|
|
}
|
|
</script>
|