feat(stage-ui): added TimeSeriesChart component
This commit is contained in:
@@ -54,6 +54,7 @@
|
||||
"@pixiv/three-vrm-animation": "^3.4.1",
|
||||
"@pixiv/three-vrm-core": "^3.4.1",
|
||||
"@proj-airi/ccc": "workspace:^",
|
||||
"@proj-airi/chromatic": "^0.0.7",
|
||||
"@proj-airi/drizzle-duckdb-wasm": "catalog:",
|
||||
"@proj-airi/font-cjkfonts-allseto": "workspace:^",
|
||||
"@proj-airi/font-xiaolai": "workspace:^",
|
||||
|
||||
@@ -0,0 +1,459 @@
|
||||
<script setup lang="ts">
|
||||
import { onMounted, onUnmounted, ref } from 'vue'
|
||||
|
||||
import TimeSeriesChart from './TimeSeriesChart.vue'
|
||||
|
||||
// Data arrays for different scenarios
|
||||
const performanceHistory = ref<number[]>([])
|
||||
const temperatureHistory = ref<number[]>([])
|
||||
const activityHistory = ref<number[]>([])
|
||||
const networkHistory = ref<number[]>([])
|
||||
|
||||
// Current values
|
||||
const currentPerformance = ref(0.65)
|
||||
const currentTemperature = ref(0.4)
|
||||
const currentActivity = ref(0.2)
|
||||
const currentNetwork = ref(0.8)
|
||||
|
||||
// Animation frame
|
||||
let animationFrame: number | null = null
|
||||
let frameCount = 0
|
||||
|
||||
const maxHistoryLength = 60
|
||||
|
||||
function animate() {
|
||||
frameCount++
|
||||
|
||||
// Update performance data (oscillating with some noise)
|
||||
const performanceBase = 0.6 + Math.sin(frameCount * 0.1) * 0.2
|
||||
currentPerformance.value = Math.max(0, Math.min(1, performanceBase + (Math.random() - 0.5) * 0.1))
|
||||
performanceHistory.value.push(currentPerformance.value)
|
||||
if (performanceHistory.value.length > maxHistoryLength) {
|
||||
performanceHistory.value.shift()
|
||||
}
|
||||
|
||||
// Update temperature data (slow rise and fall)
|
||||
const tempBase = 0.4 + Math.sin(frameCount * 0.05) * 0.3
|
||||
currentTemperature.value = Math.max(0, Math.min(1, tempBase + (Math.random() - 0.5) * 0.05))
|
||||
temperatureHistory.value.push(currentTemperature.value)
|
||||
if (temperatureHistory.value.length > maxHistoryLength) {
|
||||
temperatureHistory.value.shift()
|
||||
}
|
||||
|
||||
// Update activity data (sudden spikes)
|
||||
let activityBase = 0.1
|
||||
if (frameCount % 30 === 0) { // Spike every 30 frames
|
||||
activityBase = 0.8 + Math.random() * 0.2
|
||||
}
|
||||
currentActivity.value = Math.max(0, Math.min(1, currentActivity.value * 0.9 + activityBase * 0.1,
|
||||
))
|
||||
activityHistory.value.push(currentActivity.value)
|
||||
if (activityHistory.value.length > maxHistoryLength) {
|
||||
activityHistory.value.shift()
|
||||
}
|
||||
|
||||
// Update network data (stepped changes)
|
||||
if (frameCount % 15 === 0) {
|
||||
currentNetwork.value = Math.random()
|
||||
}
|
||||
networkHistory.value.push(currentNetwork.value)
|
||||
if (networkHistory.value.length > maxHistoryLength) {
|
||||
networkHistory.value.shift()
|
||||
}
|
||||
|
||||
animationFrame = requestAnimationFrame(animate)
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
// Initialize with some data
|
||||
for (let i = 0; i < 30; i++) {
|
||||
performanceHistory.value.push(0.5 + Math.random() * 0.3)
|
||||
temperatureHistory.value.push(0.3 + Math.random() * 0.2)
|
||||
activityHistory.value.push(Math.random() * 0.4)
|
||||
networkHistory.value.push(Math.random())
|
||||
}
|
||||
|
||||
animate()
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
if (animationFrame) {
|
||||
cancelAnimationFrame(animationFrame)
|
||||
}
|
||||
})
|
||||
|
||||
// Static data for examples
|
||||
const staticData1 = [0.2, 0.3, 0.5, 0.7, 0.6, 0.8, 0.9, 0.7, 0.5, 0.4, 0.6, 0.8, 0.9, 0.8, 0.6, 0.4, 0.3, 0.5, 0.7, 0.9]
|
||||
const staticData2 = [0.1, 0.2, 0.1, 0.9, 0.8, 0.2, 0.1, 0.3, 0.2, 0.1, 0.8, 0.9, 0.2, 0.1, 0.4, 0.3, 0.2, 0.8, 0.9, 0.3]
|
||||
const staticData3 = [0.5, 0.52, 0.48, 0.51, 0.49, 0.53, 0.47, 0.52, 0.48, 0.51, 0.49, 0.53, 0.47, 0.52, 0.48, 0.51, 0.49, 0.53, 0.47, 0.52]
|
||||
|
||||
// Custom formatters
|
||||
const formatPercentage = (value: number) => `${(value * 100).toFixed(0)}%`
|
||||
const formatTemperature = (value: number) => `${(value * 100).toFixed(0)}°C`
|
||||
const formatDecimal = (value: number) => value.toFixed(3)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Story
|
||||
title="Time Series Chart"
|
||||
group="gadgets"
|
||||
:layout="{ type: 'grid', width: '100%' }"
|
||||
>
|
||||
<template #controls>
|
||||
<ThemeColorsHueControl />
|
||||
</template>
|
||||
|
||||
<Variant
|
||||
id="real-time-monitoring"
|
||||
title="Real-time Monitoring"
|
||||
>
|
||||
<div class="space-y-6">
|
||||
<div class="grid grid-cols-1 gap-6 md:grid-cols-2">
|
||||
<!-- Performance Monitoring -->
|
||||
<div class="rounded-xl px-3 py-2 shadow-md">
|
||||
<TimeSeriesChart
|
||||
:history="performanceHistory"
|
||||
:current-value="currentPerformance"
|
||||
:threshold="0.7"
|
||||
:is-active="currentPerformance > 0.7"
|
||||
title="System Performance"
|
||||
subtitle="Real-time metrics"
|
||||
active-label="High load"
|
||||
active-legend-label="Above threshold"
|
||||
inactive-legend-label="Normal operation"
|
||||
threshold-label="Performance limit"
|
||||
:format-value="formatPercentage"
|
||||
:height="100"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Temperature Monitoring -->
|
||||
<div class="rounded-xl px-3 py-2 shadow-md">
|
||||
<TimeSeriesChart
|
||||
:history="temperatureHistory"
|
||||
:current-value="currentTemperature"
|
||||
:threshold="0.8"
|
||||
:is-active="currentTemperature > 0.8"
|
||||
title="Temperature Monitor"
|
||||
subtitle="CPU temperature"
|
||||
active-label="Overheating"
|
||||
active-legend-label="Critical"
|
||||
inactive-legend-label="Normal"
|
||||
threshold-label="Critical temp"
|
||||
:format-value="formatTemperature"
|
||||
:colors-hue="10"
|
||||
:height="100"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Variant>
|
||||
|
||||
<Variant
|
||||
id="activity-detection"
|
||||
title="Activity Detection"
|
||||
>
|
||||
<div class="space-y-6">
|
||||
<div class="grid grid-cols-1 gap-6 md:grid-cols-2">
|
||||
<!-- Event Detection -->
|
||||
<div class="rounded-xl px-3 py-2 shadow-md">
|
||||
<TimeSeriesChart
|
||||
:history="activityHistory"
|
||||
:current-value="currentActivity"
|
||||
:threshold="0.5"
|
||||
:is-active="currentActivity > 0.5"
|
||||
title="Event Detection"
|
||||
subtitle="Real-time activity"
|
||||
active-label="Event detected"
|
||||
active-legend-label="Active events"
|
||||
inactive-legend-label="Quiet"
|
||||
threshold-label="Detection threshold"
|
||||
:height="80"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Network Quality -->
|
||||
<div class="rounded-xl px-3 py-2 shadow-md">
|
||||
<TimeSeriesChart
|
||||
:history="networkHistory"
|
||||
:current-value="currentNetwork"
|
||||
:threshold="0.6"
|
||||
:is-active="currentNetwork > 0.6"
|
||||
title="Network Quality"
|
||||
subtitle="Connection strength"
|
||||
active-label="Strong signal"
|
||||
active-legend-label="Good connection"
|
||||
inactive-legend-label="Weak signal"
|
||||
threshold-label="Quality threshold"
|
||||
:colors-hue="240"
|
||||
:height="80"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Variant>
|
||||
|
||||
<Variant
|
||||
id="static-examples"
|
||||
title="Static Data Examples"
|
||||
>
|
||||
<div class="space-y-6">
|
||||
<div class="grid grid-cols-1 gap-6 md:grid-cols-3">
|
||||
<!-- Success Rate -->
|
||||
<div class="rounded-xl px-3 py-2 shadow-md">
|
||||
<TimeSeriesChart
|
||||
:history="staticData1"
|
||||
:current-value="0.9"
|
||||
:threshold="0.8"
|
||||
:is-active="true"
|
||||
title="Success Rate"
|
||||
subtitle="Last 20 measurements"
|
||||
active-label="Target met"
|
||||
:format-value="formatPercentage"
|
||||
:height="70"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Error Spikes -->
|
||||
<div class="rounded-xl px-3 py-2 shadow-md">
|
||||
<TimeSeriesChart
|
||||
:history="staticData2"
|
||||
:current-value="0.3"
|
||||
:threshold="0.5"
|
||||
:is-active="false"
|
||||
title="Error Rate"
|
||||
subtitle="Sporadic failures"
|
||||
active-label="High errors"
|
||||
:colors-hue="15"
|
||||
:format-value="formatPercentage"
|
||||
:height="70"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Stable Metrics -->
|
||||
<div class="rounded-xl px-3 py-2 shadow-md">
|
||||
<TimeSeriesChart
|
||||
:history="staticData3"
|
||||
:current-value="0.52"
|
||||
:threshold="0.6"
|
||||
:is-active="false"
|
||||
title="Stable System"
|
||||
subtitle="Low variance"
|
||||
active-label="Threshold exceeded"
|
||||
:colors-hue="160"
|
||||
:format-value="formatDecimal"
|
||||
:height="70"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Variant>
|
||||
|
||||
<Variant
|
||||
id="configuration-options"
|
||||
title="Configuration Options"
|
||||
>
|
||||
<div class="space-y-6">
|
||||
<div class="grid grid-cols-1 gap-6 lg:grid-cols-3 md:grid-cols-2">
|
||||
<!-- Minimal Display -->
|
||||
<div class="rounded-xl px-3 py-2 shadow-md">
|
||||
<h3 class="mb-4 text-lg text-neutral-700 font-medium dark:text-neutral-300">
|
||||
Minimal Chart
|
||||
</h3>
|
||||
<TimeSeriesChart
|
||||
:history="staticData1"
|
||||
:current-value="0.7"
|
||||
:threshold="null"
|
||||
:is-active="false"
|
||||
:show-header="false"
|
||||
:show-threshold="false"
|
||||
:show-current-value="false"
|
||||
:show-active-indicator="false"
|
||||
:show-legend="false"
|
||||
:height="50"
|
||||
/>
|
||||
<p class="mt-2 text-sm text-neutral-500">
|
||||
Clean, minimal visualization
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- No Area Fill -->
|
||||
<div class="rounded-xl px-3 py-2 shadow-md">
|
||||
<h3 class="mb-4 text-lg text-neutral-700 font-medium dark:text-neutral-300">
|
||||
Line Only
|
||||
</h3>
|
||||
<TimeSeriesChart
|
||||
:history="staticData2"
|
||||
:current-value="0.3"
|
||||
:threshold="0.5"
|
||||
:is-active="false"
|
||||
:show-area="false"
|
||||
:show-threshold-areas="false"
|
||||
title="Line Chart"
|
||||
:height="60"
|
||||
/>
|
||||
<p class="mt-2 text-sm text-neutral-500">
|
||||
Line without fill areas
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Custom Styling -->
|
||||
<div class="rounded-xl px-3 py-2 shadow-md">
|
||||
<h3 class="mb-4 text-lg text-neutral-700 font-medium dark:text-neutral-300">
|
||||
Custom Style
|
||||
</h3>
|
||||
<TimeSeriesChart
|
||||
:history="staticData1"
|
||||
:current-value="0.8"
|
||||
:threshold="0.6"
|
||||
:is-active="true"
|
||||
title="Custom Colors"
|
||||
:colors-hue="280"
|
||||
:line-width="3"
|
||||
:active-indicator-size="6"
|
||||
:height="60"
|
||||
/>
|
||||
<p class="mt-2 text-sm text-neutral-500">
|
||||
Purple theme with thick line
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Variant>
|
||||
|
||||
<Variant
|
||||
id="dashboard-layout"
|
||||
title="Dashboard Layout"
|
||||
>
|
||||
<div class="space-y-6">
|
||||
<div class="rounded-xl px-3 py-2 shadow-md">
|
||||
<h3 class="mb-6 text-lg text-neutral-700 font-medium dark:text-neutral-300">
|
||||
System Monitoring Dashboard
|
||||
</h3>
|
||||
|
||||
<div class="grid grid-cols-1 gap-6 lg:grid-cols-2">
|
||||
<!-- Primary Metrics -->
|
||||
<div class="space-y-4">
|
||||
<h4 class="text-sm text-neutral-600 font-medium dark:text-neutral-400">
|
||||
Primary Metrics
|
||||
</h4>
|
||||
|
||||
<TimeSeriesChart
|
||||
:history="performanceHistory"
|
||||
:current-value="currentPerformance"
|
||||
:threshold="0.7"
|
||||
:is-active="currentPerformance > 0.7"
|
||||
title="CPU Usage"
|
||||
subtitle="Real-time utilization"
|
||||
active-label="High load"
|
||||
:show-legend="false"
|
||||
:format-value="formatPercentage"
|
||||
:height="60"
|
||||
/>
|
||||
|
||||
<TimeSeriesChart
|
||||
:history="temperatureHistory"
|
||||
:current-value="currentTemperature"
|
||||
:threshold="0.8"
|
||||
:is-active="currentTemperature > 0.8"
|
||||
title="Memory Usage"
|
||||
subtitle="Available memory"
|
||||
active-label="Low memory"
|
||||
:show-legend="false"
|
||||
:format-value="formatPercentage"
|
||||
:colors-hue="10"
|
||||
:height="60"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Secondary Metrics -->
|
||||
<div class="space-y-4">
|
||||
<h4 class="text-sm text-neutral-600 font-medium dark:text-neutral-400">
|
||||
Secondary Metrics
|
||||
</h4>
|
||||
|
||||
<TimeSeriesChart
|
||||
:history="activityHistory"
|
||||
:current-value="currentActivity"
|
||||
:threshold="0.5"
|
||||
:is-active="currentActivity > 0.5"
|
||||
title="Network I/O"
|
||||
subtitle="Data transfer rate"
|
||||
active-label="High traffic"
|
||||
:show-legend="false"
|
||||
:format-value="formatPercentage"
|
||||
:colors-hue="240"
|
||||
:height="60"
|
||||
/>
|
||||
|
||||
<TimeSeriesChart
|
||||
:history="networkHistory"
|
||||
:current-value="currentNetwork"
|
||||
:threshold="0.6"
|
||||
:is-active="currentNetwork > 0.6"
|
||||
title="Disk Usage"
|
||||
subtitle="Storage utilization"
|
||||
active-label="High usage"
|
||||
:show-legend="false"
|
||||
:format-value="formatPercentage"
|
||||
:colors-hue="280"
|
||||
:height="60"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Variant>
|
||||
|
||||
<Variant
|
||||
id="special-applications"
|
||||
title="Special Applications"
|
||||
>
|
||||
<div class="space-y-6">
|
||||
<div class="grid grid-cols-1 gap-6 md:grid-cols-2">
|
||||
<!-- Audio/Voice Activity -->
|
||||
<div class="rounded-xl px-3 py-2 shadow-md">
|
||||
<h3 class="mb-4 text-lg text-neutral-700 font-medium dark:text-neutral-300">
|
||||
Voice Activity Detection
|
||||
</h3>
|
||||
<TimeSeriesChart
|
||||
:history="activityHistory"
|
||||
:current-value="currentActivity"
|
||||
:threshold="0.5"
|
||||
:is-active="currentActivity > 0.5"
|
||||
title="Voice Activity"
|
||||
subtitle="Speech detection"
|
||||
active-label="Speaking"
|
||||
active-legend-label="Voice detected"
|
||||
inactive-legend-label="Silence"
|
||||
threshold-label="Speech threshold"
|
||||
:colors-hue="120"
|
||||
:height="90"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Anomaly Detection -->
|
||||
<div class="rounded-xl px-3 py-2 shadow-md">
|
||||
<h3 class="mb-4 text-lg text-neutral-700 font-medium dark:text-neutral-300">
|
||||
Anomaly Detection
|
||||
</h3>
|
||||
<TimeSeriesChart
|
||||
:history="staticData2"
|
||||
:current-value="0.3"
|
||||
:threshold="0.4"
|
||||
:is-active="false"
|
||||
title="Anomaly Score"
|
||||
subtitle="System health"
|
||||
active-label="Anomaly detected"
|
||||
active-legend-label="Unusual behavior"
|
||||
inactive-legend-label="Normal operation"
|
||||
threshold-label="Alert threshold"
|
||||
:colors-hue="30"
|
||||
:height="90"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Variant>
|
||||
</Story>
|
||||
</template>
|
||||
@@ -0,0 +1,345 @@
|
||||
<script setup lang="ts">
|
||||
import type { Oklch } from '@proj-airi/chromatic'
|
||||
|
||||
import { chromaticPaletteFrom } from '@proj-airi/chromatic'
|
||||
import { useElementBounding } from '@vueuse/core'
|
||||
import { computed, inject, ref, toRef, watch } from 'vue'
|
||||
|
||||
import { chromaticHue as hue } from '../../constants'
|
||||
import { chromaticHueDefault as hueDefault } from '../../constants/theme'
|
||||
|
||||
interface Props {
|
||||
history: number[] // Array of values (normalized 0-1)
|
||||
currentValue: number // Current value (0-1)
|
||||
threshold?: number | null // Threshold value (0-1)
|
||||
isActive: boolean // Whether current state is "active"
|
||||
title?: string // Chart title
|
||||
colorsHue?: number // Optional hue for colors (default from theme)
|
||||
lineColor?: string // Line color (default from theme)
|
||||
thresholdColor?: string // Threshold color (default from theme)
|
||||
activeColor?: string // Active state color (default from theme)
|
||||
inactiveColor?: string // Inactive state color (default from theme)
|
||||
subtitle?: string // Chart subtitle
|
||||
activeLabel?: string // Label when active
|
||||
activeLegendLabel?: string // Legend label for active state
|
||||
inactiveLegendLabel?: string // Legend label for inactive state
|
||||
thresholdLabel?: string // Label for threshold
|
||||
height?: number // Chart height in pixels
|
||||
lineWidth?: number // Line stroke width
|
||||
chartHeight?: number // Internal chart height for calculations
|
||||
minDataPoints?: number // Minimum points needed to show chart
|
||||
precision?: number // Value display precision
|
||||
unit?: string // Value unit
|
||||
showHeader?: boolean // Show title/subtitle
|
||||
showThreshold?: boolean // Show threshold zone
|
||||
showArea?: boolean // Show filled area under curve
|
||||
showThresholdAreas?: boolean // Show highlighted threshold areas
|
||||
showCurrentValue?: boolean // Show floating current value
|
||||
showActiveIndicator?: boolean // Show active state indicator
|
||||
showLegend?: boolean // Show legend
|
||||
formatValue?: (value: number) => string // Custom value formatter
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
threshold: null,
|
||||
title: 'Time Series',
|
||||
subtitle: 'Recent data',
|
||||
activeLabel: 'Active',
|
||||
activeLegendLabel: 'Active state',
|
||||
inactiveLegendLabel: 'Inactive state',
|
||||
thresholdLabel: 'Threshold',
|
||||
height: 80,
|
||||
lineWidth: 1.5,
|
||||
minDataPoints: 5,
|
||||
precision: 0,
|
||||
unit: '%',
|
||||
showHeader: true,
|
||||
showThreshold: true,
|
||||
showArea: true,
|
||||
showThresholdAreas: true,
|
||||
showCurrentValue: true,
|
||||
showActiveIndicator: true,
|
||||
showLegend: true,
|
||||
})
|
||||
|
||||
// Use the actual display height for all calculations
|
||||
const chartHeight = computed(() => props.height)
|
||||
|
||||
const timeSeriesChartRef = ref<HTMLDivElement>()
|
||||
|
||||
const chromaticHue = inject(hue, hueDefault)
|
||||
const chromaticHueOrDefault = toRef(() => props.colorsHue || chromaticHue || hueDefault)
|
||||
const chromaticShades = computed(() => chromaticPaletteFrom(chromaticHueOrDefault.value))
|
||||
|
||||
const timeSeriesChartContainerBounding = useElementBounding(timeSeriesChartRef, { windowResize: true })
|
||||
|
||||
watch([chromaticHueOrDefault, timeSeriesChartRef], () => {
|
||||
if (timeSeriesChartRef.value) {
|
||||
timeSeriesChartRef.value.style.setProperty('--chromatic-hue', chromaticHueOrDefault.value.toString())
|
||||
}
|
||||
}, { immediate: true })
|
||||
|
||||
const lineColorProps = toRef(() => props.lineColor)
|
||||
const lineColor = computed(() => {
|
||||
if (!lineColorProps.value) {
|
||||
return chromaticShades.value.shadeBy(500).toHex()
|
||||
}
|
||||
|
||||
return lineColorProps.value
|
||||
})
|
||||
|
||||
const thresholdColorProps = toRef(() => props.thresholdColor)
|
||||
const thresholdColor = computed(() => {
|
||||
if (!thresholdColorProps.value) {
|
||||
const color = chromaticShades.value.shadeBy(500).withAlpha(0.1).color as Oklch
|
||||
return `oklch(${color.l} ${color.c} ${color.h} / ${color.alpha})`
|
||||
}
|
||||
|
||||
return thresholdColorProps.value
|
||||
})
|
||||
|
||||
const activeColorProps = toRef(() => props.activeColor)
|
||||
const activeColor = computed(() => {
|
||||
if (!activeColorProps.value) {
|
||||
return chromaticShades.value.shadeBy(600).toHex()
|
||||
}
|
||||
|
||||
return activeColorProps.value
|
||||
})
|
||||
|
||||
const inactiveColorProps = toRef(() => props.inactiveColor)
|
||||
const inactiveColor = computed(() => {
|
||||
if (!inactiveColorProps.value) {
|
||||
return chromaticShades.value.shadeBy(400).toHex()
|
||||
}
|
||||
|
||||
return inactiveColorProps.value
|
||||
})
|
||||
|
||||
// Generate unique IDs for SVG patterns
|
||||
const componentId = Math.random().toString(36).substring(2, 9)
|
||||
const gridPatternId = `grid-${componentId}`
|
||||
const areaGradientId = `area-gradient-${componentId}`
|
||||
const thresholdGradientId = `threshold-gradient-${componentId}`
|
||||
|
||||
const normalizedThreshold = computed(() =>
|
||||
props.threshold !== null ? Math.max(0, Math.min(1, props.threshold)) : 0,
|
||||
)
|
||||
|
||||
// Calculate threshold line Y position
|
||||
const thresholdLineY = computed(() => {
|
||||
if (props.threshold === null)
|
||||
return 0
|
||||
return chartHeight.value - (normalizedThreshold.value * chartHeight.value)
|
||||
})
|
||||
|
||||
// Create smooth curve path
|
||||
const smoothPath = computed(() => {
|
||||
const history = props.history
|
||||
if (history.length < 2)
|
||||
return ''
|
||||
|
||||
const width = timeSeriesChartContainerBounding.width.value
|
||||
const height = chartHeight.value
|
||||
|
||||
let path = `M0,${height - (history[0] * height)}`
|
||||
|
||||
for (let i = 1; i < history.length; i++) {
|
||||
const x = (i / (history.length - 1)) * width
|
||||
const y = height - (history[i] * height)
|
||||
|
||||
if (i === 1) {
|
||||
path += ` Q${x / 2},${height - (history[0] * height)} ${x},${y}`
|
||||
}
|
||||
else {
|
||||
const prevX = ((i - 1) / (history.length - 1)) * width
|
||||
const cpX = (prevX + x) / 2
|
||||
const prevY = height - (history[i - 1] * height)
|
||||
path += ` Q${cpX},${prevY} ${x},${y}`
|
||||
}
|
||||
}
|
||||
|
||||
return path
|
||||
})
|
||||
|
||||
// Create filled area path
|
||||
const dataAreaPath = computed(() => {
|
||||
const history = props.history
|
||||
if (history.length < 2)
|
||||
return ''
|
||||
|
||||
const width = timeSeriesChartContainerBounding.width.value
|
||||
const height = chartHeight.value
|
||||
|
||||
let path = `M0,${height} L0,${height - (history[0] * height)}`
|
||||
|
||||
for (let i = 1; i < history.length; i++) {
|
||||
const x = (i / (history.length - 1)) * width
|
||||
const y = height - (history[i] * height)
|
||||
|
||||
if (i === 1) {
|
||||
path += ` Q${x / 2},${height - (history[0] * height)} ${x},${y}`
|
||||
}
|
||||
else {
|
||||
const prevX = ((i - 1) / (history.length - 1)) * width
|
||||
const cpX = (prevX + x) / 2
|
||||
const prevY = height - (history[i - 1] * height)
|
||||
path += ` Q${cpX},${prevY} ${x},${y}`
|
||||
}
|
||||
}
|
||||
|
||||
path += ` L${width},${height} Z`
|
||||
return path
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="history.length > minDataPoints" ref="timeSeriesChartRef" class="time-series-chart space-y-3">
|
||||
<div v-if="showHeader" class="flex items-center justify-between">
|
||||
<div class="text-sm font-medium">
|
||||
{{ title }}
|
||||
</div>
|
||||
<div class="text-xs text-neutral-500">
|
||||
{{ subtitle }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Chart Visualization -->
|
||||
<div
|
||||
class="relative overflow-hidden border border-neutral-200 rounded-lg from-neutral-50 to-neutral-100 bg-gradient-to-b dark:border-neutral-800 dark:from-neutral-800 dark:to-neutral-900"
|
||||
:style="{ height: `${chartHeight}px` }"
|
||||
>
|
||||
<svg class="h-full w-full">
|
||||
<!-- Background grid (subtle) -->
|
||||
<defs>
|
||||
<pattern :id="gridPatternId" width="20" height="10" patternUnits="userSpaceOnUse">
|
||||
<path d="M 20 0 L 0 0 0 10" fill="none" stroke="rgb(156 163 175 / 0.1)" stroke-width="0.5" />
|
||||
</pattern>
|
||||
|
||||
<!-- Gradient for the filled area -->
|
||||
<linearGradient :id="areaGradientId" x1="0%" y1="0%" x2="0%" y2="100%">
|
||||
<stop offset="0%" :style="`stop-color:${lineColor};stop-opacity:0.3`" />
|
||||
<stop offset="50%" :style="`stop-color:${lineColor};stop-opacity:0.15`" />
|
||||
<stop offset="100%" :style="`stop-color:${lineColor};stop-opacity:0.05`" />
|
||||
</linearGradient>
|
||||
|
||||
<!-- Gradient for threshold areas -->
|
||||
<linearGradient :id="thresholdGradientId" x1="0%" y1="0%" x2="0%" y2="100%">
|
||||
<stop offset="0%" :style="`stop-color:${activeColor};stop-opacity:0.3`" />
|
||||
<stop offset="50%" :style="`stop-color:${activeColor};stop-opacity:0.15`" />
|
||||
<stop offset="100%" :style="`stop-color:${activeColor};stop-opacity:0.05`" />
|
||||
</linearGradient>
|
||||
|
||||
<!-- Below threshold gradient -->
|
||||
<linearGradient id="below-threshold-gradient" x1="0%" y1="0%" x2="0%" y2="100%">
|
||||
<stop offset="0%" :style="`stop-color:${thresholdColor};stop-opacity:0.2`" />
|
||||
<stop offset="50%" :style="`stop-color:${thresholdColor};stop-opacity:0.1`" />
|
||||
<stop offset="100%" :style="`stop-color:${thresholdColor};stop-opacity:0.05`" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
|
||||
<!-- Background grid -->
|
||||
<rect width="100%" height="100%" :fill="`url(#${gridPatternId})`" />
|
||||
|
||||
<!-- Below threshold area with gradient -->
|
||||
<rect
|
||||
v-if="showThreshold && threshold !== null"
|
||||
x="0"
|
||||
:y="thresholdLineY"
|
||||
width="100%"
|
||||
:height="chartHeight - thresholdLineY"
|
||||
:fill="thresholdColor"
|
||||
class="transition-all duration-300"
|
||||
/>
|
||||
|
||||
<!-- Threshold line -->
|
||||
<line
|
||||
v-if="showThreshold && threshold !== null"
|
||||
x1="0"
|
||||
:y1="thresholdLineY"
|
||||
x2="100%"
|
||||
:y2="thresholdLineY"
|
||||
:stroke="thresholdColor"
|
||||
stroke-width="1.5"
|
||||
stroke-dasharray="4,4"
|
||||
:fill="thresholdColor"
|
||||
class="transition-all duration-300"
|
||||
/>
|
||||
|
||||
<!-- Data area (filled under curve) -->
|
||||
<path
|
||||
v-if="dataAreaPath && showArea"
|
||||
:d="dataAreaPath"
|
||||
:fill="`url(#${areaGradientId})`"
|
||||
class="transition-all duration-75"
|
||||
/>
|
||||
|
||||
<!-- Main data curve -->
|
||||
<path
|
||||
v-if="smoothPath"
|
||||
:d="smoothPath"
|
||||
fill="none"
|
||||
:stroke="lineColor"
|
||||
:stroke-width="lineWidth"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
class="drop-shadow-sm transition-all duration-75"
|
||||
/>
|
||||
</svg>
|
||||
|
||||
<!-- Floating current value -->
|
||||
<div
|
||||
v-if="showCurrentValue"
|
||||
class="absolute right-2 top-2 border border-neutral-200 rounded-md bg-white px-2 py-1 shadow-sm transition-all duration-200 dark:border-neutral-700 dark:bg-neutral-800"
|
||||
:class="isActive ? `bg-primary-50 dark:bg-primary-900 border-primary-200 dark:border-primary-800` : ''"
|
||||
>
|
||||
<div class="text-xs font-medium" :class="isActive ? 'text-primary-700 dark:text-primary-300' : 'text-neutral-600 dark:text-neutral-400'">
|
||||
{{ formatValue ? formatValue(currentValue) : `${(currentValue * 100).toFixed(precision)}${unit}` }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Active state indicator -->
|
||||
<Transition name="fade">
|
||||
<div
|
||||
v-if="isActive && showActiveIndicator"
|
||||
class="absolute left-2 top-2 flex items-center gap-1.5 border border-primary-200 rounded-md bg-primary-50 px-2 py-1 dark:border-primary-800 dark:bg-primary-900"
|
||||
>
|
||||
<div class="h-1.5 w-1.5 animate-pulse rounded-full bg-primary-500" />
|
||||
<span class="text-xs text-primary-700 font-medium dark:text-primary-300">{{ activeLabel }}</span>
|
||||
</div>
|
||||
</Transition>
|
||||
</div>
|
||||
|
||||
<!-- Legend -->
|
||||
<div v-if="showLegend" class="flex flex-wrap items-center justify-between text-xs text-neutral-500">
|
||||
<div class="flex items-center gap-3">
|
||||
<span class="flex items-center gap-1 text-nowrap">
|
||||
<div class="h-2 w-2 rounded-full" :style="{ backgroundColor: activeColor }" />
|
||||
{{ activeLegendLabel }}
|
||||
</span>
|
||||
<span class="flex items-center gap-1 text-nowrap">
|
||||
<div class="h-2 w-2 rounded-full" :style="{ backgroundColor: inactiveColor }" />
|
||||
{{ inactiveLegendLabel }}
|
||||
</span>
|
||||
</div>
|
||||
<span v-if="threshold !== null" class="text-nowrap">{{ thresholdLabel }}: {{ (threshold * 100).toFixed(0) }}%</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.fade-enter-active,
|
||||
.fade-leave-active {
|
||||
transition: opacity 0.2s ease;
|
||||
}
|
||||
|
||||
.fade-enter-from,
|
||||
.fade-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.fade-enter-to,
|
||||
.fade-leave-from {
|
||||
opacity: 1;
|
||||
}
|
||||
</style>
|
||||
@@ -3,3 +3,4 @@ export { default as AudioSpectrumVisualizer } from './AudioSpectrumVisualizer.vu
|
||||
export { default as LevelMeter } from './LevelMeter.vue'
|
||||
export { default as TestDummyMarker } from './TestDummyMarker.vue'
|
||||
export { default as ThresholdMeter } from './ThresholdMeter.vue'
|
||||
export { default as TimeSeriesChart } from './TimeSeriesChart.vue'
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
export const llmInferenceEndToken = '<|llm_inference_end|>'
|
||||
|
||||
export * from './emotions'
|
||||
export * from './inject'
|
||||
export * from './prompts/system-v2'
|
||||
export * from './theme'
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
import type { InjectionKey } from 'vue'
|
||||
|
||||
export const chromaticHue: InjectionKey<number> = Symbol('@proj-airi/chromatic-hue')
|
||||
@@ -0,0 +1 @@
|
||||
export const chromaticHueDefault = 220.44
|
||||
Generated
+16
-1
@@ -990,6 +990,9 @@ importers:
|
||||
'@proj-airi/ccc':
|
||||
specifier: workspace:^
|
||||
version: link:../ccc
|
||||
'@proj-airi/chromatic':
|
||||
specifier: ^0.0.7
|
||||
version: 0.0.7
|
||||
'@proj-airi/drizzle-duckdb-wasm':
|
||||
specifier: 'catalog:'
|
||||
version: 0.4.28(drizzle-orm@0.44.2(@opentelemetry/api@1.9.0)(@types/pg@8.15.4)(gel@2.0.0)(pg@8.16.3)(postgres@3.4.7))(web-worker@1.5.0)
|
||||
@@ -4234,6 +4237,9 @@ packages:
|
||||
'@polka/url@1.0.0-next.24':
|
||||
resolution: {integrity: sha512-2LuNTFBIO0m7kKIQvvPHN6UE63VjpmL9rnEEaOOaiSPbZK+zUOYIzBAWcED+3XYzhYsd/0mD57VdxAEqqV52CQ==}
|
||||
|
||||
'@proj-airi/chromatic@0.0.7':
|
||||
resolution: {integrity: sha512-4uxJhnbdJ6vRWeivuVy4z20cTwHbH+armAHqZNJozaspCtwCyN1AMkvJmj6+4BuuQO1iwQ6KvSHrNcNUGkOtZg==}
|
||||
|
||||
'@proj-airi/drizzle-duckdb-wasm@0.4.28':
|
||||
resolution: {integrity: sha512-1Y+7YdFbVgY9vUUAzm+a5QfSLnJkyhqxTENqZQbFmfCRn3rQO4KPLvu/l6HBg7GtLtlIYVIIOG37QiRexuofGw==}
|
||||
peerDependencies:
|
||||
@@ -5769,6 +5775,9 @@ packages:
|
||||
'@xsai/shared@0.3.0-beta.5':
|
||||
resolution: {integrity: sha512-0vDW0fkEcFhfd3uG2OEMkoqq/ghh62KJUokMUwaAPKLNcVNAVcFRoslH9lKjdHJziSZ/b59f5rpUEjU3DHooZQ==}
|
||||
|
||||
'@xsai/shared@0.3.0-beta.6':
|
||||
resolution: {integrity: sha512-EeqkD4+ijKKbxr9Jy9kRnJOcPBErWL8TK2wOdxwhVQuh7VbrYQwaheGjOz9g5OWwM6A0vCje/+TyHz+tURhhqw==}
|
||||
|
||||
'@xsai/stream-text@0.3.0-beta.5':
|
||||
resolution: {integrity: sha512-o0SxwzCA2F1zKcONZJX7qrk+PnKhzldV0jRqQ+NdBzaFLmg/clfqeENlbuwmfIrV5ck0BfPEMTo4gNBfxhsGSQ==}
|
||||
|
||||
@@ -14880,6 +14889,10 @@ snapshots:
|
||||
|
||||
'@polka/url@1.0.0-next.24': {}
|
||||
|
||||
'@proj-airi/chromatic@0.0.7':
|
||||
dependencies:
|
||||
culori: 4.0.2
|
||||
|
||||
'@proj-airi/drizzle-duckdb-wasm@0.4.28(drizzle-orm@0.44.2(@opentelemetry/api@1.9.0)(@types/pg@8.15.4)(gel@2.0.0)(pg@8.16.3)(postgres@3.4.7))(web-worker@1.5.0)':
|
||||
dependencies:
|
||||
'@date-fns/tz': 1.2.0
|
||||
@@ -16826,7 +16839,7 @@ snapshots:
|
||||
|
||||
'@xsai-ext/shared-providers@0.2.2':
|
||||
dependencies:
|
||||
'@xsai/shared': 0.3.0-beta.5
|
||||
'@xsai/shared': 0.3.0-beta.6
|
||||
|
||||
'@xsai-ext/shared-providers@0.3.0-beta.5':
|
||||
dependencies:
|
||||
@@ -16880,6 +16893,8 @@ snapshots:
|
||||
|
||||
'@xsai/shared@0.3.0-beta.5': {}
|
||||
|
||||
'@xsai/shared@0.3.0-beta.6': {}
|
||||
|
||||
'@xsai/stream-text@0.3.0-beta.5':
|
||||
dependencies:
|
||||
'@xsai/shared-chat': 0.3.0-beta.5
|
||||
|
||||
Reference in New Issue
Block a user