@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.
- package/dist/Charts/ChartCard.svelte +36 -46
- package/dist/Charts/ChartCard.svelte.d.ts +25 -2
- package/dist/Charts/ChartComponent.svelte +297 -23
- package/dist/Charts/adapt.js +11 -10
- package/dist/Charts/charts.model.d.ts +5 -4
- package/dist/Charts/data-utils.d.ts +2 -9
- package/dist/Charts/data-utils.js +50 -62
- package/dist/Charts/index.d.ts +2 -3
- package/dist/Charts/index.js +1 -1
- package/dist/Desktop/launchHelpers.js +21 -30
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/package.json +6 -6
@@ -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
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
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
|
-
|
10
|
-
|
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
|
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
|
-
|
23
|
-
|
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
|
-
<!--
|
53
|
-
|
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
|
-
|
83
|
-
|
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
|
-
|
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>
|
package/dist/Charts/adapt.js
CHANGED
@@ -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 =
|
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:
|
162
|
+
y: 1, // Top of chart area
|
162
163
|
yref: 'paper',
|
163
164
|
text: marker.label,
|
164
|
-
showarrow:
|
165
|
-
|
166
|
-
|
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.
|
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
|
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: [
|
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
|
3
|
-
export declare function
|
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
|
-
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
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
|
-
|
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
|
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:
|
29
|
+
y: values,
|
34
30
|
type: 'scatter',
|
35
|
-
mode: 'lines
|
31
|
+
mode: 'lines', // Only lines, no markers
|
36
32
|
name: kpi.name,
|
37
33
|
yaxis: yaxis,
|
38
34
|
line: {
|
39
|
-
color:
|
40
|
-
width:
|
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
|
-
|
43
|
-
|
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: {
|
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
|
}
|
package/dist/Charts/index.d.ts
CHANGED
@@ -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,
|
4
|
-
export { createTimeSeriesTrace,
|
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';
|
package/dist/Charts/index.js
CHANGED
@@ -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,
|
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
|
-
|
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
|
-
|
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
|
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,
|
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
|
+
"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
|
-
"
|
36
|
-
"
|
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",
|