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`
${this.label}
`; } 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 = `
`; 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 += `${label}: ${value} Mbps
`; }); tooltipContent += `
`; 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) { 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; } } }