@@ -67,40 +67,125 @@ def load_env_with_custom_path():
6767uk-chart .apexcharts-canvas {
6868 width: 100% !important;
6969 height: 100% !important;
70+ }
71+ /* Sparklines should fill their grid cell for consistent alignment */
72+ /* We now size sparkline charts via JS by detecting chart.sparkline.enabled */
73+ .sparkline-cell uk-chart[id^="sparkline-"] {
74+ min-height: 32px !important;
75+ height: 32px !important;
76+ width: 260px !important;
77+ }
78+ .sparkline-cell uk-chart[id^="sparkline-"] .apexcharts-canvas {
79+ width: 100% !important;
80+ height: 100% !important;
81+ }
82+ .sparkline-container {
83+ display: flex;
84+ justify-content: center;
85+ align-items: center;
86+ width: 100%;
87+ height: 100%;
7088}
7189 """ ),
7290 Script ("""
73- function initializeCharts() {
74- const elements = document.querySelectorAll('uk-chart:not([data-chart-initialized])');
75-
76- elements.forEach(function(element) {
77- const script = element.querySelector('script[type="application/json"]');
78- if (script) {
91+ const __chartInitState = {
92+ observer: null,
93+ seen: new WeakSet(),
94+ };
95+
96+ function renderChartElement(el) {
97+ const script = el.querySelector('script[type="application/json"]');
98+ if (!script) return false;
99+ if (el.hasAttribute('data-chart-initialized')) return true;
100+ try {
101+ const config = JSON.parse(script.textContent);
102+ if (config.yaxis && config.yaxis[1]) {
103+ config.yaxis[1].labels = config.yaxis[1].labels || {};
104+ config.yaxis[1].labels.formatter = function(val) { return Math.round(val * 100) + '%'; };
105+ }
106+ const isSpark = !!(config.chart && config.chart.sparkline && config.chart.sparkline.enabled);
107+ el.style.display = 'block';
108+ el.style.width = '100%';
109+ el.style.minHeight = isSpark ? '32px' : '300px';
110+ el.style.height = isSpark ? '32px' : 'auto';
111+ // Ensure ApexCharts receives an explicit width to avoid 0-width renders
112+ const width = Math.max(220, Math.floor(el.getBoundingClientRect().width || el.clientWidth || 0));
113+ config.chart = config.chart || {};
114+ config.chart.width = width;
115+ config.chart.height = isSpark ? 32 : (config.chart.height || 300);
116+ if (typeof ApexCharts === 'undefined') return false;
117+ const chart = new ApexCharts(el, config);
118+ chart.render().then(() => el.setAttribute('data-chart-initialized', 'true'));
119+ return true;
120+ } catch (e) {
121+ console.error('renderChartElement error', e, el);
122+ return false;
123+ }
124+ }
125+
126+ function ensureChartObserver() {
127+ if (__chartInitState.observer) return __chartInitState.observer;
128+ __chartInitState.observer = new IntersectionObserver((entries) => {
129+ entries.forEach((entry) => {
130+ const el = entry.target;
131+ if (!entry.isIntersecting) return;
132+ if (__chartInitState.seen.has(el) || el.hasAttribute('data-chart-initialized')) {
133+ __chartInitState.observer.unobserve(el);
134+ return;
135+ }
136+ const script = el.querySelector('script[type="application/json"]');
137+ if (!script) return;
79138 try {
80139 const config = JSON.parse(script.textContent);
81-
82- // Add percentage formatting to score axis (second y-axis)
83140 if (config.yaxis && config.yaxis[1]) {
84141 config.yaxis[1].labels = config.yaxis[1].labels || {};
85- config.yaxis[1].labels.formatter = function(val) {
86- return Math.round(val * 100) + '%';
87- };
142+ config.yaxis[1].labels.formatter = function(val) { return Math.round(val * 100) + '%'; };
88143 }
89-
90- // Ensure chart has dimensions
91- element.style.minHeight = '300px';
92- element.style.width = '100%';
93- element.style.display = 'block';
94-
95- const chart = new ApexCharts(element, config);
96- chart.render().then(() => {
97- element.setAttribute('data-chart-initialized', 'true');
144+ const isSpark = !!(config.chart && config.chart.sparkline && config.chart.sparkline.enabled);
145+ el.style.display = 'block';
146+ el.style.width = '100%';
147+ el.style.minHeight = isSpark ? '32px' : '300px';
148+ el.style.height = isSpark ? '32px' : 'auto';
149+ // Explicit width
150+ const width = Math.max(220, Math.floor(el.getBoundingClientRect().width || el.clientWidth || 0));
151+ config.chart = config.chart || {};
152+ config.chart.width = width;
153+ config.chart.height = isSpark ? 32 : (config.chart.height || 300);
154+ requestAnimationFrame(() => {
155+ const chart = new ApexCharts(el, config);
156+ chart.render().then(() => {
157+ el.setAttribute('data-chart-initialized', 'true');
158+ __chartInitState.seen.add(el);
159+ __chartInitState.observer.unobserve(el);
160+ });
98161 });
99162 } catch (e) {
100- console.error('Error initializing chart:', e, element );
163+ console.error('Error initializing chart:', e, el );
101164 }
165+ });
166+ }, { root: null, rootMargin: '200px 0px', threshold: 0.01 });
167+ return __chartInitState.observer;
168+ }
169+
170+ function initializeCharts() {
171+ // Prefer immediate render inside anomaly list to avoid races
172+ const anomalyContainer = document.getElementById('anomaly-list');
173+ if (anomalyContainer) {
174+ const charts = Array.from(anomalyContainer.querySelectorAll('uk-chart:not([data-chart-initialized])'));
175+ let rendered = 0;
176+ charts.forEach((el) => { if (renderChartElement(el)) rendered++; });
177+ if (rendered < charts.length) {
178+ // Retry after a short delay (e.g. if ApexCharts not ready yet)
179+ setTimeout(() => {
180+ charts.forEach((el) => { if (!el.hasAttribute('data-chart-initialized')) renderChartElement(el); });
181+ }, 300);
102182 }
103- });
183+ return; // Skip observer path for anomalies page
184+ }
185+
186+ // Fallback: use lazy observer for other pages
187+ const observer = ensureChartObserver();
188+ document.querySelectorAll('uk-chart:not([data-chart-initialized])').forEach((el) => observer.observe(el));
104189}
105190
106191// Wait for both DOM and ApexCharts to be ready
@@ -123,6 +208,27 @@ def load_env_with_custom_path():
123208// Re-initialize charts after HTMX requests
124209document.addEventListener('htmx:afterSwap', initializeCharts);
125210document.addEventListener('htmx:afterSettle', initializeCharts);
211+ // Debug: log counts and row metadata after HTMX updates
212+ document.addEventListener('htmx:afterSettle', function() {
213+ try {
214+ const container = document.getElementById('anomaly-list');
215+ if (!container) return;
216+ const rows = Array.from(container.querySelectorAll('[data-row]'));
217+ const charts = Array.from(container.querySelectorAll('uk-chart'));
218+ const visibleCharts = charts.filter(c => c.offsetParent !== null);
219+ console.log('[anomstack] rows:', rows.length, 'charts:', charts.length, 'visibleCharts:', visibleCharts.length);
220+ rows.slice(0, 5).forEach(r => {
221+ const ds = r.dataset || {};
222+ console.log('[row]', ds.row, ds.metric, ds.ts);
223+ });
224+ const initialized = container.querySelectorAll('uk-chart[data-chart-initialized]');
225+ console.log('[anomstack] initialized charts:', initialized.length);
226+ // Force initialize any visible charts not initialized yet (safety net)
227+ if (visibleCharts.length && initialized.length < visibleCharts.length) {
228+ visibleCharts.forEach((el) => { if (!el.hasAttribute('data-chart-initialized')) renderChartElement(el); });
229+ }
230+ } catch (e) { console.warn('debug error', e); }
231+ });
126232 """ ),
127233 Script (POSTHOG_SCRIPT ) if posthog_api_key else None ,
128234 Link (
0 commit comments