2025-06-10 15:21:07 +08:00
|
|
|
|
// 确保 Chart.js 已加载
|
|
|
|
|
if (!window.Chart) {
|
|
|
|
|
const script = document.createElement('script');
|
|
|
|
|
script.src = '../chart.min.js';
|
|
|
|
|
document.head.appendChild(script);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
class FloatingChartWindow extends HTMLElement {
|
|
|
|
|
// 添加静态计数器
|
|
|
|
|
static zIndexCounter = 1;
|
|
|
|
|
|
|
|
|
|
constructor() {
|
|
|
|
|
super();
|
|
|
|
|
this.attachShadow({ mode: 'open' });
|
|
|
|
|
|
|
|
|
|
// 初始化状态
|
|
|
|
|
this.position = { x: 100, y: 100 };
|
|
|
|
|
this.size = { width: 400, height: 300 };
|
|
|
|
|
this.isDragging = false;
|
|
|
|
|
this.dragOffset = { x: 0, y: 0 };
|
|
|
|
|
this.isResizing = false;
|
|
|
|
|
this.resizeStart = { x: 0, y: 0 };
|
|
|
|
|
this.isMinimized = false;
|
|
|
|
|
this.chartInstance = null;
|
|
|
|
|
this.zIndex = FloatingChartWindow.zIndexCounter++;
|
|
|
|
|
|
|
|
|
|
// 存储初始属性值
|
|
|
|
|
this._title = this.getAttribute('title') || '图表窗口';
|
|
|
|
|
this._initialPosition = this.getAttribute('initial-position') ?
|
|
|
|
|
JSON.parse(this.getAttribute('initial-position')) : { x: 100, y: 100 };
|
|
|
|
|
this._initialSize = this.getAttribute('initial-size') ?
|
|
|
|
|
JSON.parse(this.getAttribute('initial-size')) : { width: 400, height: 300 };
|
|
|
|
|
this._chartData = this.getAttribute('chart-data') ?
|
|
|
|
|
JSON.parse(this.getAttribute('chart-data')) : { labels: [], datasets: [] };
|
|
|
|
|
this._chartOptions = this.getAttribute('chart-options') ?
|
|
|
|
|
JSON.parse(this.getAttribute('chart-options')) : {};
|
|
|
|
|
|
|
|
|
|
// 绑定方法
|
|
|
|
|
this.handleMouseDown = this.handleMouseDown.bind(this);
|
|
|
|
|
this.handleMouseMove = this.handleMouseMove.bind(this);
|
|
|
|
|
this.handleMouseUp = this.handleMouseUp.bind(this);
|
|
|
|
|
this.handleResizeStart = this.handleResizeStart.bind(this);
|
|
|
|
|
this.handleResize = this.handleResize.bind(this);
|
|
|
|
|
this.handleResizeEnd = this.handleResizeEnd.bind(this);
|
|
|
|
|
this.handleMinimize = this.handleMinimize.bind(this);
|
|
|
|
|
this.handleClose = this.handleClose.bind(this);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static get observedAttributes() {
|
|
|
|
|
return ['title', 'initial-position', 'initial-size', 'chart-data', 'chart-options', 'z-index'];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
connectedCallback() {
|
|
|
|
|
this.render();
|
|
|
|
|
this.initChart();
|
|
|
|
|
this.addEventListeners();
|
|
|
|
|
|
|
|
|
|
// 应用初始属性值
|
|
|
|
|
this.updateTitle(this._title);
|
|
|
|
|
this.position = this._initialPosition;
|
|
|
|
|
this.size = this._initialSize;
|
|
|
|
|
this.updatePosition();
|
|
|
|
|
this.updateSize();
|
|
|
|
|
this.updateChartData(this._chartData);
|
|
|
|
|
this.updateChartOptions(this._chartOptions);
|
|
|
|
|
this.updateZIndex(this.zIndex);
|
|
|
|
|
|
|
|
|
|
// 触发 connected 事件
|
|
|
|
|
this.dispatchEvent(new CustomEvent('connected'));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
disconnectedCallback() {
|
|
|
|
|
this.removeEventListeners();
|
|
|
|
|
if (this.chartInstance) {
|
|
|
|
|
this.chartInstance.destroy();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
attributeChangedCallback(name, oldValue, newValue) {
|
|
|
|
|
if (oldValue === newValue) return;
|
|
|
|
|
|
|
|
|
|
// 存储新的属性值
|
|
|
|
|
switch (name) {
|
|
|
|
|
case 'title':
|
|
|
|
|
this._title = newValue;
|
|
|
|
|
break;
|
|
|
|
|
case 'initial-position':
|
|
|
|
|
this._initialPosition = JSON.parse(newValue);
|
|
|
|
|
break;
|
|
|
|
|
case 'initial-size':
|
|
|
|
|
this._initialSize = JSON.parse(newValue);
|
|
|
|
|
break;
|
|
|
|
|
case 'chart-data':
|
|
|
|
|
this._chartData = JSON.parse(newValue);
|
|
|
|
|
break;
|
|
|
|
|
case 'chart-options':
|
|
|
|
|
this._chartOptions = JSON.parse(newValue);
|
|
|
|
|
break;
|
|
|
|
|
case 'z-index':
|
|
|
|
|
this.zIndex = parseInt(newValue);
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 如果组件已经渲染,则更新相应的值
|
|
|
|
|
if (this.shadowRoot) {
|
|
|
|
|
switch (name) {
|
|
|
|
|
case 'title':
|
|
|
|
|
this.updateTitle(this._title);
|
|
|
|
|
break;
|
|
|
|
|
case 'initial-position':
|
|
|
|
|
this.position = this._initialPosition;
|
|
|
|
|
this.updatePosition();
|
|
|
|
|
break;
|
|
|
|
|
case 'initial-size':
|
|
|
|
|
this.size = this._initialSize;
|
|
|
|
|
this.updateSize();
|
|
|
|
|
break;
|
|
|
|
|
case 'chart-data':
|
|
|
|
|
this.updateChartData(this._chartData);
|
|
|
|
|
break;
|
|
|
|
|
case 'chart-options':
|
|
|
|
|
this.updateChartOptions(this._chartOptions);
|
|
|
|
|
break;
|
|
|
|
|
case 'z-index':
|
|
|
|
|
this.updateZIndex(this.zIndex);
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
render() {
|
|
|
|
|
const style = document.createElement('style');
|
|
|
|
|
style.textContent = `
|
|
|
|
|
.floating-window {
|
|
|
|
|
position: fixed;
|
|
|
|
|
background: white;
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
|
|
|
|
display: flex;
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.window-header {
|
|
|
|
|
background: #f5f5f5;
|
|
|
|
|
padding: 8px 12px;
|
|
|
|
|
display: flex;
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
align-items: center;
|
|
|
|
|
cursor: move;
|
|
|
|
|
user-select: none;
|
|
|
|
|
border-bottom: 1px solid #e8e8e8;
|
|
|
|
|
flex-shrink: 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.window-title {
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
color: #262626;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.window-controls {
|
|
|
|
|
display: flex;
|
|
|
|
|
gap: 8px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.window-control-button {
|
|
|
|
|
width: 24px;
|
|
|
|
|
height: 24px;
|
|
|
|
|
border: none;
|
|
|
|
|
background: transparent;
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
justify-content: center;
|
|
|
|
|
font-size: 16px;
|
|
|
|
|
color: #666;
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
transition: all 0.3s;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.window-control-button:hover {
|
|
|
|
|
background: rgba(0, 0, 0, 0.05);
|
|
|
|
|
color: #262626;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.window-control-button.close-button:hover {
|
|
|
|
|
background: #ff4d4f;
|
|
|
|
|
color: white;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.window-content {
|
|
|
|
|
flex: 1;
|
|
|
|
|
padding: 12px;
|
|
|
|
|
position: relative;
|
|
|
|
|
min-height: 0;
|
|
|
|
|
display: flex;
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.chart-container {
|
|
|
|
|
flex: 1;
|
|
|
|
|
position: relative;
|
|
|
|
|
min-height: 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
canvas {
|
|
|
|
|
width: 100% !important;
|
|
|
|
|
height: 100% !important;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.resize-handle {
|
|
|
|
|
position: absolute;
|
|
|
|
|
right: 0;
|
|
|
|
|
bottom: 0;
|
|
|
|
|
width: 10px;
|
|
|
|
|
height: 10px;
|
|
|
|
|
cursor: se-resize;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.resize-handle::after {
|
|
|
|
|
content: '';
|
|
|
|
|
position: absolute;
|
|
|
|
|
right: 2px;
|
|
|
|
|
bottom: 2px;
|
|
|
|
|
width: 6px;
|
|
|
|
|
height: 6px;
|
|
|
|
|
border-right: 2px solid #ccc;
|
|
|
|
|
border-bottom: 2px solid #ccc;
|
|
|
|
|
}
|
|
|
|
|
`;
|
|
|
|
|
|
|
|
|
|
const template = document.createElement('template');
|
|
|
|
|
template.innerHTML = `
|
|
|
|
|
<div class="floating-window">
|
|
|
|
|
<div class="window-header">
|
|
|
|
|
<span class="window-title">${this.getAttribute('title') || '图表窗口'}</span>
|
|
|
|
|
<div class="window-controls">
|
|
|
|
|
<button class="window-control-button close-button">×</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="window-content">
|
|
|
|
|
<div class="chart-container">
|
|
|
|
|
<canvas></canvas>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="resize-handle"></div>
|
|
|
|
|
</div>
|
|
|
|
|
`;
|
|
|
|
|
|
|
|
|
|
this.shadowRoot.appendChild(style);
|
|
|
|
|
this.shadowRoot.appendChild(template.content.cloneNode(true));
|
|
|
|
|
|
|
|
|
|
// 更新位置和大小
|
|
|
|
|
this.updatePosition();
|
|
|
|
|
this.updateSize();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
initChart() {
|
|
|
|
|
const canvas = this.shadowRoot.querySelector('canvas');
|
|
|
|
|
const ctx = canvas.getContext('2d');
|
|
|
|
|
|
|
|
|
|
const chartData = JSON.parse(this.getAttribute('chart-data') || '{"labels":[],"datasets":[]}');
|
|
|
|
|
const chartOptions = JSON.parse(this.getAttribute('chart-options') || '{}');
|
|
|
|
|
|
|
|
|
|
// 基础配置
|
|
|
|
|
const baseOptions = {
|
|
|
|
|
responsive: true,
|
|
|
|
|
maintainAspectRatio: false,
|
|
|
|
|
animation: false,
|
|
|
|
|
elements: {
|
|
|
|
|
point: {
|
|
|
|
|
radius: 0
|
|
|
|
|
},
|
|
|
|
|
line: {
|
|
|
|
|
tension: 0
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
scales: {
|
|
|
|
|
y: {
|
|
|
|
|
beginAtZero: false,
|
|
|
|
|
ticks: {
|
|
|
|
|
callback: function(value) {
|
|
|
|
|
// 如果数值的绝对值大于1000或小于0.1,使用科学计数法
|
|
|
|
|
if (Math.abs(value) > 1000 || (Math.abs(value) < 0.1 && value !== 0)) {
|
|
|
|
|
return value.toExponential(1);
|
|
|
|
|
}
|
|
|
|
|
// 否则使用普通数字,保留一位小数
|
|
|
|
|
return value.toFixed(1);
|
|
|
|
|
},
|
|
|
|
|
maxTicksLimit: 8 // 限制刻度数量
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
x: {
|
|
|
|
|
display: true,
|
|
|
|
|
ticks: {
|
|
|
|
|
callback: function(value) {
|
|
|
|
|
// 如果数值大于1000,使用科学计数法
|
|
|
|
|
if (value > 1000) {
|
|
|
|
|
return value.toExponential(1);
|
|
|
|
|
}
|
|
|
|
|
return value;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
plugins: {
|
|
|
|
|
legend: {
|
|
|
|
|
display: true,
|
|
|
|
|
position: 'top'
|
|
|
|
|
},
|
|
|
|
|
tooltip: {
|
|
|
|
|
enabled: true,
|
|
|
|
|
mode: 'index',
|
|
|
|
|
intersect: false,
|
|
|
|
|
callbacks: {
|
|
|
|
|
label: function(context) {
|
|
|
|
|
const value = context.raw;
|
|
|
|
|
// 始终使用普通数字格式,保留一位小数
|
|
|
|
|
return `${context.dataset.label}: ${value.toFixed(1)}`;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// 深度合并配置
|
|
|
|
|
const finalOptions = {
|
|
|
|
|
...baseOptions,
|
|
|
|
|
scales: {
|
|
|
|
|
y: {
|
|
|
|
|
...baseOptions.scales.y,
|
|
|
|
|
...(chartOptions.scales?.y || {}),
|
|
|
|
|
ticks: {
|
|
|
|
|
...baseOptions.scales.y.ticks,
|
|
|
|
|
...(chartOptions.scales?.y?.ticks || {})
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
x: {
|
|
|
|
|
...baseOptions.scales.x,
|
|
|
|
|
...(chartOptions.scales?.x || {}),
|
|
|
|
|
ticks: {
|
|
|
|
|
...baseOptions.scales.x.ticks,
|
|
|
|
|
...(chartOptions.scales?.x?.ticks || {})
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
plugins: {
|
|
|
|
|
...baseOptions.plugins,
|
|
|
|
|
...(chartOptions.plugins || {}),
|
|
|
|
|
tooltip: {
|
|
|
|
|
...baseOptions.plugins.tooltip,
|
|
|
|
|
...(chartOptions.plugins?.tooltip || {}),
|
|
|
|
|
callbacks: {
|
|
|
|
|
...baseOptions.plugins.tooltip.callbacks,
|
|
|
|
|
...(chartOptions.plugins?.tooltip?.callbacks || {})
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2025-06-10 16:09:27 +08:00
|
|
|
|
// 确保数据集配置正确
|
|
|
|
|
if (chartData.datasets && chartData.datasets.length > 0) {
|
|
|
|
|
chartData.datasets = chartData.datasets.map(dataset => ({
|
|
|
|
|
...dataset,
|
|
|
|
|
borderWidth: 2,
|
|
|
|
|
pointRadius: 0,
|
|
|
|
|
tension: 0
|
|
|
|
|
}));
|
|
|
|
|
}
|
|
|
|
|
|
2025-06-10 15:21:07 +08:00
|
|
|
|
this.chartInstance = new Chart(ctx, {
|
|
|
|
|
type: 'line',
|
|
|
|
|
data: chartData,
|
|
|
|
|
options: finalOptions
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
addEventListeners() {
|
|
|
|
|
const header = this.shadowRoot.querySelector('.window-header');
|
|
|
|
|
const closeButton = this.shadowRoot.querySelector('.close-button');
|
|
|
|
|
const resizeHandle = this.shadowRoot.querySelector('.resize-handle');
|
|
|
|
|
|
|
|
|
|
header.addEventListener('mousedown', this.handleMouseDown);
|
|
|
|
|
closeButton.addEventListener('click', this.handleClose);
|
|
|
|
|
resizeHandle.addEventListener('mousedown', this.handleResizeStart);
|
|
|
|
|
|
|
|
|
|
document.addEventListener('mousemove', this.handleMouseMove);
|
|
|
|
|
document.addEventListener('mouseup', this.handleMouseUp);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
removeEventListeners() {
|
|
|
|
|
document.removeEventListener('mousemove', this.handleMouseMove);
|
|
|
|
|
document.removeEventListener('mouseup', this.handleMouseUp);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
handleMouseDown(e) {
|
|
|
|
|
// 检查点击是否在标题栏或其子元素上
|
|
|
|
|
const header = this.shadowRoot.querySelector('.window-header');
|
2025-06-10 16:09:27 +08:00
|
|
|
|
if (header && (e.target === header || header.contains(e.target))) {
|
2025-06-10 15:21:07 +08:00
|
|
|
|
this.isDragging = true;
|
|
|
|
|
this.dragOffset = {
|
|
|
|
|
x: e.clientX - this.position.x,
|
|
|
|
|
y: e.clientY - this.position.y
|
|
|
|
|
};
|
|
|
|
|
// 点击标题栏时置顶
|
|
|
|
|
const window = this.shadowRoot.querySelector('.floating-window');
|
|
|
|
|
if (window) {
|
|
|
|
|
this.zIndex = FloatingChartWindow.zIndexCounter++;
|
|
|
|
|
window.style.zIndex = this.zIndex;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
handleMouseMove(e) {
|
|
|
|
|
if (this.isDragging) {
|
|
|
|
|
// 限制拖动范围在当前页面区域
|
|
|
|
|
const minX = 300; // 左侧工具栏宽度
|
|
|
|
|
const minY = 100; // 顶部标签页栏高度
|
|
|
|
|
|
|
|
|
|
this.position = {
|
|
|
|
|
x: Math.max(minX, e.clientX - this.dragOffset.x),
|
|
|
|
|
y: Math.max(minY, e.clientY - this.dragOffset.y)
|
|
|
|
|
};
|
|
|
|
|
this.updatePosition();
|
2025-06-10 16:09:27 +08:00
|
|
|
|
} else if (this.isResizing) {
|
2025-06-10 15:21:07 +08:00
|
|
|
|
const deltaX = e.clientX - this.resizeStart.x;
|
|
|
|
|
const deltaY = e.clientY - this.resizeStart.y;
|
|
|
|
|
|
|
|
|
|
this.size = {
|
|
|
|
|
width: Math.max(300, this.size.width + deltaX),
|
|
|
|
|
height: Math.max(200, this.size.height + deltaY)
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
this.resizeStart = {
|
|
|
|
|
x: e.clientX,
|
|
|
|
|
y: e.clientY
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
this.updateSize();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
handleMouseUp() {
|
|
|
|
|
this.isDragging = false;
|
|
|
|
|
this.isResizing = false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
handleResizeStart(e) {
|
|
|
|
|
this.isResizing = true;
|
|
|
|
|
this.resizeStart = {
|
|
|
|
|
x: e.clientX,
|
|
|
|
|
y: e.clientY
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
handleResize(e) {
|
2025-06-10 16:09:27 +08:00
|
|
|
|
if (this.isResizing) {
|
2025-06-10 15:21:07 +08:00
|
|
|
|
const deltaX = e.clientX - this.resizeStart.x;
|
|
|
|
|
const deltaY = e.clientY - this.resizeStart.y;
|
|
|
|
|
|
|
|
|
|
this.size = {
|
|
|
|
|
width: Math.max(300, this.size.width + deltaX),
|
|
|
|
|
height: Math.max(200, this.size.height + deltaY)
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
this.resizeStart = {
|
|
|
|
|
x: e.clientX,
|
|
|
|
|
y: e.clientY
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
this.updateSize();
|
|
|
|
|
|
|
|
|
|
// 更新图表大小
|
|
|
|
|
if (this.chartInstance) {
|
|
|
|
|
this.chartInstance.resize();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
handleResizeEnd() {
|
|
|
|
|
this.isResizing = false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
handleMinimize() {
|
|
|
|
|
this.isMinimized = !this.isMinimized;
|
|
|
|
|
const content = this.shadowRoot.querySelector('.window-content');
|
|
|
|
|
const minimizeButton = this.shadowRoot.querySelector('.minimize-button');
|
|
|
|
|
|
|
|
|
|
if (this.isMinimized) {
|
|
|
|
|
content.classList.add('minimized');
|
|
|
|
|
minimizeButton.textContent = '□';
|
|
|
|
|
} else {
|
|
|
|
|
content.classList.remove('minimized');
|
|
|
|
|
minimizeButton.textContent = '−';
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
handleClose() {
|
|
|
|
|
this.dispatchEvent(new CustomEvent('close'));
|
|
|
|
|
this.remove();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
updatePosition() {
|
|
|
|
|
const window = this.shadowRoot.querySelector('.floating-window');
|
|
|
|
|
window.style.left = `${this.position.x}px`;
|
|
|
|
|
window.style.top = `${this.position.y}px`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
updateSize() {
|
|
|
|
|
const window = this.shadowRoot.querySelector('.floating-window');
|
|
|
|
|
window.style.width = `${this.size.width}px`;
|
|
|
|
|
window.style.height = `${this.size.height}px`;
|
|
|
|
|
|
|
|
|
|
// 强制图表重新渲染
|
|
|
|
|
if (this.chartInstance) {
|
|
|
|
|
this.chartInstance.resize();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
updateTitle(title) {
|
|
|
|
|
const titleElement = this.shadowRoot.querySelector('.window-title');
|
|
|
|
|
titleElement.textContent = title;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
updateChartData(data) {
|
|
|
|
|
if (this.chartInstance) {
|
|
|
|
|
// 更新数据
|
|
|
|
|
this.chartInstance.data = data;
|
|
|
|
|
|
|
|
|
|
// 强制更新配置
|
|
|
|
|
const options = this.chartInstance.options;
|
|
|
|
|
options.scales.y.ticks.callback = function(value) {
|
|
|
|
|
if (Math.abs(value) > 1000 || (Math.abs(value) < 0.1 && value !== 0)) {
|
|
|
|
|
return value.toExponential(1);
|
|
|
|
|
}
|
|
|
|
|
return value.toFixed(1);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
options.scales.x.ticks.callback = function(value) {
|
|
|
|
|
if (value > 1000) {
|
|
|
|
|
return value.toExponential(1);
|
|
|
|
|
}
|
|
|
|
|
return value;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
options.plugins.tooltip.callbacks.label = function(context) {
|
|
|
|
|
const value = context.raw;
|
|
|
|
|
// 始终使用普通数字格式,保留一位小数
|
|
|
|
|
return `${context.dataset.label}: ${value.toFixed(1)}`;
|
|
|
|
|
};
|
2025-06-10 16:09:27 +08:00
|
|
|
|
|
|
|
|
|
// 计算所有数据点的范围
|
|
|
|
|
let min = Infinity;
|
|
|
|
|
let max = -Infinity;
|
|
|
|
|
|
|
|
|
|
data.datasets.forEach(dataset => {
|
|
|
|
|
if (dataset.data && dataset.data.length > 0) {
|
|
|
|
|
const datasetMin = Math.min(...dataset.data);
|
|
|
|
|
const datasetMax = Math.max(...dataset.data);
|
|
|
|
|
min = Math.min(min, datasetMin);
|
|
|
|
|
max = Math.max(max, datasetMax);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (min !== Infinity && max !== -Infinity) {
|
|
|
|
|
const range = max - min;
|
|
|
|
|
// 如果所有数据都是0,使用固定范围
|
|
|
|
|
if (min === 0 && max === 0) {
|
|
|
|
|
options.scales.y.min = -1;
|
|
|
|
|
options.scales.y.max = 1;
|
|
|
|
|
}
|
|
|
|
|
// 如果范围很小,使用固定比例
|
|
|
|
|
else if (range < 1) {
|
|
|
|
|
options.scales.y.min = min - 0.5;
|
|
|
|
|
options.scales.y.max = max + 0.5;
|
|
|
|
|
} else {
|
|
|
|
|
// 如果范围较大,使用百分比
|
|
|
|
|
const margin = range * 0.2; // 使用20%的边距
|
|
|
|
|
options.scales.y.min = min - margin;
|
|
|
|
|
options.scales.y.max = max + margin;
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-06-10 15:21:07 +08:00
|
|
|
|
|
|
|
|
|
// 更新图表
|
|
|
|
|
this.chartInstance.update('none');
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
updateChartOptions(options) {
|
|
|
|
|
if (this.chartInstance) {
|
|
|
|
|
Object.assign(this.chartInstance.options, options);
|
|
|
|
|
this.chartInstance.update('none');
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
updateZIndex(zIndex) {
|
|
|
|
|
const window = this.shadowRoot.querySelector('.floating-window');
|
|
|
|
|
if (window) {
|
|
|
|
|
window.style.zIndex = zIndex;
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-06-10 16:09:27 +08:00
|
|
|
|
|
|
|
|
|
// 添加新方法:处理数据更新
|
|
|
|
|
handleDataUpdate(values, interfaceName) {
|
|
|
|
|
if (!this.chartInstance) return;
|
|
|
|
|
|
|
|
|
|
const chartData = this.chartInstance.data;
|
|
|
|
|
|
|
|
|
|
// 如果是第一次收到数据,创建数据集
|
|
|
|
|
if (chartData.datasets.length === 0) {
|
|
|
|
|
if (values.length > 1) {
|
|
|
|
|
// 创建多个数据集
|
|
|
|
|
chartData.datasets = values.map((_, index) => ({
|
|
|
|
|
label: `${interfaceName} (${index + 1})`,
|
|
|
|
|
data: [],
|
|
|
|
|
borderColor: this.getRandomColor(),
|
|
|
|
|
fill: false,
|
|
|
|
|
borderWidth: 2,
|
|
|
|
|
pointRadius: 0,
|
|
|
|
|
tension: 0
|
|
|
|
|
}));
|
|
|
|
|
} else {
|
|
|
|
|
// 创建单个数据集
|
|
|
|
|
chartData.datasets = [{
|
|
|
|
|
label: interfaceName,
|
|
|
|
|
data: [],
|
|
|
|
|
borderColor: this.getRandomColor(),
|
|
|
|
|
fill: false,
|
|
|
|
|
borderWidth: 2,
|
|
|
|
|
pointRadius: 0,
|
|
|
|
|
tension: 0
|
|
|
|
|
}];
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 添加新的数据点
|
|
|
|
|
chartData.labels.push(this.dataPointIndex.toString());
|
|
|
|
|
values.forEach((value, index) => {
|
|
|
|
|
if (index < chartData.datasets.length) {
|
|
|
|
|
chartData.datasets[index].data.push(value);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
this.dataPointIndex++;
|
|
|
|
|
|
|
|
|
|
// 保持最近100个数据点
|
|
|
|
|
if (chartData.labels.length > 100) {
|
|
|
|
|
chartData.labels.shift();
|
|
|
|
|
chartData.datasets.forEach(dataset => {
|
|
|
|
|
dataset.data.shift();
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 更新图表
|
|
|
|
|
this.updateChartData(chartData);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 添加新方法:获取随机颜色
|
|
|
|
|
getRandomColor() {
|
|
|
|
|
const letters = '0123456789ABCDEF';
|
|
|
|
|
let color = '#';
|
|
|
|
|
for (let i = 0; i < 6; i++) {
|
|
|
|
|
color += letters[Math.floor(Math.random() * 16)];
|
|
|
|
|
}
|
|
|
|
|
return color;
|
|
|
|
|
}
|
2025-06-10 15:21:07 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
customElements.define('floating-chart-window', FloatingChartWindow);
|