diff --git a/css/rl-charts.css b/css/rl-charts.css index 42db21c..1686f11 100644 --- a/css/rl-charts.css +++ b/css/rl-charts.css @@ -7,24 +7,65 @@ margin-bottom: 2em; } -.rl-chart-row { +.rl-chart-tabs-wrapper { + margin-bottom: 1.5em; +} + +.rl-chart-tabs { display: flex; flex-wrap: wrap; - gap: 1.5em; - margin-bottom: 1.5em; + gap: 0.25em; + border-bottom: 2px solid #d0d0d0; + margin-bottom: 0; +} + +.rl-chart-tab { + appearance: none; + background: transparent; + border: 2px solid transparent; + border-bottom: none; + border-radius: 8px 8px 0 0; + padding: 0.5em 1.25em; + margin-bottom: -2px; + font: inherit; + font-weight: 500; + color: #555; + cursor: pointer; +} + +.rl-chart-tab:hover { + color: #000; +} + +.rl-chart-tab:focus-visible { + outline: 2px solid #0d6efd; + outline-offset: -2px; } -.rl-chart-box { - flex: 1 1 100%; - min-width: 0; +.rl-chart-tab.is-active { + background: #f8f9fa; + border-color: #d0d0d0; + color: #000; +} + +.rl-chart-pane-box { background: #f8f9fa; border: 2px solid #d0d0d0; - border-radius: 8px; + border-top: none; + border-radius: 0 8px 8px 8px; padding: clamp(8px, 1.5vw, 20px); box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08); position: relative; } +.rl-chart-pane { + display: none; +} + +.rl-chart-pane.is-active { + display: block; +} + .rl-chart-filters { display: flex; flex-wrap: wrap; diff --git a/js/rl-plotly-charts.js b/js/rl-plotly-charts.js index 3131023..3af6770 100644 --- a/js/rl-plotly-charts.js +++ b/js/rl-plotly-charts.js @@ -21,17 +21,16 @@ } }; - /** - * Get the actual height of a chart container element. - */ function getContainerHeight(elementId) { const el = document.getElementById(elementId); if (el) { const height = el.clientHeight || el.offsetHeight; - // Return at least a minimum height - return Math.max(height, 200); + if (height > 0) { + return height; + } } - return 400; // Fallback + // Hidden tab fallback matches the 70vh min-height in rl-charts.css. + return Math.max(Math.round(window.innerHeight * 0.7), 400); } /** @@ -97,7 +96,6 @@ margin: config.margin }; - // Determine number of arms and which chart to show let numArms = 0; if (data.lineChartData && data.lineChartData.arms) { numArms = data.lineChartData.arms.length; @@ -105,26 +103,16 @@ numArms = data.surface3d.zMatrixScore.length; } - // Chart selection based on arm count (threshold from config or default 10): - // 1-threshold arms: 2D line chart - // threshold+1 arms: 3D Posterior Landscape const lineChartThreshold = data.chartLineThreshold || 9; - const showLineChart = numArms >= 1 && numArms <= lineChartThreshold; - const showLandscape = numArms > lineChartThreshold; + const defaultTab = numArms > lineChartThreshold ? '3d' : '2d'; - // Hide unused chart containers const lineChartEl = document.getElementById('rl-plotly-2d-lines'); const surface3dEl = document.getElementById('rl-plotly-3d-surface'); - if (lineChartEl) { - lineChartEl.parentElement.parentElement.style.display = showLineChart ? 'block' : 'none'; - } - if (surface3dEl) { - surface3dEl.parentElement.parentElement.style.display = showLandscape ? 'block' : 'none'; - } - - // 1. 2D Line Chart (up to threshold arms) - if (showLineChart && data.lineChartData && data.lineChartData.arms && data.lineChartData.arms.length > 0 && lineChartEl) { + function render2d() { + if (!(data.lineChartData && data.lineChartData.arms && data.lineChartData.arms.length > 0 && lineChartEl)) { + return; + } try { const traces2d = []; const lineChartNumArms = data.lineChartData.arms.length; @@ -158,7 +146,7 @@ }); } - const lineChartHeight = config.height2d; + const lineChartHeight = getContainerHeight('rl-plotly-2d-lines'); // Configure x-axis based on time axis type const xAxisConfig = { @@ -213,9 +201,11 @@ } } - // 2. 3D Posterior Landscape (loss-landscape style) - threshold+1 arms - const zMatrix = metric === 'score' ? data.surface3d.zMatrixScore : data.surface3d.zMatrixRate; - if (showLandscape && data.surface3d && zMatrix && zMatrix.length > 0 && surface3dEl) { + function render3d() { + const zMatrix = data.surface3d && (metric === 'score' ? data.surface3d.zMatrixScore : data.surface3d.zMatrixRate); + if (!(zMatrix && zMatrix.length > 0 && surface3dEl)) { + return; + } try { const landscapeNumArms = zMatrix.length; const numTimePoints = data.surface3d.xValues.length; @@ -378,7 +368,7 @@ }, aspectratio: { x: 1.5, y: 1, z: 0.8 } }, - height: config.height + height: getContainerHeight('rl-plotly-3d-surface') }), { responsive: true }); // Update tip text if showing subset of variants @@ -394,6 +384,63 @@ } } + const rendered = { '2d': false, '3d': false }; + const renderers = { '2d': render2d, '3d': render3d }; + + function renderTab(tab) { + if (rendered[tab]) { + return; + } + const fn = renderers[tab]; + if (fn) { + fn(); + rendered[tab] = true; + } + } + + function activateTab(target) { + const tabs = document.querySelectorAll('.rl-chart-tab'); + tabs.forEach(function(btn) { + const on = btn.dataset.rlTab === target; + btn.classList.toggle('is-active', on); + btn.setAttribute('aria-selected', on ? 'true' : 'false'); + }); + const panes = document.querySelectorAll('.rl-chart-pane'); + panes.forEach(function(pane) { + pane.classList.toggle('is-active', pane.dataset.rlPane === target); + }); + + renderTab(target); + + const chartId = target === '3d' ? 'rl-plotly-3d-surface' : 'rl-plotly-2d-lines'; + const chartEl = document.getElementById(chartId); + if (chartEl && chartEl.data && chartEl.layout) { + Plotly.Plots.resize(chartEl); + } + } + + const tabButtons = once('rl-chart-tabs', '.rl-chart-tab'); + tabButtons.forEach(function(btn) { + btn.addEventListener('click', function() { + activateTab(btn.dataset.rlTab); + }); + btn.addEventListener('keydown', function(e) { + if (e.key !== 'ArrowLeft' && e.key !== 'ArrowRight') { + return; + } + e.preventDefault(); + const all = Array.from(document.querySelectorAll('.rl-chart-tab')); + const idx = all.indexOf(btn); + if (idx === -1) return; + const next = e.key === 'ArrowRight' + ? all[(idx + 1) % all.length] + : all[(idx - 1 + all.length) % all.length]; + next.focus(); + activateTab(next.dataset.rlTab); + }); + }); + + activateTab(defaultTab); } /** diff --git a/templates/rl-charts.html.twig b/templates/rl-charts.html.twig index 1649ed9..451ad8b 100644 --- a/templates/rl-charts.html.twig +++ b/templates/rl-charts.html.twig @@ -13,31 +13,35 @@ #}

{{ title }}

-
-
- {% if date_filter %} -
- {{ date_filter }} -
- {% endif %} -
-
- {{ 'Tip:'|t }} {{ tip_hover }} +
+ {% if date_filter %} +
+ {{ date_filter }}
+ {% endif %} + +
+ +
-
-
-
- {% if date_filter %} -
- {{ date_filter }} +
+
+
+
+ {{ 'Tip:'|t }} {{ tip_hover }} +
+
+
+
{{ interaction_hint }}
+
+
+ {{ 'Tip:'|t }} {{ tip_taller }}
- {% endif %} -
{{ interaction_hint }}
-
-
- {{ 'Tip:'|t }} {{ tip_taller }}