feat: 添加逻辑分析仪
This commit is contained in:
parent
e8a16fd446
commit
4d6c06a0e0
|
@ -1,6 +1,12 @@
|
|||
<template>
|
||||
<div class="w-full h-100">
|
||||
<v-chart v-if="true" class="w-full h-full" :option="option" autoresize />
|
||||
<div class="w-full h-150">
|
||||
<v-chart
|
||||
v-if="data"
|
||||
class="w-full h-full"
|
||||
:option="option"
|
||||
autoresize
|
||||
:update-options="updateOptions"
|
||||
/>
|
||||
<div
|
||||
v-else
|
||||
class="w-full h-full flex items-center justify-center text-gray-500"
|
||||
|
@ -11,144 +17,194 @@
|
|||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from "vue";
|
||||
import { computed, shallowRef } from "vue";
|
||||
import VChart from "vue-echarts";
|
||||
import type { LogicDataType } from "./index";
|
||||
|
||||
// Echarts
|
||||
import { use } from "echarts/core";
|
||||
import { LineChart } from "echarts/charts";
|
||||
import {
|
||||
TitleComponent,
|
||||
TooltipComponent,
|
||||
LegendComponent,
|
||||
ToolboxComponent,
|
||||
GridComponent,
|
||||
DataZoomComponent,
|
||||
AxisPointerComponent,
|
||||
ToolboxComponent,
|
||||
} from "echarts/components";
|
||||
import { CanvasRenderer } from "echarts/renderers";
|
||||
import type { ComposeOption } from "echarts/core";
|
||||
import type { LineSeriesOption } from "echarts/charts";
|
||||
import type {
|
||||
TitleComponentOption,
|
||||
AxisPointerComponentOption,
|
||||
TooltipComponentOption,
|
||||
LegendComponentOption,
|
||||
ToolboxComponentOption,
|
||||
GridComponentOption,
|
||||
DataZoomComponentOption,
|
||||
} from "echarts/components";
|
||||
import type {
|
||||
ToolboxComponentOption,
|
||||
XAXisOption,
|
||||
YAXisOption,
|
||||
} from "echarts/types/dist/shared";
|
||||
|
||||
use([
|
||||
TitleComponent,
|
||||
TooltipComponent,
|
||||
LegendComponent,
|
||||
ToolboxComponent,
|
||||
GridComponent,
|
||||
AxisPointerComponent,
|
||||
DataZoomComponent,
|
||||
LineChart,
|
||||
CanvasRenderer,
|
||||
]);
|
||||
|
||||
type EChartsOption = ComposeOption<
|
||||
| TitleComponentOption
|
||||
| AxisPointerComponentOption
|
||||
| TooltipComponentOption
|
||||
| LegendComponentOption
|
||||
| ToolboxComponentOption
|
||||
| GridComponentOption
|
||||
| DataZoomComponentOption
|
||||
| LineSeriesOption
|
||||
>;
|
||||
|
||||
// Define props
|
||||
interface Props {
|
||||
data?: LogicDataType;
|
||||
}
|
||||
|
||||
const props = defineProps<Props>();
|
||||
|
||||
// 添加更新选项来减少重绘
|
||||
const updateOptions = shallowRef({
|
||||
notMerge: false,
|
||||
lazyUpdate: true,
|
||||
silent: false
|
||||
});
|
||||
|
||||
const option = computed((): EChartsOption => {
|
||||
return {
|
||||
title: {
|
||||
text: "Stacked Area Chart",
|
||||
if (!props.data) return {};
|
||||
|
||||
const channelCount = props.data.y.length;
|
||||
const channelSpacing = 2; // 每个通道之间的间距
|
||||
|
||||
// 使用单个网格
|
||||
const grids: GridComponentOption[] = [
|
||||
{
|
||||
left: "5%",
|
||||
right: "5%",
|
||||
top: "5%",
|
||||
bottom: "15%",
|
||||
},
|
||||
];
|
||||
|
||||
// 单个X轴
|
||||
const xAxis: XAXisOption[] = [
|
||||
{
|
||||
type: "category",
|
||||
boundaryGap: false,
|
||||
data: props.data!.x.map((x) => x.toFixed(3)),
|
||||
axisLabel: {
|
||||
formatter: (value: string) => `${value}${props.data!.xUnit}`,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
// 单个Y轴,范围根据通道数量调整
|
||||
const yAxis: YAXisOption[] = [
|
||||
{
|
||||
type: "value",
|
||||
min: -0.5,
|
||||
max: channelCount * channelSpacing - 0.5,
|
||||
interval: channelSpacing,
|
||||
axisLabel: {
|
||||
formatter: (value: number) => {
|
||||
const channelIndex = Math.round(value / channelSpacing);
|
||||
return channelIndex < channelCount
|
||||
? props.data!.channelNames[channelIndex]
|
||||
: "";
|
||||
},
|
||||
},
|
||||
splitLine: { show: false },
|
||||
},
|
||||
];
|
||||
|
||||
// 创建系列数据,每个通道有不同的Y偏移
|
||||
const series: LineSeriesOption[] = props.data.y.map((channelData, index) => ({
|
||||
name: props.data!.channelNames[index],
|
||||
type: "line",
|
||||
data: channelData.map((value) => value + index * channelSpacing + 0.2),
|
||||
step: "end",
|
||||
lineStyle: {
|
||||
width: 2,
|
||||
},
|
||||
areaStyle: {
|
||||
opacity: 0.3,
|
||||
origin: index * channelSpacing,
|
||||
},
|
||||
symbol: "none",
|
||||
// 优化性能配置
|
||||
sampling: "lttb",
|
||||
// large: true,
|
||||
// largeThreshold: 2000,
|
||||
// progressive: 2000,
|
||||
// 减少动画以避免闪烁
|
||||
animation: false,
|
||||
}));
|
||||
|
||||
return {
|
||||
// 全局动画配置
|
||||
animation: false,
|
||||
tooltip: {
|
||||
trigger: "axis",
|
||||
axisPointer: {
|
||||
type: "cross",
|
||||
type: "line",
|
||||
label: {
|
||||
backgroundColor: "#6a7985",
|
||||
},
|
||||
// 减少axisPointer的动画
|
||||
animation: false,
|
||||
},
|
||||
},
|
||||
legend: {
|
||||
data: ["Email", "Union Ads", "Video Ads", "Direct", "Search Engine"],
|
||||
formatter: (params: any) => {
|
||||
if (Array.isArray(params) && params.length > 0) {
|
||||
const timeValue = props.data!.x[params[0].dataIndex];
|
||||
const dataIndex = params[0].dataIndex;
|
||||
|
||||
let tooltip = `Time: ${timeValue.toFixed(3)}${props.data!.xUnit}<br/>`;
|
||||
|
||||
// 显示所有通道在当前时间点的原始数值(0或1)
|
||||
props.data!.channelNames.forEach((channelName, index) => {
|
||||
const originalValue = props.data!.y[index][dataIndex];
|
||||
tooltip += `${channelName}: ${originalValue}<br/>`;
|
||||
});
|
||||
|
||||
return tooltip;
|
||||
}
|
||||
return "";
|
||||
},
|
||||
// 优化tooltip性能
|
||||
hideDelay: 100,
|
||||
},
|
||||
toolbox: {
|
||||
feature: {
|
||||
saveAsImage: {},
|
||||
restore: {},
|
||||
},
|
||||
},
|
||||
grid: {
|
||||
left: "3%",
|
||||
right: "4%",
|
||||
bottom: "3%",
|
||||
containLabel: true,
|
||||
},
|
||||
xAxis: [
|
||||
grid: grids,
|
||||
xAxis: xAxis,
|
||||
yAxis: yAxis,
|
||||
dataZoom: [
|
||||
{
|
||||
type: "category",
|
||||
boundaryGap: false,
|
||||
data: ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"],
|
||||
},
|
||||
],
|
||||
yAxis: [
|
||||
{
|
||||
type: "value",
|
||||
},
|
||||
],
|
||||
series: [
|
||||
{
|
||||
name: "Email",
|
||||
type: "line",
|
||||
stack: "Total",
|
||||
areaStyle: {},
|
||||
emphasis: {
|
||||
focus: "series",
|
||||
},
|
||||
data: [120, 132, 101, 134, 90, 230, 210],
|
||||
},
|
||||
{
|
||||
name: "Union Ads",
|
||||
type: "line",
|
||||
stack: "Total",
|
||||
areaStyle: {},
|
||||
emphasis: {
|
||||
focus: "series",
|
||||
},
|
||||
data: [220, 182, 191, 234, 290, 330, 310],
|
||||
},
|
||||
{
|
||||
name: "Video Ads",
|
||||
type: "line",
|
||||
stack: "Total",
|
||||
areaStyle: {},
|
||||
emphasis: {
|
||||
focus: "series",
|
||||
},
|
||||
data: [150, 232, 201, 154, 190, 330, 410],
|
||||
},
|
||||
{
|
||||
name: "Direct",
|
||||
type: "line",
|
||||
stack: "Total",
|
||||
areaStyle: {},
|
||||
emphasis: {
|
||||
focus: "series",
|
||||
},
|
||||
data: [320, 332, 301, 334, 390, 330, 320],
|
||||
},
|
||||
{
|
||||
name: "Search Engine",
|
||||
type: "line",
|
||||
stack: "Total",
|
||||
label: {
|
||||
show: true,
|
||||
position: "top",
|
||||
},
|
||||
areaStyle: {},
|
||||
emphasis: {
|
||||
focus: "series",
|
||||
},
|
||||
data: [820, 932, 901, 934, 1290, 1330, 1320],
|
||||
show: true,
|
||||
realtime: true,
|
||||
start: 0,
|
||||
end: 100,
|
||||
},
|
||||
{
|
||||
type: "inside",
|
||||
realtime: true,
|
||||
start: 0,
|
||||
end: 100,
|
||||
},
|
||||
],
|
||||
series: series,
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
|
|
@ -0,0 +1,76 @@
|
|||
import LogicalWaveFormDisplay from "./LogicalWaveFormDisplay.vue";
|
||||
|
||||
type LogicDataType = {
|
||||
x: number[];
|
||||
y: number[][]; // 8 channels of digital data (0 or 1)
|
||||
xUnit: "s" | "ms" | "us" | "ns";
|
||||
channelNames: string[];
|
||||
};
|
||||
|
||||
// Test data generator for 8-channel digital signals
|
||||
function generateTestLogicData(): LogicDataType {
|
||||
const sampleRate = 10000; // 10kHz sampling
|
||||
const duration = 1;
|
||||
const points = Math.floor(sampleRate * duration);
|
||||
|
||||
const x = Array.from({ length: points }, (_, i) => (i / sampleRate) * 1000); // time in ms
|
||||
|
||||
// Generate 8 channels with different digital patterns
|
||||
const y = [
|
||||
// Channel 0: Clock signal 100Hz
|
||||
Array.from(
|
||||
{ length: points },
|
||||
(_, i) => Math.floor((100 * i) / sampleRate) % 2,
|
||||
),
|
||||
// Channel 1: Clock/2 signal 50Hz
|
||||
Array.from(
|
||||
{ length: points },
|
||||
(_, i) => Math.floor((50 * i) / sampleRate) % 2,
|
||||
),
|
||||
// Channel 2: Clock/4 signal 25Hz
|
||||
Array.from(
|
||||
{ length: points },
|
||||
(_, i) => Math.floor((25 * i) / sampleRate) % 2,
|
||||
),
|
||||
// Channel 3: Clock/8 signal 12.5Hz
|
||||
Array.from(
|
||||
{ length: points },
|
||||
(_, i) => Math.floor((12.5 * i) / sampleRate) % 2,
|
||||
),
|
||||
// Channel 4: Data signal (pseudo-random pattern)
|
||||
Array.from(
|
||||
{ length: points },
|
||||
(_, i) => Math.abs( Math.floor(Math.sin(i * 0.01) * 10) % 2 ),
|
||||
),
|
||||
// Channel 5: Enable signal (periodic pulse)
|
||||
Array.from(
|
||||
{ length: points },
|
||||
(_, i) => (Math.floor(i / 50) % 10) < 3 ? 1 : 0,
|
||||
),
|
||||
// Channel 6: Reset signal (occasional pulse)
|
||||
Array.from(
|
||||
{ length: points },
|
||||
(_, i) => (Math.floor(i / 200) % 20) === 0 ? 1 : 0,
|
||||
),
|
||||
// Channel 7: Status signal (slow changing)
|
||||
Array.from(
|
||||
{ length: points },
|
||||
(_, i) => Math.floor(i / 1000) % 2,
|
||||
),
|
||||
];
|
||||
|
||||
const channelNames = [
|
||||
"CLK",
|
||||
"CLK/2",
|
||||
"CLK/4",
|
||||
"CLK/8",
|
||||
"PWM",
|
||||
"ENABLE",
|
||||
"RESET",
|
||||
"STATUS"
|
||||
];
|
||||
|
||||
return { x, y, xUnit: "ms", channelNames };
|
||||
}
|
||||
|
||||
export { LogicalWaveFormDisplay, generateTestLogicData, type LogicDataType };
|
|
@ -31,7 +31,6 @@ import { CanvasRenderer } from "echarts/renderers";
|
|||
import type { ComposeOption } from "echarts/core";
|
||||
import type { LineSeriesOption } from "echarts/charts";
|
||||
import type {
|
||||
TitleComponentOption,
|
||||
TooltipComponentOption,
|
||||
LegendComponentOption,
|
||||
ToolboxComponentOption,
|
||||
|
@ -40,7 +39,6 @@ import type {
|
|||
} from "echarts/components";
|
||||
|
||||
use([
|
||||
TitleComponent,
|
||||
TooltipComponent,
|
||||
LegendComponent,
|
||||
ToolboxComponent,
|
||||
|
@ -51,7 +49,6 @@ use([
|
|||
]);
|
||||
|
||||
type EChartsOption = ComposeOption<
|
||||
| TitleComponentOption
|
||||
| TooltipComponentOption
|
||||
| LegendComponentOption
|
||||
| ToolboxComponentOption
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
type="radio"
|
||||
name="function-bar"
|
||||
id="1"
|
||||
checked
|
||||
:checked="checkID === 1"
|
||||
@change="handleTabChange"
|
||||
/>
|
||||
<TerminalIcon class="icon" />
|
||||
|
@ -17,6 +17,7 @@
|
|||
type="radio"
|
||||
name="function-bar"
|
||||
id="2"
|
||||
:checked="checkID === 2"
|
||||
@change="handleTabChange"
|
||||
/>
|
||||
<VideoIcon class="icon" />
|
||||
|
@ -27,6 +28,7 @@
|
|||
type="radio"
|
||||
name="function-bar"
|
||||
id="3"
|
||||
:checked="checkID === 3"
|
||||
@change="handleTabChange"
|
||||
/>
|
||||
<SquareActivityIcon class="icon" />
|
||||
|
@ -37,6 +39,7 @@
|
|||
type="radio"
|
||||
name="function-bar"
|
||||
id="4"
|
||||
:checked="checkID === 4"
|
||||
@change="handleTabChange"
|
||||
/>
|
||||
<Zap class="icon" />
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
<Zap class="w-5 h-5" />
|
||||
逻辑信号分析
|
||||
</h2>
|
||||
<LogicalWaveFormDisplay />
|
||||
<LogicalWaveFormDisplay :data="generateTestLogicData()" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@ -38,7 +38,7 @@
|
|||
<script setup lang="ts">
|
||||
import { Zap, Settings, Layers } from "lucide-vue-next";
|
||||
import { useEquipments } from "@/stores/equipments";
|
||||
import LogicalWaveFormDisplay from "@/components/LogicAnalyzer/LogicalWaveFormDisplay.vue";
|
||||
import { LogicalWaveFormDisplay, generateTestLogicData } from "@/components/LogicAnalyzer";
|
||||
|
||||
// 使用全局设备配置
|
||||
const equipments = useEquipments();
|
||||
|
|
Loading…
Reference in New Issue