@smartnet360/svelte-components 0.0.3 → 0.0.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,83 +1,73 @@
1
1
  <svelte:options runes={true} />
2
2
 
3
3
  <script lang="ts">
4
- import { onMount } from 'svelte';
4
+ import { onMount, createEventDispatcher } from 'svelte';
5
5
  import Plotly from 'plotly.js-dist-min';
6
6
  import type { Chart as ChartModel, ChartMarker } from './charts.model.js';
7
- import { createTimeSeriesTrace, getYAxisTitle } from './data-utils.js';
7
+ import { createTimeSeriesTrace, getYAxisTitle, createDefaultPlotlyLayout } from './data-utils.js';
8
8
  import { adaptPlotlyLayout, addMarkersToLayout, type ContainerSize } from './adapt.js';
9
9
 
10
+ const dispatch = createEventDispatcher<{
11
+ chartcontextmenu: {
12
+ chart: ChartModel;
13
+ sectionId?: string;
14
+ clientX: number;
15
+ clientY: number;
16
+ };
17
+ }>();
18
+
10
19
  interface Props {
11
20
  chart: ChartModel;
12
21
  data: any[];
13
22
  markers?: ChartMarker[]; // Global markers for all charts
14
23
  plotlyLayout?: any; // Optional custom Plotly layout for styling/theming
15
24
  enableAdaptation?: boolean; // Enable size-based adaptations (default: true)
25
+ sectionId?: string;
16
26
  }
17
27
 
18
- let { chart, data, markers, plotlyLayout, enableAdaptation = true }: Props = $props();
28
+ let { chart, data, markers, plotlyLayout, enableAdaptation = true, sectionId }: Props = $props();
19
29
 
20
30
  // Chart container div and state
21
31
  let chartDiv: HTMLElement;
22
32
  let containerSize = $state<ContainerSize>({ width: 0, height: 0 });
23
33
 
34
+ function handleContextMenu(event: MouseEvent) {
35
+ event.preventDefault();
36
+ dispatch('chartcontextmenu', {
37
+ chart,
38
+ sectionId,
39
+ clientX: event.clientX,
40
+ clientY: event.clientY
41
+ });
42
+ }
43
+
24
44
  function renderChart() {
25
45
  if (!chartDiv || !data?.length) return;
26
46
 
27
47
  const traces: any[] = [];
48
+ let colorIndex = 0;
28
49
 
29
50
  // Add left Y-axis traces
30
51
  chart.yLeft.forEach(kpi => {
31
- const trace = createTimeSeriesTrace(data, kpi, 'TIMESTAMP', 'y1');
52
+ const trace = createTimeSeriesTrace(data, kpi, 'TIMESTAMP', 'y1', colorIndex);
32
53
  traces.push(trace);
54
+ colorIndex++;
33
55
  });
34
56
 
35
57
  // Add right Y-axis traces
36
58
  chart.yRight.forEach(kpi => {
37
- const trace = createTimeSeriesTrace(data, kpi, 'TIMESTAMP', 'y2');
59
+ const trace = createTimeSeriesTrace(data, kpi, 'TIMESTAMP', 'y2', colorIndex);
38
60
  traces.push(trace);
61
+ colorIndex++;
39
62
  });
40
63
 
41
- // Create default modern layout
42
- const defaultLayout: any = {
43
- title: {
44
- text: chart.title,
45
- font: {
46
- size: 16,
47
- color: '#2c3e50',
48
- weight: 600
49
- },
50
- x: 0.5,
51
- xanchor: 'center'
52
- },
53
- showlegend: true,
54
- legend: {
55
- x: 1,
56
- y: 1,
57
- xanchor: 'right',
58
- yanchor: 'top',
59
- font: { size: 12 }
60
- },
61
- xaxis: {
62
- showgrid: true,
63
- gridcolor: '#ecf0f1',
64
- linecolor: '#bdc3c7',
65
- tickfont: { size: 11 }
66
- },
67
- yaxis: {
68
- title: {
69
- text: getYAxisTitle(chart.yLeft),
70
- font: { size: 12, color: '#7f8c8d' }
71
- },
72
- showgrid: true,
73
- gridcolor: '#ecf0f1',
74
- linecolor: '#bdc3c7',
75
- tickfont: { size: 11 }
76
- },
77
- margin: { l: 60, r: 60, t: 60, b: 50 },
78
- paper_bgcolor: 'rgba(0,0,0,0)',
79
- plot_bgcolor: 'rgba(0,0,0,0)',
80
- font: { family: 'Inter, -apple-system, BlinkMacSystemFont, sans-serif' }
64
+ // Create default modern layout using the centralized function
65
+ const defaultLayout: any = createDefaultPlotlyLayout(chart.title);
66
+
67
+ // Override specific properties for this chart
68
+ defaultLayout.yaxis.title = {
69
+ text: getYAxisTitle(chart.yLeft),
70
+ font: { size: 12, color: '#7f8c8d' }
81
71
  };
82
72
 
83
73
  // Add second Y-axis if we have right-side KPIs
@@ -167,7 +157,7 @@
167
157
  });
168
158
  </script>
169
159
 
170
- <div class="chart-card">
160
+ <div class="chart-card" role="group" oncontextmenu={handleContextMenu}>
171
161
  <div
172
162
  bind:this={chartDiv}
173
163
  class="chart-container"
@@ -5,7 +5,30 @@ interface Props {
5
5
  markers?: ChartMarker[];
6
6
  plotlyLayout?: any;
7
7
  enableAdaptation?: boolean;
8
+ sectionId?: string;
8
9
  }
9
- declare const ChartCard: import("svelte").Component<Props, {}, "">;
10
- type ChartCard = ReturnType<typeof ChartCard>;
10
+ interface $$__sveltets_2_IsomorphicComponent<Props extends Record<string, any> = any, Events extends Record<string, any> = any, Slots extends Record<string, any> = any, Exports = {}, Bindings = string> {
11
+ new (options: import('svelte').ComponentConstructorOptions<Props>): import('svelte').SvelteComponent<Props, Events, Slots> & {
12
+ $$bindings?: Bindings;
13
+ } & Exports;
14
+ (internal: unknown, props: Props & {
15
+ $$events?: Events;
16
+ $$slots?: Slots;
17
+ }): Exports & {
18
+ $set?: any;
19
+ $on?: any;
20
+ };
21
+ z_$$bindings?: Bindings;
22
+ }
23
+ declare const ChartCard: $$__sveltets_2_IsomorphicComponent<Props, {
24
+ chartcontextmenu: CustomEvent<{
25
+ chart: ChartModel;
26
+ sectionId?: string;
27
+ clientX: number;
28
+ clientY: number;
29
+ }>;
30
+ } & {
31
+ [evt: string]: CustomEvent<any>;
32
+ }, {}, {}, "">;
33
+ type ChartCard = InstanceType<typeof ChartCard>;
11
34
  export default ChartCard;
@@ -1,7 +1,8 @@
1
1
  <svelte:options runes={true} />
2
2
 
3
3
  <script lang="ts">
4
- import type { Layout, Mode, ChartMarker } from './charts.model.js';
4
+ import { onMount } from 'svelte';
5
+ import type { Layout, Mode, ChartMarker, Section, ChartGrid, Chart } from './charts.model.js';
5
6
  import ChartCard from './ChartCard.svelte';
6
7
 
7
8
  interface Props {
@@ -13,14 +14,197 @@
13
14
  enableAdaptation?: boolean; // Enable size-based adaptations
14
15
  }
15
16
 
17
+ const GRID_DIMENSIONS: Record<ChartGrid, { rows: number; columns: number }> = {
18
+ '2x2': { rows: 2, columns: 2 },
19
+ '3x3': { rows: 3, columns: 3 },
20
+ '1x2': { rows: 1, columns: 2 },
21
+ '1x4': { rows: 1, columns: 4 },
22
+ '1x8': { rows: 1, columns: 8 }
23
+ };
24
+
25
+ const DEFAULT_GRID: ChartGrid = '2x2';
26
+
27
+ function getGridConfig(grid: ChartGrid | undefined) {
28
+ const key = grid && GRID_DIMENSIONS[grid] ? grid : DEFAULT_GRID;
29
+ return { key, ...GRID_DIMENSIONS[key] };
30
+ }
31
+
32
+ interface ContextMenuDetail {
33
+ chart: Chart;
34
+ sectionId?: string;
35
+ clientX: number;
36
+ clientY: number;
37
+ }
38
+
39
+ interface ContextMenuState {
40
+ visible: boolean;
41
+ x: number;
42
+ y: number;
43
+ chart: Chart | null;
44
+ section: Section | null;
45
+ }
46
+
47
+ interface ZoomState {
48
+ chart: Chart;
49
+ section: Section;
50
+ }
51
+
16
52
  let { layout, data, mode, markers, plotlyLayout, enableAdaptation = true }: Props = $props();
17
53
 
18
54
  // Internal tab state management
19
55
  let activeTabId = $state(layout.sections[0]?.id || '');
56
+
57
+ let componentElement: HTMLDivElement;
58
+ let contextMenuElement = $state<HTMLDivElement | null>(null);
59
+
60
+ const contextMenu = $state<ContextMenuState>({
61
+ visible: false,
62
+ x: 0,
63
+ y: 0,
64
+ chart: null,
65
+ section: null
66
+ });
67
+
68
+ let zoomedChart = $state<ZoomState | null>(null);
69
+
70
+ const MENU_WIDTH = 168;
71
+ const MENU_HEIGHT = 96;
72
+
73
+ function computeMenuPosition(clientX: number, clientY: number) {
74
+ if (!componentElement) {
75
+ return { x: clientX, y: clientY };
76
+ }
77
+
78
+ const rect = componentElement.getBoundingClientRect();
79
+ let x = clientX - rect.left;
80
+ let y = clientY - rect.top;
81
+
82
+ const maxX = Math.max(0, rect.width - MENU_WIDTH);
83
+ const maxY = Math.max(0, rect.height - MENU_HEIGHT);
84
+
85
+ x = Math.min(Math.max(0, x), maxX);
86
+ y = Math.min(Math.max(0, y), maxY);
87
+
88
+ return { x, y };
89
+ }
90
+
91
+ function closeContextMenu() {
92
+ contextMenu.visible = false;
93
+ contextMenu.chart = null;
94
+ contextMenu.section = null;
95
+ }
96
+
97
+ function handleChartContextMenu(detail: ContextMenuDetail, section: Section) {
98
+ const { x, y } = computeMenuPosition(detail.clientX, detail.clientY);
99
+
100
+ contextMenu.visible = true;
101
+ contextMenu.x = x;
102
+ contextMenu.y = y;
103
+ contextMenu.chart = detail.chart;
104
+ contextMenu.section = section;
105
+ }
106
+
107
+ function zoomSelectedChart() {
108
+ if (contextMenu.chart && contextMenu.section) {
109
+ zoomedChart = { chart: contextMenu.chart, section: contextMenu.section };
110
+ }
111
+ closeContextMenu();
112
+ }
113
+
114
+ function exitZoom() {
115
+ zoomedChart = null;
116
+ closeContextMenu();
117
+ }
118
+
119
+ function handleOverlayKeydown(event: KeyboardEvent) {
120
+ if (event.key === 'Enter' || event.key === ' ') {
121
+ event.preventDefault();
122
+ exitZoom();
123
+ }
124
+ }
125
+
126
+ function handleOverlayClick(event: MouseEvent) {
127
+ if (event.target === event.currentTarget) {
128
+ exitZoom();
129
+ }
130
+ }
131
+
132
+ onMount(() => {
133
+ const handleKeydown = (event: KeyboardEvent) => {
134
+ if (event.key === 'Escape') {
135
+ if (zoomedChart) {
136
+ exitZoom();
137
+ }
138
+ closeContextMenu();
139
+ }
140
+ };
141
+
142
+ const handleGlobalClick = (event: MouseEvent) => {
143
+ if (!contextMenu.visible) return;
144
+ const target = event.target as Node;
145
+ if (contextMenuElement && contextMenuElement.contains(target)) return;
146
+ closeContextMenu();
147
+ };
148
+
149
+ window.addEventListener('keydown', handleKeydown);
150
+ window.addEventListener('click', handleGlobalClick);
151
+
152
+ return () => {
153
+ window.removeEventListener('keydown', handleKeydown);
154
+ window.removeEventListener('click', handleGlobalClick);
155
+ };
156
+ });
20
157
  </script>
21
158
 
22
- <div class="chart-component">
23
- {#if mode === 'tabs'}
159
+ <!-- Reusable chart grid snippet -->
160
+ {#snippet chartGrid(section: Section)}
161
+ {@const gridConfig = getGridConfig(section.grid)}
162
+ <div
163
+ class="chart-grid"
164
+ data-grid={gridConfig.key}
165
+ style:grid-template-columns={`repeat(${gridConfig.columns}, minmax(0, 1fr))`}
166
+ style:grid-template-rows={`repeat(${gridConfig.rows}, minmax(0, 1fr))`}
167
+ >
168
+ {#each section.charts as chart}
169
+ <div class="chart-slot">
170
+ <ChartCard
171
+ {chart}
172
+ {data}
173
+ {markers}
174
+ {plotlyLayout}
175
+ {enableAdaptation}
176
+ sectionId={section.id}
177
+ on:chartcontextmenu={(event) => handleChartContextMenu(event.detail, section)}
178
+ />
179
+ </div>
180
+ {/each}
181
+ </div>
182
+ {/snippet}
183
+
184
+ <div class="chart-component" bind:this={componentElement}>
185
+ {#if zoomedChart}
186
+ {@const activeZoom = zoomedChart as ZoomState}
187
+ <div
188
+ class="zoom-overlay"
189
+ role="button"
190
+ tabindex="0"
191
+ onclick={handleOverlayClick}
192
+ onkeydown={handleOverlayKeydown}
193
+ >
194
+ <div class="zoom-container">
195
+ <button type="button" class="zoom-close" onclick={exitZoom} aria-label="Exit zoom">×</button>
196
+ <ChartCard
197
+ chart={activeZoom.chart}
198
+ {data}
199
+ {markers}
200
+ {plotlyLayout}
201
+ {enableAdaptation}
202
+ sectionId={activeZoom.section.id}
203
+ on:chartcontextmenu={(event) => handleChartContextMenu(event.detail, activeZoom.section)}
204
+ />
205
+ </div>
206
+ </div>
207
+ {:else if mode === 'tabs'}
24
208
  <!-- Tab Mode with Navigation -->
25
209
  <div class="tabs-container">
26
210
  <!-- Tab Navigation -->
@@ -49,14 +233,8 @@
49
233
  class="tab-section {section.id === activeTabId ? 'active' : 'hidden'}"
50
234
  data-section-id="{section.id}"
51
235
  >
52
- <!-- 2x2 Grid -->
53
- <div class="chart-grid">
54
- {#each section.charts as chart}
55
- <div class="chart-slot">
56
- <ChartCard {chart} {data} {plotlyLayout} />
57
- </div>
58
- {/each}
59
- </div>
236
+ <!-- Chart Grid -->
237
+ {@render chartGrid(section)}
60
238
  </div>
61
239
  {/each}
62
240
  </div>
@@ -79,19 +257,33 @@
79
257
  <div class="scrollspy-content">
80
258
  {#each layout.sections as section}
81
259
  <div class="section-content" id="{section.id}">
82
- <!-- 2x2 Grid -->
83
- <div class="chart-grid">
84
- {#each section.charts as chart}
85
- <div class="chart-slot">
86
- <ChartCard {chart} {data} {markers} {plotlyLayout} {enableAdaptation} />
87
- </div>
88
- {/each}
89
- </div>
260
+ <!-- Chart Grid -->
261
+ {@render chartGrid(section)}
90
262
  </div>
91
263
  {/each}
92
264
  </div>
93
265
  </div>
94
266
  {/if}
267
+
268
+ {#if contextMenu.visible}
269
+ <div
270
+ class="chart-context-menu"
271
+ style:top={`${contextMenu.y}px`}
272
+ style:left={`${contextMenu.x}px`}
273
+ bind:this={contextMenuElement}
274
+ >
275
+ {#if (!zoomedChart || zoomedChart.chart !== contextMenu.chart) && contextMenu.chart && contextMenu.section}
276
+ <button type="button" class="menu-item" onclick={zoomSelectedChart}>
277
+ Zoom to chart
278
+ </button>
279
+ {/if}
280
+ {#if zoomedChart}
281
+ <button type="button" class="menu-item" onclick={exitZoom}>
282
+ Exit zoom
283
+ </button>
284
+ {/if}
285
+ </div>
286
+ {/if}
95
287
  </div>
96
288
 
97
289
  <style>
@@ -101,6 +293,7 @@
101
293
  height: 100%;
102
294
  display: flex;
103
295
  flex-direction: column;
296
+ position: relative;
104
297
  }
105
298
 
106
299
  /* Tab Mode */
@@ -191,16 +384,97 @@
191
384
  margin-bottom: 0.5rem; /* Reduce margin */
192
385
  }
193
386
 
194
- /* 2x2 Chart Grid - responsive to container */
387
+ .chart-context-menu {
388
+ position: absolute;
389
+ display: flex;
390
+ flex-direction: column;
391
+ gap: 0.25rem;
392
+ padding: 0.5rem;
393
+ background: #ffffff;
394
+ border: 1px solid rgba(0, 0, 0, 0.1);
395
+ border-radius: 6px;
396
+ box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
397
+ z-index: 20;
398
+ min-width: 140px;
399
+ }
400
+
401
+ .chart-context-menu .menu-item {
402
+ background: none;
403
+ border: none;
404
+ text-align: left;
405
+ padding: 0.375rem 0.5rem;
406
+ border-radius: 4px;
407
+ font-size: 0.875rem;
408
+ cursor: pointer;
409
+ }
410
+
411
+ .chart-context-menu .menu-item:hover {
412
+ background-color: #f1f3f5;
413
+ }
414
+
415
+ .zoom-overlay {
416
+ position: absolute;
417
+ top: 0;
418
+ left: 0;
419
+ right: 0;
420
+ bottom: 0;
421
+ display: flex;
422
+ align-items: center;
423
+ justify-content: center;
424
+ background: rgba(0, 0, 0, 0.4);
425
+ z-index: 15;
426
+ padding: 1.5rem;
427
+ box-sizing: border-box;
428
+ }
429
+
430
+ .zoom-container {
431
+ position: relative;
432
+ width: 100%;
433
+ height: 100%;
434
+ max-width: 1200px;
435
+ max-height: 100%;
436
+ display: flex;
437
+ flex-direction: column;
438
+ }
439
+
440
+ .zoom-container :global(.chart-card) {
441
+ flex: 1;
442
+ }
443
+
444
+ .zoom-close {
445
+ position: absolute;
446
+ top: 0.5rem;
447
+ right: 0.5rem;
448
+ background: rgba(0, 0, 0, 0.6);
449
+ color: #ffffff;
450
+ border: none;
451
+ width: 2rem;
452
+ height: 2rem;
453
+ border-radius: 50%;
454
+ cursor: pointer;
455
+ font-size: 1.25rem;
456
+ line-height: 1;
457
+ display: flex;
458
+ align-items: center;
459
+ justify-content: center;
460
+ transition: background 0.2s ease;
461
+ z-index: 1;
462
+ }
463
+
464
+ .zoom-close:hover {
465
+ background: rgba(0, 0, 0, 0.75);
466
+ }
467
+
468
+ /* Chart grid adapts to layout configuration */
195
469
  .chart-grid {
196
470
  display: grid;
197
- grid-template-columns: 1fr 1fr;
198
- grid-template-rows: 1fr 1fr;
199
471
  gap: 0.25rem; /* Reduce gap */
200
472
  width: 100%;
201
473
  height: 100%;
202
474
  min-height: 0; /* Remove fixed minimum to allow full flexibility */
203
475
  max-height: 100%; /* Ensure it doesn't exceed container */
476
+ grid-auto-rows: minmax(0, 1fr);
477
+ grid-auto-columns: minmax(0, 1fr);
204
478
  }
205
479
 
206
480
  .chart-slot {
@@ -223,4 +497,4 @@
223
497
  min-height: 80px; /* Even smaller minimum for grid */
224
498
  max-height: 100%; /* Constrain to slot */
225
499
  }
226
- </style>
500
+ </style>
@@ -90,7 +90,10 @@ export function adaptPlotlyLayout(baseLayout, containerSize, chartInfo, config =
90
90
  // Adaptive legend font size
91
91
  if (adaptedLayout.legend?.font && adaptedLayout.showlegend) {
92
92
  if (isSmall) {
93
- adaptedLayout.legend.font.size = 10;
93
+ adaptedLayout.legend.font.size = 9;
94
+ }
95
+ else {
96
+ adaptedLayout.legend.font.size = 11;
94
97
  }
95
98
  }
96
99
  return adaptedLayout;
@@ -154,24 +157,22 @@ export function createMarkerAnnotations(markers, containerSize, enableAdaptation
154
157
  else if (sizeCategory === 'large')
155
158
  fontSize = 10;
156
159
  }
157
- // Adaptive label positioning
158
- const yPosition = sizeCategory === 'small' ? 0.95 : 0.9;
159
160
  return {
160
161
  x: marker.date,
161
- y: yPosition,
162
+ y: 1, // Top of chart area
162
163
  yref: 'paper',
163
164
  text: marker.label,
164
- showarrow: true,
165
- arrowhead: 2,
166
- arrowcolor: marker.color || '#ff0000',
167
- arrowsize: sizeCategory === 'small' ? 0.8 : 1,
165
+ showarrow: false, // Remove arrow
166
+ yanchor: 'bottom', // Anchor to bottom so text appears below the top line
167
+ xanchor: 'center', // Center the text horizontally
168
168
  font: {
169
169
  size: fontSize,
170
170
  color: marker.color || '#ff0000'
171
171
  },
172
- bgcolor: 'rgba(255,255,255,0.8)',
172
+ bgcolor: 'rgba(255,255,255,0.9)', // More opaque background
173
173
  bordercolor: marker.color || '#ff0000',
174
- borderwidth: 1
174
+ borderwidth: 1,
175
+ borderpad: 2
175
176
  };
176
177
  });
177
178
  }
@@ -1,15 +1,15 @@
1
1
  export type Scale = "percent" | "absolute";
2
- export type Aggregation = "avg" | "max" | "min" | "sum";
3
2
  export interface KPI {
4
3
  rawName: string;
5
4
  name: string;
6
5
  scale: Scale;
7
6
  unit: string;
8
7
  color?: string;
9
- aggregation: Aggregation;
10
8
  }
9
+ export type ChartPosition = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9;
10
+ export type ChartGrid = "2x2" | "3x3" | "1x2" | "1x4" | "1x8";
11
11
  export interface Chart {
12
- pos: 1 | 2 | 3 | 4;
12
+ pos?: ChartPosition;
13
13
  title: string;
14
14
  yLeft: KPI[];
15
15
  yRight: KPI[];
@@ -17,7 +17,8 @@ export interface Chart {
17
17
  export interface Section {
18
18
  id: string;
19
19
  title: string;
20
- charts: [Chart, Chart, Chart, Chart];
20
+ charts: Chart[];
21
+ grid?: ChartGrid;
21
22
  }
22
23
  export type Mode = "tabs" | "scrollspy";
23
24
  export interface Layout {
@@ -1,13 +1,6 @@
1
1
  import type { KPI } from './charts.model.js';
2
- export type AggregationType = 'avg' | 'sum' | 'max' | 'min';
3
- export declare function aggregateValue(values: number[], aggregation: AggregationType): number;
4
- export declare function processKPIData(data: any[], kpi: KPI): {
5
- values: number[];
6
- aggregated: number;
7
- };
8
- export declare function createTimeSeriesTrace(data: any[], kpi: KPI, timestampField?: string, yaxis?: 'y1' | 'y2'): any;
9
- export declare function createBarTrace(data: any[], kpi: KPI, timestampField?: string, yaxis?: 'y1' | 'y2'): any;
10
- export declare function createPieTrace(data: any[], kpi: KPI, timestampField?: string): any;
2
+ export declare function processKPIData(data: any[], kpi: KPI): number[];
3
+ export declare function createTimeSeriesTrace(data: any[], kpi: KPI, timestampField?: string, yaxis?: 'y1' | 'y2', colorIndex?: number): any;
11
4
  export declare function getYAxisTitle(kpis: KPI[]): string;
12
5
  export declare function formatValue(value: number, scale: 'percent' | 'absolute', unit: string): string;
13
6
  export declare function createDefaultPlotlyLayout(title?: string): any;
@@ -1,77 +1,46 @@
1
- export function aggregateValue(values, aggregation) {
2
- switch (aggregation) {
3
- case 'avg':
4
- return values.reduce((sum, val) => sum + val, 0) / values.length;
5
- case 'sum':
6
- return values.reduce((sum, val) => sum + val, 0);
7
- case 'max':
8
- return Math.max(...values);
9
- case 'min':
10
- return Math.min(...values);
11
- default:
12
- return values[0] || 0;
13
- }
14
- }
1
+ // Modern color palette similar to Chart.js
2
+ const modernColors = [
3
+ '#3B82F6', // Blue
4
+ '#EF4444', // Red
5
+ '#10B981', // Emerald
6
+ '#F59E0B', // Amber
7
+ '#8B5CF6', // Violet
8
+ '#06B6D4', // Cyan
9
+ '#F97316', // Orange
10
+ '#84CC16', // Lime
11
+ '#EC4899', // Pink
12
+ '#6B7280' // Gray
13
+ ];
15
14
  export function processKPIData(data, kpi) {
16
- const rawValues = data
15
+ return data
17
16
  .map(row => {
18
17
  const val = row[kpi.rawName];
19
18
  return typeof val === 'number' ? val : parseFloat(val);
20
19
  })
21
20
  .filter(val => !isNaN(val));
22
- const aggregated = aggregateValue(rawValues, kpi.aggregation);
23
- return {
24
- values: rawValues,
25
- aggregated
26
- };
27
21
  }
28
- export function createTimeSeriesTrace(data, kpi, timestampField = 'TIMESTAMP', yaxis = 'y1') {
29
- const processed = processKPIData(data, kpi);
22
+ export function createTimeSeriesTrace(data, kpi, timestampField = 'TIMESTAMP', yaxis = 'y1', colorIndex = 0) {
23
+ const values = processKPIData(data, kpi);
30
24
  const timestamps = data.map(row => row[timestampField]);
25
+ // Use KPI color if provided, otherwise cycle through modern colors
26
+ const traceColor = kpi.color || modernColors[colorIndex % modernColors.length];
31
27
  return {
32
28
  x: timestamps,
33
- y: processed.values,
29
+ y: values,
34
30
  type: 'scatter',
35
- mode: 'lines+markers',
31
+ mode: 'lines', // Only lines, no markers
36
32
  name: kpi.name,
37
33
  yaxis: yaxis,
38
34
  line: {
39
- color: kpi.color || '#1f77b4',
40
- width: 2
35
+ color: traceColor,
36
+ width: 3,
37
+ shape: 'spline',
38
+ smoothing: 0.3,
39
+ dash: yaxis === 'y1' ? 'solid' : 'dot' // Y1 = solid, Y2 = dotted
41
40
  },
42
- marker: {
43
- size: 4
44
- }
45
- };
46
- }
47
- export function createBarTrace(data, kpi, timestampField = 'TIMESTAMP', yaxis = 'y1') {
48
- const processed = processKPIData(data, kpi);
49
- const timestamps = data.map(row => row[timestampField]);
50
- return {
51
- x: timestamps,
52
- y: processed.values,
53
- type: 'bar',
54
- name: kpi.name,
55
- yaxis: yaxis,
56
- marker: {
57
- color: kpi.color || '#1f77b4'
58
- }
59
- };
60
- }
61
- export function createPieTrace(data, kpi, timestampField = 'TIMESTAMP') {
62
- const processed = processKPIData(data, kpi);
63
- const timestamps = data.map(row => row[timestampField]);
64
- // For pie charts, we'll show the latest value
65
- const latestValue = processed.values[processed.values.length - 1];
66
- const latestTimestamp = timestamps[timestamps.length - 1];
67
- return {
68
- values: [latestValue],
69
- labels: [kpi.name],
70
- type: 'pie',
71
- name: `${kpi.name} (${latestTimestamp})`,
72
- marker: {
73
- colors: [kpi.color || '#1f77b4']
74
- }
41
+ hovertemplate: `<b>${kpi.name}</b><br>` +
42
+ `Value: %{y:,.2f} ${kpi.unit}<br>` +
43
+ '<extra></extra>'
75
44
  };
76
45
  }
77
46
  export function getYAxisTitle(kpis) {
@@ -105,7 +74,14 @@ export function createDefaultPlotlyLayout(title) {
105
74
  y: 1,
106
75
  xanchor: 'right',
107
76
  yanchor: 'top',
108
- font: { size: 12 }
77
+ font: {
78
+ family: 'Inter, Segoe UI, Tahoma, Geneva, Verdana, sans-serif',
79
+ size: 11,
80
+ color: '#6B7280'
81
+ },
82
+ bgcolor: 'rgba(255,255,255,0.9)',
83
+ bordercolor: '#6B7280',
84
+ borderwidth: 1
109
85
  },
110
86
  xaxis: {
111
87
  title: {
@@ -115,7 +91,9 @@ export function createDefaultPlotlyLayout(title) {
115
91
  showgrid: true,
116
92
  gridcolor: '#ecf0f1',
117
93
  linecolor: '#bdc3c7',
118
- tickfont: { size: 11 }
94
+ tickfont: { size: 11 },
95
+ tickformat: '%m-%d',
96
+ hoverformat: '%Y-%m-%d'
119
97
  },
120
98
  yaxis: {
121
99
  showgrid: true,
@@ -126,6 +104,16 @@ export function createDefaultPlotlyLayout(title) {
126
104
  margin: { l: 60, r: 60, t: 60, b: 50 },
127
105
  paper_bgcolor: 'rgba(0,0,0,0)',
128
106
  plot_bgcolor: 'rgba(0,0,0,0)',
129
- font: { family: 'Inter, -apple-system, BlinkMacSystemFont, sans-serif' }
107
+ font: { family: 'Inter, -apple-system, BlinkMacSystemFont, sans-serif' },
108
+ hovermode: 'x unified',
109
+ hoverlabel: {
110
+ font: {
111
+ family: 'Inter, Segoe UI, Tahoma, Geneva, Verdana, sans-serif',
112
+ size: 11,
113
+ color: '#6B7280'
114
+ },
115
+ bgcolor: 'rgba(255,255,255,0.9)',
116
+ bordercolor: '#6B7280'
117
+ }
130
118
  };
131
119
  }
@@ -1,7 +1,6 @@
1
1
  export { default as ChartComponent } from './ChartComponent.svelte';
2
2
  export { default as ChartCard } from './ChartCard.svelte';
3
- export type { Layout, Section, Chart, KPI, Mode, Scale, Aggregation, ChartMarker } from './charts.model.js';
4
- export { createTimeSeriesTrace, createBarTrace, createPieTrace, getYAxisTitle, formatValue, aggregateValue, processKPIData, createDefaultPlotlyLayout } from './data-utils.js';
5
- export type { AggregationType } from './data-utils.js';
3
+ export type { Layout, Section, Chart, KPI, Mode, Scale, ChartMarker, ChartGrid, ChartPosition } from './charts.model.js';
4
+ export { createTimeSeriesTrace, getYAxisTitle, formatValue, processKPIData, createDefaultPlotlyLayout } from './data-utils.js';
6
5
  export { adaptPlotlyLayout, getSizeCategory, createMarkerShapes, createMarkerAnnotations, addMarkersToLayout } from './adapt.js';
7
6
  export type { ContainerSize, ChartInfo, AdaptationConfig } from './adapt.js';
@@ -1,4 +1,4 @@
1
1
  export { default as ChartComponent } from './ChartComponent.svelte';
2
2
  export { default as ChartCard } from './ChartCard.svelte';
3
- export { createTimeSeriesTrace, createBarTrace, createPieTrace, getYAxisTitle, formatValue, aggregateValue, processKPIData, createDefaultPlotlyLayout } from './data-utils.js';
3
+ export { createTimeSeriesTrace, getYAxisTitle, formatValue, processKPIData, createDefaultPlotlyLayout } from './data-utils.js';
4
4
  export { adaptPlotlyLayout, getSizeCategory, createMarkerShapes, createMarkerAnnotations, addMarkersToLayout } from './adapt.js';
@@ -4,28 +4,31 @@
4
4
  * Utility functions to help parent applications implement grid launching functionality.
5
5
  * These are optional helpers - parents can implement their own launch logic.
6
6
  */
7
+ const createLaunchKey = () => `grid-viewer-${Date.now()}-${Math.random().toString(36).slice(2, 11)}`;
8
+ const snapshotComponents = (components) => components.map(({ id, name, icon, description, category }) => ({
9
+ id,
10
+ name,
11
+ icon,
12
+ description,
13
+ category
14
+ }));
15
+ const persistGridConfiguration = (grid, componentConfigs) => {
16
+ const key = createLaunchKey();
17
+ const payload = {
18
+ grid,
19
+ componentConfigs
20
+ };
21
+ localStorage.setItem(key, JSON.stringify(payload));
22
+ return key;
23
+ };
7
24
  /**
8
25
  * Create a launch handler that opens grids in new tabs using localStorage
9
26
  * This recreates the previous built-in behavior as a reusable helper
10
27
  */
11
28
  export function createWindowLauncher(gridViewerUrl, availableComponents) {
29
+ const componentSnapshot = snapshotComponents(availableComponents);
12
30
  return (grid) => {
13
- // Create temporary storage key
14
- const key = `grid-viewer-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
15
- // Store only the grid configuration and component IDs - not the actual components
16
- const data = {
17
- grid,
18
- // Store component configs without the actual component objects
19
- componentConfigs: availableComponents.map(comp => ({
20
- id: comp.id,
21
- name: comp.name,
22
- icon: comp.icon,
23
- description: comp.description,
24
- category: comp.category
25
- }))
26
- };
27
- localStorage.setItem(key, JSON.stringify(data));
28
- // Open new tab with the key parameter
31
+ const key = persistGridConfiguration(grid, componentSnapshot);
29
32
  const url = `${gridViewerUrl}?key=${key}`;
30
33
  const newTab = window.open(url, '_blank');
31
34
  // Focus the new tab if it opened successfully
@@ -49,21 +52,9 @@ export function createTabLauncher(gridViewerUrl, availableComponents) {
49
52
  * Create a launch handler that uses direct URL navigation (same tab)
50
53
  */
51
54
  export function createNavigationLauncher(gridViewerUrl, availableComponents) {
55
+ const componentSnapshot = snapshotComponents(availableComponents);
52
56
  return (grid) => {
53
- // Create temporary storage key
54
- const key = `grid-viewer-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
55
- const data = {
56
- grid,
57
- componentConfigs: availableComponents.map(comp => ({
58
- id: comp.id,
59
- name: comp.name,
60
- icon: comp.icon,
61
- description: comp.description,
62
- category: comp.category
63
- }))
64
- };
65
- localStorage.setItem(key, JSON.stringify(data));
66
- // Navigate to the grid viewer in the same tab
57
+ const key = persistGridConfiguration(grid, componentSnapshot);
67
58
  window.location.href = `${gridViewerUrl}?key=${key}`;
68
59
  };
69
60
  }
package/dist/index.d.ts CHANGED
@@ -1,2 +1,2 @@
1
1
  export { Half, Quarter, ResizeHandle, resizeStore, Desktop, GridSelector, GridRenderer, GridViewer, type ComponentConfig, getAllLayouts, getLayoutById, getLayoutsByCategory, getLayoutsOrdered, validateComponentAssignments, createGridConfiguration, type GridLayoutDefinition, type GridSlot, type ComponentAssignment, type GridConfiguration, createWindowLauncher, createTabLauncher, createNavigationLauncher, createModalLauncher } from './Desktop/index.js';
2
- export { ChartComponent, ChartCard, type Layout, type Section, type Chart, type KPI, type Mode, type Scale, type Aggregation, createTimeSeriesTrace, createBarTrace, createPieTrace, getYAxisTitle, formatValue, aggregateValue, processKPIData, createDefaultPlotlyLayout, type AggregationType } from './Charts/index.js';
2
+ export { ChartComponent, ChartCard, type Layout, type Section, type Chart, type KPI, type Mode, type Scale, type ChartMarker, createTimeSeriesTrace, getYAxisTitle, formatValue, processKPIData, createDefaultPlotlyLayout } from './Charts/index.js';
package/dist/index.js CHANGED
@@ -1,2 +1,2 @@
1
1
  export { Half, Quarter, ResizeHandle, resizeStore, Desktop, GridSelector, GridRenderer, GridViewer, getAllLayouts, getLayoutById, getLayoutsByCategory, getLayoutsOrdered, validateComponentAssignments, createGridConfiguration, createWindowLauncher, createTabLauncher, createNavigationLauncher, createModalLauncher } from './Desktop/index.js';
2
- export { ChartComponent, ChartCard, createTimeSeriesTrace, createBarTrace, createPieTrace, getYAxisTitle, formatValue, aggregateValue, processKPIData, createDefaultPlotlyLayout } from './Charts/index.js';
2
+ export { ChartComponent, ChartCard, createTimeSeriesTrace, getYAxisTitle, formatValue, processKPIData, createDefaultPlotlyLayout } from './Charts/index.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@smartnet360/svelte-components",
3
- "version": "0.0.3",
3
+ "version": "0.0.10",
4
4
  "scripts": {
5
5
  "dev": "vite dev",
6
6
  "build": "vite build && npm run prepack",
@@ -11,9 +11,9 @@
11
11
  "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
12
12
  "format": "prettier --write .",
13
13
  "lint": "prettier --check . && eslint .",
14
- "release:patch": "npm version patch && npm publish --access public",
15
- "release:minor": "npm version minor && npm publish --access public",
16
- "release:major": "npm version major && npm publish --access public"
14
+ "release:patch": "npm version patch && npm publish --access public --no-browser",
15
+ "release:minor": "npm version minor && npm publish --access public --no-browser",
16
+ "release:major": "npm version major && npm publish --access public --no-browser"
17
17
  },
18
18
  "files": [
19
19
  "dist",
@@ -32,8 +32,8 @@
32
32
  }
33
33
  },
34
34
  "peerDependencies": {
35
- "svelte": "^5.0.0",
36
- "plotly.js-dist-min": "^3.1.0"
35
+ "plotly.js-dist-min": "^3.1.0",
36
+ "svelte": "^5.0.0"
37
37
  },
38
38
  "devDependencies": {
39
39
  "@eslint/compat": "^1.2.5",