Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 48 additions & 7 deletions css/rl-charts.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
101 changes: 74 additions & 27 deletions js/rl-plotly-charts.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

/**
Expand Down Expand Up @@ -97,34 +96,23 @@
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;
} else if (data.surface3d && data.surface3d.zMatrixScore) {
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;
Expand Down Expand Up @@ -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 = {
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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
Expand All @@ -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);
}

/**
Expand Down
46 changes: 25 additions & 21 deletions templates/rl-charts.html.twig
Original file line number Diff line number Diff line change
Expand Up @@ -13,31 +13,35 @@
#}
<h3>{{ title }}</h3>

<div class="rl-chart-row">
<div class="rl-chart-box">
{% if date_filter %}
<div class="rl-chart-filters">
{{ date_filter }}
</div>
{% endif %}
<div class="rl-chart-area-2d" id="rl-plotly-2d-lines"></div>
<div class="form-item__description rl-chart-tip">
<strong>{{ 'Tip:'|t }}</strong> {{ tip_hover }}
<div class="rl-chart-tabs-wrapper">
{% if date_filter %}
<div class="rl-chart-filters">
{{ date_filter }}
</div>
{% endif %}

<div class="rl-chart-tabs" role="tablist" aria-label="{{ 'Chart view'|t }}">
<button type="button" class="rl-chart-tab is-active" id="rl-chart-tab-2d" role="tab" aria-selected="true" aria-controls="rl-chart-pane-2d" data-rl-tab="2d">
{{ '2D Line Chart'|t }}
</button>
<button type="button" class="rl-chart-tab" id="rl-chart-tab-3d" role="tab" aria-selected="false" aria-controls="rl-chart-pane-3d" data-rl-tab="3d">
{{ '3D Posterior Landscape'|t }}
</button>
</div>
</div>

<div class="rl-chart-row">
<div class="rl-chart-box">
{% if date_filter %}
<div class="rl-chart-filters">
{{ date_filter }}
<div class="rl-chart-pane-box">
<div class="rl-chart-pane is-active" id="rl-chart-pane-2d" role="tabpanel" aria-labelledby="rl-chart-tab-2d" data-rl-pane="2d">
<div class="rl-chart-area-2d" id="rl-plotly-2d-lines"></div>
<div class="form-item__description rl-chart-tip">
<strong>{{ 'Tip:'|t }}</strong> {{ tip_hover }}
</div>
</div>
<div class="rl-chart-pane" id="rl-chart-pane-3d" role="tabpanel" aria-labelledby="rl-chart-tab-3d" data-rl-pane="3d">
<div class="form-item__description rl-chart-interaction-hint">{{ interaction_hint }}</div>
<div class="rl-chart-area-3d" id="rl-plotly-3d-surface"></div>
<div class="form-item__description rl-chart-tip">
<strong>{{ 'Tip:'|t }}</strong> {{ tip_taller }}
</div>
{% endif %}
<div class="form-item__description rl-chart-interaction-hint">{{ interaction_hint }}</div>
<div class="rl-chart-area-3d" id="rl-plotly-3d-surface"></div>
<div class="form-item__description rl-chart-tip">
<strong>{{ 'Tip:'|t }}</strong> {{ tip_taller }}
</div>
</div>
</div>