@smartnet360/svelte-components 0.0.1 → 0.0.2
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 +204 -0
- package/dist/Charts/ChartCard.svelte.d.ts +11 -0
- package/dist/Charts/ChartComponent.svelte +226 -0
- package/dist/Charts/ChartComponent.svelte.d.ts +12 -0
- package/dist/Charts/adapt.d.ts +37 -0
- package/dist/Charts/adapt.js +192 -0
- package/dist/Charts/charts.model.d.ts +34 -0
- package/dist/Charts/charts.model.js +1 -0
- package/dist/Charts/data-utils.d.ts +13 -0
- package/dist/Charts/data-utils.js +131 -0
- package/dist/Charts/index.d.ts +7 -0
- package/dist/Charts/index.js +4 -0
- package/dist/Charts/plotly.d.ts +4 -0
- package/dist/Desktop/Desktop.svelte +2 -0
- package/dist/Desktop/Desktop.svelte.d.ts +1 -0
- package/dist/Desktop/GridRenderer.svelte +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/package.json +5 -3
- package/dist/Desktop/Grid/README.md +0 -331
- package/dist/Desktop/README.md +0 -279
@@ -0,0 +1,204 @@
|
|
1
|
+
<svelte:options runes={true} />
|
2
|
+
|
3
|
+
<script lang="ts">
|
4
|
+
import { onMount } from 'svelte';
|
5
|
+
import Plotly from 'plotly.js-dist-min';
|
6
|
+
import type { Chart as ChartModel, ChartMarker } from './charts.model.js';
|
7
|
+
import { createTimeSeriesTrace, getYAxisTitle } from './data-utils.js';
|
8
|
+
import { adaptPlotlyLayout, addMarkersToLayout, type ContainerSize } from './adapt.js';
|
9
|
+
|
10
|
+
interface Props {
|
11
|
+
chart: ChartModel;
|
12
|
+
data: any[];
|
13
|
+
markers?: ChartMarker[]; // Global markers for all charts
|
14
|
+
plotlyLayout?: any; // Optional custom Plotly layout for styling/theming
|
15
|
+
enableAdaptation?: boolean; // Enable size-based adaptations (default: true)
|
16
|
+
}
|
17
|
+
|
18
|
+
let { chart, data, markers, plotlyLayout, enableAdaptation = true }: Props = $props();
|
19
|
+
|
20
|
+
// Chart container div and state
|
21
|
+
let chartDiv: HTMLElement;
|
22
|
+
let containerSize = $state<ContainerSize>({ width: 0, height: 0 });
|
23
|
+
|
24
|
+
function renderChart() {
|
25
|
+
if (!chartDiv || !data?.length) return;
|
26
|
+
|
27
|
+
const traces: any[] = [];
|
28
|
+
|
29
|
+
// Add left Y-axis traces
|
30
|
+
chart.yLeft.forEach(kpi => {
|
31
|
+
const trace = createTimeSeriesTrace(data, kpi, 'TIMESTAMP', 'y1');
|
32
|
+
traces.push(trace);
|
33
|
+
});
|
34
|
+
|
35
|
+
// Add right Y-axis traces
|
36
|
+
chart.yRight.forEach(kpi => {
|
37
|
+
const trace = createTimeSeriesTrace(data, kpi, 'TIMESTAMP', 'y2');
|
38
|
+
traces.push(trace);
|
39
|
+
});
|
40
|
+
|
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' }
|
81
|
+
};
|
82
|
+
|
83
|
+
// Add second Y-axis if we have right-side KPIs
|
84
|
+
if (chart.yRight.length > 0) {
|
85
|
+
defaultLayout.yaxis2 = {
|
86
|
+
title: {
|
87
|
+
text: getYAxisTitle(chart.yRight),
|
88
|
+
font: { size: 12, color: '#7f8c8d' }
|
89
|
+
},
|
90
|
+
overlaying: 'y',
|
91
|
+
side: 'right',
|
92
|
+
showgrid: false,
|
93
|
+
linecolor: '#bdc3c7',
|
94
|
+
tickfont: { size: 11 }
|
95
|
+
};
|
96
|
+
}
|
97
|
+
|
98
|
+
// Merge external layout with defaults, then apply size adaptations
|
99
|
+
let finalLayout = plotlyLayout ?
|
100
|
+
{ ...defaultLayout, ...plotlyLayout } :
|
101
|
+
defaultLayout;
|
102
|
+
|
103
|
+
// Apply size-based adaptations using helper
|
104
|
+
finalLayout = adaptPlotlyLayout(
|
105
|
+
finalLayout,
|
106
|
+
containerSize,
|
107
|
+
{
|
108
|
+
leftSeriesCount: chart.yLeft.length,
|
109
|
+
rightSeriesCount: chart.yRight.length
|
110
|
+
},
|
111
|
+
{ enableAdaptation }
|
112
|
+
);
|
113
|
+
|
114
|
+
// Add markers to the layout
|
115
|
+
finalLayout = addMarkersToLayout(finalLayout, markers || [], containerSize, enableAdaptation);
|
116
|
+
|
117
|
+
const config = {
|
118
|
+
responsive: true,
|
119
|
+
displayModeBar: false,
|
120
|
+
displaylogo: false
|
121
|
+
};
|
122
|
+
|
123
|
+
Plotly.newPlot(chartDiv, traces, finalLayout, config);
|
124
|
+
|
125
|
+
// Resize immediately after creation to ensure proper sizing
|
126
|
+
setTimeout(() => {
|
127
|
+
if (chartDiv) {
|
128
|
+
Plotly.Plots.resize(chartDiv);
|
129
|
+
}
|
130
|
+
}, 0);
|
131
|
+
}
|
132
|
+
|
133
|
+
onMount(() => {
|
134
|
+
// Initial container size measurement
|
135
|
+
if (chartDiv) {
|
136
|
+
const rect = chartDiv.getBoundingClientRect();
|
137
|
+
containerSize.width = rect.width;
|
138
|
+
containerSize.height = rect.height;
|
139
|
+
}
|
140
|
+
|
141
|
+
renderChart();
|
142
|
+
|
143
|
+
// Set up ResizeObserver to handle container size changes
|
144
|
+
if (chartDiv && window.ResizeObserver) {
|
145
|
+
const resizeObserver = new ResizeObserver((entries) => {
|
146
|
+
for (const entry of entries) {
|
147
|
+
const { width, height } = entry.contentRect;
|
148
|
+
|
149
|
+
// Update container size state
|
150
|
+
containerSize.width = width;
|
151
|
+
containerSize.height = height;
|
152
|
+
|
153
|
+
if (chartDiv && chartDiv.children.length > 0) {
|
154
|
+
// Re-render chart with new adaptive layout
|
155
|
+
renderChart();
|
156
|
+
}
|
157
|
+
}
|
158
|
+
});
|
159
|
+
|
160
|
+
resizeObserver.observe(chartDiv);
|
161
|
+
|
162
|
+
// Clean up observer on component destroy
|
163
|
+
return () => {
|
164
|
+
resizeObserver.disconnect();
|
165
|
+
};
|
166
|
+
}
|
167
|
+
});
|
168
|
+
</script>
|
169
|
+
|
170
|
+
<div class="chart-card">
|
171
|
+
<div
|
172
|
+
bind:this={chartDiv}
|
173
|
+
class="chart-container"
|
174
|
+
></div>
|
175
|
+
</div>
|
176
|
+
|
177
|
+
<style>
|
178
|
+
.chart-card {
|
179
|
+
width: 100%;
|
180
|
+
height: 100%;
|
181
|
+
min-height: 100px; /* Much smaller minimum for grid usage */
|
182
|
+
max-height: 100%; /* Prevent exceeding parent */
|
183
|
+
background: white;
|
184
|
+
border-radius: 8px;
|
185
|
+
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
186
|
+
transition: box-shadow 0.2s ease;
|
187
|
+
overflow: hidden; /* Prevent content overflow */
|
188
|
+
box-sizing: border-box; /* Include padding in size calculations */
|
189
|
+
}
|
190
|
+
|
191
|
+
.chart-card:hover {
|
192
|
+
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
|
193
|
+
}
|
194
|
+
|
195
|
+
.chart-container {
|
196
|
+
width: 100%;
|
197
|
+
height: 100%;
|
198
|
+
min-height: 100px; /* Much smaller minimum for grid usage */
|
199
|
+
max-height: 100%; /* Prevent exceeding parent */
|
200
|
+
padding: 4px; /* Even smaller padding for grid constraints */
|
201
|
+
box-sizing: border-box; /* Include padding in size calculations */
|
202
|
+
overflow: hidden; /* Prevent Plotly overflow */
|
203
|
+
}
|
204
|
+
</style>
|
@@ -0,0 +1,11 @@
|
|
1
|
+
import type { Chart as ChartModel, ChartMarker } from './charts.model.js';
|
2
|
+
interface Props {
|
3
|
+
chart: ChartModel;
|
4
|
+
data: any[];
|
5
|
+
markers?: ChartMarker[];
|
6
|
+
plotlyLayout?: any;
|
7
|
+
enableAdaptation?: boolean;
|
8
|
+
}
|
9
|
+
declare const ChartCard: import("svelte").Component<Props, {}, "">;
|
10
|
+
type ChartCard = ReturnType<typeof ChartCard>;
|
11
|
+
export default ChartCard;
|
@@ -0,0 +1,226 @@
|
|
1
|
+
<svelte:options runes={true} />
|
2
|
+
|
3
|
+
<script lang="ts">
|
4
|
+
import type { Layout, Mode, ChartMarker } from './charts.model.js';
|
5
|
+
import ChartCard from './ChartCard.svelte';
|
6
|
+
|
7
|
+
interface Props {
|
8
|
+
layout: Layout;
|
9
|
+
data: any[];
|
10
|
+
mode: Mode;
|
11
|
+
markers?: ChartMarker[]; // Global markers for all charts
|
12
|
+
plotlyLayout?: any; // Optional custom Plotly layout
|
13
|
+
enableAdaptation?: boolean; // Enable size-based adaptations
|
14
|
+
}
|
15
|
+
|
16
|
+
let { layout, data, mode, markers, plotlyLayout, enableAdaptation = true }: Props = $props();
|
17
|
+
|
18
|
+
// Internal tab state management
|
19
|
+
let activeTabId = $state(layout.sections[0]?.id || '');
|
20
|
+
</script>
|
21
|
+
|
22
|
+
<div class="chart-component">
|
23
|
+
{#if mode === 'tabs'}
|
24
|
+
<!-- Tab Mode with Navigation -->
|
25
|
+
<div class="tabs-container">
|
26
|
+
<!-- Tab Navigation -->
|
27
|
+
<ul class="nav nav-tabs" role="tablist">
|
28
|
+
{#each layout.sections as section, index}
|
29
|
+
<li class="nav-item" role="presentation">
|
30
|
+
<button
|
31
|
+
class="nav-link {section.id === activeTabId ? 'active' : ''}"
|
32
|
+
id="{section.id}-tab"
|
33
|
+
type="button"
|
34
|
+
role="tab"
|
35
|
+
aria-controls="{section.id}"
|
36
|
+
aria-selected="{section.id === activeTabId}"
|
37
|
+
onclick={() => activeTabId = section.id}
|
38
|
+
>
|
39
|
+
{section.title}
|
40
|
+
</button>
|
41
|
+
</li>
|
42
|
+
{/each}
|
43
|
+
</ul>
|
44
|
+
|
45
|
+
<!-- Tab Content -->
|
46
|
+
<div class="tab-content">
|
47
|
+
{#each layout.sections as section, index}
|
48
|
+
<div
|
49
|
+
class="tab-section {section.id === activeTabId ? 'active' : 'hidden'}"
|
50
|
+
data-section-id="{section.id}"
|
51
|
+
>
|
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>
|
60
|
+
</div>
|
61
|
+
{/each}
|
62
|
+
</div>
|
63
|
+
</div>
|
64
|
+
{:else if mode === 'scrollspy'}
|
65
|
+
<!-- ScrollSpy Mode with Navigation -->
|
66
|
+
<div class="scrollspy-container">
|
67
|
+
<!-- ScrollSpy Navigation -->
|
68
|
+
<nav class="scrollspy-nav">
|
69
|
+
<ul class="nav nav-pills">
|
70
|
+
{#each layout.sections as section}
|
71
|
+
<li class="nav-item">
|
72
|
+
<a class="nav-link" href="#{section.id}">{section.title}</a>
|
73
|
+
</li>
|
74
|
+
{/each}
|
75
|
+
</ul>
|
76
|
+
</nav>
|
77
|
+
|
78
|
+
<!-- ScrollSpy Content -->
|
79
|
+
<div class="scrollspy-content">
|
80
|
+
{#each layout.sections as section}
|
81
|
+
<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>
|
90
|
+
</div>
|
91
|
+
{/each}
|
92
|
+
</div>
|
93
|
+
</div>
|
94
|
+
{/if}
|
95
|
+
</div>
|
96
|
+
|
97
|
+
<style>
|
98
|
+
/* Child-first component - adapts to parent container */
|
99
|
+
.chart-component {
|
100
|
+
width: 100%;
|
101
|
+
height: 100%;
|
102
|
+
display: flex;
|
103
|
+
flex-direction: column;
|
104
|
+
}
|
105
|
+
|
106
|
+
/* Tab Mode */
|
107
|
+
.tabs-container {
|
108
|
+
width: 100%;
|
109
|
+
height: 100%;
|
110
|
+
display: flex;
|
111
|
+
flex-direction: column;
|
112
|
+
}
|
113
|
+
|
114
|
+
.nav-tabs {
|
115
|
+
flex-shrink: 0;
|
116
|
+
margin-bottom: 0.125rem; /* Minimal margin */
|
117
|
+
padding: 0; /* Remove any padding */
|
118
|
+
}
|
119
|
+
|
120
|
+
/* Compact tabs for grid usage */
|
121
|
+
.nav-tabs .nav-link {
|
122
|
+
padding: 0.25rem 0.5rem; /* Much smaller padding */
|
123
|
+
font-size: 0.75rem; /* Smaller font */
|
124
|
+
border: none;
|
125
|
+
border-bottom: 2px solid transparent;
|
126
|
+
}
|
127
|
+
|
128
|
+
.nav-tabs .nav-link.active {
|
129
|
+
border-bottom-color: #007bff;
|
130
|
+
background: none;
|
131
|
+
}
|
132
|
+
|
133
|
+
.tab-content {
|
134
|
+
flex-grow: 1;
|
135
|
+
min-height: 0;
|
136
|
+
position: relative;
|
137
|
+
overflow: hidden; /* Prevent overflow */
|
138
|
+
}
|
139
|
+
|
140
|
+
.tab-section {
|
141
|
+
position: absolute;
|
142
|
+
top: 0;
|
143
|
+
left: 0;
|
144
|
+
width: 100%;
|
145
|
+
height: 100%;
|
146
|
+
transition: opacity 0.2s ease;
|
147
|
+
}
|
148
|
+
|
149
|
+
.tab-section.active {
|
150
|
+
opacity: 1;
|
151
|
+
z-index: 1;
|
152
|
+
}
|
153
|
+
|
154
|
+
.tab-section.hidden {
|
155
|
+
opacity: 0;
|
156
|
+
z-index: 0;
|
157
|
+
pointer-events: none;
|
158
|
+
}
|
159
|
+
|
160
|
+
/* ScrollSpy Mode */
|
161
|
+
.scrollspy-container {
|
162
|
+
width: 100%;
|
163
|
+
height: 100%;
|
164
|
+
display: flex;
|
165
|
+
flex-direction: column;
|
166
|
+
}
|
167
|
+
|
168
|
+
.scrollspy-nav {
|
169
|
+
flex-shrink: 0;
|
170
|
+
padding: 0.125rem; /* Minimal padding */
|
171
|
+
background-color: #f8f9fa;
|
172
|
+
margin-bottom: 0.125rem; /* Minimal margin */
|
173
|
+
}
|
174
|
+
|
175
|
+
/* Compact scrollspy nav for grid usage */
|
176
|
+
.scrollspy-nav .nav-pills .nav-link {
|
177
|
+
padding: 0.25rem 0.5rem; /* Much smaller padding */
|
178
|
+
font-size: 0.75rem; /* Smaller font */
|
179
|
+
margin-right: 0.25rem;
|
180
|
+
}
|
181
|
+
|
182
|
+
.scrollspy-content {
|
183
|
+
flex-grow: 1;
|
184
|
+
overflow-y: auto;
|
185
|
+
min-height: 0;
|
186
|
+
}
|
187
|
+
|
188
|
+
.section-content {
|
189
|
+
width: 100%;
|
190
|
+
height: 100%;
|
191
|
+
margin-bottom: 0.5rem; /* Reduce margin */
|
192
|
+
}
|
193
|
+
|
194
|
+
/* 2x2 Chart Grid - responsive to container */
|
195
|
+
.chart-grid {
|
196
|
+
display: grid;
|
197
|
+
grid-template-columns: 1fr 1fr;
|
198
|
+
grid-template-rows: 1fr 1fr;
|
199
|
+
gap: 0.25rem; /* Reduce gap */
|
200
|
+
width: 100%;
|
201
|
+
height: 100%;
|
202
|
+
min-height: 0; /* Remove fixed minimum to allow full flexibility */
|
203
|
+
max-height: 100%; /* Ensure it doesn't exceed container */
|
204
|
+
}
|
205
|
+
|
206
|
+
.chart-slot {
|
207
|
+
display: flex;
|
208
|
+
min-height: 0; /* Allow grid to shrink */
|
209
|
+
min-width: 0;
|
210
|
+
max-height: 100%; /* Prevent exceeding grid cell */
|
211
|
+
max-width: 100%;
|
212
|
+
overflow: hidden; /* Prevent slot overflow */
|
213
|
+
}
|
214
|
+
|
215
|
+
/* Ensure ChartCard adapts to slot size */
|
216
|
+
.chart-slot :global(.chart-card) {
|
217
|
+
flex-grow: 1;
|
218
|
+
min-height: 80px; /* Even smaller minimum for grid */
|
219
|
+
max-height: 100%; /* Constrain to slot */
|
220
|
+
}
|
221
|
+
|
222
|
+
.chart-slot :global(.chart-container) {
|
223
|
+
min-height: 80px; /* Even smaller minimum for grid */
|
224
|
+
max-height: 100%; /* Constrain to slot */
|
225
|
+
}
|
226
|
+
</style>
|
@@ -0,0 +1,12 @@
|
|
1
|
+
import type { Layout, Mode, ChartMarker } from './charts.model.js';
|
2
|
+
interface Props {
|
3
|
+
layout: Layout;
|
4
|
+
data: any[];
|
5
|
+
mode: Mode;
|
6
|
+
markers?: ChartMarker[];
|
7
|
+
plotlyLayout?: any;
|
8
|
+
enableAdaptation?: boolean;
|
9
|
+
}
|
10
|
+
declare const ChartComponent: import("svelte").Component<Props, {}, "">;
|
11
|
+
type ChartComponent = ReturnType<typeof ChartComponent>;
|
12
|
+
export default ChartComponent;
|
@@ -0,0 +1,37 @@
|
|
1
|
+
/**
|
2
|
+
* Chart-specific adaptation utilities for Plotly layouts
|
3
|
+
* Handles size-based adaptations while preserving external styling/theming
|
4
|
+
*/
|
5
|
+
import type { ChartMarker } from './charts.model.js';
|
6
|
+
export interface ContainerSize {
|
7
|
+
width: number;
|
8
|
+
height: number;
|
9
|
+
}
|
10
|
+
export interface ChartInfo {
|
11
|
+
leftSeriesCount: number;
|
12
|
+
rightSeriesCount: number;
|
13
|
+
}
|
14
|
+
export interface AdaptationConfig {
|
15
|
+
enableAdaptation?: boolean;
|
16
|
+
}
|
17
|
+
/**
|
18
|
+
* Adapts a Plotly layout based on container size
|
19
|
+
* Preserves external styling while optimizing functional properties
|
20
|
+
*/
|
21
|
+
export declare function adaptPlotlyLayout(baseLayout: any, containerSize: ContainerSize, chartInfo: ChartInfo, config?: AdaptationConfig): any;
|
22
|
+
/**
|
23
|
+
* Helper to get size category for debugging/logging
|
24
|
+
*/
|
25
|
+
export declare function getSizeCategory(containerSize: ContainerSize): 'tiny' | 'small' | 'medium' | 'large';
|
26
|
+
/**
|
27
|
+
* Create Plotly shapes for chart markers (vertical lines)
|
28
|
+
*/
|
29
|
+
export declare function createMarkerShapes(markers: ChartMarker[], yAxisRange?: [number, number]): any[];
|
30
|
+
/**
|
31
|
+
* Create Plotly annotations for chart marker labels
|
32
|
+
*/
|
33
|
+
export declare function createMarkerAnnotations(markers: ChartMarker[], containerSize: ContainerSize, enableAdaptation?: boolean): any[];
|
34
|
+
/**
|
35
|
+
* Add markers to a Plotly layout
|
36
|
+
*/
|
37
|
+
export declare function addMarkersToLayout(layout: any, markers: ChartMarker[], containerSize: ContainerSize, enableAdaptation?: boolean): any;
|
@@ -0,0 +1,192 @@
|
|
1
|
+
/**
|
2
|
+
* Chart-specific adaptation utilities for Plotly layouts
|
3
|
+
* Handles size-based adaptations while preserving external styling/theming
|
4
|
+
*/
|
5
|
+
/**
|
6
|
+
* Adapts a Plotly layout based on container size
|
7
|
+
* Preserves external styling while optimizing functional properties
|
8
|
+
*/
|
9
|
+
export function adaptPlotlyLayout(baseLayout, containerSize, chartInfo, config = {}) {
|
10
|
+
const { enableAdaptation = true } = config;
|
11
|
+
if (!enableAdaptation)
|
12
|
+
return baseLayout;
|
13
|
+
const { width, height } = containerSize;
|
14
|
+
const adaptedLayout = { ...baseLayout };
|
15
|
+
// Size categories for adaptation rules
|
16
|
+
const isTiny = width < 250 || height < 200;
|
17
|
+
const isSmall = width < 400 || height < 300;
|
18
|
+
const isMedium = width < 600 || height < 400;
|
19
|
+
// Adaptive font scaling for title
|
20
|
+
if (adaptedLayout.title?.font) {
|
21
|
+
if (isTiny) {
|
22
|
+
adaptedLayout.title.font.size = Math.max(10, adaptedLayout.title.font.size * 0.7);
|
23
|
+
}
|
24
|
+
else if (isSmall) {
|
25
|
+
adaptedLayout.title.font.size = Math.max(12, adaptedLayout.title.font.size * 0.8);
|
26
|
+
}
|
27
|
+
}
|
28
|
+
// Adaptive legend behavior
|
29
|
+
if (isTiny) {
|
30
|
+
adaptedLayout.showlegend = false;
|
31
|
+
}
|
32
|
+
else if (isSmall && chartInfo.leftSeriesCount + chartInfo.rightSeriesCount > 2) {
|
33
|
+
// Hide legend for small containers with many series
|
34
|
+
adaptedLayout.showlegend = false;
|
35
|
+
}
|
36
|
+
// Adaptive margins
|
37
|
+
if (isTiny) {
|
38
|
+
adaptedLayout.margin = { l: 35, r: 25, t: 30, b: 25 };
|
39
|
+
}
|
40
|
+
else if (isSmall) {
|
41
|
+
adaptedLayout.margin = { l: 45, r: 35, t: 40, b: 35 };
|
42
|
+
}
|
43
|
+
else if (isMedium) {
|
44
|
+
adaptedLayout.margin = { l: 50, r: 45, t: 50, b: 40 };
|
45
|
+
}
|
46
|
+
// Adaptive axis font sizes
|
47
|
+
if (adaptedLayout.xaxis?.tickfont) {
|
48
|
+
if (isTiny) {
|
49
|
+
adaptedLayout.xaxis.tickfont.size = 8;
|
50
|
+
}
|
51
|
+
else if (isSmall) {
|
52
|
+
adaptedLayout.xaxis.tickfont.size = 9;
|
53
|
+
}
|
54
|
+
}
|
55
|
+
if (adaptedLayout.yaxis?.tickfont) {
|
56
|
+
if (isTiny) {
|
57
|
+
adaptedLayout.yaxis.tickfont.size = 8;
|
58
|
+
}
|
59
|
+
else if (isSmall) {
|
60
|
+
adaptedLayout.yaxis.tickfont.size = 9;
|
61
|
+
}
|
62
|
+
}
|
63
|
+
if (adaptedLayout.yaxis?.title?.font) {
|
64
|
+
if (isTiny) {
|
65
|
+
adaptedLayout.yaxis.title.font.size = 9;
|
66
|
+
}
|
67
|
+
else if (isSmall) {
|
68
|
+
adaptedLayout.yaxis.title.font.size = 10;
|
69
|
+
}
|
70
|
+
}
|
71
|
+
// Apply same adaptations to second Y-axis
|
72
|
+
if (adaptedLayout.yaxis2) {
|
73
|
+
if (adaptedLayout.yaxis2.tickfont) {
|
74
|
+
if (isTiny) {
|
75
|
+
adaptedLayout.yaxis2.tickfont.size = 8;
|
76
|
+
}
|
77
|
+
else if (isSmall) {
|
78
|
+
adaptedLayout.yaxis2.tickfont.size = 9;
|
79
|
+
}
|
80
|
+
}
|
81
|
+
if (adaptedLayout.yaxis2.title?.font) {
|
82
|
+
if (isTiny) {
|
83
|
+
adaptedLayout.yaxis2.title.font.size = 9;
|
84
|
+
}
|
85
|
+
else if (isSmall) {
|
86
|
+
adaptedLayout.yaxis2.title.font.size = 10;
|
87
|
+
}
|
88
|
+
}
|
89
|
+
}
|
90
|
+
// Adaptive legend font size
|
91
|
+
if (adaptedLayout.legend?.font && adaptedLayout.showlegend) {
|
92
|
+
if (isSmall) {
|
93
|
+
adaptedLayout.legend.font.size = 10;
|
94
|
+
}
|
95
|
+
}
|
96
|
+
return adaptedLayout;
|
97
|
+
}
|
98
|
+
/**
|
99
|
+
* Helper to get size category for debugging/logging
|
100
|
+
*/
|
101
|
+
export function getSizeCategory(containerSize) {
|
102
|
+
const { width, height } = containerSize;
|
103
|
+
if (width < 250 || height < 200)
|
104
|
+
return 'tiny';
|
105
|
+
if (width < 400 || height < 300)
|
106
|
+
return 'small';
|
107
|
+
if (width < 600 || height < 400)
|
108
|
+
return 'medium';
|
109
|
+
return 'large';
|
110
|
+
}
|
111
|
+
/**
|
112
|
+
* Create Plotly shapes for chart markers (vertical lines)
|
113
|
+
*/
|
114
|
+
export function createMarkerShapes(markers, yAxisRange) {
|
115
|
+
if (!markers || markers.length === 0)
|
116
|
+
return [];
|
117
|
+
return markers.map(marker => ({
|
118
|
+
type: 'line',
|
119
|
+
x0: marker.date,
|
120
|
+
x1: marker.date,
|
121
|
+
y0: yAxisRange ? yAxisRange[0] : 0,
|
122
|
+
y1: yAxisRange ? yAxisRange[1] : 1,
|
123
|
+
yref: yAxisRange ? 'y' : 'paper',
|
124
|
+
line: {
|
125
|
+
color: marker.color || '#ff0000',
|
126
|
+
width: 2,
|
127
|
+
dash: marker.style === 'dashed' ? 'dash' : marker.style === 'dotted' ? 'dot' : 'solid'
|
128
|
+
}
|
129
|
+
}));
|
130
|
+
}
|
131
|
+
/**
|
132
|
+
* Create Plotly annotations for chart marker labels
|
133
|
+
*/
|
134
|
+
export function createMarkerAnnotations(markers, containerSize, enableAdaptation = true) {
|
135
|
+
if (!markers || markers.length === 0)
|
136
|
+
return [];
|
137
|
+
const sizeCategory = getSizeCategory(containerSize);
|
138
|
+
return markers
|
139
|
+
.filter(marker => {
|
140
|
+
// Hide labels in tiny containers if adaptation is enabled
|
141
|
+
if (enableAdaptation && sizeCategory === 'tiny')
|
142
|
+
return false;
|
143
|
+
// Respect individual marker showLabel setting
|
144
|
+
return marker.showLabel !== false;
|
145
|
+
})
|
146
|
+
.map(marker => {
|
147
|
+
// Adaptive font sizing
|
148
|
+
let fontSize = 10;
|
149
|
+
if (enableAdaptation) {
|
150
|
+
if (sizeCategory === 'small')
|
151
|
+
fontSize = 8;
|
152
|
+
else if (sizeCategory === 'medium')
|
153
|
+
fontSize = 9;
|
154
|
+
else if (sizeCategory === 'large')
|
155
|
+
fontSize = 10;
|
156
|
+
}
|
157
|
+
// Adaptive label positioning
|
158
|
+
const yPosition = sizeCategory === 'small' ? 0.95 : 0.9;
|
159
|
+
return {
|
160
|
+
x: marker.date,
|
161
|
+
y: yPosition,
|
162
|
+
yref: 'paper',
|
163
|
+
text: marker.label,
|
164
|
+
showarrow: true,
|
165
|
+
arrowhead: 2,
|
166
|
+
arrowcolor: marker.color || '#ff0000',
|
167
|
+
arrowsize: sizeCategory === 'small' ? 0.8 : 1,
|
168
|
+
font: {
|
169
|
+
size: fontSize,
|
170
|
+
color: marker.color || '#ff0000'
|
171
|
+
},
|
172
|
+
bgcolor: 'rgba(255,255,255,0.8)',
|
173
|
+
bordercolor: marker.color || '#ff0000',
|
174
|
+
borderwidth: 1
|
175
|
+
};
|
176
|
+
});
|
177
|
+
}
|
178
|
+
/**
|
179
|
+
* Add markers to a Plotly layout
|
180
|
+
*/
|
181
|
+
export function addMarkersToLayout(layout, markers, containerSize, enableAdaptation = true) {
|
182
|
+
if (!markers || markers.length === 0)
|
183
|
+
return layout;
|
184
|
+
const updatedLayout = { ...layout };
|
185
|
+
// Add marker shapes (vertical lines)
|
186
|
+
const shapes = createMarkerShapes(markers);
|
187
|
+
updatedLayout.shapes = updatedLayout.shapes ? [...updatedLayout.shapes, ...shapes] : shapes;
|
188
|
+
// Add marker annotations (labels)
|
189
|
+
const annotations = createMarkerAnnotations(markers, containerSize, enableAdaptation);
|
190
|
+
updatedLayout.annotations = updatedLayout.annotations ? [...updatedLayout.annotations, ...annotations] : annotations;
|
191
|
+
return updatedLayout;
|
192
|
+
}
|