// 确保 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 = `
`;
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 || {})
}
}
}
};
// 确保数据集配置正确
if (chartData.datasets && chartData.datasets.length > 0) {
chartData.datasets = chartData.datasets.map(dataset => ({
...dataset,
borderWidth: 2,
pointRadius: 0,
tension: 0
}));
}
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');
if (header && (e.target === header || header.contains(e.target))) {
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();
} else if (this.isResizing) {
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) {
if (this.isResizing) {
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)}`;
};
// 计算所有数据点的范围
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;
}
}
// 更新图表
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;
}
}
// 添加新方法:处理数据更新
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;
}
}
customElements.define('floating-chart-window', FloatingChartWindow);