532 lines
15 KiB
TypeScript
532 lines
15 KiB
TypeScript
import {
|
|
DeesElement,
|
|
css,
|
|
cssManager,
|
|
customElement,
|
|
html,
|
|
property,
|
|
state,
|
|
type TemplateResult,
|
|
} from '@design.estate/dees-element';
|
|
|
|
import * as domtools from '@design.estate/dees-domtools';
|
|
import { demoFunc } from './dees-chart-area.demo.js';
|
|
|
|
import ApexCharts from 'apexcharts';
|
|
|
|
declare global {
|
|
interface HTMLElementTagNameMap {
|
|
'dees-chart-area': DeesChartArea;
|
|
}
|
|
}
|
|
|
|
@customElement('dees-chart-area')
|
|
export class DeesChartArea extends DeesElement {
|
|
public static demo = demoFunc;
|
|
|
|
// instance
|
|
@state()
|
|
public chart: ApexCharts;
|
|
|
|
@property()
|
|
public label: string = 'Untitled Chart';
|
|
|
|
@property({ type: Array })
|
|
public series: ApexAxisChartSeries = [];
|
|
|
|
// Override getter to return internal chart data
|
|
get chartSeries(): ApexAxisChartSeries {
|
|
return this.internalChartData.length > 0 ? this.internalChartData : this.series;
|
|
}
|
|
|
|
@property({ attribute: false })
|
|
public yAxisFormatter: (value: number) => string = (val) => `${val} Mbps`;
|
|
|
|
@property({ type: Number })
|
|
public rollingWindow: number = 0; // 0 means no rolling window
|
|
|
|
@property({ type: Boolean })
|
|
public realtimeMode: boolean = false;
|
|
|
|
@property({ type: String })
|
|
public yAxisScaling: 'fixed' | 'dynamic' | 'percentage' = 'dynamic';
|
|
|
|
@property({ type: Number })
|
|
public yAxisMax: number = 100; // Used when yAxisScaling is 'fixed' or 'percentage'
|
|
|
|
@property({ type: Number })
|
|
public autoScrollInterval: number = 1000; // Auto-scroll interval in milliseconds (0 to disable)
|
|
|
|
private resizeObserver: ResizeObserver;
|
|
private resizeTimeout: number;
|
|
private internalChartData: ApexAxisChartSeries = [];
|
|
private autoScrollTimer: number | null = null;
|
|
|
|
constructor() {
|
|
super();
|
|
domtools.elementBasic.setup();
|
|
|
|
this.resizeObserver = new ResizeObserver((entries) => {
|
|
// Debounce resize calls to prevent excessive updates
|
|
if (this.resizeTimeout) {
|
|
clearTimeout(this.resizeTimeout);
|
|
}
|
|
|
|
this.resizeTimeout = window.setTimeout(() => {
|
|
for (let entry of entries) {
|
|
if (entry.target.classList.contains('mainbox') && this.chart) {
|
|
this.resizeChart();
|
|
}
|
|
}
|
|
}, 100); // 100ms debounce
|
|
});
|
|
|
|
this.registerStartupFunction(async () => {
|
|
this.updateComplete.then(() => {
|
|
const mainbox = this.shadowRoot.querySelector('.mainbox');
|
|
if (mainbox) {
|
|
this.resizeObserver.observe(mainbox);
|
|
}
|
|
});
|
|
});
|
|
|
|
this.registerGarbageFunction(async () => {
|
|
if (this.resizeTimeout) {
|
|
clearTimeout(this.resizeTimeout);
|
|
}
|
|
this.resizeObserver.disconnect();
|
|
this.stopAutoScroll();
|
|
});
|
|
}
|
|
|
|
public static styles = [
|
|
cssManager.defaultStyles,
|
|
css`
|
|
:host {
|
|
font-family: 'Geist Sans', sans-serif;
|
|
color: #ccc;
|
|
font-weight: 600;
|
|
font-size: 12px;
|
|
}
|
|
.mainbox {
|
|
position: relative;
|
|
width: 100%;
|
|
height: 400px;
|
|
background: #111;
|
|
border-radius: 8px;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.chartTitle {
|
|
position: absolute;
|
|
top: 0;
|
|
left: 0;
|
|
width: 100%;
|
|
text-align: center;
|
|
padding-top: 16px;
|
|
z-index: 10;
|
|
}
|
|
.chartContainer {
|
|
position: absolute;
|
|
top: 0px;
|
|
left: 0px;
|
|
bottom: 0px;
|
|
right: 0px;
|
|
padding: 32px 16px 16px 0px;
|
|
overflow: hidden;
|
|
}
|
|
`,
|
|
];
|
|
|
|
public render(): TemplateResult {
|
|
return html`
|
|
<div class="mainbox">
|
|
<div class="chartTitle">${this.label}</div>
|
|
<div class="chartContainer"></div>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
public async firstUpdated() {
|
|
await this.domtoolsPromise;
|
|
|
|
// Wait for next animation frame to ensure layout is complete
|
|
await new Promise(resolve => requestAnimationFrame(resolve));
|
|
|
|
// Get actual dimensions of the container
|
|
const mainbox: HTMLDivElement = this.shadowRoot.querySelector('.mainbox');
|
|
const chartContainer: HTMLDivElement = this.shadowRoot.querySelector('.chartContainer');
|
|
|
|
if (!mainbox || !chartContainer) {
|
|
console.error('Chart containers not found');
|
|
return;
|
|
}
|
|
|
|
// Calculate initial dimensions
|
|
const styleChartContainer = window.getComputedStyle(chartContainer);
|
|
const paddingTop = parseInt(styleChartContainer.paddingTop, 10);
|
|
const paddingBottom = parseInt(styleChartContainer.paddingBottom, 10);
|
|
const paddingLeft = parseInt(styleChartContainer.paddingLeft, 10);
|
|
const paddingRight = parseInt(styleChartContainer.paddingRight, 10);
|
|
|
|
const initialWidth = mainbox.clientWidth - paddingLeft - paddingRight;
|
|
const initialHeight = mainbox.offsetHeight - paddingTop - paddingBottom;
|
|
|
|
// Use provided series data or default demo data
|
|
const chartSeries = this.series.length > 0 ? this.series : [
|
|
{
|
|
name: 'cpu',
|
|
data: [
|
|
{ x: '2025-01-15T03:00:00', y: 25 },
|
|
{ x: '2025-01-15T07:00:00', y: 30 },
|
|
{ x: '2025-01-15T11:00:00', y: 20 },
|
|
{ x: '2025-01-15T15:00:00', y: 35 },
|
|
{ x: '2025-01-15T19:00:00', y: 25 },
|
|
],
|
|
},
|
|
{
|
|
name: 'memory',
|
|
data: [
|
|
{ x: '2025-01-15T03:00:00', y: 10 },
|
|
{ x: '2025-01-15T07:00:00', y: 12 },
|
|
{ x: '2025-01-15T11:00:00', y: 10 },
|
|
{ x: '2025-01-15T15:00:00', y: 30 },
|
|
{ x: '2025-01-15T19:00:00', y: 40 },
|
|
],
|
|
},
|
|
];
|
|
|
|
// Store internal data
|
|
this.internalChartData = chartSeries;
|
|
|
|
var options: ApexCharts.ApexOptions = {
|
|
series: chartSeries,
|
|
chart: {
|
|
width: initialWidth || 100, // Use actual width or fallback
|
|
height: initialHeight || 100, // Use actual height or fallback
|
|
type: 'area',
|
|
toolbar: {
|
|
show: false, // This line disables the toolbar
|
|
},
|
|
animations: {
|
|
enabled: !this.realtimeMode, // Disable animations in realtime mode
|
|
speed: 400,
|
|
animateGradually: {
|
|
enabled: false, // Disable gradual animation for cleaner updates
|
|
delay: 0
|
|
},
|
|
dynamicAnimation: {
|
|
enabled: !this.realtimeMode,
|
|
speed: 350
|
|
}
|
|
},
|
|
},
|
|
dataLabels: {
|
|
enabled: false,
|
|
},
|
|
stroke: {
|
|
width: 1,
|
|
curve: 'smooth',
|
|
},
|
|
xaxis: {
|
|
type: 'datetime', // Time-series data
|
|
labels: {
|
|
format: 'HH:mm:ss', // Time formatting with seconds
|
|
datetimeUTC: false,
|
|
style: {
|
|
colors: '#9e9e9e', // Label color
|
|
fontSize: '11px',
|
|
},
|
|
},
|
|
axisBorder: {
|
|
show: false, // Hide x-axis border
|
|
},
|
|
axisTicks: {
|
|
show: false, // Hide x-axis ticks
|
|
},
|
|
},
|
|
yaxis: {
|
|
min: 0,
|
|
max: this.yAxisScaling === 'dynamic' ? undefined : this.yAxisMax,
|
|
labels: {
|
|
formatter: this.yAxisFormatter,
|
|
style: {
|
|
colors: '#9e9e9e', // Label color
|
|
fontSize: '12px',
|
|
},
|
|
},
|
|
axisBorder: {
|
|
show: false, // Hide y-axis border
|
|
},
|
|
axisTicks: {
|
|
show: false, // Hide y-axis ticks
|
|
},
|
|
},
|
|
tooltip: {
|
|
shared: true, // Enables the tooltip to display across series
|
|
intersect: false, // Allows hovering anywhere on the chart
|
|
followCursor: true, // Makes tooltip follow mouse even between points
|
|
x: {
|
|
format: 'dd/MM/yy HH:mm',
|
|
},
|
|
custom: function ({ series, dataPointIndex, w }: any) {
|
|
// Iterate through each series and get its value
|
|
let tooltipContent = `<div style="padding: 10px; background: #1e1e2f; color: white; border-radius: 5px;">`;
|
|
|
|
series.forEach((s: number[], index: number) => {
|
|
const label = w.globals.seriesNames[index]; // Get series label
|
|
const value = s[dataPointIndex]; // Get value at data point
|
|
tooltipContent += `<strong>${label}:</strong> ${value} Mbps<br/>`;
|
|
});
|
|
|
|
tooltipContent += `</div>`;
|
|
return tooltipContent;
|
|
},
|
|
},
|
|
grid: {
|
|
xaxis: {
|
|
lines: {
|
|
show: true, // This enables the grid lines along the x-axis
|
|
},
|
|
},
|
|
yaxis: {
|
|
lines: {
|
|
show: true,
|
|
},
|
|
},
|
|
borderColor: '#333', // Set the color of the grid lines
|
|
strokeDashArray: 0, // Solid line
|
|
row: {
|
|
colors: [], // This can be used to alternate the shading of the horizontal rows
|
|
opacity: 0.1,
|
|
},
|
|
column: {
|
|
colors: [], // For vertical column bands, not needed here but available for customization
|
|
opacity: 0.1,
|
|
},
|
|
},
|
|
fill: {
|
|
type: 'gradient', // Gradient fill for the area
|
|
gradient: {
|
|
shade: 'dark',
|
|
type: 'vertical',
|
|
gradientToColors: ['#9c27b0'], // Gradient color ending
|
|
stops: [0, 100],
|
|
},
|
|
},
|
|
};
|
|
this.chart = new ApexCharts(this.shadowRoot.querySelector('.chartContainer'), options);
|
|
await this.chart.render();
|
|
|
|
// Give the chart a moment to fully initialize before resizing
|
|
await new Promise(resolve => setTimeout(resolve, 100));
|
|
await this.resizeChart();
|
|
}
|
|
|
|
public async updated(changedProperties: Map<string, any>) {
|
|
super.updated(changedProperties);
|
|
|
|
// Update chart if series data changes
|
|
if (changedProperties.has('series') && this.chart && this.series.length > 0) {
|
|
await this.updateSeries(this.series);
|
|
}
|
|
|
|
// Update y-axis formatter if it changes
|
|
if (changedProperties.has('yAxisFormatter') && this.chart) {
|
|
await this.chart.updateOptions({
|
|
yaxis: {
|
|
labels: {
|
|
formatter: this.yAxisFormatter,
|
|
},
|
|
},
|
|
});
|
|
}
|
|
|
|
// Handle realtime mode changes
|
|
if (changedProperties.has('realtimeMode') && this.chart) {
|
|
await this.chart.updateOptions({
|
|
chart: {
|
|
animations: {
|
|
enabled: !this.realtimeMode,
|
|
speed: 400,
|
|
animateGradually: {
|
|
enabled: false,
|
|
delay: 0
|
|
},
|
|
dynamicAnimation: {
|
|
enabled: !this.realtimeMode,
|
|
speed: 350
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
// Start/stop auto-scroll based on realtime mode
|
|
if (this.realtimeMode && this.rollingWindow > 0 && this.autoScrollInterval > 0) {
|
|
this.startAutoScroll();
|
|
} else {
|
|
this.stopAutoScroll();
|
|
}
|
|
}
|
|
|
|
// Handle auto-scroll interval changes
|
|
if (changedProperties.has('autoScrollInterval') && this.chart) {
|
|
this.stopAutoScroll();
|
|
if (this.realtimeMode && this.rollingWindow > 0 && this.autoScrollInterval > 0) {
|
|
this.startAutoScroll();
|
|
}
|
|
}
|
|
|
|
// Handle y-axis scaling changes
|
|
if ((changedProperties.has('yAxisScaling') || changedProperties.has('yAxisMax')) && this.chart) {
|
|
await this.chart.updateOptions({
|
|
yaxis: {
|
|
min: 0,
|
|
max: this.yAxisScaling === 'dynamic' ? undefined : this.yAxisMax
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
public async updateSeries(newSeries: ApexAxisChartSeries, animate: boolean = true) {
|
|
if (!this.chart) {
|
|
return;
|
|
}
|
|
|
|
// Store the new data first
|
|
this.internalChartData = newSeries;
|
|
|
|
// Handle rolling window if enabled
|
|
if (this.rollingWindow > 0 && this.realtimeMode) {
|
|
const now = Date.now();
|
|
const cutoffTime = now - this.rollingWindow;
|
|
|
|
// Filter data to only include points within the rolling window
|
|
const filteredSeries = newSeries.map(series => ({
|
|
name: series.name,
|
|
data: (series.data as any[]).filter(point => {
|
|
if (typeof point === 'object' && point !== null && 'x' in point) {
|
|
return new Date(point.x).getTime() > cutoffTime;
|
|
}
|
|
return false;
|
|
})
|
|
}));
|
|
|
|
// Only update if we have data
|
|
if (filteredSeries.some(s => s.data.length > 0)) {
|
|
// Handle y-axis scaling first
|
|
if (this.yAxisScaling === 'dynamic') {
|
|
const allValues = filteredSeries.flatMap(s => (s.data as any[]).map(d => d.y));
|
|
if (allValues.length > 0) {
|
|
const maxValue = Math.max(...allValues);
|
|
const dynamicMax = Math.ceil(maxValue * 1.1);
|
|
await this.chart.updateOptions({
|
|
yaxis: {
|
|
min: 0,
|
|
max: dynamicMax
|
|
}
|
|
}, false, false);
|
|
}
|
|
}
|
|
|
|
this.chart.updateSeries(filteredSeries, false);
|
|
}
|
|
} else {
|
|
this.chart.updateSeries(newSeries, animate);
|
|
}
|
|
}
|
|
|
|
// New method to update just the x-axis for smooth scrolling
|
|
public async updateTimeWindow() {
|
|
if (!this.chart || this.rollingWindow <= 0) {
|
|
return;
|
|
}
|
|
|
|
const now = Date.now();
|
|
const cutoffTime = now - this.rollingWindow;
|
|
|
|
await this.chart.updateOptions({
|
|
xaxis: {
|
|
min: cutoffTime,
|
|
max: now,
|
|
labels: {
|
|
format: 'HH:mm:ss',
|
|
datetimeUTC: false,
|
|
style: {
|
|
colors: '#9e9e9e',
|
|
fontSize: '11px',
|
|
},
|
|
},
|
|
tickAmount: 6,
|
|
}
|
|
}, false, false);
|
|
}
|
|
|
|
public async appendData(newData: { data: any[] }[]) {
|
|
if (!this.chart) {
|
|
return;
|
|
}
|
|
|
|
// Use ApexCharts' appendData method for smoother real-time updates
|
|
this.chart.appendData(newData);
|
|
}
|
|
|
|
public async updateOptions(options: ApexCharts.ApexOptions, redrawPaths?: boolean, animate?: boolean) {
|
|
if (!this.chart) {
|
|
return;
|
|
}
|
|
|
|
return this.chart.updateOptions(options, redrawPaths, animate);
|
|
}
|
|
|
|
public async resizeChart() {
|
|
if (!this.chart) {
|
|
return;
|
|
}
|
|
|
|
const mainbox: HTMLDivElement = this.shadowRoot.querySelector('.mainbox');
|
|
const chartContainer: HTMLDivElement = this.shadowRoot.querySelector('.chartContainer');
|
|
|
|
if (!mainbox || !chartContainer) {
|
|
return;
|
|
}
|
|
|
|
// Get computed style of the element
|
|
const styleChartContainer = window.getComputedStyle(chartContainer);
|
|
|
|
// Extract padding values
|
|
const paddingTop = parseInt(styleChartContainer.paddingTop, 10);
|
|
const paddingBottom = parseInt(styleChartContainer.paddingBottom, 10);
|
|
const paddingLeft = parseInt(styleChartContainer.paddingLeft, 10);
|
|
const paddingRight = parseInt(styleChartContainer.paddingRight, 10);
|
|
|
|
// Calculate the actual width and height to use, subtracting padding
|
|
const actualWidth = mainbox.clientWidth - paddingLeft - paddingRight;
|
|
const actualHeight = mainbox.offsetHeight - paddingTop - paddingBottom;
|
|
|
|
await this.chart.updateOptions({
|
|
chart: {
|
|
width: actualWidth,
|
|
height: actualHeight,
|
|
},
|
|
});
|
|
}
|
|
|
|
private startAutoScroll() {
|
|
if (this.autoScrollTimer) {
|
|
return; // Already running
|
|
}
|
|
|
|
this.autoScrollTimer = window.setInterval(() => {
|
|
this.updateTimeWindow();
|
|
}, this.autoScrollInterval);
|
|
}
|
|
|
|
private stopAutoScroll() {
|
|
if (this.autoScrollTimer) {
|
|
window.clearInterval(this.autoScrollTimer);
|
|
this.autoScrollTimer = null;
|
|
}
|
|
}
|
|
}
|