diff --git a/package.json b/package.json index 9af8c45..2ce6fbc 100644 --- a/package.json +++ b/package.json @@ -43,7 +43,7 @@ "@git.zone/tswatch": "^2.0.37", "@push.rocks/projectinfo": "^5.0.2", "@push.rocks/tapbundle": "^6.0.3", - "@types/node": "^24.0.0" + "@types/node": "^22.0.0" }, "files": [ "ts/**/*", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b88e8cb..37e7bc9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -88,8 +88,8 @@ importers: specifier: ^6.0.3 version: 6.0.3(@aws-sdk/credential-providers@3.826.0)(socks@2.8.5) '@types/node': - specifier: ^24.0.0 - version: 24.0.0 + specifier: ^22.0.0 + version: 22.15.31 packages: @@ -1609,8 +1609,8 @@ packages: '@types/node-forge@1.3.11': resolution: {integrity: sha512-FQx220y22OKNTqaByeBGqHWYz4cl94tpcxeFdvBo3wjG6XPBuZ0BNgNZRV5J5TFmmcsJ4IzsLkmGRiQbnYsBEQ==} - '@types/node@24.0.0': - resolution: {integrity: sha512-yZQa2zm87aRVcqDyH5+4Hv9KYgSdgwX1rFnGvpbzMaC7YAljmhBET93TPiTd3ObwTL+gSpIzPKg5BqVxdCvxKg==} + '@types/node@22.15.31': + resolution: {integrity: sha512-jnVe5ULKl6tijxUhvQeNbQG/84fHfg+yMak02cT8QVhBx/F05rAVxCGBYYTh2EKz22D6JF5ktXuNwdx7b9iEGw==} '@types/parse5@6.0.3': resolution: {integrity: sha512-SuT16Q1K51EAVPz1K29DJ/sXjhSQ0zjvsypYJ6tlwVsRV9jwW5Adq2ch8Dq8kDBCkYnELS7N7VNCSB5nC56t/g==} @@ -4265,8 +4265,8 @@ packages: resolution: {integrity: sha512-ZPtzy0hu4cZjv3z5NW9gfKnNLjoz4y6uv4HlelAjDK7sY/xOkKZv9xK/WQpcsBB3jEybChz9DPC2U/+cusjJVQ==} engines: {node: '>=18'} - undici-types@7.8.0: - resolution: {integrity: sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw==} + undici-types@6.21.0: + resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} unified@11.0.5: resolution: {integrity: sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==} @@ -5504,7 +5504,7 @@ snapshots: '@jest/schemas': 29.6.3 '@types/istanbul-lib-coverage': 2.0.6 '@types/istanbul-reports': 3.0.4 - '@types/node': 24.0.0 + '@types/node': 22.15.31 '@types/yargs': 17.0.33 chalk: 4.1.2 @@ -6932,19 +6932,19 @@ snapshots: '@types/accepts@1.3.7': dependencies: - '@types/node': 24.0.0 + '@types/node': 22.15.31 '@types/babel__code-frame@7.0.6': {} '@types/body-parser@1.19.5': dependencies: '@types/connect': 3.4.38 - '@types/node': 24.0.0 + '@types/node': 22.15.31 '@types/body-parser@1.19.6': dependencies: '@types/connect': 3.4.38 - '@types/node': 24.0.0 + '@types/node': 22.15.31 '@types/buffer-json@2.0.3': {} @@ -6960,17 +6960,17 @@ snapshots: '@types/clean-css@4.2.11': dependencies: - '@types/node': 24.0.0 + '@types/node': 22.15.31 source-map: 0.6.1 '@types/co-body@6.1.3': dependencies: - '@types/node': 24.0.0 + '@types/node': 22.15.31 '@types/qs': 6.14.0 '@types/connect@3.4.38': dependencies: - '@types/node': 24.0.0 + '@types/node': 22.15.31 '@types/content-disposition@0.5.9': {} @@ -6981,11 +6981,11 @@ snapshots: '@types/connect': 3.4.38 '@types/express': 5.0.3 '@types/keygrip': 1.0.6 - '@types/node': 24.0.0 + '@types/node': 22.15.31 '@types/cors@2.8.17': dependencies: - '@types/node': 24.0.0 + '@types/node': 22.15.31 '@types/debounce@1.2.4': {} @@ -6999,7 +6999,7 @@ snapshots: '@types/express-serve-static-core@5.0.6': dependencies: - '@types/node': 24.0.0 + '@types/node': 22.15.31 '@types/qs': 6.9.18 '@types/range-parser': 1.2.7 '@types/send': 0.17.4 @@ -7022,30 +7022,30 @@ snapshots: '@types/from2@2.3.5': dependencies: - '@types/node': 24.0.0 + '@types/node': 22.15.31 '@types/fs-extra@11.0.4': dependencies: '@types/jsonfile': 6.1.4 - '@types/node': 24.0.0 + '@types/node': 22.15.31 '@types/fs-extra@9.0.13': dependencies: - '@types/node': 24.0.0 + '@types/node': 22.15.31 '@types/glob@7.2.0': dependencies: '@types/minimatch': 5.1.2 - '@types/node': 24.0.0 + '@types/node': 22.15.31 '@types/glob@8.1.0': dependencies: '@types/minimatch': 5.1.2 - '@types/node': 24.0.0 + '@types/node': 22.15.31 '@types/gunzip-maybe@1.4.2': dependencies: - '@types/node': 24.0.0 + '@types/node': 22.15.31 '@types/hast@3.0.4': dependencies: @@ -7081,7 +7081,7 @@ snapshots: '@types/jsonfile@6.1.4': dependencies: - '@types/node': 24.0.0 + '@types/node': 22.15.31 '@types/keygrip@1.0.6': {} @@ -7098,7 +7098,7 @@ snapshots: '@types/http-errors': 2.0.5 '@types/keygrip': 1.0.6 '@types/koa-compose': 3.2.8 - '@types/node': 24.0.0 + '@types/node': 22.15.31 '@types/mdast@4.0.4': dependencies: @@ -7116,11 +7116,11 @@ snapshots: '@types/node-forge@1.3.11': dependencies: - '@types/node': 24.0.0 + '@types/node': 22.15.31 - '@types/node@24.0.0': + '@types/node@22.15.31': dependencies: - undici-types: 7.8.0 + undici-types: 6.21.0 '@types/parse5@6.0.3': {} @@ -7138,30 +7138,30 @@ snapshots: '@types/s3rver@3.7.4': dependencies: - '@types/node': 24.0.0 + '@types/node': 22.15.31 '@types/semver@7.7.0': {} '@types/send@0.17.4': dependencies: '@types/mime': 1.3.5 - '@types/node': 24.0.0 + '@types/node': 22.15.31 '@types/send@0.17.5': dependencies: '@types/mime': 1.3.5 - '@types/node': 24.0.0 + '@types/node': 22.15.31 '@types/serve-static@1.15.7': dependencies: '@types/http-errors': 2.0.4 - '@types/node': 24.0.0 + '@types/node': 22.15.31 '@types/send': 0.17.4 '@types/serve-static@1.15.8': dependencies: '@types/http-errors': 2.0.5 - '@types/node': 24.0.0 + '@types/node': 22.15.31 '@types/send': 0.17.5 '@types/sinon-chai@3.2.12': @@ -7181,11 +7181,11 @@ snapshots: '@types/tar-stream@2.2.3': dependencies: - '@types/node': 24.0.0 + '@types/node': 22.15.31 '@types/through2@2.0.41': dependencies: - '@types/node': 24.0.0 + '@types/node': 22.15.31 '@types/triple-beam@1.3.5': {} @@ -7209,18 +7209,18 @@ snapshots: '@types/whatwg-url@8.2.2': dependencies: - '@types/node': 24.0.0 + '@types/node': 22.15.31 '@types/webidl-conversions': 7.0.3 '@types/which@3.0.4': {} '@types/ws@7.4.7': dependencies: - '@types/node': 24.0.0 + '@types/node': 22.15.31 '@types/ws@8.18.1': dependencies: - '@types/node': 24.0.0 + '@types/node': 22.15.31 '@types/yargs-parser@21.0.3': {} @@ -7230,7 +7230,7 @@ snapshots: '@types/yauzl@2.10.3': dependencies: - '@types/node': 24.0.0 + '@types/node': 22.15.31 optional: true '@ungap/structured-clone@1.3.0': {} @@ -7857,7 +7857,7 @@ snapshots: engine.io@6.6.4: dependencies: '@types/cors': 2.8.17 - '@types/node': 24.0.0 + '@types/node': 22.15.31 accepts: 1.3.8 base64id: 2.0.0 cookie: 0.7.2 @@ -8643,7 +8643,7 @@ snapshots: jest-util@29.7.0: dependencies: '@jest/types': 29.6.3 - '@types/node': 24.0.0 + '@types/node': 22.15.31 chalk: 4.1.2 ci-info: 3.9.0 graceful-fs: 4.2.11 @@ -10217,7 +10217,7 @@ snapshots: uint8array-extras@1.4.0: {} - undici-types@7.8.0: {} + undici-types@6.21.0: {} unified@11.0.5: dependencies: diff --git a/readme.hints.md b/readme.hints.md index b77b61c..0d83bdc 100644 --- a/readme.hints.md +++ b/readme.hints.md @@ -9,9 +9,22 @@ - Fully functional area chart component using ApexCharts - Displays time-series data with gradient fills - Responsive with ResizeObserver (debounced to prevent flicker) -- Demo shows CPU and memory usage metrics - Fixed: Chart now properly respects container boundaries on initial render - Overflow prevention with proper CSS containment +- Enhanced demo features: + - Multiple dataset examples (System Usage, Network Traffic, Sales Analytics) + - Real-time data simulation with automatic updates + - Dynamic dataset switching + - Customizable Y-axis formatters (percentages, currency, units) + - Data randomization for testing + - Manual data point addition +- Properties: + - `label`: Chart title + - `series`: ApexAxisChartSeries data + - `yAxisFormatter`: Custom Y-axis label formatter function +- Methods: + - `updateSeries()`: Update chart data + - `appendData()`: Add new data points to existing series ### dees-chart-log - Server log viewer component (not a chart despite the name) diff --git a/ts_web/elements/dees-chart-area.demo.ts b/ts_web/elements/dees-chart-area.demo.ts index 50ec508..34f9fef 100644 --- a/ts_web/elements/dees-chart-area.demo.ts +++ b/ts_web/elements/dees-chart-area.demo.ts @@ -1,21 +1,285 @@ -import { html } from '@design.estate/dees-element'; +import { html, css } from '@design.estate/dees-element'; +import type { DeesChartArea } from './dees-chart-area.js'; export const demoFunc = () => { + let chartElement: DeesChartArea; + let intervalId: number; + let currentDataset = 'system'; + + // Get element reference after render + setTimeout(() => { + const charts = document.querySelectorAll('dees-chart-area'); + if (charts.length > 0) { + chartElement = charts[charts.length - 1] as DeesChartArea; + } + }, 100); + + // Y-axis formatters for different datasets + const formatters = { + system: (val: number) => `${val}%`, + network: (val: number) => `${val} Mbps`, + sales: (val: number) => `$${val.toLocaleString()}`, + }; + + // Different datasets to showcase + const datasets = { + system: { + label: 'System Usage (%)', + series: [ + { + name: 'CPU', + data: [ + { x: new Date(Date.now() - 300000).toISOString(), y: 25 }, + { x: new Date(Date.now() - 240000).toISOString(), y: 30 }, + { x: new Date(Date.now() - 180000).toISOString(), y: 28 }, + { x: new Date(Date.now() - 120000).toISOString(), y: 35 }, + { x: new Date(Date.now() - 60000).toISOString(), y: 32 }, + { x: new Date().toISOString(), y: 38 }, + ], + }, + { + name: 'Memory', + data: [ + { x: new Date(Date.now() - 300000).toISOString(), y: 45 }, + { x: new Date(Date.now() - 240000).toISOString(), y: 48 }, + { x: new Date(Date.now() - 180000).toISOString(), y: 46 }, + { x: new Date(Date.now() - 120000).toISOString(), y: 52 }, + { x: new Date(Date.now() - 60000).toISOString(), y: 50 }, + { x: new Date().toISOString(), y: 55 }, + ], + }, + ], + }, + network: { + label: 'Network Traffic (Mbps)', + series: [ + { + name: 'Download', + data: [ + { x: new Date(Date.now() - 300000).toISOString(), y: 120 }, + { x: new Date(Date.now() - 240000).toISOString(), y: 150 }, + { x: new Date(Date.now() - 180000).toISOString(), y: 180 }, + { x: new Date(Date.now() - 120000).toISOString(), y: 165 }, + { x: new Date(Date.now() - 60000).toISOString(), y: 190 }, + { x: new Date().toISOString(), y: 175 }, + ], + }, + { + name: 'Upload', + data: [ + { x: new Date(Date.now() - 300000).toISOString(), y: 25 }, + { x: new Date(Date.now() - 240000).toISOString(), y: 30 }, + { x: new Date(Date.now() - 180000).toISOString(), y: 35 }, + { x: new Date(Date.now() - 120000).toISOString(), y: 28 }, + { x: new Date(Date.now() - 60000).toISOString(), y: 32 }, + { x: new Date().toISOString(), y: 40 }, + ], + }, + ], + }, + sales: { + label: 'Sales Analytics', + series: [ + { + name: 'Revenue', + data: [ + { x: '2025-01-01', y: 45000 }, + { x: '2025-01-02', y: 52000 }, + { x: '2025-01-03', y: 48000 }, + { x: '2025-01-04', y: 61000 }, + { x: '2025-01-05', y: 58000 }, + { x: '2025-01-06', y: 65000 }, + ], + }, + { + name: 'Profit', + data: [ + { x: '2025-01-01', y: 12000 }, + { x: '2025-01-02', y: 14000 }, + { x: '2025-01-03', y: 11000 }, + { x: '2025-01-04', y: 18000 }, + { x: '2025-01-05', y: 16000 }, + { x: '2025-01-06', y: 20000 }, + ], + }, + ], + }, + }; + + // Generate random value within range + const getRandomValue = (min: number, max: number) => { + return Math.floor(Math.random() * (max - min + 1)) + min; + }; + + // Add real-time data + const addRealtimeData = () => { + if (!chartElement) return; + + const dataset = datasets[currentDataset]; + const newTimestamp = new Date().toISOString(); + + // Generate new data points based on dataset type + let newData: any[][] = []; + + if (currentDataset === 'system') { + newData = [ + [{ x: newTimestamp, y: getRandomValue(25, 45) }], // CPU + [{ x: newTimestamp, y: getRandomValue(45, 65) }], // Memory + ]; + } else if (currentDataset === 'network') { + newData = [ + [{ x: newTimestamp, y: getRandomValue(100, 250) }], // Download + [{ x: newTimestamp, y: getRandomValue(20, 50) }], // Upload + ]; + } + + // Keep only last 10 data points + const currentSeries = chartElement.series.map((series, index) => ({ + ...series, + data: [...series.data.slice(-9), ...(newData[index] || [])], + })); + + chartElement.series = currentSeries; + }; + + // Switch dataset + const switchDataset = (name: string) => { + currentDataset = name; + if (chartElement) { + const dataset = datasets[name]; + chartElement.label = dataset.label; + chartElement.series = dataset.series; + chartElement.yAxisFormatter = formatters[name]; + } + }; + + // Start/stop real-time updates + const startRealtime = () => { + if (!intervalId && (currentDataset === 'system' || currentDataset === 'network')) { + intervalId = window.setInterval(() => addRealtimeData(), 2000); + } + }; + + const stopRealtime = () => { + if (intervalId) { + window.clearInterval(intervalId); + intervalId = null; + } + }; + + // Randomize current data + const randomizeData = () => { + if (!chartElement) return; + + const currentSeries = chartElement.series.map(series => ({ + ...series, + data: series.data.map(point => ({ + ...point, + y: typeof point.y === 'number' + ? point.y * (0.8 + Math.random() * 0.4) // +/- 20% variation + : point.y, + })), + })); + + chartElement.series = currentSeries; + }; + return html`
- +
+
+ + switchDataset('system')} + type=${currentDataset === 'system' ? 'highlighted' : 'normal'} + >System Usage + switchDataset('network')} + type=${currentDataset === 'network' ? 'highlighted' : 'normal'} + >Network Traffic + switchDataset('sales')} + type=${currentDataset === 'sales' ? 'highlighted' : 'normal'} + >Sales Data +
+ +
+ + startRealtime()}>Start Live + stopRealtime()}>Stop Live +
+ +
+ + randomizeData()}>Randomize Values + addRealtimeData()}>Add Point +
+
+ +
+ +
+ +
+ Real-time updates work with System Usage and Network Traffic datasets • + Chart updates every 2 seconds when live mode is active • + Try switching datasets and randomizing values +
`; }; diff --git a/ts_web/elements/dees-chart-area.ts b/ts_web/elements/dees-chart-area.ts index 6ff6bcd..56b0e74 100644 --- a/ts_web/elements/dees-chart-area.ts +++ b/ts_web/elements/dees-chart-area.ts @@ -32,6 +32,12 @@ export class DeesChartArea extends DeesElement { @property() public label: string = 'Untitled Chart'; + @property({ type: Array }) + public series: ApexAxisChartSeries = []; + + @property({ type: Function }) + public yAxisFormatter: (value: number) => string = (val) => `${val} Mbps`; + private resizeObserver: ResizeObserver; private resizeTimeout: number; @@ -144,29 +150,32 @@ export class DeesChartArea extends DeesElement { 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 }, + ], + }, + ]; + var options: ApexCharts.ApexOptions = { - 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 }, - ], - }, - ], + series: chartSeries, chart: { width: initialWidth || 100, // Use actual width or fallback height: initialHeight || 100, // Use actual height or fallback @@ -209,9 +218,7 @@ export class DeesChartArea extends DeesElement { yaxis: { min: 0, labels: { - formatter: function (val: number) { - return `${val} Mbps`; // Format Y-axis labels - }, + formatter: this.yAxisFormatter, style: { colors: '#9e9e9e', // Label color fontSize: '12px', @@ -285,6 +292,46 @@ export class DeesChartArea extends DeesElement { 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, + }, + }, + }); + } + } + + public async updateSeries(newSeries: ApexAxisChartSeries) { + if (!this.chart) { + return; + } + + await this.chart.updateSeries(newSeries, true); + } + + public async appendData(seriesIndex: number, newData: any[]) { + if (!this.chart) { + return; + } + + const currentSeries = [...this.series]; + if (currentSeries[seriesIndex]) { + currentSeries[seriesIndex].data = [...currentSeries[seriesIndex].data, ...newData]; + await this.updateSeries(currentSeries); + } + } + public async resizeChart() { if (!this.chart) { return;