图表(Chart):运行时、Chart.js 绘制、设计器的"系列和图例"
- System.Windows.Forms.DataVisualization.Charting:ChartAreas、Legends、Titles、Series (ChartType、Points.AddXY、图例文字等),DataSource + DataBind(DataTable / DataView / DataSet, 按 XValueMember、YValueMembers 取列);数据变化时把各系列的点发给浏览器 - 浏览器用 Chart.js 画:SeriesChartType 对应折线、柱形、饼图等,WinForms 默认的 BrightPastel 配色,不要动画 - 设计器工具箱加"图表",和 VS 一样带 ChartArea1、Legend1、Series1;属性窗口"系列和图例"对话框 编辑系列(名称、图表类型、图例、图例文字、X / Y 值字段)和图例,按 VS 的格式和顺序写回 Designer.cs, 对话框不管的属性原样保留,教材项目 8 的图表读入再写出逐字节不变;删除图表时一起删掉局部变量; 设计时用示例数据预览 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
d60868d912
commit
2cd0111967
13 files changed
+1227
-4
No files matched your search
@@ -0,0 +1,113 @@
|
||||
<script setup lang="ts">
|
||||
// 图表(WinForms 的 Chart,教材项目 8 的 7 天温度折线图):运行器发来各个系列的点,用 Chart.js 画。
|
||||
// 图表类型按 WinForms 的 SeriesChartType 对应;配色是 WinForms 默认的 BrightPastel;图例默认在右边;不要动画。
|
||||
import {
|
||||
ArcElement, BarController, BarElement, CategoryScale, Chart, DoughnutController, Filler, Legend, LinearScale, LineController,
|
||||
LineElement, PieController, PointElement, RadarController, RadialLinearScale, Title, Tooltip, type ChartConfiguration,
|
||||
} from 'chart.js'
|
||||
import { computed, onBeforeUnmount, onMounted, ref, watch, type CSSProperties } from 'vue'
|
||||
import type { ControlNode } from '@iot/shared'
|
||||
|
||||
Chart.register(ArcElement, BarController, BarElement, CategoryScale, DoughnutController, Filler, Legend, LinearScale, LineController,
|
||||
LineElement, PieController, PointElement, RadarController, RadialLinearScale, Title, Tooltip)
|
||||
|
||||
interface ChartSeries {
|
||||
name: string
|
||||
type: string
|
||||
legend: string
|
||||
inLegend: boolean
|
||||
color: string | null
|
||||
width: number
|
||||
labels: boolean
|
||||
points: { x: string | number; y: number | null }[]
|
||||
}
|
||||
|
||||
interface ChartData {
|
||||
series: ChartSeries[]
|
||||
titles: string[]
|
||||
legendDocking: string
|
||||
yMin: number | null
|
||||
yMax: number | null
|
||||
}
|
||||
|
||||
const props = defineProps<{ node: ControlNode; style: CSSProperties }>()
|
||||
|
||||
/** WinForms 图表默认的 BrightPastel 配色 */
|
||||
const PALETTE = ['#418CF0', '#FCB441', '#E0400A', '#056492', '#BFBFBF', '#1A3B69', '#FFE382', '#129CDD', '#CA6B4B', '#005CDB', '#F3D288', '#506381', '#F1B9A8', '#E0830A', '#7893BE']
|
||||
|
||||
const data = computed(() => props.node.props.Chart as ChartData | undefined)
|
||||
const canvas = ref<HTMLCanvasElement>()
|
||||
let chart: Chart | undefined
|
||||
|
||||
const kind = (type: string) => {
|
||||
if (type === 'Pie' || type === 'Funnel' || type === 'Pyramid') return 'pie'
|
||||
if (type === 'Doughnut') return 'doughnut'
|
||||
if (type === 'Radar' || type === 'Polar') return 'radar'
|
||||
if (type.includes('Column') || type.includes('Bar') || type.includes('Range')) return 'bar'
|
||||
return 'line'
|
||||
}
|
||||
|
||||
function config(d: ChartData): ChartConfiguration {
|
||||
const first = d.series[0]
|
||||
const chartKind = first ? kind(first.type) : 'line'
|
||||
const labels = (first?.points ?? []).map((p) => String(p.x))
|
||||
const pie = chartKind === 'pie' || chartKind === 'doughnut'
|
||||
const horizontal = d.series.some((s) => s.type === 'Bar' || s.type.startsWith('StackedBar'))
|
||||
const stacked = d.series.some((s) => s.type.startsWith('Stacked'))
|
||||
const datasets = d.series.map((s, i) => {
|
||||
const color = s.color ?? PALETTE[i % PALETTE.length]
|
||||
const type = kind(s.type)
|
||||
return {
|
||||
type: pie ? chartKind : type,
|
||||
label: s.legend,
|
||||
data: s.points.map((p) => p.y),
|
||||
borderColor: pie ? '#fff' : color,
|
||||
backgroundColor: pie ? s.points.map((_, j) => PALETTE[j % PALETTE.length]) : type === 'line' && !s.type.includes('Area') ? color : `${color}cc`,
|
||||
borderWidth: Math.max(1, s.width) * (type === 'line' ? 2 : 1),
|
||||
tension: s.type.includes('Spline') ? 0.4 : 0,
|
||||
stepped: s.type === 'StepLine',
|
||||
showLine: !(s.type === 'Point' || s.type === 'FastPoint' || s.type === 'Bubble'),
|
||||
fill: s.type.includes('Area'),
|
||||
pointRadius: type === 'line' ? 3 : undefined,
|
||||
}
|
||||
})
|
||||
const position = ({ Top: 'top', Bottom: 'bottom', Left: 'left' } as Record<string, 'top' | 'bottom' | 'left'>)[d.legendDocking] ?? 'right'
|
||||
return {
|
||||
type: chartKind,
|
||||
data: { labels, datasets },
|
||||
options: {
|
||||
animation: false,
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
indexAxis: horizontal ? 'y' : 'x',
|
||||
plugins: {
|
||||
legend: { display: d.series.some((s) => s.inLegend), position, labels: { filter: (item) => d.series[item.datasetIndex ?? 0]?.inLegend ?? true } },
|
||||
title: { display: d.titles.length > 0, text: d.titles },
|
||||
},
|
||||
scales: pie || chartKind === 'radar'
|
||||
? {}
|
||||
: {
|
||||
x: { stacked, grid: { color: '#d9d9d9' } },
|
||||
y: { stacked, min: d.yMin ?? undefined, max: d.yMax ?? undefined, beginAtZero: true, grid: { color: '#d9d9d9' } },
|
||||
},
|
||||
},
|
||||
} as ChartConfiguration
|
||||
}
|
||||
|
||||
function draw() {
|
||||
chart?.destroy()
|
||||
chart = undefined
|
||||
if (!canvas.value || !data.value) return
|
||||
chart = new Chart(canvas.value, config(data.value))
|
||||
}
|
||||
|
||||
onMounted(draw)
|
||||
watch(data, draw)
|
||||
onBeforeUnmount(() => chart?.destroy())
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="wf-control wf-chart" :style="style">
|
||||
<canvas ref="canvas" />
|
||||
</div>
|
||||
</template>
|
||||
@@ -5,6 +5,7 @@ import { computed, inject, provide, ref, type CSSProperties } from 'vue'
|
||||
import type { ControlNode, UserAction } from '@iot/shared'
|
||||
import { AMBIENT_BACK_COLOR, AMBIENT_BACK_TYPES, boxCss, DEFAULT_BACK_COLOR, displayText } from './format'
|
||||
import { backgroundCss, RUN_IMAGES, type ImageValue } from './images'
|
||||
import ChartControl from './ChartControl.vue'
|
||||
import DataGridViewControl from './DataGridViewControl.vue'
|
||||
import DateTimePickerControl from './DateTimePickerControl.vue'
|
||||
import LedControl from './LedControl.vue'
|
||||
@@ -189,6 +190,7 @@ const itemHeight = computed(() => `${(p.value.ItemHeight as number | undefined)
|
||||
|
||||
<DateTimePickerControl v-else-if="node.type === 'DateTimePicker'" :node="node" :style="style" :enabled="enabled" :act="act" />
|
||||
|
||||
<ChartControl v-else-if="node.type === 'Chart'" :node="node" :style="style" />
|
||||
|
||||
<TrackBarControl v-else-if="node.type === 'TrackBar'" :node="node" :style="style" :enabled="enabled" :act="act" />
|
||||
|
||||
|
||||
Reference in new issue
Block a user