diff --git a/ts_web/elements/dees-chart-area.demo.ts b/ts_web/elements/dees-chart-area.demo.ts index 97aa610..694ed38 100644 --- a/ts_web/elements/dees-chart-area.demo.ts +++ b/ts_web/elements/dees-chart-area.demo.ts @@ -1,4 +1,4 @@ -import { html, css } from '@design.estate/dees-element'; +import { html, css, cssManager } from '@design.estate/dees-element'; import type { DeesChartArea } from './dees-chart-area.js'; import '@design.estate/dees-wcctools/demotools'; @@ -402,7 +402,7 @@ export const demoFunc = () => { ${css` .demoBox { position: relative; - background: #000000; + background: ${cssManager.bdTheme('hsl(0 0% 95%)', 'hsl(0 0% 9%)')}; height: 100%; width: 100%; padding: 40px; @@ -425,9 +425,9 @@ export const demoFunc = () => { } .info { - color: #666; - font-size: 11px; - font-family: 'Geist Sans', sans-serif; + color: ${cssManager.bdTheme('hsl(215.4 16.3% 56.9%)', 'hsl(215 20.2% 55.1%)')}; + font-size: 12px; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Geist Sans', sans-serif; text-align: center; margin-top: 8px; } diff --git a/ts_web/elements/dees-chart-area.ts b/ts_web/elements/dees-chart-area.ts index 13e70ce..4c0c453 100644 --- a/ts_web/elements/dees-chart-area.ts +++ b/ts_web/elements/dees-chart-area.ts @@ -61,6 +61,23 @@ export class DeesChartArea extends DeesElement { private resizeTimeout: number; private internalChartData: ApexAxisChartSeries = []; private autoScrollTimer: number | null = null; + private readonly DEBUG_RESIZE = false; // Set to true to enable resize debugging + + // Chart color schemes + private readonly CHART_COLORS = { + dark: [ + 'hsl(217.2 91.2% 59.8%)', // Blue + 'hsl(173.4 80.4% 40%)', // Teal + 'hsl(280.3 87.4% 66.7%)', // Purple + 'hsl(24.6 95% 53.1%)', // Orange + ], + light: [ + 'hsl(222.2 47.4% 51.2%)', // Blue (shadcn primary) + 'hsl(142.1 76.2% 36.3%)', // Green (shadcn success) + 'hsl(280.3 47.7% 50.2%)', // Purple (muted) + 'hsl(20.5 90.2% 48.2%)', // Orange (shadcn destructive variant) + ] + }; constructor() { super(); @@ -73,46 +90,72 @@ export class DeesChartArea extends DeesElement { } this.resizeTimeout = window.setTimeout(() => { - for (let entry of entries) { - if (entry.target.classList.contains('mainbox') && this.chart) { - this.resizeChart(); + // Simply resize if we have a chart, since we're only observing the mainbox + if (this.chart) { + // Log resize event for debugging + if (this.DEBUG_RESIZE && entries.length > 0) { + const entry = entries[0]; + console.log('DeesChartArea - Resize detected:', { + width: entry.contentRect.width, + height: entry.contentRect.height + }); } + this.resizeChart(); } }, 100); // 100ms debounce }); - this.registerStartupFunction(async () => { - this.updateComplete.then(() => { - const mainbox = this.shadowRoot.querySelector('.mainbox'); - if (mainbox) { - this.resizeObserver.observe(mainbox); - } - }); - }); + // Note: ResizeObserver is now set up after chart initialization in firstUpdated() + // to ensure proper timing and avoid race conditions this.registerGarbageFunction(async () => { if (this.resizeTimeout) { clearTimeout(this.resizeTimeout); } - this.resizeObserver.disconnect(); + if (this.resizeObserver) { + this.resizeObserver.disconnect(); + } this.stopAutoScroll(); + + // Critical: Destroy chart instance to prevent memory leak + if (this.chart) { + try { + this.chart.destroy(); + this.chart = null; + } catch (error) { + console.error('Error destroying chart:', error); + } + } }); } + + public async connectedCallback() { + super.connectedCallback(); + + // Trigger resize when element is connected to DOM + // This helps with dynamically added charts + if (this.chart) { + // Wait a frame for layout to settle + await new Promise(resolve => requestAnimationFrame(resolve)); + await this.resizeChart(); + } + } public static styles = [ cssManager.defaultStyles, css` :host { - font-family: 'Geist Sans', sans-serif; - color: #ccc; - font-weight: 600; - font-size: 12px; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif; + color: ${cssManager.bdTheme('hsl(0 0% 3.9%)', 'hsl(0 0% 98%)')}; + font-weight: 400; + font-size: 14px; } .mainbox { position: relative; width: 100%; height: 400px; - background: #111; + background: ${cssManager.bdTheme('hsl(0 0% 100%)', 'hsl(0 0% 3.9%)')}; + border: 1px solid ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 14.9%)')}; border-radius: 8px; overflow: hidden; } @@ -122,9 +165,13 @@ export class DeesChartArea extends DeesElement { top: 0; left: 0; width: 100%; - text-align: center; - padding-top: 16px; + text-align: left; + padding: 16px 24px; z-index: 10; + font-size: 14px; + font-weight: 500; + letter-spacing: -0.01em; + color: ${cssManager.bdTheme('hsl(0 0% 20%)', 'hsl(0 0% 63.9%)')}; } .chartContainer { position: absolute; @@ -132,8 +179,22 @@ export class DeesChartArea extends DeesElement { left: 0px; bottom: 0px; right: 0px; - padding: 32px 16px 16px 0px; + padding: 44px 16px 16px 0px; overflow: hidden; + background: transparent; /* Ensure container doesn't override chart background */ + } + + /* ApexCharts theme overrides */ + .apexcharts-canvas { + background: transparent !important; + } + + .apexcharts-inner { + background: transparent !important; + } + + .apexcharts-graphical { + background: transparent !important; } `, ]; @@ -199,12 +260,17 @@ export class DeesChartArea extends DeesElement { // Store internal data this.internalChartData = chartSeries; + // Get current theme + const isDark = !this.goBright; + const theme = isDark ? 'dark' : 'light'; + 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', + background: 'transparent', // Transparent background to inherit from container toolbar: { show: false, // This line disables the toolbar }, @@ -220,12 +286,18 @@ export class DeesChartArea extends DeesElement { speed: 350 } }, + zoom: { + enabled: false, // Disable zoom for cleaner interaction + }, + selection: { + enabled: false, // Disable selection + }, }, dataLabels: { enabled: false, }, stroke: { - width: 1, + width: 2, curve: 'smooth', }, xaxis: { @@ -234,8 +306,10 @@ export class DeesChartArea extends DeesElement { format: 'HH:mm:ss', // Time formatting with seconds datetimeUTC: false, style: { - colors: '#9e9e9e', // Label color - fontSize: '11px', + colors: [isDark ? 'hsl(0 0% 63.9%)' : 'hsl(0 0% 20%)'], // Label color + fontSize: '12px', + fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif', + fontWeight: '400', }, }, axisBorder: { @@ -251,8 +325,10 @@ export class DeesChartArea extends DeesElement { labels: { formatter: this.yAxisFormatter, style: { - colors: '#9e9e9e', // Label color + colors: [isDark ? 'hsl(0 0% 63.9%)' : 'hsl(0 0% 20%)'], // Label color fontSize: '12px', + fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif', + fontWeight: '400', }, }, axisBorder: { @@ -269,14 +345,30 @@ export class DeesChartArea extends DeesElement { x: { format: 'dd/MM/yy HH:mm', }, - custom: function ({ series, dataPointIndex, w }: any) { + custom: ({ series, dataPointIndex, w }: any) => { // Iterate through each series and get its value - let tooltipContent = `
`; + // Note: We can't access component instance here, so we'll use w.config.theme.mode + const currentTheme = w.config.theme.mode; + const isDarkMode = currentTheme === 'dark'; + const bgColor = isDarkMode ? 'hsl(0 0% 9%)' : 'hsl(0 0% 100%)'; + const textColor = isDarkMode ? 'hsl(0 0% 95%)' : 'hsl(0 0% 9%)'; + const borderColor = isDarkMode ? 'hsl(0 0% 14.9%)' : 'hsl(0 0% 89.8%)'; + + // Get formatter from chart config + const formatter = w.config.yaxis[0]?.labels?.formatter || ((val: number) => val.toString()); + + 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
`; + const color = w.globals.colors[index]; + const formattedValue = formatter(value); + tooltipContent += `
+ + ${label}: + ${formattedValue} +
`; }); tooltipContent += `
`; @@ -286,7 +378,7 @@ export class DeesChartArea extends DeesElement { grid: { xaxis: { lines: { - show: true, // This enables the grid lines along the x-axis + show: false, // Hide vertical grid lines for cleaner look }, }, yaxis: { @@ -294,38 +386,67 @@ export class DeesChartArea extends DeesElement { show: true, }, }, - borderColor: '#333', // Set the color of the grid lines + borderColor: isDark ? 'hsl(0 0% 14.9%)' : 'hsl(0 0% 94%)', // Very subtle 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, + padding: { + top: 10, + right: 20, + bottom: 10, + left: 20, }, }, fill: { type: 'gradient', // Gradient fill for the area gradient: { - shade: 'dark', + shade: isDark ? 'dark' : 'light', type: 'vertical', - gradientToColors: ['#9c27b0'], // Gradient color ending + shadeIntensity: 0.1, + opacityFrom: isDark ? 0.2 : 0.3, + opacityTo: 0, stops: [0, 100], }, }, + colors: isDark ? this.CHART_COLORS.dark : this.CHART_COLORS.light, + theme: { + mode: theme, + }, }; - 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(); + try { + 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(); + + // Ensure resize observer is watching the mainbox + const mainbox = this.shadowRoot.querySelector('.mainbox'); + if (mainbox && this.resizeObserver) { + // Disconnect any previous observations + this.resizeObserver.disconnect(); + // Start observing the mainbox + this.resizeObserver.observe(mainbox); + if (this.DEBUG_RESIZE) { + console.log('DeesChartArea - ResizeObserver attached to mainbox'); + } + } + } catch (error) { + console.error('Failed to initialize chart:', error); + // Optionally, you could set an error state here + // this.chartState = 'error'; + // this.errorMessage = 'Failed to initialize chart'; + } } public async updated(changedProperties: Map) { super.updated(changedProperties); + // Update chart theme when goBright changes + if (changedProperties.has('goBright') && this.chart) { + await this.updateChartTheme(); + } + // Update chart if series data changes if (changedProperties.has('series') && this.chart && this.series.length > 0) { await this.updateSeries(this.series); @@ -393,50 +514,55 @@ export class DeesChartArea extends DeesElement { 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; + try { + // Store the new data first + this.internalChartData = newSeries; - // 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); - } - } + // Handle rolling window if enabled + if (this.rollingWindow > 0 && this.realtimeMode) { + const now = Date.now(); + const cutoffTime = now - this.rollingWindow; - this.chart.updateSeries(filteredSeries, false); + // 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); + } + } + + await this.chart.updateSeries(filteredSeries, false); + } + } else { + await this.chart.updateSeries(newSeries, animate); } - } else { - this.chart.updateSeries(newSeries, animate); + } catch (error) { + console.error('Failed to update chart series:', error); } } - // New method to update just the x-axis for smooth scrolling + // Update just the x-axis for smooth scrolling in realtime mode + // Public for advanced usage in demos, but typically handled automatically public async updateTimeWindow() { if (!this.chart || this.rollingWindow <= 0) { return; @@ -453,8 +579,10 @@ export class DeesChartArea extends DeesElement { format: 'HH:mm:ss', datetimeUTC: false, style: { - colors: '#9e9e9e', - fontSize: '11px', + colors: [!this.goBright ? 'hsl(0 0% 63.9%)' : 'hsl(0 0% 20%)'], + fontSize: '12px', + fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif', + fontWeight: '400', }, }, tickAmount: 6, @@ -484,32 +612,61 @@ export class DeesChartArea extends DeesElement { return; } - const mainbox: HTMLDivElement = this.shadowRoot.querySelector('.mainbox'); - const chartContainer: HTMLDivElement = this.shadowRoot.querySelector('.chartContainer'); - - if (!mainbox || !chartContainer) { - return; + if (this.DEBUG_RESIZE) { + console.log('DeesChartArea - resizeChart called'); } + + try { + 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); + // Force layout recalculation + void mainbox.offsetHeight; - // 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); + // Get computed style of the element + const styleChartContainer = window.getComputedStyle(chartContainer); - // Calculate the actual width and height to use, subtracting padding - const actualWidth = mainbox.clientWidth - paddingLeft - paddingRight; - const actualHeight = mainbox.offsetHeight - paddingTop - paddingBottom; + // 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); - await this.chart.updateOptions({ - chart: { - width: actualWidth, - height: actualHeight, - }, - }); + // Calculate the actual width and height to use, subtracting padding + const actualWidth = mainbox.clientWidth - paddingLeft - paddingRight; + const actualHeight = mainbox.offsetHeight - paddingTop - paddingBottom; + + // Validate dimensions + if (actualWidth > 0 && actualHeight > 0) { + if (this.DEBUG_RESIZE) { + console.log('DeesChartArea - Updating chart dimensions:', { + width: actualWidth, + height: actualHeight + }); + } + + await this.chart.updateOptions({ + chart: { + width: actualWidth, + height: actualHeight, + }, + }, true, false); // Redraw paths but don't animate + } + } catch (error) { + console.error('Failed to resize chart:', error); + } + } + + /** + * Manually trigger a chart resize. Useful when automatic detection doesn't work. + * This is a convenience method that can be called from outside the component. + */ + public async forceResize() { + await this.resizeChart(); } private startAutoScroll() { @@ -528,4 +685,43 @@ export class DeesChartArea extends DeesElement { this.autoScrollTimer = null; } } + + private async updateChartTheme() { + if (!this.chart) { + return; + } + + const isDark = !this.goBright; + const theme = isDark ? 'dark' : 'light'; + + await this.chart.updateOptions({ + theme: { + mode: theme, + }, + colors: isDark ? this.CHART_COLORS.dark : this.CHART_COLORS.light, + xaxis: { + labels: { + style: { + colors: [isDark ? 'hsl(0 0% 63.9%)' : 'hsl(0 0% 20%)'], + }, + }, + }, + yaxis: { + labels: { + style: { + colors: [isDark ? 'hsl(0 0% 63.9%)' : 'hsl(0 0% 20%)'], + }, + }, + }, + grid: { + borderColor: isDark ? 'hsl(0 0% 14.9%)' : 'hsl(0 0% 94%)', + }, + fill: { + gradient: { + shade: isDark ? 'dark' : 'light', + opacityFrom: isDark ? 0.2 : 0.3, + }, + }, + }); + } }