apache / apache/echarts

[Bug] 折线图动态加载数据时,设置tooltip无法正常显示,设置了多表联合 同步更新的时候

Open
#21,310 1 comment 0 reactions 0 assignees View on GitHub
bug pending
Dominant language
TypeScript
Stars
67.3k
Forks
19.8k
Avg merge
11d 14h
Merged PRs (30d)
8

Description

### Version

6.0.0

### Link to Minimal Reproduction

下面贴源码

### Steps to Reproduce





数据类型:


槽号:


时间选择:




确定
重置






{{ title }}
{{ title }}




A



{{ item.name }}
{{ item.value }}


B



{{ item.name }}
{{ item.value }}




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 = [];
});

#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;
}

### Current Behavior

动态添加数据后无法显示tooltip

### Expected Behavior

动态添加数据后正常显示tooltip,并且是联动显示

### Environment

```markdown
- OS:win11
- Browser:Chrome 96.0.4664.55
- Framework:Vue@3
```

### Any additional comments?

主要是在setoption方法上的问题

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.