|
@@ -3,7 +3,7 @@
|
|
|
</template>
|
|
</template>
|
|
|
|
|
|
|
|
<script setup lang="ts">
|
|
<script setup lang="ts">
|
|
|
-import { Chart, ChartEvent } from '@antv/g2'
|
|
|
|
|
|
|
+import { Chart } from '@antv/g2'
|
|
|
import { ref, onMounted, onBeforeUnmount, watch } from 'vue'
|
|
import { ref, onMounted, onBeforeUnmount, watch } from 'vue'
|
|
|
import { usePersonalization } from '@/composable/usePersonalization'
|
|
import { usePersonalization } from '@/composable/usePersonalization'
|
|
|
|
|
|
|
@@ -22,55 +22,101 @@ export interface PowerChartData {
|
|
|
Power?: number
|
|
Power?: number
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+export interface SocChartData {
|
|
|
|
|
+ Time: string
|
|
|
|
|
+ Soc?: number
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
let isVoidData = true
|
|
let isVoidData = true
|
|
|
-const voidData: PowerChartData[] = (() => {
|
|
|
|
|
|
|
+// 缓存最近一次数据,供主题切换时复用
|
|
|
|
|
+let lastPowerData: PowerChartData[] | undefined = undefined
|
|
|
|
|
+let lastSocData: SocChartData[] | undefined = undefined
|
|
|
|
|
+
|
|
|
|
|
+// 生成一天的时间刻度(每 5 分钟)
|
|
|
|
|
+const buildTimes = (): string[] => {
|
|
|
const times: string[] = []
|
|
const times: string[] = []
|
|
|
const totalMinutes = 24 * 60
|
|
const totalMinutes = 24 * 60
|
|
|
-
|
|
|
|
|
for (let m = 0; m < totalMinutes; m += 5) {
|
|
for (let m = 0; m < totalMinutes; m += 5) {
|
|
|
const hh = String(Math.floor(m / 60)).padStart(2, '0')
|
|
const hh = String(Math.floor(m / 60)).padStart(2, '0')
|
|
|
const mm = String(m % 60).padStart(2, '0')
|
|
const mm = String(m % 60).padStart(2, '0')
|
|
|
times.push(`${hh}:${mm}`)
|
|
times.push(`${hh}:${mm}`)
|
|
|
}
|
|
}
|
|
|
|
|
+ return times
|
|
|
|
|
+}
|
|
|
|
|
|
|
|
- return times.map((t) => {
|
|
|
|
|
- return [
|
|
|
|
|
- { powerType: 'aPower', Time: t },
|
|
|
|
|
- { powerType: 'Generator', Time: t },
|
|
|
|
|
- { powerType: 'HomeLoad', Time: t },
|
|
|
|
|
- { powerType: 'PV', Time: t },
|
|
|
|
|
- { powerType: 'Grid', Time: t },
|
|
|
|
|
- ]
|
|
|
|
|
- }).flat()
|
|
|
|
|
-})()
|
|
|
|
|
|
|
+// 空功率数据
|
|
|
|
|
+const voidPowerData: PowerChartData[] = buildTimes()
|
|
|
|
|
+ .map((t) => [
|
|
|
|
|
+ { powerType: 'aPower', Time: t },
|
|
|
|
|
+ { powerType: 'Generator', Time: t },
|
|
|
|
|
+ { powerType: 'HomeLoad', Time: t },
|
|
|
|
|
+ { powerType: 'PV', Time: t },
|
|
|
|
|
+ { powerType: 'Grid', Time: t },
|
|
|
|
|
+ ])
|
|
|
|
|
+ .flat()
|
|
|
|
|
+
|
|
|
|
|
+// 空 SOC 数据
|
|
|
|
|
+const voidSocData: SocChartData[] = buildTimes().map((t) => ({ Time: t }))
|
|
|
|
|
+
|
|
|
|
|
+// 颜色映射(功率类型)
|
|
|
|
|
+const powerColorRelations: [string, string][] = [
|
|
|
|
|
+ ['aPower', '#41D187'],
|
|
|
|
|
+ ['Generator', '#5398FF'],
|
|
|
|
|
+ ['HomeLoad', '#0BF5FF'],
|
|
|
|
|
+ ['PV', '#F1A502'],
|
|
|
|
|
+ ['Grid', '#BB56FD'],
|
|
|
|
|
+]
|
|
|
|
|
+
|
|
|
|
|
+// 构建图表 options(上:功率堆叠面积;下:SOC 折线;共享 X 轴 Time)
|
|
|
|
|
+const buildOptions = (powerData: PowerChartData[], socData: SocChartData[]) => {
|
|
|
|
|
+ return {
|
|
|
|
|
+ type: 'spaceFlex' as const,
|
|
|
|
|
+ direction: 'col' as const,
|
|
|
|
|
+ ratio: [2, 1],
|
|
|
|
|
+ theme: { type: isDark.value ? 'classicDark' : 'classic' },
|
|
|
|
|
+ children: [
|
|
|
|
|
+ {
|
|
|
|
|
+ // 上图:功率堆叠面积
|
|
|
|
|
+ type: 'area' as const,
|
|
|
|
|
+ data: powerData,
|
|
|
|
|
+ // 与下图左内边距对齐,保证 X 轴严格对齐
|
|
|
|
|
+ paddingLeft: 60,
|
|
|
|
|
+ paddingBottom: 0,
|
|
|
|
|
+ // 压缩与下图的间隔,仅留一点缝隙
|
|
|
|
|
+ marginBottom: 6,
|
|
|
|
|
+ transform: [{ type: 'stackY' as const }],
|
|
|
|
|
+ encode: { x: 'Time', y: 'Power', color: 'powerType' },
|
|
|
|
|
+ scale: {
|
|
|
|
|
+ color: { palette: 'accent', relations: powerColorRelations },
|
|
|
|
|
+ },
|
|
|
|
|
+ axis: { x: false },
|
|
|
|
|
+ interaction: { elementHighlight: { background: true } },
|
|
|
|
|
+ },
|
|
|
|
|
+ {
|
|
|
|
|
+ // 下图:SOC 折线
|
|
|
|
|
+ type: 'line' as const,
|
|
|
|
|
+ data: socData,
|
|
|
|
|
+ paddingLeft: 60,
|
|
|
|
|
+ paddingTop: 0,
|
|
|
|
|
+ // 压缩与上图的间隔,仅留一点缝隙
|
|
|
|
|
+ marginTop: 6,
|
|
|
|
|
+ encode: { x: 'Time', y: 'Soc' },
|
|
|
|
|
+ scale: { y: { domain: [0, 100], nice: false } },
|
|
|
|
|
+ style: { stroke: '#F5A623', lineWidth: 2 },
|
|
|
|
|
+ axis: { y: { title: 'SOC (%)' } },
|
|
|
|
|
+ },
|
|
|
|
|
+ ],
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
|
|
|
onMounted(() => {
|
|
onMounted(() => {
|
|
|
chart = new Chart({
|
|
chart = new Chart({
|
|
|
container: chartContainer.value!,
|
|
container: chartContainer.value!,
|
|
|
autoFit: true,
|
|
autoFit: true,
|
|
|
- height: props.height || 300,
|
|
|
|
|
|
|
+ height: props.height || 380,
|
|
|
})
|
|
})
|
|
|
|
|
|
|
|
- chart
|
|
|
|
|
- .area()
|
|
|
|
|
- .theme({ type: isDark ? 'dark' : 'light' })
|
|
|
|
|
- .data(voidData)
|
|
|
|
|
- .encode('x', 'Time')
|
|
|
|
|
- .encode('y', 'Power')
|
|
|
|
|
- .encode('color', 'powerType')
|
|
|
|
|
- .transform({ type: 'stackY' })
|
|
|
|
|
- .interaction('elementHighlight', { background: true })
|
|
|
|
|
- .scale('color', {
|
|
|
|
|
- palette: 'accent',
|
|
|
|
|
- relations: [
|
|
|
|
|
- ['aPower', '#41D187'],
|
|
|
|
|
- ['Generator', '#5398FF'],
|
|
|
|
|
- ['HomeLoad', '#0BF5FF'],
|
|
|
|
|
- ['PV', '#F1A502'],
|
|
|
|
|
- ['Grid', '#BB56FD'],
|
|
|
|
|
- ],
|
|
|
|
|
- })
|
|
|
|
|
-
|
|
|
|
|
|
|
+ chart.options(buildOptions(voidPowerData, voidSocData))
|
|
|
chart.render()
|
|
chart.render()
|
|
|
})
|
|
})
|
|
|
|
|
|
|
@@ -79,27 +125,42 @@ onBeforeUnmount(() => {
|
|
|
})
|
|
})
|
|
|
|
|
|
|
|
// 重绘数据
|
|
// 重绘数据
|
|
|
-const redraw = (data: PowerChartData[] | undefined) => {
|
|
|
|
|
|
|
+const redraw = (
|
|
|
|
|
+ powerData: PowerChartData[] | undefined,
|
|
|
|
|
+ socData: SocChartData[] | undefined,
|
|
|
|
|
+) => {
|
|
|
if (chart === null) return
|
|
if (chart === null) return
|
|
|
|
|
|
|
|
- if (data === undefined) {
|
|
|
|
|
|
|
+ if (powerData === undefined) {
|
|
|
if (!isVoidData) {
|
|
if (!isVoidData) {
|
|
|
- chart.changeData(voidData)
|
|
|
|
|
|
|
+ isVoidData = true
|
|
|
|
|
+ chart.options(buildOptions(voidPowerData, voidSocData))
|
|
|
|
|
+ chart.render()
|
|
|
}
|
|
}
|
|
|
} else {
|
|
} else {
|
|
|
isVoidData = false
|
|
isVoidData = false
|
|
|
- chart.changeData(data)
|
|
|
|
|
|
|
+ chart.options(buildOptions(powerData, socData ?? voidSocData))
|
|
|
|
|
+ chart.render()
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-watch(isDark, (newVal) => {
|
|
|
|
|
|
|
+watch(isDark, () => {
|
|
|
if (chart) {
|
|
if (chart) {
|
|
|
- chart.theme({ type: newVal ? 'dark' : 'light' })
|
|
|
|
|
- chart.render()
|
|
|
|
|
|
|
+ // 主题变化时重建 options 以应用新主题
|
|
|
|
|
+ redraw(lastPowerData, lastSocData)
|
|
|
}
|
|
}
|
|
|
})
|
|
})
|
|
|
|
|
|
|
|
|
|
+const redrawWithCache = (
|
|
|
|
|
+ powerData: PowerChartData[] | undefined,
|
|
|
|
|
+ socData: SocChartData[] | undefined,
|
|
|
|
|
+) => {
|
|
|
|
|
+ lastPowerData = powerData
|
|
|
|
|
+ lastSocData = socData
|
|
|
|
|
+ redraw(powerData, socData)
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
defineExpose({
|
|
defineExpose({
|
|
|
- redraw,
|
|
|
|
|
|
|
+ redraw: redrawWithCache,
|
|
|
})
|
|
})
|
|
|
</script>
|
|
</script>
|