ZH-GL opened a new issue, #21310:
URL: https://github.com/apache/echarts/issues/21310
### Version
6.0.0
### Link to Minimal Reproduction
下面贴源码
### Steps to Reproduce
<template>
<div>
<!-- 查询卡片 -->
<div id="QueryCard" class="Card_Item">
<div class="QueryCard_Item">
<div class="query-item"><span>数据类型:</span>
<a-select v-model:value="typeValue" :options="typeOptions"
placeholder="全部" style="width: 7vw" />
</div>
<div class="query-item"><span>槽号:</span>
<a-input-number v-model:value="areaValue" :min="2101" :max="2836"
style="width: 7vw" />
</div>
<div class="query-item"><span>时间选择:</span>
<a-config-provider :locale="locale">
<a-date-picker v-model:value="dateValue" @change="dateChange"
style="width: 10vw" />
</a-config-provider>
</div>
<button id="search" class="Button_Item"
@click="sureQuery">确定</button>
<button id="cancel" class="Button_Item"
@click="cancelQuery">重置</button>
</div>
</div>
<!-- 主容器:树在左、图在中、右侧数据列 -->
<div class="main-container">
<div id="treebox" class="tree-container">
<a-tree v-model:expandedKeys="expandedKeys"
v-model:selectedKeys="selectedKeys"
v-model:checkedKeys="checkedKeys" :tree-data="treeData"
@select="onTreeSelect">
<template #title="{ title, key }">
<span v-if="key === '0-0-1-0'" style="color: #1890ff">{{ title
}}</span>
<template v-else>{{ title }}</template>
</template>
</a-tree>
</div>
<div ref="chartRef" class="chart-container"></div>
<div class="data-columns">
<div class="column">
<h3>A</h3>
<div v-for="(item, index) in dataColumnA" :key="index"
class="data-item">
<span class="data-name">{{ item.name }}</span>
<span class="data-value">{{ item.value }}</span>
</div>
</div>
<div class="column">
<h3>B</h3>
<div v-for="(item, index) in dataColumnB" :key="index"
class="data-item">
<span class="data-name">{{ item.name }}</span>
<span class="data-value">{{ item.value }}</span>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, onMounted, onBeforeUnmount, nextTick } from 'vue';
import * as echarts from 'echarts';
import locale from 'ant-design-vue/es/locale/zh_CN';
import axios from 'axios';
import dayjs from 'dayjs';
import 'dayjs/locale/zh-cn';
import { message } from 'ant-design-vue';
import Config from '@/config/Config';
dayjs.locale('zh-cn');
// ---------- 查询条件 ----------
const typeOptions = [
{ value: '实时数据', label: '实时数据' },
{ value: '历史数据', label: '历史数据' },
];
const typeValue = ref('实时数据');
const areaValue = ref(2101);
const dateValue = ref();
const localeRef = locale;
const sureQuery = async () => {
if (typeValue.value === '实时数据') {
await initRealtime();
startWebSocket();
} else {
if (!dateValue.value) {
message.warn("请选择时间!");
return;
}
if (ws) { ws.close(); ws = null; }
await fetchHistoryAndInit();
}
};
const cancelQuery = () => {
typeValue.value = '实时数据';
areaValue.value = 2301;
dateValue.value = [];
apiData.value = [];
latestPoint.value = null;
clearChartContainers();
updateRightColumnsFromLatest();
};
const dateChange = (d) => {
dateValue.value = d;
console.log(dateValue.value.format("YYYY-MM-DD 00:00:00"));
};
// ---------- 树 ----------
const generateList = (prefix, keyPrefix) =>
Array.from({ length: 36 }, (_, index) => ({
title: `${prefix}${String(index + 1).padStart(2, '0')}`,
key: `${keyPrefix}-${index}`,
}));
const lists = Array.from({ length: 8 }, (_, i) => generateList(`2${i + 1}`,
`0-0-${i}`));
const treeData = ref([
{
title: '原料二厂',
key: '0-0',
children: Array.from({ length: 8 }, (_, index) => ({
title: `${index + 1}工区`,
key: `0-0-${index}`,
children: lists[index],
})),
},
]);
const expandedKeys = ref(['0-0']);
const selectedKeys = ref([]);
const checkedKeys = ref([]);
function onTreeSelect(selected, { node }) {
if (!node.children || node.children.length === 0) {
areaValue.value = Number(node.title);
selectedKeys.value = [node.key];
}
}
// ---------- 图表 ----------
const chartRef = ref(null);
const charts = ref([]);
const MAX_POINTS_KEEP = 120;
const lastTimestamps = ref({}); // 记录每个系列的最后时间戳
const _matrixDimensionData = {
x: ['A', 'B'],
y: [
{ value: '电压' },
{ value: '01~03' },
{ value: '04~06' },
{ value: '07~09' },
{ value: '10~12' },
],
};
const lineColors = ['#5470C6', '#91CC75', '#EE6666'];
const lineNames = ['线1', '线2', '线3'];
const txtName = [
[],
['1', '2', '3'],
['4', '5', '6'],
['7', '8', '9'],
['10', '11', '12']
];
function mapSeriesToKey(xidx, yidx, lineIndex) {
if (yidx === 0) return xidx === 0 ? 'a01' : 'b01';
const offset = (yidx - 1) * 3 + lineIndex + 1;
return (xidx === 0 ? 'a' : 'b') + String(offset + 1 - 1).padStart(2, '0');
}
function parseTimeToMs(str) {
const time = dayjs(str);
if (!time.isValid()) {
console.error('无效时间戳:', str);
return null; // 返回 null 表示无效
}
return time.valueOf();
}
const apiData = ref([]);
const latestPoint = ref(null);
async function initRealtime() {
try {
const res = await
axios.post(`${Config.Index}/api/iotdb/query/now/data/graph`, {
start_time: '2025-09-20 00:00:00',
end_time: '2025-09-30 00:00:00',
mt_id: areaValue.value,
point: 'point',
});
apiData.value = res.data.data || [];
await nextTick();
renderMatrixCharts();
updateRightColumnsFromLatest();
} catch (error) {
console.error('initRealtime error:', error);
}
}
async function fetchHistoryAndInit() {
try {
const res = await
axios.post(`${Config.Index}api/iotdb/query/history/data/graph`, {
start_time: dateValue.value.format("YYYY-MM-DD 00:00:00"),
end_time: dateValue.value.format("YYYY-MM-DD 24:00:00"),
mt_id: areaValue.value,
point: "point",
});
console.log(res);
apiData.value = res.data.data || [];
await nextTick();
renderMatrixCharts();
updateRightColumnsFromLatest();
} catch (error) {
console.error('Fetch error:', error);
}
}
function buildSeriesDataFor(xidx, yidx, lineIndex) {
if (!apiData.value || !apiData.value.length) return [];
const arr = [];
// ✅ 电压行:固定 a01 / b01
if (yidx === 0) {
const key = xidx === 0 ? 'a01' : 'b01';
for (const r of apiData.value) arr.push([parseTimeToMs(r.time), r[key]
?? 0]);
return arr;
}
// ✅ 行 1(01~03),第一列(A01/B01)复用电压
if (yidx === 1 && lineIndex === 0) {
const key = xidx === 0 ? 'a01' : 'b01';
for (const r of apiData.value) arr.push([parseTimeToMs(r.time), r[key]
?? 0]);
return arr;
}
// ✅ 其他正常递增,从 a02/b02 开始
const baseIndex = (yidx - 1) * 3 + lineIndex + 1; // 改成 +1 而不是 +2
const key = (xidx === 0 ? 'a' : 'b') + String(baseIndex).padStart(2, '0');
for (const r of apiData.value) arr.push([parseTimeToMs(r.time), r[key] ??
0]);
return arr;
}
const MAX_TOOLTIP_POINTS = 5000; // 超过这个点数就不显示 tooltip
function buildSeriesForCell(xidx, yidx) {
const series = [];
const xval = _matrixDimensionData.x[xidx];
const isRealtime = typeValue.value === '实时数据';
if (yidx === 0) {
series.push({
name: `${xval} 电压`,
type: 'line',
showSymbol: false,
sampling: isRealtime ? undefined : 'lttb',
large: isRealtime ? false : true,
progressive: 50000, // 每批绘制 5000 点
lineStyle: { width: 1, color: lineColors[0] },
color: lineColors[0],
data: buildSeriesDataFor(xidx, yidx, 0),
});
} else {
for (let li = 0; li < lineNames.length; li++) {
const legendName = xidx === 0 ? `A${txtName[yidx][li]}` :
`B${txtName[yidx][li]}`;
series.push({
name: legendName,
type: 'line',
showSymbol: false,
sampling: isRealtime ? undefined : 'lttb',
large: isRealtime ? false : true,
lineStyle: { width: 1, color: lineColors[li] },
color: lineColors[li],
data: buildSeriesDataFor(xidx, yidx, li),
});
}
}
return series;
}
function clearChartContainers() {
charts.value.forEach(c => c.dispose && c.dispose());
charts.value = [];
if (chartRef.value) chartRef.value.innerHTML = '';
}
function renderMatrixCharts() {
clearChartContainers();
if (!chartRef.value) return;
const rows = _matrixDimensionData.y.length;
const cols = _matrixDimensionData.x.length;
for (let yidx = 0; yidx < rows; yidx++) {
for (let xidx = 0; xidx < cols; xidx++) {
const wrapper = document.createElement('div');
wrapper.style.position = 'absolute';
wrapper.style.left = `${xidx * (100 / cols)}%`;
wrapper.style.top = `${yidx * (100 / rows)}%`;
wrapper.style.width = `${100 / cols}%`;
wrapper.style.height = `${100 / rows}%`;
wrapper.style.padding = '6px';
wrapper.style.boxSizing = 'border-box';
chartRef.value.appendChild(wrapper);
const chart = echarts.init(wrapper);
chart.group = 'matrixGroup';
const seriesData = buildSeriesForCell(xidx, yidx);
chart.setOption({
tooltip: {
trigger: 'axis',
axisPointer: { type: 'line', snap: true },
confine: true,
order: 'none', // 提高 tooltip 响应性
// position: (point) => [point[0] + 10, point[1] + 10],
formatter: (params) => {
console.log('收到参数:', params);
if (!params || !params.length) return '';
const time = dayjs(params[0].axisValue).format('YYYY-MM-DD
HH:mm:ss');
let content = `<div style="font-weight:bold;
margin-bottom:4px;">${time}</div>`;
params.forEach(p => {
content += `<div style="color:${p.color}">${p.seriesName}:
${p.data[1]}</div>`;
});
return content;
}
},
legend: {
data: seriesData.map(s => s.name),
top: -5,
textStyle: { fontSize: 10 },
itemWidth: 12,
itemHeight: 8,
type: 'scroll',
},
grid: { left: 40, right: 20, top: 20, bottom: 10 },
xAxis: { type: 'time', axisLabel: { formatter: v =>
dayjs(v).format('HH:mm:ss') } },
yAxis: { scale: true },
dataZoom: [{ type: 'inside', xAxisIndex: 0 }],
series: seriesData,
});
charts.value.push(chart);
}
}
echarts.connect(charts.value);
setTimeout(() => charts.value.forEach(c => c.resize()), 50);
}
// ---------- WebSocket ----------
let ws = null;
function startWebSocket() {
if (ws) { ws.close(); ws = null; }
const wsUrl = `${Config.wsUrl}data?mt_id=${areaValue.value}`;
ws = new WebSocket(wsUrl);
let lastUpdateTime = 0; // 节流控制
ws.onmessage = (evt) => {
try {
const now = Date.now();
if (now - lastUpdateTime < 200) return; // 节流
lastUpdateTime = now;
const arr = JSON.parse(evt.data);
if (!arr.length) return;
const newest = Array.isArray(arr) ? arr[arr.length - 1] : arr;
latestPoint.value = newest;
const currentTime = parseTimeToMs(newest.time);
if (currentTime === null) return;
let idx = 0;
for (let yidx = 0; yidx < _matrixDimensionData.y.length; yidx++) {
for (let xidx = 0; xidx < _matrixDimensionData.x.length; xidx++) {
const chart = charts.value[idx++];
if (!chart) continue;
const seriesCount = (yidx === 0 ? 1 : 3);
const curOpt = chart.getOption();
for (let si = 0; si < seriesCount; si++) {
const key = mapSeriesToKey(xidx, yidx, si);
const val = newest[key] ?? 0;
const seriesKey = `${idx}-${si}`;
// 跳过时间戳不递增的数据
if (lastTimestamps.value[seriesKey] &&
lastTimestamps.value[seriesKey] >= currentTime)
continue;
lastTimestamps.value[seriesKey] = currentTime;
// ✅ 直接修改原 series.data 引用
const s = curOpt.series[si];
s.data.push([currentTime, val]);
if (s.data.length > MAX_POINTS_KEEP) s.data.shift();
}
// 更新 xAxis 范围
const allData = curOpt.series.flatMap(s => s.data);
const minTime = allData.length > 0 ? allData[0][0] : currentTime -
60 * 1000;
const maxTime = currentTime + 1000;
// 只更新 xAxis 和 series,保留 tooltip/legend/grid 的引用
chart.setOption({
xAxis: { min: minTime, max: maxTime },
series: curOpt.series
}, { notMerge: false, lazyUpdate: false });
}
}
updateRightColumnsFromLatest();
} catch (e) {
console.error('WebSocket 数据处理错误:', e);
}
};
ws.onerror = (error) => {
console.error('WebSocket 错误:', error);
ws.close();
ws = null;
setTimeout(startWebSocket, 5000);
};
ws.onclose = () => {
console.log('WebSocket 连接关闭');
ws = null;
};
}
// ---------- 右侧数据列 ----------
const dataColumnA = ref([]);
const dataColumnB = ref([]);
function updateRightColumnsFromLatest() {
const src = latestPoint.value ?? (apiData.value.length ?
apiData.value[apiData.value.length - 1] : null);
if (!src) {
dataColumnA.value = [];
dataColumnB.value = [];
return;
}
dataColumnA.value = [];
dataColumnB.value = [];
_matrixDimensionData.y.forEach((yItem, yidx) => {
if (yidx === 0) {
// ✅ 电压行
dataColumnA.value.push({ name: '电压', value: src.a01 ?? 0 });
dataColumnB.value.push({ name: '电压', value: src.b01 ?? 0 });
} else {
const names = txtName[yidx];
names.forEach((name, li) => {
let keyA, keyB;
// ✅ A01 / B01 复用电压
if (yidx === 1 && li === 0) {
keyA = 'a01';
keyB = 'b01';
} else {
const baseIndex = (yidx - 1) * 3 + li + 1; // 改为 +1,保持与上方一致
keyA = 'a' + String(baseIndex).padStart(2, '0');
keyB = 'b' + String(baseIndex).padStart(2, '0');
}
dataColumnA.value.push({ name: `A${name}`, value: src[keyA] ?? 0 });
dataColumnB.value.push({ name: `B${name}`, value: src[keyB] ?? 0 });
});
}
});
}
// ---------- 生命周期 ----------
onMounted(async () => {
nextTick(() => renderMatrixCharts());
});
onBeforeUnmount(() => {
if (ws) { ws.close(); ws = null; }
charts.value.forEach(c => c.dispose());
charts.value = [];
});
</script>
<style scoped>
#QueryCard {
border-radius: 8px;
background: linear-gradient(135deg, #ffffff, #f6f8fa);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
padding: 1vh;
font-size: 1.6vh;
cursor: default;
}
.Card_Item {
width: 90%;
margin: 1vh auto;
}
.QueryCard_Item {
display: flex;
align-items: center;
justify-content: center;
gap: 1.5vw;
}
.query-item {
display: flex;
align-items: center;
gap: 0.5vw;
}
.query-item span {
font-weight: 500;
color: #333;
}
#search {
background: linear-gradient(45deg, #399bff, #5abaff);
transition: transform 0.2s, box-shadow 0.2s;
}
#cancel {
background: linear-gradient(45deg, #efca04, #ffdb4d);
transition: transform 0.2s, box-shadow 0.2s;
}
.Button_Item {
color: #fff;
border-radius: 6px;
border: none;
width: 4vw;
height: 3.5vh;
letter-spacing: 0.2vw;
font-size: 0.8vw;
font-weight: 500;
cursor: pointer;
}
.Button_Item:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
.main-container {
display: flex;
width: 100%;
height: 79vh;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
overflow: visible;
}
.tree-container {
width: 12vw;
min-width: 150px;
max-width: 200px;
padding: 1vh;
background: #fafafa;
border-right: 1px solid #e8e8e8;
overflow: auto;
}
.chart-container {
flex: 1;
position: relative;
height: 100%;
background: #fff;
}
.data-columns {
width: 14vw;
min-width: 180px;
max-width: 220px;
padding: 1.5vh;
background: linear-gradient(#fff, #f9f9f9);
border-left: 1px solid #e8e8e8;
display: flex;
gap: 1rem;
box-sizing: border-box;
}
.column {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
}
.data-item {
width: 100%;
display: flex;
justify-content: space-between;
padding: 0.5rem;
background: #fff;
border-radius: 6px;
margin-bottom: 6px;
}
</style>
### Current Behavior
动态添加数据后无法显示tooltip
### Expected Behavior
动态添加数据后正常显示tooltip,并且是联动显示
### Environment
```markdown
- OS:win11
- Browser:Chrome 96.0.4664.55
- Framework:Vue@3
```
### Any additional comments?
主要是在setoption方法上的问题
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]