From 7f3361f1f4ee6508cceeb53ec5769813cae85a01 Mon Sep 17 00:00:00 2001 From: vreddy Date: Wed, 3 Jun 2026 18:12:18 +0100 Subject: [PATCH 01/39] feature: Implemented the new KS4 chart interaction across both ks4headlinemeasures and ks4coresubjects --- SAPSec.Web/AssetSrc/js/ks4-chart-toggle.js | 128 ++++++++++++++++++ SAPSec.Web/AssetSrc/scss/main.scss | 49 +++++++ .../Views/School/Ks4CoreSubjects.cshtml | 1 + .../Views/School/Ks4HeadlineMeasures.cshtml | 1 + 4 files changed, 179 insertions(+) create mode 100644 SAPSec.Web/AssetSrc/js/ks4-chart-toggle.js diff --git a/SAPSec.Web/AssetSrc/js/ks4-chart-toggle.js b/SAPSec.Web/AssetSrc/js/ks4-chart-toggle.js new file mode 100644 index 00000000..73b7e2fa --- /dev/null +++ b/SAPSec.Web/AssetSrc/js/ks4-chart-toggle.js @@ -0,0 +1,128 @@ +(function () { + function getTabText(tabLink) { + return (tabLink && tabLink.textContent ? tabLink.textContent : "").trim().toLowerCase(); + } + + function setHidden(element, hidden) { + if (!element) { + return; + } + + if (hidden) { + element.setAttribute("hidden", "hidden"); + } else { + element.removeAttribute("hidden"); + } + } + + function resizeCharts(container) { + if (!window.Chart || !container) { + return; + } + + container.querySelectorAll("canvas").forEach(function (canvas) { + var chart = window.Chart.getChart(canvas); + if (!chart) { + return; + } + + chart.resize(); + chart.update("none"); + }); + } + + function buildToggleHeader() { + var header = document.createElement("div"); + header.className = "app-ks4-chart-toggle"; + + var title = document.createElement("h3"); + title.className = "govuk-heading-m app-ks4-chart-toggle__title"; + title.textContent = "3-year average"; + + var button = document.createElement("button"); + button.type = "button"; + button.className = "app-ks4-chart-toggle__button"; + button.textContent = "Show year by year"; + button.setAttribute("aria-pressed", "false"); + + header.appendChild(title); + header.appendChild(button); + + return { header: header, title: title, button: button }; + } + + function initialiseTabSet(tabSet) { + var listItems = tabSet.querySelectorAll(".govuk-tabs__list-item"); + if (listItems.length < 2) { + return; + } + + var firstTab = listItems[0].querySelector(".govuk-tabs__tab"); + var secondTab = listItems[1].querySelector(".govuk-tabs__tab"); + var firstPanel = tabSet.querySelector(".govuk-tabs__panel"); + var secondPanel = listItems[1] && secondTab + ? tabSet.querySelector(secondTab.getAttribute("href")) + : null; + + if (!firstTab || !secondTab || !firstPanel || !secondPanel) { + return; + } + + if (getTabText(firstTab) !== "3-year average" || getTabText(secondTab) !== "year by year") { + return; + } + + var averageChart = firstPanel.querySelector(".app-ks4-chart-container"); + var yearlyChart = secondPanel.querySelector(".app-ks4-chart-container"); + + if (!averageChart || !yearlyChart) { + return; + } + + firstTab.textContent = "Charts"; + listItems[1].remove(); + + averageChart.classList.add("app-ks4-chart-view"); + yearlyChart.classList.add("app-ks4-chart-view"); + averageChart.classList.add("app-ks4-chart-view--active"); + + setHidden(yearlyChart, true); + + firstPanel.insertBefore(yearlyChart, firstPanel.firstChild); + firstPanel.insertBefore(averageChart, yearlyChart); + + var toggle = buildToggleHeader(); + firstPanel.insertBefore(toggle.header, firstPanel.firstChild); + + var showingYearly = false; + + toggle.button.addEventListener("click", function () { + showingYearly = !showingYearly; + + averageChart.classList.toggle("app-ks4-chart-view--active", !showingYearly); + yearlyChart.classList.toggle("app-ks4-chart-view--active", showingYearly); + + setHidden(averageChart, showingYearly); + setHidden(yearlyChart, !showingYearly); + + toggle.title.textContent = showingYearly ? "Year by year" : "3-year average"; + toggle.button.textContent = showingYearly ? "Show 3-year average" : "Show year by year"; + toggle.button.setAttribute("aria-pressed", showingYearly ? "true" : "false"); + + resizeCharts(showingYearly ? yearlyChart : averageChart); + }); + + secondPanel.remove(); + } + + function init() { + document.querySelectorAll(".app-ks4-tabs").forEach(initialiseTabSet); + } + + if (document.readyState === "loading") { + document.addEventListener("DOMContentLoaded", init); + return; + } + + init(); +})(); diff --git a/SAPSec.Web/AssetSrc/scss/main.scss b/SAPSec.Web/AssetSrc/scss/main.scss index 4ced26d3..56ea26d8 100644 --- a/SAPSec.Web/AssetSrc/scss/main.scss +++ b/SAPSec.Web/AssetSrc/scss/main.scss @@ -1091,6 +1091,45 @@ $app-ks4-color-track: #f3f2f1; height: 330px; } +.app-ks4-chart-toggle { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 16px; + margin-bottom: 20px; +} + +.app-ks4-chart-toggle__title { + margin-bottom: 0; +} + +.app-ks4-chart-toggle__button { + border: 0; + border-bottom: 2px solid currentColor; + background: #f3f2f1; + color: #0b0c0c; + cursor: pointer; + font-family: "GDS Transport", Arial, sans-serif; + font-size: 1.5rem; + line-height: 1.25; + padding: 12px 18px; + text-decoration: none; +} + +.app-ks4-chart-toggle__button:hover { + background: #e7ecf0; +} + +.app-ks4-chart-toggle__button:focus { + outline: 3px solid #fd0; + outline-offset: 0; + box-shadow: inset 0 -2px 0 #0b0c0c; +} + +.app-ks4-chart-view[hidden] { + display: none !important; +} + .app-ks4-copy { max-width: 660px; line-height: 1.45; @@ -1175,6 +1214,16 @@ $app-ks4-color-track: #f3f2f1; -webkit-overflow-scrolling: touch; } + .app-ks4-chart-toggle { + align-items: stretch; + flex-direction: column; + } + + .app-ks4-chart-toggle__button { + align-self: flex-start; + font-size: 1.1875rem; + } + .app-ks4-tabs .govuk-table { min-width: 40rem; } diff --git a/SAPSec.Web/Views/School/Ks4CoreSubjects.cshtml b/SAPSec.Web/Views/School/Ks4CoreSubjects.cshtml index c4fdecb9..71e5804b 100644 --- a/SAPSec.Web/Views/School/Ks4CoreSubjects.cshtml +++ b/SAPSec.Web/Views/School/Ks4CoreSubjects.cshtml @@ -825,5 +825,6 @@ + } diff --git a/SAPSec.Web/Views/School/Ks4HeadlineMeasures.cshtml b/SAPSec.Web/Views/School/Ks4HeadlineMeasures.cshtml index fb51e86f..6e1e4449 100644 --- a/SAPSec.Web/Views/School/Ks4HeadlineMeasures.cshtml +++ b/SAPSec.Web/Views/School/Ks4HeadlineMeasures.cshtml @@ -390,5 +390,6 @@ + } From 6f39524e6aae600d0a7851a042abe20767c0da74 Mon Sep 17 00:00:00 2001 From: vreddy Date: Thu, 4 Jun 2026 10:13:23 +0100 Subject: [PATCH 02/39] fixed some css issues --- SAPSec.Web/AssetSrc/scss/main.scss | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/SAPSec.Web/AssetSrc/scss/main.scss b/SAPSec.Web/AssetSrc/scss/main.scss index 56ea26d8..04c8bbe7 100644 --- a/SAPSec.Web/AssetSrc/scss/main.scss +++ b/SAPSec.Web/AssetSrc/scss/main.scss @@ -1101,11 +1101,13 @@ $app-ks4-color-track: #f3f2f1; .app-ks4-chart-toggle__title { margin-bottom: 0; + font-size: 1.1875rem; + line-height: 1.3157894737; } .app-ks4-chart-toggle__button { border: 0; - border-bottom: 2px solid currentColor; + border-bottom: 2px solid #949494; background: #f3f2f1; color: #0b0c0c; cursor: pointer; @@ -1116,10 +1118,6 @@ $app-ks4-color-track: #f3f2f1; text-decoration: none; } -.app-ks4-chart-toggle__button:hover { - background: #e7ecf0; -} - .app-ks4-chart-toggle__button:focus { outline: 3px solid #fd0; outline-offset: 0; From e71086ac3be69769916eb3935202c754eb37ac24 Mon Sep 17 00:00:00 2001 From: vreddy Date: Thu, 4 Jun 2026 14:55:48 +0100 Subject: [PATCH 03/39] fix css --- SAPSec.Web/AssetSrc/scss/main.scss | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/SAPSec.Web/AssetSrc/scss/main.scss b/SAPSec.Web/AssetSrc/scss/main.scss index 04c8bbe7..06cc7384 100644 --- a/SAPSec.Web/AssetSrc/scss/main.scss +++ b/SAPSec.Web/AssetSrc/scss/main.scss @@ -1093,10 +1093,11 @@ $app-ks4-color-track: #f3f2f1; .app-ks4-chart-toggle { display: flex; - align-items: flex-start; + align-items: center; justify-content: space-between; gap: 16px; margin-bottom: 20px; + padding-top: 10px; } .app-ks4-chart-toggle__title { @@ -1218,8 +1219,11 @@ $app-ks4-color-track: #f3f2f1; } .app-ks4-chart-toggle__button { - align-self: flex-start; + align-self: stretch; font-size: 1.1875rem; + line-height: 1.3157894737; + text-align: center; + width: 100%; } .app-ks4-tabs .govuk-table { From 838c5702ec0ead259cd0d4714efdb9eadc10078a Mon Sep 17 00:00:00 2001 From: vreddy Date: Fri, 5 Jun 2026 08:55:52 +0100 Subject: [PATCH 04/39] fix button size --- SAPSec.Web/AssetSrc/scss/main.scss | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/SAPSec.Web/AssetSrc/scss/main.scss b/SAPSec.Web/AssetSrc/scss/main.scss index 06cc7384..8904f84d 100644 --- a/SAPSec.Web/AssetSrc/scss/main.scss +++ b/SAPSec.Web/AssetSrc/scss/main.scss @@ -1102,8 +1102,7 @@ $app-ks4-color-track: #f3f2f1; .app-ks4-chart-toggle__title { margin-bottom: 0; - font-size: 1.1875rem; - line-height: 1.3157894737; + @include govuk-font($size: 19, $weight: bold); } .app-ks4-chart-toggle__button { @@ -1113,8 +1112,7 @@ $app-ks4-color-track: #f3f2f1; color: #0b0c0c; cursor: pointer; font-family: "GDS Transport", Arial, sans-serif; - font-size: 1.5rem; - line-height: 1.25; + @include govuk-font($size: 24); padding: 12px 18px; text-decoration: none; } @@ -1122,7 +1120,6 @@ $app-ks4-color-track: #f3f2f1; .app-ks4-chart-toggle__button:focus { outline: 3px solid #fd0; outline-offset: 0; - box-shadow: inset 0 -2px 0 #0b0c0c; } .app-ks4-chart-view[hidden] { @@ -1220,8 +1217,7 @@ $app-ks4-color-track: #f3f2f1; .app-ks4-chart-toggle__button { align-self: stretch; - font-size: 1.1875rem; - line-height: 1.3157894737; + @include govuk-font($size: 19); text-align: center; width: 100%; } From a02132babfd299bef544e883e9648ad2c1c09afc Mon Sep 17 00:00:00 2001 From: vreddy Date: Fri, 5 Jun 2026 09:08:10 +0100 Subject: [PATCH 05/39] fix button and charts --- SAPSec.Web/AssetSrc/js/chart-factory.js | 125 +++++++++++++++++++-- SAPSec.Web/AssetSrc/js/ks4-chart-toggle.js | 3 +- SAPSec.Web/AssetSrc/scss/main.scss | 14 +-- 3 files changed, 118 insertions(+), 24 deletions(-) diff --git a/SAPSec.Web/AssetSrc/js/chart-factory.js b/SAPSec.Web/AssetSrc/js/chart-factory.js index b4eea571..4e50d71b 100644 --- a/SAPSec.Web/AssetSrc/js/chart-factory.js +++ b/SAPSec.Web/AssetSrc/js/chart-factory.js @@ -259,7 +259,90 @@ }; } - function buildChartOptions(type, gdsStyles, axisStep, axisSuffix, axisMin, axisMax, axisAutoSkip, showLegend, showDataLabels, showXGrid, barLabelAlign) { + function getNumericSeriesValues(chartData) { + if (!chartData || !Array.isArray(chartData.datasets)) { + return []; + } + + return chartData.datasets + .flatMap(function (dataset) { + return Array.isArray(dataset.data) ? dataset.data : []; + }) + .filter(function (value) { + return value !== null && value !== undefined && !Number.isNaN(Number(value)); + }) + .map(Number); + } + + function getNiceStepSize(range) { + if (!range || range <= 0) { + return 1; + } + + const roughStep = range / 4; + const magnitude = Math.pow(10, Math.floor(Math.log10(roughStep))); + const normalised = roughStep / magnitude; + + if (normalised <= 1) { + return magnitude; + } + + if (normalised <= 2) { + return 2 * magnitude; + } + + if (normalised <= 5) { + return 5 * magnitude; + } + + return 10 * magnitude; + } + + function roundDownToStep(value, step) { + return Math.floor(value / step) * step; + } + + function roundUpToStep(value, step) { + return Math.ceil(value / step) * step; + } + + function getDynamicLineAxisConfig(chartData, axisSuffix) { + const values = getNumericSeriesValues(chartData); + if (!values.length) { + return null; + } + + const rawMin = Math.min.apply(null, values); + const rawMax = Math.max.apply(null, values); + const range = rawMax - rawMin; + const padding = range === 0 + ? Math.max(Math.abs(rawMax) * 0.1, axisSuffix === '%' ? 2 : 1) + : Math.max(range * 0.2, axisSuffix === '%' ? 2 : 1); + + let min = rawMin - padding; + let max = rawMax + padding; + + if (axisSuffix === '%') { + min = Math.max(0, min); + max = Math.min(100, max); + } else { + min = Math.max(0, min); + } + + if (min === max) { + max = min + (axisSuffix === '%' ? 4 : 2); + } + + const step = getNiceStepSize(max - min); + + return { + min: roundDownToStep(min, step), + max: roundUpToStep(max, step), + step + }; + } + + function buildChartOptions(type, gdsStyles, axisStep, axisSuffix, axisMin, axisMax, axisAutoSkip, showLegend, showDataLabels, showXGrid, barLabelAlign, dynamicLineAxis) { const common = { responsive: true, maintainAspectRatio: false, @@ -271,11 +354,13 @@ size: gdsStyles.fontSize }; - const stepSize = axisStep; - const axisTickCount = axisMin !== null && axisMax !== null && stepSize - ? Math.floor((axisMax - axisMin) / stepSize) + 1 + const resolvedAxisMin = dynamicLineAxis ? dynamicLineAxis.min : axisMin; + const resolvedAxisMax = dynamicLineAxis ? dynamicLineAxis.max : axisMax; + const stepSize = dynamicLineAxis ? dynamicLineAxis.step : axisStep; + const axisTickCount = resolvedAxisMin !== null && resolvedAxisMax !== null && stepSize + ? Math.floor((resolvedAxisMax - resolvedAxisMin) / stepSize) + 1 : undefined; - const explicitTicks = buildExplicitTicks(axisMin, axisMax, stepSize); + const explicitTicks = buildExplicitTicks(resolvedAxisMin, resolvedAxisMax, stepSize); const legendOptions = { display: showLegend, @@ -300,9 +385,9 @@ }, scales: { y: { - beginAtZero: true, - min: axisMin ?? undefined, - max: axisMax ?? undefined, + beginAtZero: !dynamicLineAxis, + min: resolvedAxisMin ?? undefined, + max: resolvedAxisMax ?? undefined, grace: CHART_CONFIG.line.axis.grace, afterBuildTicks: explicitTicks, grid: { @@ -340,7 +425,16 @@ } }, plugins: { - tooltip: { enabled: false }, + tooltip: { + enabled: true, + callbacks: { + label: function (context) { + const label = context.dataset.label ? context.dataset.label + ': ' : ''; + const value = context.parsed.y; + return `${label}${value}${axisSuffix}`; + } + } + }, legend: legendOptions, title: { display: false, @@ -573,6 +667,13 @@ return ks4CoreSubjectYearByYearChartIds.has(canvas.id); } + function isYearByYearLineChart(canvas) { + return canvas && ( + canvas.id.includes('yearbyyear-chart') + || canvas.id.includes('year-by-year-chart') + ); + } + function initCharts() { document.querySelectorAll('.js-chart').forEach(canvas => { if (charts[canvas.id]) { @@ -612,6 +713,9 @@ const labelDecimals = canvas.dataset.labelDecimals ? parseInt(canvas.dataset.labelDecimals, 10) : null; + const dynamicLineAxis = type === 'line' && isYearByYearLineChart(canvas) + ? getDynamicLineAxisConfig(chartData, axisSuffix) + : null; const rawColors = canvas.dataset.colors ? JSON.parse(canvas.dataset.colors) @@ -652,7 +756,8 @@ showLegend, showDataLabels, showXGrid, - barLabelAlign), + barLabelAlign, + dynamicLineAxis), plugins: [ ...(showDataLabels ? [ChartDataLabels] : []), noDataBarLabelsPlugin diff --git a/SAPSec.Web/AssetSrc/js/ks4-chart-toggle.js b/SAPSec.Web/AssetSrc/js/ks4-chart-toggle.js index 73b7e2fa..c8b6621d 100644 --- a/SAPSec.Web/AssetSrc/js/ks4-chart-toggle.js +++ b/SAPSec.Web/AssetSrc/js/ks4-chart-toggle.js @@ -41,9 +41,10 @@ var button = document.createElement("button"); button.type = "button"; - button.className = "app-ks4-chart-toggle__button"; + button.className = "govuk-button govuk-button--secondary app-ks4-chart-toggle__button"; button.textContent = "Show year by year"; button.setAttribute("aria-pressed", "false"); + button.setAttribute("data-module", "govuk-button"); header.appendChild(title); header.appendChild(button); diff --git a/SAPSec.Web/AssetSrc/scss/main.scss b/SAPSec.Web/AssetSrc/scss/main.scss index 8904f84d..5303112a 100644 --- a/SAPSec.Web/AssetSrc/scss/main.scss +++ b/SAPSec.Web/AssetSrc/scss/main.scss @@ -1106,20 +1106,8 @@ $app-ks4-color-track: #f3f2f1; } .app-ks4-chart-toggle__button { - border: 0; - border-bottom: 2px solid #949494; - background: #f3f2f1; - color: #0b0c0c; - cursor: pointer; - font-family: "GDS Transport", Arial, sans-serif; @include govuk-font($size: 24); - padding: 12px 18px; - text-decoration: none; -} - -.app-ks4-chart-toggle__button:focus { - outline: 3px solid #fd0; - outline-offset: 0; + margin-bottom: 0; } .app-ks4-chart-view[hidden] { From 1f7a95cbc2416af428f9ebd2bacb838809327698 Mon Sep 17 00:00:00 2001 From: vreddy Date: Fri, 5 Jun 2026 10:06:20 +0100 Subject: [PATCH 06/39] fix charts label --- SAPSec.Web/AssetSrc/js/chart-factory.js | 26 ++++++++++---- .../Ks4CoreSubjects.cshtml | 1 + .../Ks4HeadlineMeasures.cshtml | 1 + .../SchoolKs4HeadlineMeasuresPageTests.cs | 35 ++++++++++++++----- ...sComparisonKs4HeadlineMeasuresPageTests.cs | 27 +++++++++++--- 5 files changed, 71 insertions(+), 19 deletions(-) diff --git a/SAPSec.Web/AssetSrc/js/chart-factory.js b/SAPSec.Web/AssetSrc/js/chart-factory.js index 4e50d71b..8c18ddb8 100644 --- a/SAPSec.Web/AssetSrc/js/chart-factory.js +++ b/SAPSec.Web/AssetSrc/js/chart-factory.js @@ -312,21 +312,17 @@ return null; } - const rawMin = Math.min.apply(null, values); const rawMax = Math.max.apply(null, values); - const range = rawMax - rawMin; + const range = rawMax; const padding = range === 0 ? Math.max(Math.abs(rawMax) * 0.1, axisSuffix === '%' ? 2 : 1) : Math.max(range * 0.2, axisSuffix === '%' ? 2 : 1); - let min = rawMin - padding; + let min = 0; let max = rawMax + padding; if (axisSuffix === '%') { - min = Math.max(0, min); max = Math.min(100, max); - } else { - min = Math.max(0, min); } if (min === max) { @@ -342,6 +338,14 @@ }; } + function formatTooltipValue(value, axisSuffix) { + if (value === null || value === undefined || Number.isNaN(Number(value))) { + return 'No data'; + } + + return `${Number(value)}${axisSuffix}`; + } + function buildChartOptions(type, gdsStyles, axisStep, axisSuffix, axisMin, axisMax, axisAutoSkip, showLegend, showDataLabels, showXGrid, barLabelAlign, dynamicLineAxis) { const common = { responsive: true, @@ -377,6 +381,10 @@ if (type === 'line') { return { ...common, + interaction: { + mode: 'index', + intersect: false + }, layout: { padding: { top: CHART_CONFIG.line.layout.topPadding, @@ -427,11 +435,15 @@ plugins: { tooltip: { enabled: true, + displayColors: false, callbacks: { + title: function (contexts) { + return contexts?.[0]?.label ?? ''; + }, label: function (context) { const label = context.dataset.label ? context.dataset.label + ': ' : ''; const value = context.parsed.y; - return `${label}${value}${axisSuffix}`; + return `${label}${formatTooltipValue(value, axisSuffix)}`; } } }, diff --git a/SAPSec.Web/Views/SimilarSchoolsComparison/Ks4CoreSubjects.cshtml b/SAPSec.Web/Views/SimilarSchoolsComparison/Ks4CoreSubjects.cshtml index 49b0cde0..16944775 100644 --- a/SAPSec.Web/Views/SimilarSchoolsComparison/Ks4CoreSubjects.cshtml +++ b/SAPSec.Web/Views/SimilarSchoolsComparison/Ks4CoreSubjects.cshtml @@ -288,5 +288,6 @@ + } diff --git a/SAPSec.Web/Views/SimilarSchoolsComparison/Ks4HeadlineMeasures.cshtml b/SAPSec.Web/Views/SimilarSchoolsComparison/Ks4HeadlineMeasures.cshtml index 212e946f..44dda926 100644 --- a/SAPSec.Web/Views/SimilarSchoolsComparison/Ks4HeadlineMeasures.cshtml +++ b/SAPSec.Web/Views/SimilarSchoolsComparison/Ks4HeadlineMeasures.cshtml @@ -24,6 +24,7 @@ + } diff --git a/Tests/SAPSec.UI.Tests/SchoolKs4HeadlineMeasuresPageTests.cs b/Tests/SAPSec.UI.Tests/SchoolKs4HeadlineMeasuresPageTests.cs index 5092ebc8..0d75b54d 100644 --- a/Tests/SAPSec.UI.Tests/SchoolKs4HeadlineMeasuresPageTests.cs +++ b/Tests/SAPSec.UI.Tests/SchoolKs4HeadlineMeasuresPageTests.cs @@ -1,6 +1,8 @@ using FluentAssertions; using Microsoft.Playwright; using SAPSec.UI.Tests.Infrastructure; +using System.Linq; +using System.Text.Json; using Xunit; namespace SAPSec.UI.Tests; @@ -17,6 +19,12 @@ private async Task NavigateAsync() await Expect(Page.Locator("#ks4-attainment8-school-chart")).ToBeVisibleAsync(); } + private async Task ToggleChartViewAsync(int chartGroupIndex = 0) + { + var chartTabs = Page.Locator(".app-ks4-tabs").Nth(chartGroupIndex); + await chartTabs.GetByRole(AriaRole.Button, new() { Name = "Show year by year" }).ClickAsync(); + } + [Fact] public async Task Ks4HeadlineMeasures_LoadsSuccessfully() { @@ -36,7 +44,7 @@ public async Task Ks4HeadlineMeasures_UsesExpectedColoursForAttainmentCharts() var lineChart = Page.Locator("#ks4-attainment8-school-yearbyyear-chart"); await Expect(barChart).ToBeVisibleAsync(); - await Page.Locator(".govuk-tabs__tab[href='#year-by-year']").ClickAsync(); + await ToggleChartViewAsync(); await Expect(lineChart).ToBeVisibleAsync(); var barColours = await barChart.EvaluateAsync(@" @@ -62,18 +70,29 @@ public async Task Ks4HeadlineMeasures_Attainment8YearByYear_ShowsExpectedYAxisTi { await NavigateAsync(); - await Page.Locator(".govuk-tabs__tab[href='#year-by-year']").ClickAsync(); + await ToggleChartViewAsync(); var lineChart = Page.Locator("#ks4-attainment8-school-yearbyyear-chart"); await Expect(lineChart).ToBeVisibleAsync(); - var tickLabels = await lineChart.EvaluateAsync(@" + var axis = await lineChart.EvaluateAsync(@" el => { const chart = window.Chart && window.Chart.getChart(el); - return chart?.scales?.y?.ticks?.map(tick => tick.label) ?? []; + return { + min: chart?.scales?.y?.min ?? null, + max: chart?.scales?.y?.max ?? null, + ticks: chart?.scales?.y?.ticks?.map(tick => tick.label) ?? [] + }; } "); - tickLabels.Should().Equal("0", "30", "60", "90"); + var min = axis.GetProperty("min").GetDouble(); + var max = axis.GetProperty("max").GetDouble(); + var ticks = axis.GetProperty("ticks").EnumerateArray().Select(tick => tick.GetString()).ToArray(); + + min.Should().BeGreaterThan(0d); + max.Should().BeGreaterThan(min); + (max - min).Should().BeLessThan(90d); + ticks.Should().HaveCountGreaterThan(1); } [Fact] @@ -101,9 +120,9 @@ public async Task Ks4HeadlineMeasures_UsesExpectedColoursForAllSchoolCharts() colours.Should().Equal("#ca357c", "#2a1950", "#2a1950", "#2a1950"); } - await Page.Locator(".govuk-tabs__tab[href='#year-by-year']").ClickAsync(); - await Page.Locator(".govuk-tabs__tab[href='#eng-maths-year-by-year']").ClickAsync(); - await Page.Locator(".govuk-tabs__tab[href='#destinations-year-by-year']").ClickAsync(); + await ToggleChartViewAsync(0); + await ToggleChartViewAsync(1); + await ToggleChartViewAsync(2); var lineChartSelectors = new[] { diff --git a/Tests/SAPSec.UI.Tests/SimilarSchoolsComparisonKs4HeadlineMeasuresPageTests.cs b/Tests/SAPSec.UI.Tests/SimilarSchoolsComparisonKs4HeadlineMeasuresPageTests.cs index 7d4bf94d..b54a39ba 100644 --- a/Tests/SAPSec.UI.Tests/SimilarSchoolsComparisonKs4HeadlineMeasuresPageTests.cs +++ b/Tests/SAPSec.UI.Tests/SimilarSchoolsComparisonKs4HeadlineMeasuresPageTests.cs @@ -1,6 +1,8 @@ using FluentAssertions; using Microsoft.Playwright; using SAPSec.UI.Tests.Infrastructure; +using System.Linq; +using System.Text.Json; using Xunit; namespace SAPSec.UI.Tests; @@ -10,6 +12,12 @@ public class SimilarSchoolsComparisonKs4HeadlineMeasuresPageTests(WebApplication { private const string Path = "/school/108088/view-similar-schools/137621/Ks4HeadlineMeasures"; + private async Task ToggleChartViewAsync(int chartGroupIndex = 0) + { + var chartTabs = Page.Locator(".app-ks4-tabs").Nth(chartGroupIndex); + await chartTabs.GetByRole(AriaRole.Button, new() { Name = "Show year by year" }).ClickAsync(); + } + [Fact] public async Task Ks4HeadlineMeasuresComparison_LoadsSuccessfully() { @@ -47,17 +55,28 @@ public async Task Ks4HeadlineMeasuresComparison_Attainment8YearByYear_ShowsExpec await Page.GotoAsync(Path); await Page.WaitForLoadStateAsync(LoadState.NetworkIdle); - await Page.Locator(".govuk-tabs__tab[href='#year-by-year']").ClickAsync(); + await ToggleChartViewAsync(); var lineChart = Page.Locator("#ks4-attainment8-comparison-yearbyyear-chart"); await Expect(lineChart).ToBeVisibleAsync(); - var tickLabels = await lineChart.EvaluateAsync(@" + var axis = await lineChart.EvaluateAsync(@" el => { const chart = window.Chart && window.Chart.getChart(el); - return chart?.scales?.y?.ticks?.map(tick => tick.label) ?? []; + return { + min: chart?.scales?.y?.min ?? null, + max: chart?.scales?.y?.max ?? null, + ticks: chart?.scales?.y?.ticks?.map(tick => tick.label) ?? [] + }; } "); - tickLabels.Should().Equal("0", "30", "60", "90"); + var min = axis.GetProperty("min").GetDouble(); + var max = axis.GetProperty("max").GetDouble(); + var ticks = axis.GetProperty("ticks").EnumerateArray().Select(tick => tick.GetString()).ToArray(); + + min.Should().BeGreaterThan(0d); + max.Should().BeGreaterThan(min); + (max - min).Should().BeLessThan(90d); + ticks.Should().HaveCountGreaterThan(1); } } From 0ba4b35c843a37db97116282aad0849e8194f05e Mon Sep 17 00:00:00 2001 From: vreddy Date: Fri, 5 Jun 2026 11:29:06 +0100 Subject: [PATCH 07/39] add component --- SAPSec.Web/AssetSrc/js/chart-factory.js | 43 +- SAPSec.Web/AssetSrc/js/content-toggle.js | 84 +++ SAPSec.Web/AssetSrc/js/ks4-chart-toggle.js | 78 ++- SAPSec.Web/AssetSrc/scss/main.scss | 12 +- SAPSec.Web/TagHelpers/ContentToggleItem.cs | 5 + .../TagHelpers/ContentToggleTagHelper.cs | 89 +++ .../TagHelpers/ToggleContentTagHelper.cs | 31 + .../Views/School/Ks4HeadlineMeasures.cshtml | 186 ++--- .../Ks4CoreSubjects.cshtml | 96 +-- .../Ks4HeadlineMeasures.cshtml | 637 +++++++++--------- .../SchoolKs4HeadlineMeasuresPageTests.cs | 2 +- ...sComparisonKs4HeadlineMeasuresPageTests.cs | 2 +- 12 files changed, 769 insertions(+), 496 deletions(-) create mode 100644 SAPSec.Web/AssetSrc/js/content-toggle.js create mode 100644 SAPSec.Web/TagHelpers/ContentToggleItem.cs create mode 100644 SAPSec.Web/TagHelpers/ContentToggleTagHelper.cs create mode 100644 SAPSec.Web/TagHelpers/ToggleContentTagHelper.cs diff --git a/SAPSec.Web/AssetSrc/js/chart-factory.js b/SAPSec.Web/AssetSrc/js/chart-factory.js index 8c18ddb8..fd387443 100644 --- a/SAPSec.Web/AssetSrc/js/chart-factory.js +++ b/SAPSec.Web/AssetSrc/js/chart-factory.js @@ -245,7 +245,15 @@ : gdsStyles.text; } - function buildExplicitTicks(axisMin, axisMax, stepSize) { + function buildExplicitTicks(axisMin, axisMax, stepSize, tickValues) { + if (Array.isArray(tickValues) && tickValues.length) { + return function (axis) { + axis.ticks = tickValues.map(function (value) { + return { value }; + }); + }; + } + if (axisMin === null || axisMax === null || !stepSize) { return undefined; } @@ -312,29 +320,43 @@ return null; } + const rawMin = Math.min.apply(null, values); const rawMax = Math.max.apply(null, values); - const range = rawMax; + const range = rawMax - rawMin; const padding = range === 0 ? Math.max(Math.abs(rawMax) * 0.1, axisSuffix === '%' ? 2 : 1) : Math.max(range * 0.2, axisSuffix === '%' ? 2 : 1); - let min = 0; + const zoomedMin = Math.max(0, rawMin - padding); let max = rawMax + padding; if (axisSuffix === '%') { max = Math.min(100, max); } - if (min === max) { - max = min + (axisSuffix === '%' ? 4 : 2); + let step = getNiceStepSize(max - zoomedMin); + let clusteredMin = roundDownToStep(zoomedMin, step); + let clusteredMax = roundUpToStep(max, step); + + if (clusteredMin === clusteredMax) { + clusteredMax = clusteredMin + (axisSuffix === '%' ? 4 : 2); + step = getNiceStepSize(clusteredMax - clusteredMin); + clusteredMin = roundDownToStep(clusteredMin, step); + clusteredMax = roundUpToStep(clusteredMax, step); } - const step = getNiceStepSize(max - min); + const tickValues = [0]; + for (let value = clusteredMin; value <= clusteredMax; value += step) { + if (value > 0 && !tickValues.includes(value)) { + tickValues.push(value); + } + } return { - min: roundDownToStep(min, step), - max: roundUpToStep(max, step), - step + min: 0, + max: clusteredMax, + step, + tickValues }; } @@ -361,10 +383,11 @@ const resolvedAxisMin = dynamicLineAxis ? dynamicLineAxis.min : axisMin; const resolvedAxisMax = dynamicLineAxis ? dynamicLineAxis.max : axisMax; const stepSize = dynamicLineAxis ? dynamicLineAxis.step : axisStep; + const tickValues = dynamicLineAxis ? dynamicLineAxis.tickValues : null; const axisTickCount = resolvedAxisMin !== null && resolvedAxisMax !== null && stepSize ? Math.floor((resolvedAxisMax - resolvedAxisMin) / stepSize) + 1 : undefined; - const explicitTicks = buildExplicitTicks(resolvedAxisMin, resolvedAxisMax, stepSize); + const explicitTicks = buildExplicitTicks(resolvedAxisMin, resolvedAxisMax, stepSize, tickValues); const legendOptions = { display: showLegend, diff --git a/SAPSec.Web/AssetSrc/js/content-toggle.js b/SAPSec.Web/AssetSrc/js/content-toggle.js new file mode 100644 index 00000000..3e4404a4 --- /dev/null +++ b/SAPSec.Web/AssetSrc/js/content-toggle.js @@ -0,0 +1,84 @@ +(function () { + function setHidden(element, hidden) { + if (!element) { + return; + } + + if (hidden) { + element.setAttribute("hidden", "hidden"); + } else { + element.removeAttribute("hidden"); + } + } + + function resizeCharts(container) { + if (!window.Chart || !container) { + return; + } + + container.querySelectorAll("canvas").forEach(function (canvas) { + var chart = window.Chart.getChart(canvas); + if (!chart) { + return; + } + + chart.resize(); + chart.update("none"); + }); + } + + function initialiseToggle(toggle) { + var title = toggle.querySelector(".app-content-toggle__title"); + var button = toggle.querySelector(".app-content-toggle__button"); + var panels = Array.prototype.slice.call(toggle.querySelectorAll("[data-content-toggle-panel]")); + + if (!title || !button || panels.length < 2) { + return; + } + + var activeIndex = panels.findIndex(function (panel) { + return !panel.hasAttribute("hidden"); + }); + + if (activeIndex < 0) { + activeIndex = 0; + } + + function render(index) { + var nextIndex = (index + 1) % panels.length; + var activePanel = panels[index]; + var nextPanel = panels[nextIndex]; + var activeName = activePanel.getAttribute("data-content-toggle-name") || ""; + var nextName = nextPanel.getAttribute("data-content-toggle-name") || ""; + + panels.forEach(function (panel, panelIndex) { + panel.classList.toggle("app-content-toggle__panel--active", panelIndex === index); + setHidden(panel, panelIndex !== index); + }); + + title.textContent = activeName; + button.textContent = "Show " + nextName.toLowerCase(); + button.setAttribute("aria-pressed", index === 0 ? "false" : "true"); + + resizeCharts(activePanel); + } + + button.addEventListener("click", function () { + activeIndex = (activeIndex + 1) % panels.length; + render(activeIndex); + }); + + render(activeIndex); + } + + function init() { + document.querySelectorAll(".app-content-toggle").forEach(initialiseToggle); + } + + if (document.readyState === "loading") { + document.addEventListener("DOMContentLoaded", init); + return; + } + + init(); +})(); diff --git a/SAPSec.Web/AssetSrc/js/ks4-chart-toggle.js b/SAPSec.Web/AssetSrc/js/ks4-chart-toggle.js index c8b6621d..b6c0ed3e 100644 --- a/SAPSec.Web/AssetSrc/js/ks4-chart-toggle.js +++ b/SAPSec.Web/AssetSrc/js/ks4-chart-toggle.js @@ -1,8 +1,4 @@ (function () { - function getTabText(tabLink) { - return (tabLink && tabLink.textContent ? tabLink.textContent : "").trim().toLowerCase(); - } - function setHidden(element, hidden) { if (!element) { return; @@ -31,17 +27,33 @@ }); } + function getTabTarget(tabLink) { + if (!tabLink) { + return ""; + } + + return tabLink.getAttribute("href") || ""; + } + + function isAverageTabTarget(target) { + return /^#.+three-year-average$/i.test(target) || target === "#three-year-average"; + } + + function isYearByYearTabTarget(target) { + return /^#.+year-by-year$/i.test(target) || target === "#year-by-year"; + } + function buildToggleHeader() { var header = document.createElement("div"); - header.className = "app-ks4-chart-toggle"; + header.className = "app-content-toggle__header"; var title = document.createElement("h3"); - title.className = "govuk-heading-m app-ks4-chart-toggle__title"; + title.className = "govuk-heading-m app-content-toggle__title"; title.textContent = "3-year average"; var button = document.createElement("button"); button.type = "button"; - button.className = "govuk-button govuk-button--secondary app-ks4-chart-toggle__button"; + button.className = "govuk-button govuk-button--secondary app-content-toggle__button"; button.textContent = "Show year by year"; button.setAttribute("aria-pressed", "false"); button.setAttribute("data-module", "govuk-button"); @@ -69,48 +81,70 @@ return; } - if (getTabText(firstTab) !== "3-year average" || getTabText(secondTab) !== "year by year") { + var firstTabTarget = getTabTarget(firstTab); + var secondTabTarget = getTabTarget(secondTab); + + if (!isAverageTabTarget(firstTabTarget) || !isYearByYearTabTarget(secondTabTarget)) { return; } var averageChart = firstPanel.querySelector(".app-ks4-chart-container"); var yearlyChart = secondPanel.querySelector(".app-ks4-chart-container"); - if (!averageChart || !yearlyChart) { + if (!averageChart || !yearlyChart || firstPanel.querySelector(".app-content-toggle")) { return; } + var chartsPanelId = firstTabTarget.slice(1).replace(/three-year-average$/i, "charts"); + firstTab.textContent = "Charts"; + firstTab.setAttribute("href", "#" + chartsPanelId); + firstPanel.id = chartsPanelId; listItems[1].remove(); - averageChart.classList.add("app-ks4-chart-view"); - yearlyChart.classList.add("app-ks4-chart-view"); - averageChart.classList.add("app-ks4-chart-view--active"); + var toggleContainer = document.createElement("div"); + toggleContainer.className = "app-content-toggle"; + toggleContainer.setAttribute("data-module", "app-content-toggle"); - setHidden(yearlyChart, true); + var toggle = buildToggleHeader(); + toggleContainer.appendChild(toggle.header); - firstPanel.insertBefore(yearlyChart, firstPanel.firstChild); - firstPanel.insertBefore(averageChart, yearlyChart); + var averagePanel = document.createElement("div"); + averagePanel.className = "app-content-toggle__panel app-content-toggle__panel--active"; + averagePanel.setAttribute("data-content-toggle-panel", "true"); + averagePanel.setAttribute("data-content-toggle-name", "3-year average"); + averagePanel.id = firstTabTarget.slice(1); + averagePanel.appendChild(averageChart); - var toggle = buildToggleHeader(); - firstPanel.insertBefore(toggle.header, firstPanel.firstChild); + var yearlyPanel = document.createElement("div"); + yearlyPanel.className = "app-content-toggle__panel"; + yearlyPanel.setAttribute("data-content-toggle-panel", "true"); + yearlyPanel.setAttribute("data-content-toggle-name", "Year by year"); + yearlyPanel.id = secondTabTarget.slice(1); + yearlyPanel.setAttribute("hidden", "hidden"); + yearlyPanel.appendChild(yearlyChart); + + toggleContainer.appendChild(averagePanel); + toggleContainer.appendChild(yearlyPanel); + + firstPanel.insertBefore(toggleContainer, firstPanel.firstChild); var showingYearly = false; toggle.button.addEventListener("click", function () { showingYearly = !showingYearly; - averageChart.classList.toggle("app-ks4-chart-view--active", !showingYearly); - yearlyChart.classList.toggle("app-ks4-chart-view--active", showingYearly); + averagePanel.classList.toggle("app-content-toggle__panel--active", !showingYearly); + yearlyPanel.classList.toggle("app-content-toggle__panel--active", showingYearly); - setHidden(averageChart, showingYearly); - setHidden(yearlyChart, !showingYearly); + setHidden(averagePanel, showingYearly); + setHidden(yearlyPanel, !showingYearly); toggle.title.textContent = showingYearly ? "Year by year" : "3-year average"; toggle.button.textContent = showingYearly ? "Show 3-year average" : "Show year by year"; toggle.button.setAttribute("aria-pressed", showingYearly ? "true" : "false"); - resizeCharts(showingYearly ? yearlyChart : averageChart); + resizeCharts(showingYearly ? yearlyPanel : averagePanel); }); secondPanel.remove(); diff --git a/SAPSec.Web/AssetSrc/scss/main.scss b/SAPSec.Web/AssetSrc/scss/main.scss index 5303112a..9178f42d 100644 --- a/SAPSec.Web/AssetSrc/scss/main.scss +++ b/SAPSec.Web/AssetSrc/scss/main.scss @@ -1091,7 +1091,7 @@ $app-ks4-color-track: #f3f2f1; height: 330px; } -.app-ks4-chart-toggle { +.app-content-toggle__header { display: flex; align-items: center; justify-content: space-between; @@ -1100,17 +1100,17 @@ $app-ks4-color-track: #f3f2f1; padding-top: 10px; } -.app-ks4-chart-toggle__title { +.app-content-toggle__title { margin-bottom: 0; @include govuk-font($size: 19, $weight: bold); } -.app-ks4-chart-toggle__button { +.app-content-toggle__button { @include govuk-font($size: 24); margin-bottom: 0; } -.app-ks4-chart-view[hidden] { +.app-content-toggle__panel[hidden] { display: none !important; } @@ -1198,12 +1198,12 @@ $app-ks4-color-track: #f3f2f1; -webkit-overflow-scrolling: touch; } - .app-ks4-chart-toggle { + .app-content-toggle__header { align-items: stretch; flex-direction: column; } - .app-ks4-chart-toggle__button { + .app-content-toggle__button { align-self: stretch; @include govuk-font($size: 19); text-align: center; diff --git a/SAPSec.Web/TagHelpers/ContentToggleItem.cs b/SAPSec.Web/TagHelpers/ContentToggleItem.cs new file mode 100644 index 00000000..14375569 --- /dev/null +++ b/SAPSec.Web/TagHelpers/ContentToggleItem.cs @@ -0,0 +1,5 @@ +using Microsoft.AspNetCore.Html; + +namespace SAPSec.Web.TagHelpers; + +public sealed record ContentToggleItem(string Id, string Name, IHtmlContent Content, bool Active); diff --git a/SAPSec.Web/TagHelpers/ContentToggleTagHelper.cs b/SAPSec.Web/TagHelpers/ContentToggleTagHelper.cs new file mode 100644 index 00000000..4dbb32b3 --- /dev/null +++ b/SAPSec.Web/TagHelpers/ContentToggleTagHelper.cs @@ -0,0 +1,89 @@ +using Microsoft.AspNetCore.Mvc.Rendering; +using Microsoft.AspNetCore.Razor.TagHelpers; + +namespace SAPSec.Web.TagHelpers; + +[HtmlTargetElement("content-toggle")] +public class ContentToggleTagHelper : TagHelper +{ + [HtmlAttributeName("id")] + public string? Id { get; set; } + + public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output) + { + var items = new List(); + context.Items[ToggleContentTagHelper.ContextKey] = items; + + await output.GetChildContentAsync(); + + if (items.Count < 2) + { + output.SuppressOutput(); + return; + } + + var activeIndex = items.FindIndex(item => item.Active); + if (activeIndex < 0) + { + activeIndex = 0; + } + + output.TagName = "div"; + output.TagMode = TagMode.StartTagAndEndTag; + output.Attributes.SetAttribute("class", "app-content-toggle"); + output.Attributes.SetAttribute("data-module", "app-content-toggle"); + + if (!string.IsNullOrWhiteSpace(Id)) + { + output.Attributes.SetAttribute("id", Id); + } + + var header = new TagBuilder("div"); + header.AddCssClass("app-content-toggle__header"); + + var title = new TagBuilder("h3"); + title.AddCssClass("govuk-heading-m"); + title.AddCssClass("app-content-toggle__title"); + title.InnerHtml.Append(items[activeIndex].Name); + + var button = new TagBuilder("button"); + button.Attributes["type"] = "button"; + button.AddCssClass("govuk-button"); + button.AddCssClass("govuk-button--secondary"); + button.AddCssClass("app-content-toggle__button"); + button.Attributes["aria-pressed"] = activeIndex == 0 ? "false" : "true"; + button.Attributes["data-module"] = "govuk-button"; + button.InnerHtml.Append($"Show {items[(activeIndex + 1) % items.Count].Name.ToLowerInvariant()}"); + + header.InnerHtml.AppendHtml(title); + header.InnerHtml.AppendHtml(button); + + output.Content.AppendHtml(header); + + for (var i = 0; i < items.Count; i++) + { + var item = items[i]; + var panel = new TagBuilder("div"); + panel.AddCssClass("app-content-toggle__panel"); + panel.Attributes["data-content-toggle-panel"] = "true"; + panel.Attributes["data-content-toggle-name"] = item.Name; + + if (!string.IsNullOrWhiteSpace(item.Id)) + { + panel.Attributes["id"] = item.Id; + } + + if (i == activeIndex) + { + panel.AddCssClass("app-content-toggle__panel--active"); + } + else + { + panel.Attributes["hidden"] = "hidden"; + } + + panel.InnerHtml.AppendHtml(item.Content); + output.Content.AppendHtml(panel); + } + } +} diff --git a/SAPSec.Web/TagHelpers/ToggleContentTagHelper.cs b/SAPSec.Web/TagHelpers/ToggleContentTagHelper.cs new file mode 100644 index 00000000..04f4d607 --- /dev/null +++ b/SAPSec.Web/TagHelpers/ToggleContentTagHelper.cs @@ -0,0 +1,31 @@ +using Microsoft.AspNetCore.Html; +using Microsoft.AspNetCore.Razor.TagHelpers; + +namespace SAPSec.Web.TagHelpers; + +[HtmlTargetElement("toggle-content", ParentTag = "content-toggle")] +public class ToggleContentTagHelper : TagHelper +{ + internal static readonly object ContextKey = new(); + + [HtmlAttributeName("id")] + public string Id { get; set; } = string.Empty; + + [HtmlAttributeName("name")] + public string Name { get; set; } = string.Empty; + + [HtmlAttributeName("active")] + public bool Active { get; set; } + + public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output) + { + if (context.Items.TryGetValue(ContextKey, out var items) + && items is IList toggleItems) + { + var childContent = await output.GetChildContentAsync(); + toggleItems.Add(new ContentToggleItem(Id, Name, new HtmlString(childContent.GetContent()), Active)); + } + + output.SuppressOutput(); + } +} diff --git a/SAPSec.Web/Views/School/Ks4HeadlineMeasures.cshtml b/SAPSec.Web/Views/School/Ks4HeadlineMeasures.cshtml index 6e1e4449..9ac2c823 100644 --- a/SAPSec.Web/Views/School/Ks4HeadlineMeasures.cshtml +++ b/SAPSec.Web/Views/School/Ks4HeadlineMeasures.cshtml @@ -58,56 +58,58 @@
-
-
- @{ - var attainmentBarData = new decimal?[] { Model.SchoolAttainment8ThreeYearAverage, Model.SimilarSchoolsAttainment8ThreeYearAverage, Model.LocalAuthorityAttainment8ThreeYearAverage, Model.EnglandAttainment8ThreeYearAverage }; - } - @if (HasAnyData(attainmentBarData)) - { - - - } - else - { -

No available data

- } -
-
- -
-
- - -
+
+ + +
+ @{ + var attainmentBarData = new decimal?[] { Model.SchoolAttainment8ThreeYearAverage, Model.SimilarSchoolsAttainment8ThreeYearAverage, Model.LocalAuthorityAttainment8ThreeYearAverage, Model.EnglandAttainment8ThreeYearAverage }; + } + @if (HasAnyData(attainmentBarData)) + { + + + } + else + { +

No available data

+ } +
+
+ +
+ + +
+
+
@@ -190,30 +192,33 @@
-
-
- @{ - var engMathsBarData = new decimal?[] { Model.SchoolEngMathsThreeYearAverage, Model.SimilarSchoolsEngMathsThreeYearAverage, Model.LocalAuthorityEngMathsThreeYearAverage, Model.EnglandEngMathsThreeYearAverage }; - } - @if (HasAnyData(engMathsBarData)) - { - - } - else - { -

No available data

- } -
-
-
-
- -
+
+ + +
+ @{ + var engMathsBarData = new decimal?[] { Model.SchoolEngMathsThreeYearAverage, Model.SimilarSchoolsEngMathsThreeYearAverage, Model.LocalAuthorityEngMathsThreeYearAverage, Model.EnglandEngMathsThreeYearAverage }; + } + @if (HasAnyData(engMathsBarData)) + { + + } + else + { +

No available data

+ } +
+
+ +
+ +
+
+

These are the top performing schools for this measure.

@@ -306,30 +311,33 @@
-
-
- @{ - var destinationsBarData = new decimal?[] { Model.SchoolDestinationsThreeYearAverage, Model.SimilarSchoolsDestinationsThreeYearAverage, Model.LocalAuthorityDestinationsThreeYearAverage, Model.EnglandDestinationsThreeYearAverage }; - } - @if (HasAnyData(destinationsBarData)) - { - - } - else - { -

No available data

- } -
-
-
-
- -
+
+ + +
+ @{ + var destinationsBarData = new decimal?[] { Model.SchoolDestinationsThreeYearAverage, Model.SimilarSchoolsDestinationsThreeYearAverage, Model.LocalAuthorityDestinationsThreeYearAverage, Model.EnglandDestinationsThreeYearAverage }; + } + @if (HasAnyData(destinationsBarData)) + { + + } + else + { +

No available data

+ } +
+
+ +
+ +
+
+

These are the top performing schools for this measure.

@@ -390,6 +398,6 @@ - + } diff --git a/SAPSec.Web/Views/SimilarSchoolsComparison/Ks4CoreSubjects.cshtml b/SAPSec.Web/Views/SimilarSchoolsComparison/Ks4CoreSubjects.cshtml index 16944775..beadb775 100644 --- a/SAPSec.Web/Views/SimilarSchoolsComparison/Ks4CoreSubjects.cshtml +++ b/SAPSec.Web/Views/SimilarSchoolsComparison/Ks4CoreSubjects.cshtml @@ -188,59 +188,59 @@
-
-
- @if (HasAnyData(chartData.data)) - { - - - } - else - { -

No available data

- } -
-
- -
-
- - -
+
+ + +
+ @if (HasAnyData(chartData.data)) + { + + + } + else + { +

No available data

+ } +
+
+ +
+ + +
+
+
@@ -288,6 +288,6 @@ - + } diff --git a/SAPSec.Web/Views/SimilarSchoolsComparison/Ks4HeadlineMeasures.cshtml b/SAPSec.Web/Views/SimilarSchoolsComparison/Ks4HeadlineMeasures.cshtml index 44dda926..88d81304 100644 --- a/SAPSec.Web/Views/SimilarSchoolsComparison/Ks4HeadlineMeasures.cshtml +++ b/SAPSec.Web/Views/SimilarSchoolsComparison/Ks4HeadlineMeasures.cshtml @@ -24,7 +24,7 @@ - + } @@ -35,129 +35,129 @@

Attainment 8 views

-
- @{ - var chartData = new - { - labels = new[] - { - Model.Name, - Model.SimilarSchoolName, - "Schools in England average" - }, - data = new decimal?[] - { - Model.ThisSchoolAttainment8ThreeYearAverage, - Model.SelectedSchoolAttainment8ThreeYearAverage, - Model.EnglandAttainment8ThreeYearAverage - } - }; - } -
- @if (HasAnyData(chartData.data)) - { - - - } - else - { -

No available data

- } -
-
- -
- @{ - var yearByYearData = new - { - labels = ks4YearLabels, - datasets = new object[] - { - new +
+ + + @{ + var chartData = new { - label = Model.Name, - borderWidth = 2, - pointRadius = 4, - pointHoverRadius = 5, - tension = 0, - data = new decimal[] + labels = new[] { - Model.ThisSchoolAttainment8YearByYear?.Previous2 ?? 0m, - Model.ThisSchoolAttainment8YearByYear?.Previous ?? 0m, - Model.ThisSchoolAttainment8YearByYear?.Current ?? 0m - } - }, - new - { - label = Model.SimilarSchoolName, - borderWidth = 2, - pointRadius = 4, - pointHoverRadius = 5, - tension = 0, - data = new decimal[] + Model.Name, + Model.SimilarSchoolName, + "Schools in England average" + }, + data = new decimal?[] { - Model.SelectedSchoolAttainment8YearByYear?.Previous2 ?? 0m, - Model.SelectedSchoolAttainment8YearByYear?.Previous ?? 0m, - Model.SelectedSchoolAttainment8YearByYear?.Current ?? 0m + Model.ThisSchoolAttainment8ThreeYearAverage, + Model.SelectedSchoolAttainment8ThreeYearAverage, + Model.EnglandAttainment8ThreeYearAverage } - }, - new + }; + } +
+ @if (HasAnyData(chartData.data)) { - label = "Schools in England average", - borderWidth = 2, - borderColor = yearByYearColors[2], - backgroundColor = yearByYearColors[2], - pointRadius = 4, - pointHoverRadius = 5, - tension = 0, - data = new decimal[] + + + } + else + { +

No available data

+ } +
+
+ + @{ + var yearByYearData = new + { + labels = ks4YearLabels, + datasets = new object[] { - Model.EnglandAttainment8YearByYear?.Previous2 ?? 0m, - Model.EnglandAttainment8YearByYear?.Previous ?? 0m, - Model.EnglandAttainment8YearByYear?.Current ?? 0m + new + { + label = Model.Name, + borderWidth = 2, + pointRadius = 4, + pointHoverRadius = 5, + tension = 0, + data = new decimal[] + { + Model.ThisSchoolAttainment8YearByYear?.Previous2 ?? 0m, + Model.ThisSchoolAttainment8YearByYear?.Previous ?? 0m, + Model.ThisSchoolAttainment8YearByYear?.Current ?? 0m + } + }, + new + { + label = Model.SimilarSchoolName, + borderWidth = 2, + pointRadius = 4, + pointHoverRadius = 5, + tension = 0, + data = new decimal[] + { + Model.SelectedSchoolAttainment8YearByYear?.Previous2 ?? 0m, + Model.SelectedSchoolAttainment8YearByYear?.Previous ?? 0m, + Model.SelectedSchoolAttainment8YearByYear?.Current ?? 0m + } + }, + new + { + label = "Schools in England average", + borderWidth = 2, + borderColor = yearByYearColors[2], + backgroundColor = yearByYearColors[2], + pointRadius = 4, + pointHoverRadius = 5, + tension = 0, + data = new decimal[] + { + Model.EnglandAttainment8YearByYear?.Previous2 ?? 0m, + Model.EnglandAttainment8YearByYear?.Previous ?? 0m, + Model.EnglandAttainment8YearByYear?.Current ?? 0m + } + } } - } + }; } - }; - } -
- - -
+
+ + +
+
+
@@ -226,126 +226,125 @@
-
- @{ - var engMathsChartData = new - { - labels = new[] - { - Model.Name, - Model.SimilarSchoolName, - "Schools in England average" - }, - data = new decimal?[] - { - Model.ThisSchoolEngMaths49ThreeYearAverage, - Model.SelectedSchoolEngMaths49ThreeYearAverage, - Model.EnglandEngMaths49ThreeYearAverage - } - }; - } -
- @if (HasAnyData(engMathsChartData.data)) - { - - - } - else - { -

No available data

- } -
- -
- -
- @{ - var engMathsYearByYearData = new - { - labels = ks4YearLabels, - datasets = new object[] - { - new +
+ + + @{ + var engMathsChartData = new { - label = Model.Name, - borderWidth = 2, - pointRadius = 4, - pointHoverRadius = 5, - tension = 0, - data = new decimal?[] + labels = new[] { - Model.ThisSchoolEngMaths49YearByYear?.Previous2, - Model.ThisSchoolEngMaths49YearByYear?.Previous, - Model.ThisSchoolEngMaths49YearByYear?.Current - } - }, - new - { - label = Model.SimilarSchoolName, - borderWidth = 2, - pointRadius = 4, - pointHoverRadius = 5, - tension = 0, + Model.Name, + Model.SimilarSchoolName, + "Schools in England average" + }, data = new decimal?[] { - Model.SelectedSchoolEngMaths49YearByYear?.Previous2, - Model.SelectedSchoolEngMaths49YearByYear?.Previous, - Model.SelectedSchoolEngMaths49YearByYear?.Current + Model.ThisSchoolEngMaths49ThreeYearAverage, + Model.SelectedSchoolEngMaths49ThreeYearAverage, + Model.EnglandEngMaths49ThreeYearAverage } - }, - new + }; + } +
+ @if (HasAnyData(engMathsChartData.data)) { - label = "Schools in England average", - borderWidth = 2, - pointRadius = 4, - pointHoverRadius = 5, - tension = 0, - data = new decimal?[] + + + } + else + { +

No available data

+ } +
+
+ + @{ + var engMathsYearByYearData = new + { + labels = ks4YearLabels, + datasets = new object[] { - Model.EnglandEngMaths49YearByYear?.Previous2, - Model.EnglandEngMaths49YearByYear?.Previous, - Model.EnglandEngMaths49YearByYear?.Current + new + { + label = Model.Name, + borderWidth = 2, + pointRadius = 4, + pointHoverRadius = 5, + tension = 0, + data = new decimal?[] + { + Model.ThisSchoolEngMaths49YearByYear?.Previous2, + Model.ThisSchoolEngMaths49YearByYear?.Previous, + Model.ThisSchoolEngMaths49YearByYear?.Current + } + }, + new + { + label = Model.SimilarSchoolName, + borderWidth = 2, + pointRadius = 4, + pointHoverRadius = 5, + tension = 0, + data = new decimal?[] + { + Model.SelectedSchoolEngMaths49YearByYear?.Previous2, + Model.SelectedSchoolEngMaths49YearByYear?.Previous, + Model.SelectedSchoolEngMaths49YearByYear?.Current + } + }, + new + { + label = "Schools in England average", + borderWidth = 2, + pointRadius = 4, + pointHoverRadius = 5, + tension = 0, + data = new decimal?[] + { + Model.EnglandEngMaths49YearByYear?.Previous2, + Model.EnglandEngMaths49YearByYear?.Previous, + Model.EnglandEngMaths49YearByYear?.Current + } + } } - } + }; } - }; - } -
- - -
+
+ + +
+
+
@@ -429,125 +428,125 @@
-
- @{ - var destinationsChartData = new - { - labels = new[] - { - Model.Name, - Model.SimilarSchoolName, - "Schools in England average" - }, - data = new decimal?[] - { - Model.ThisSchoolDestinationsThreeYearAverage, - Model.SelectedSchoolDestinationsThreeYearAverage, - Model.EnglandDestinationsThreeYearAverage - } - }; - } -
- @if (HasAnyData(destinationsChartData.data)) - { - - - } - else - { -

No available data

- } -
-
- -
- @{ - var destinationsYearByYearData = new - { - labels = ks4YearLabels, - datasets = new object[] - { - new +
+ + + @{ + var destinationsChartData = new { - label = Model.Name, - borderWidth = 2, - pointRadius = 4, - pointHoverRadius = 5, - tension = 0, - data = new decimal?[] + labels = new[] { - Model.ThisSchoolDestinationsYearByYear?.Previous2, - Model.ThisSchoolDestinationsYearByYear?.Previous, - Model.ThisSchoolDestinationsYearByYear?.Current - } - }, - new - { - label = Model.SimilarSchoolName, - borderWidth = 2, - pointRadius = 4, - pointHoverRadius = 5, - tension = 0, + Model.Name, + Model.SimilarSchoolName, + "Schools in England average" + }, data = new decimal?[] { - Model.SelectedSchoolDestinationsYearByYear?.Previous2, - Model.SelectedSchoolDestinationsYearByYear?.Previous, - Model.SelectedSchoolDestinationsYearByYear?.Current + Model.ThisSchoolDestinationsThreeYearAverage, + Model.SelectedSchoolDestinationsThreeYearAverage, + Model.EnglandDestinationsThreeYearAverage } - }, - new + }; + } +
+ @if (HasAnyData(destinationsChartData.data)) { - label = "Schools in England average", - borderWidth = 2, - pointRadius = 4, - pointHoverRadius = 5, - tension = 0, - data = new decimal?[] + + + } + else + { +

No available data

+ } +
+
+ + @{ + var destinationsYearByYearData = new + { + labels = ks4YearLabels, + datasets = new object[] { - Model.EnglandDestinationsYearByYear?.Previous2, - Model.EnglandDestinationsYearByYear?.Previous, - Model.EnglandDestinationsYearByYear?.Current + new + { + label = Model.Name, + borderWidth = 2, + pointRadius = 4, + pointHoverRadius = 5, + tension = 0, + data = new decimal?[] + { + Model.ThisSchoolDestinationsYearByYear?.Previous2, + Model.ThisSchoolDestinationsYearByYear?.Previous, + Model.ThisSchoolDestinationsYearByYear?.Current + } + }, + new + { + label = Model.SimilarSchoolName, + borderWidth = 2, + pointRadius = 4, + pointHoverRadius = 5, + tension = 0, + data = new decimal?[] + { + Model.SelectedSchoolDestinationsYearByYear?.Previous2, + Model.SelectedSchoolDestinationsYearByYear?.Previous, + Model.SelectedSchoolDestinationsYearByYear?.Current + } + }, + new + { + label = "Schools in England average", + borderWidth = 2, + pointRadius = 4, + pointHoverRadius = 5, + tension = 0, + data = new decimal?[] + { + Model.EnglandDestinationsYearByYear?.Previous2, + Model.EnglandDestinationsYearByYear?.Previous, + Model.EnglandDestinationsYearByYear?.Current + } + } } - } + }; } - }; - } -
- - -
+
+ + +
+
+
diff --git a/Tests/SAPSec.UI.Tests/SchoolKs4HeadlineMeasuresPageTests.cs b/Tests/SAPSec.UI.Tests/SchoolKs4HeadlineMeasuresPageTests.cs index 0d75b54d..0e4a02ec 100644 --- a/Tests/SAPSec.UI.Tests/SchoolKs4HeadlineMeasuresPageTests.cs +++ b/Tests/SAPSec.UI.Tests/SchoolKs4HeadlineMeasuresPageTests.cs @@ -89,7 +89,7 @@ public async Task Ks4HeadlineMeasures_Attainment8YearByYear_ShowsExpectedYAxisTi var max = axis.GetProperty("max").GetDouble(); var ticks = axis.GetProperty("ticks").EnumerateArray().Select(tick => tick.GetString()).ToArray(); - min.Should().BeGreaterThan(0d); + min.Should().Be(0d); max.Should().BeGreaterThan(min); (max - min).Should().BeLessThan(90d); ticks.Should().HaveCountGreaterThan(1); diff --git a/Tests/SAPSec.UI.Tests/SimilarSchoolsComparisonKs4HeadlineMeasuresPageTests.cs b/Tests/SAPSec.UI.Tests/SimilarSchoolsComparisonKs4HeadlineMeasuresPageTests.cs index b54a39ba..f3d89fe2 100644 --- a/Tests/SAPSec.UI.Tests/SimilarSchoolsComparisonKs4HeadlineMeasuresPageTests.cs +++ b/Tests/SAPSec.UI.Tests/SimilarSchoolsComparisonKs4HeadlineMeasuresPageTests.cs @@ -74,7 +74,7 @@ public async Task Ks4HeadlineMeasuresComparison_Attainment8YearByYear_ShowsExpec var max = axis.GetProperty("max").GetDouble(); var ticks = axis.GetProperty("ticks").EnumerateArray().Select(tick => tick.GetString()).ToArray(); - min.Should().BeGreaterThan(0d); + min.Should().Be(0d); max.Should().BeGreaterThan(min); (max - min).Should().BeLessThan(90d); ticks.Should().HaveCountGreaterThan(1); From 9567c4aefed1a99462c37362e6624d8426aaab3d Mon Sep 17 00:00:00 2001 From: vreddy Date: Fri, 5 Jun 2026 12:29:36 +0100 Subject: [PATCH 08/39] fix charts --- SAPSec.Web/AssetSrc/js/chart-factory.js | 41 +++++-------------- .../SchoolKs4HeadlineMeasuresPageTests.cs | 2 +- ...sComparisonKs4HeadlineMeasuresPageTests.cs | 2 +- 3 files changed, 12 insertions(+), 33 deletions(-) diff --git a/SAPSec.Web/AssetSrc/js/chart-factory.js b/SAPSec.Web/AssetSrc/js/chart-factory.js index fd387443..e63811fc 100644 --- a/SAPSec.Web/AssetSrc/js/chart-factory.js +++ b/SAPSec.Web/AssetSrc/js/chart-factory.js @@ -245,15 +245,7 @@ : gdsStyles.text; } - function buildExplicitTicks(axisMin, axisMax, stepSize, tickValues) { - if (Array.isArray(tickValues) && tickValues.length) { - return function (axis) { - axis.ticks = tickValues.map(function (value) { - return { value }; - }); - }; - } - + function buildExplicitTicks(axisMin, axisMax, stepSize) { if (axisMin === null || axisMax === null || !stepSize) { return undefined; } @@ -327,36 +319,24 @@ ? Math.max(Math.abs(rawMax) * 0.1, axisSuffix === '%' ? 2 : 1) : Math.max(range * 0.2, axisSuffix === '%' ? 2 : 1); - const zoomedMin = Math.max(0, rawMin - padding); + let min = rawMin - padding; let max = rawMax + padding; if (axisSuffix === '%') { + min = Math.max(0, min); max = Math.min(100, max); } - let step = getNiceStepSize(max - zoomedMin); - let clusteredMin = roundDownToStep(zoomedMin, step); - let clusteredMax = roundUpToStep(max, step); - - if (clusteredMin === clusteredMax) { - clusteredMax = clusteredMin + (axisSuffix === '%' ? 4 : 2); - step = getNiceStepSize(clusteredMax - clusteredMin); - clusteredMin = roundDownToStep(clusteredMin, step); - clusteredMax = roundUpToStep(clusteredMax, step); + if (min === max) { + max = min + (axisSuffix === '%' ? 4 : 2); } - const tickValues = [0]; - for (let value = clusteredMin; value <= clusteredMax; value += step) { - if (value > 0 && !tickValues.includes(value)) { - tickValues.push(value); - } - } + const step = getNiceStepSize(max - min); return { - min: 0, - max: clusteredMax, - step, - tickValues + min: roundDownToStep(min, step), + max: roundUpToStep(max, step), + step }; } @@ -383,11 +363,10 @@ const resolvedAxisMin = dynamicLineAxis ? dynamicLineAxis.min : axisMin; const resolvedAxisMax = dynamicLineAxis ? dynamicLineAxis.max : axisMax; const stepSize = dynamicLineAxis ? dynamicLineAxis.step : axisStep; - const tickValues = dynamicLineAxis ? dynamicLineAxis.tickValues : null; const axisTickCount = resolvedAxisMin !== null && resolvedAxisMax !== null && stepSize ? Math.floor((resolvedAxisMax - resolvedAxisMin) / stepSize) + 1 : undefined; - const explicitTicks = buildExplicitTicks(resolvedAxisMin, resolvedAxisMax, stepSize, tickValues); + const explicitTicks = buildExplicitTicks(resolvedAxisMin, resolvedAxisMax, stepSize); const legendOptions = { display: showLegend, diff --git a/Tests/SAPSec.UI.Tests/SchoolKs4HeadlineMeasuresPageTests.cs b/Tests/SAPSec.UI.Tests/SchoolKs4HeadlineMeasuresPageTests.cs index 0e4a02ec..0d75b54d 100644 --- a/Tests/SAPSec.UI.Tests/SchoolKs4HeadlineMeasuresPageTests.cs +++ b/Tests/SAPSec.UI.Tests/SchoolKs4HeadlineMeasuresPageTests.cs @@ -89,7 +89,7 @@ public async Task Ks4HeadlineMeasures_Attainment8YearByYear_ShowsExpectedYAxisTi var max = axis.GetProperty("max").GetDouble(); var ticks = axis.GetProperty("ticks").EnumerateArray().Select(tick => tick.GetString()).ToArray(); - min.Should().Be(0d); + min.Should().BeGreaterThan(0d); max.Should().BeGreaterThan(min); (max - min).Should().BeLessThan(90d); ticks.Should().HaveCountGreaterThan(1); diff --git a/Tests/SAPSec.UI.Tests/SimilarSchoolsComparisonKs4HeadlineMeasuresPageTests.cs b/Tests/SAPSec.UI.Tests/SimilarSchoolsComparisonKs4HeadlineMeasuresPageTests.cs index f3d89fe2..b54a39ba 100644 --- a/Tests/SAPSec.UI.Tests/SimilarSchoolsComparisonKs4HeadlineMeasuresPageTests.cs +++ b/Tests/SAPSec.UI.Tests/SimilarSchoolsComparisonKs4HeadlineMeasuresPageTests.cs @@ -74,7 +74,7 @@ public async Task Ks4HeadlineMeasuresComparison_Attainment8YearByYear_ShowsExpec var max = axis.GetProperty("max").GetDouble(); var ticks = axis.GetProperty("ticks").EnumerateArray().Select(tick => tick.GetString()).ToArray(); - min.Should().Be(0d); + min.Should().BeGreaterThan(0d); max.Should().BeGreaterThan(min); (max - min).Should().BeLessThan(90d); ticks.Should().HaveCountGreaterThan(1); From 8fb35ec2826a8a2142f357ccdf39ba1c85e7e5ff Mon Sep 17 00:00:00 2001 From: vreddy Date: Fri, 5 Jun 2026 13:03:46 +0100 Subject: [PATCH 09/39] fixd the charts label position --- SAPSec.Web/AssetSrc/js/chart-factory.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SAPSec.Web/AssetSrc/js/chart-factory.js b/SAPSec.Web/AssetSrc/js/chart-factory.js index e63811fc..d04dc86f 100644 --- a/SAPSec.Web/AssetSrc/js/chart-factory.js +++ b/SAPSec.Web/AssetSrc/js/chart-factory.js @@ -370,7 +370,7 @@ const legendOptions = { display: showLegend, - position: CHART_CONFIG.legend.position, + position: type === 'line' ? 'top' : CHART_CONFIG.legend.position, labels: { usePointStyle: true, pointStyle: CHART_CONFIG.legend.pointStyle, From 1e8c5a5004fb8c68f934d3f2f5d9821cc46bc6ce Mon Sep 17 00:00:00 2001 From: vreddy Date: Fri, 5 Jun 2026 13:52:12 +0100 Subject: [PATCH 10/39] button fix --- SAPSec.Web/AssetSrc/js/ks4-chart-toggle.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SAPSec.Web/AssetSrc/js/ks4-chart-toggle.js b/SAPSec.Web/AssetSrc/js/ks4-chart-toggle.js index b6c0ed3e..0f13e571 100644 --- a/SAPSec.Web/AssetSrc/js/ks4-chart-toggle.js +++ b/SAPSec.Web/AssetSrc/js/ks4-chart-toggle.js @@ -53,7 +53,7 @@ var button = document.createElement("button"); button.type = "button"; - button.className = "govuk-button govuk-button--secondary app-content-toggle__button"; + button.className = "govuk-button govuk-button--secondary"; button.textContent = "Show year by year"; button.setAttribute("aria-pressed", "false"); button.setAttribute("data-module", "govuk-button"); From 30b18fffa5ebcc9335c8f30f766cf83935d1f84c Mon Sep 17 00:00:00 2001 From: vreddy Date: Fri, 5 Jun 2026 14:15:56 +0100 Subject: [PATCH 11/39] removed the unwanted class --- SAPSec.Web/TagHelpers/ContentToggleTagHelper.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/SAPSec.Web/TagHelpers/ContentToggleTagHelper.cs b/SAPSec.Web/TagHelpers/ContentToggleTagHelper.cs index 4dbb32b3..8f46727a 100644 --- a/SAPSec.Web/TagHelpers/ContentToggleTagHelper.cs +++ b/SAPSec.Web/TagHelpers/ContentToggleTagHelper.cs @@ -50,7 +50,6 @@ public override async Task ProcessAsync(TagHelperContext context, TagHelperOutpu button.Attributes["type"] = "button"; button.AddCssClass("govuk-button"); button.AddCssClass("govuk-button--secondary"); - button.AddCssClass("app-content-toggle__button"); button.Attributes["aria-pressed"] = activeIndex == 0 ? "false" : "true"; button.Attributes["data-module"] = "govuk-button"; button.InnerHtml.Append($"Show {items[(activeIndex + 1) % items.Count].Name.ToLowerInvariant()}"); From 76547603a569b87138ce5d64ddf7265c6ccb9158 Mon Sep 17 00:00:00 2001 From: vreddy Date: Fri, 5 Jun 2026 15:00:47 +0100 Subject: [PATCH 12/39] fix toggle button --- SAPSec.Web/AssetSrc/js/content-toggle.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SAPSec.Web/AssetSrc/js/content-toggle.js b/SAPSec.Web/AssetSrc/js/content-toggle.js index 3e4404a4..86336154 100644 --- a/SAPSec.Web/AssetSrc/js/content-toggle.js +++ b/SAPSec.Web/AssetSrc/js/content-toggle.js @@ -29,7 +29,7 @@ function initialiseToggle(toggle) { var title = toggle.querySelector(".app-content-toggle__title"); - var button = toggle.querySelector(".app-content-toggle__button"); + var button = toggle.querySelector(".app-content-toggle__header button[type='button']"); var panels = Array.prototype.slice.call(toggle.querySelectorAll("[data-content-toggle-panel]")); if (!title || !button || panels.length < 2) { From cc5ad203aa88f23d26dcef86d413ebe5f24ca4cd Mon Sep 17 00:00:00 2001 From: vreddy Date: Tue, 9 Jun 2026 10:53:58 +0100 Subject: [PATCH 13/39] custom axe chart --- SAPSec.Web/AssetSrc/js/chart-factory.js | 169 ++++++++++++++++++++++-- 1 file changed, 157 insertions(+), 12 deletions(-) diff --git a/SAPSec.Web/AssetSrc/js/chart-factory.js b/SAPSec.Web/AssetSrc/js/chart-factory.js index d04dc86f..3617eb47 100644 --- a/SAPSec.Web/AssetSrc/js/chart-factory.js +++ b/SAPSec.Web/AssetSrc/js/chart-factory.js @@ -39,7 +39,8 @@ minor: 1 }, axis: { - grace: '5%' + grace: '5%', + breakRatio: 0.15 }, series: { tension: 0.2, @@ -96,6 +97,90 @@ const charts = {}; + function registerBrokenLinearScale() { + if (!window.Chart || window.__appBrokenLinearScaleRegistered) { + return; + } + + class BrokenLinearScale extends Chart.LinearScale { + static id = 'brokenLinear'; + + static defaults = { + ...Chart.LinearScale.defaults, + breakStart: null, + breakRatio: CHART_CONFIG.line.axis.breakRatio + }; + + getPixelForValue(value) { + if (this.isHorizontal()) { + return super.getPixelForValue(value); + } + + const numericValue = Number(value); + const min = Number(this.min); + const max = Number(this.max); + const breakStart = Number(this.options.breakStart); + const breakRatio = Number(this.options.breakRatio ?? CHART_CONFIG.line.axis.breakRatio); + + if (!Number.isFinite(numericValue) + || !Number.isFinite(min) + || !Number.isFinite(max) + || !Number.isFinite(breakStart) + || breakStart <= min + || breakStart >= max) { + return super.getPixelForValue(value); + } + + const totalHeight = this.bottom - this.top; + const lowerHeight = totalHeight * breakRatio; + const upperHeight = totalHeight - lowerHeight; + + if (numericValue <= breakStart) { + const lowerFraction = (numericValue - min) / Math.max(breakStart - min, 1e-6); + return this.bottom - (lowerFraction * lowerHeight); + } + + const upperFraction = (numericValue - breakStart) / Math.max(max - breakStart, 1e-6); + return (this.bottom - lowerHeight) - (upperFraction * upperHeight); + } + + getValueForPixel(pixel) { + if (this.isHorizontal()) { + return super.getValueForPixel(pixel); + } + + const min = Number(this.min); + const max = Number(this.max); + const breakStart = Number(this.options.breakStart); + const breakRatio = Number(this.options.breakRatio ?? CHART_CONFIG.line.axis.breakRatio); + + if (!Number.isFinite(min) + || !Number.isFinite(max) + || !Number.isFinite(breakStart) + || breakStart <= min + || breakStart >= max) { + return super.getValueForPixel(pixel); + } + + const totalHeight = this.bottom - this.top; + const lowerHeight = totalHeight * breakRatio; + const breakPixel = this.bottom - lowerHeight; + + if (pixel >= breakPixel) { + const lowerFraction = (this.bottom - pixel) / Math.max(lowerHeight, 1e-6); + return min + (lowerFraction * (breakStart - min)); + } + + const upperHeight = totalHeight - lowerHeight; + const upperFraction = (breakPixel - pixel) / Math.max(upperHeight, 1e-6); + return breakStart + (upperFraction * (max - breakStart)); + } + } + + Chart.register(BrokenLinearScale); + window.__appBrokenLinearScaleRegistered = true; + } + function gdsVars(canvas) { const s = getComputedStyle(canvas); @@ -245,7 +330,15 @@ : gdsStyles.text; } - function buildExplicitTicks(axisMin, axisMax, stepSize) { + function buildExplicitTicks(axisMin, axisMax, stepSize, tickValues) { + if (Array.isArray(tickValues) && tickValues.length) { + return function (axis) { + axis.ticks = tickValues.map(function (value) { + return { value }; + }); + }; + } + if (axisMin === null || axisMax === null || !stepSize) { return undefined; } @@ -319,24 +412,37 @@ ? Math.max(Math.abs(rawMax) * 0.1, axisSuffix === '%' ? 2 : 1) : Math.max(range * 0.2, axisSuffix === '%' ? 2 : 1); - let min = rawMin - padding; + const zoomedMin = Math.max(0, rawMin - padding); let max = rawMax + padding; if (axisSuffix === '%') { - min = Math.max(0, min); max = Math.min(100, max); } - if (min === max) { - max = min + (axisSuffix === '%' ? 4 : 2); + let step = getNiceStepSize(max - zoomedMin); + let clusteredMin = roundDownToStep(zoomedMin, step); + let clusteredMax = roundUpToStep(max, step); + + if (clusteredMin === clusteredMax) { + clusteredMax = clusteredMin + (axisSuffix === '%' ? 4 : 2); + step = getNiceStepSize(clusteredMax - clusteredMin); + clusteredMin = roundDownToStep(clusteredMin, step); + clusteredMax = roundUpToStep(clusteredMax, step); } - const step = getNiceStepSize(max - min); + const tickValues = [0]; + for (let value = clusteredMin; value <= clusteredMax; value += step) { + if (value > 0 && !tickValues.includes(value)) { + tickValues.push(value); + } + } return { - min: roundDownToStep(min, step), - max: roundUpToStep(max, step), - step + min: 0, + max: clusteredMax, + step, + breakStart: clusteredMin, + tickValues }; } @@ -363,10 +469,11 @@ const resolvedAxisMin = dynamicLineAxis ? dynamicLineAxis.min : axisMin; const resolvedAxisMax = dynamicLineAxis ? dynamicLineAxis.max : axisMax; const stepSize = dynamicLineAxis ? dynamicLineAxis.step : axisStep; + const tickValues = dynamicLineAxis ? dynamicLineAxis.tickValues : null; const axisTickCount = resolvedAxisMin !== null && resolvedAxisMax !== null && stepSize ? Math.floor((resolvedAxisMax - resolvedAxisMin) / stepSize) + 1 : undefined; - const explicitTicks = buildExplicitTicks(resolvedAxisMin, resolvedAxisMax, stepSize); + const explicitTicks = buildExplicitTicks(resolvedAxisMin, resolvedAxisMax, stepSize, tickValues); const legendOptions = { display: showLegend, @@ -395,9 +502,12 @@ }, scales: { y: { + type: dynamicLineAxis?.breakStart ? 'brokenLinear' : undefined, beginAtZero: !dynamicLineAxis, min: resolvedAxisMin ?? undefined, max: resolvedAxisMax ?? undefined, + breakStart: dynamicLineAxis?.breakStart ?? undefined, + breakRatio: dynamicLineAxis?.breakStart ? CHART_CONFIG.line.axis.breakRatio : undefined, grace: CHART_CONFIG.line.axis.grace, afterBuildTicks: explicitTicks, grid: { @@ -603,6 +713,38 @@ } }; + const brokenAxisMarkerPlugin = { + id: 'brokenAxisMarker', + afterDraw(chart) { + const yScale = chart.scales?.y; + if (!yScale || yScale.options?.type !== 'brokenLinear') { + return; + } + + const breakStart = Number(yScale.options.breakStart); + if (!Number.isFinite(breakStart) || breakStart <= 0) { + return; + } + + const y = yScale.getPixelForValue(breakStart); + const x = yScale.left + 8; + const ctx = chart.ctx; + + ctx.save(); + ctx.strokeStyle = '#0b0c0c'; + ctx.lineWidth = 2; + ctx.beginPath(); + ctx.moveTo(x - 4, y + 6); + ctx.lineTo(x + 2, y); + ctx.lineTo(x - 4, y - 6); + ctx.moveTo(x + 4, y + 6); + ctx.lineTo(x + 10, y); + ctx.lineTo(x + 4, y - 6); + ctx.stroke(); + ctx.restore(); + } + }; + function buildDatasets(type, chartData, colorConfig, barOptions) { if (type === 'line') { return chartData.datasets.map((ds, i) => { @@ -689,6 +831,8 @@ } function initCharts() { + registerBrokenLinearScale(); + document.querySelectorAll('.js-chart').forEach(canvas => { if (charts[canvas.id]) { charts[canvas.id].destroy(); @@ -774,7 +918,8 @@ dynamicLineAxis), plugins: [ ...(showDataLabels ? [ChartDataLabels] : []), - noDataBarLabelsPlugin + noDataBarLabelsPlugin, + brokenAxisMarkerPlugin ] }; From 1925259bc7402f676199b6f9eb7bfa4c9d1a8044 Mon Sep 17 00:00:00 2001 From: vreddy Date: Tue, 9 Jun 2026 11:47:13 +0100 Subject: [PATCH 14/39] updated the chart.js --- SAPSec.Web/AssetSrc/js/chart-factory.js | 173 ++---------------------- 1 file changed, 15 insertions(+), 158 deletions(-) diff --git a/SAPSec.Web/AssetSrc/js/chart-factory.js b/SAPSec.Web/AssetSrc/js/chart-factory.js index 3617eb47..e363aaa5 100644 --- a/SAPSec.Web/AssetSrc/js/chart-factory.js +++ b/SAPSec.Web/AssetSrc/js/chart-factory.js @@ -39,8 +39,7 @@ minor: 1 }, axis: { - grace: '5%', - breakRatio: 0.15 + grace: '5%' }, series: { tension: 0.2, @@ -97,90 +96,6 @@ const charts = {}; - function registerBrokenLinearScale() { - if (!window.Chart || window.__appBrokenLinearScaleRegistered) { - return; - } - - class BrokenLinearScale extends Chart.LinearScale { - static id = 'brokenLinear'; - - static defaults = { - ...Chart.LinearScale.defaults, - breakStart: null, - breakRatio: CHART_CONFIG.line.axis.breakRatio - }; - - getPixelForValue(value) { - if (this.isHorizontal()) { - return super.getPixelForValue(value); - } - - const numericValue = Number(value); - const min = Number(this.min); - const max = Number(this.max); - const breakStart = Number(this.options.breakStart); - const breakRatio = Number(this.options.breakRatio ?? CHART_CONFIG.line.axis.breakRatio); - - if (!Number.isFinite(numericValue) - || !Number.isFinite(min) - || !Number.isFinite(max) - || !Number.isFinite(breakStart) - || breakStart <= min - || breakStart >= max) { - return super.getPixelForValue(value); - } - - const totalHeight = this.bottom - this.top; - const lowerHeight = totalHeight * breakRatio; - const upperHeight = totalHeight - lowerHeight; - - if (numericValue <= breakStart) { - const lowerFraction = (numericValue - min) / Math.max(breakStart - min, 1e-6); - return this.bottom - (lowerFraction * lowerHeight); - } - - const upperFraction = (numericValue - breakStart) / Math.max(max - breakStart, 1e-6); - return (this.bottom - lowerHeight) - (upperFraction * upperHeight); - } - - getValueForPixel(pixel) { - if (this.isHorizontal()) { - return super.getValueForPixel(pixel); - } - - const min = Number(this.min); - const max = Number(this.max); - const breakStart = Number(this.options.breakStart); - const breakRatio = Number(this.options.breakRatio ?? CHART_CONFIG.line.axis.breakRatio); - - if (!Number.isFinite(min) - || !Number.isFinite(max) - || !Number.isFinite(breakStart) - || breakStart <= min - || breakStart >= max) { - return super.getValueForPixel(pixel); - } - - const totalHeight = this.bottom - this.top; - const lowerHeight = totalHeight * breakRatio; - const breakPixel = this.bottom - lowerHeight; - - if (pixel >= breakPixel) { - const lowerFraction = (this.bottom - pixel) / Math.max(lowerHeight, 1e-6); - return min + (lowerFraction * (breakStart - min)); - } - - const upperHeight = totalHeight - lowerHeight; - const upperFraction = (breakPixel - pixel) / Math.max(upperHeight, 1e-6); - return breakStart + (upperFraction * (max - breakStart)); - } - } - - Chart.register(BrokenLinearScale); - window.__appBrokenLinearScaleRegistered = true; - } - function gdsVars(canvas) { const s = getComputedStyle(canvas); @@ -330,15 +245,7 @@ : gdsStyles.text; } - function buildExplicitTicks(axisMin, axisMax, stepSize, tickValues) { - if (Array.isArray(tickValues) && tickValues.length) { - return function (axis) { - axis.ticks = tickValues.map(function (value) { - return { value }; - }); - }; - } - + function buildExplicitTicks(axisMin, axisMax, stepSize) { if (axisMin === null || axisMax === null || !stepSize) { return undefined; } @@ -412,37 +319,24 @@ ? Math.max(Math.abs(rawMax) * 0.1, axisSuffix === '%' ? 2 : 1) : Math.max(range * 0.2, axisSuffix === '%' ? 2 : 1); - const zoomedMin = Math.max(0, rawMin - padding); + let min = rawMin - padding; let max = rawMax + padding; if (axisSuffix === '%') { + min = Math.max(0, min); max = Math.min(100, max); } - let step = getNiceStepSize(max - zoomedMin); - let clusteredMin = roundDownToStep(zoomedMin, step); - let clusteredMax = roundUpToStep(max, step); - - if (clusteredMin === clusteredMax) { - clusteredMax = clusteredMin + (axisSuffix === '%' ? 4 : 2); - step = getNiceStepSize(clusteredMax - clusteredMin); - clusteredMin = roundDownToStep(clusteredMin, step); - clusteredMax = roundUpToStep(clusteredMax, step); + if (min === max) { + max = min + (axisSuffix === '%' ? 4 : 2); } - const tickValues = [0]; - for (let value = clusteredMin; value <= clusteredMax; value += step) { - if (value > 0 && !tickValues.includes(value)) { - tickValues.push(value); - } - } + const step = getNiceStepSize(max - min); return { - min: 0, - max: clusteredMax, - step, - breakStart: clusteredMin, - tickValues + min: roundDownToStep(min, step), + max: roundUpToStep(max, step), + step }; } @@ -469,15 +363,15 @@ const resolvedAxisMin = dynamicLineAxis ? dynamicLineAxis.min : axisMin; const resolvedAxisMax = dynamicLineAxis ? dynamicLineAxis.max : axisMax; const stepSize = dynamicLineAxis ? dynamicLineAxis.step : axisStep; - const tickValues = dynamicLineAxis ? dynamicLineAxis.tickValues : null; const axisTickCount = resolvedAxisMin !== null && resolvedAxisMax !== null && stepSize ? Math.floor((resolvedAxisMax - resolvedAxisMin) / stepSize) + 1 : undefined; - const explicitTicks = buildExplicitTicks(resolvedAxisMin, resolvedAxisMax, stepSize, tickValues); + const explicitTicks = buildExplicitTicks(resolvedAxisMin, resolvedAxisMax, stepSize); const legendOptions = { display: showLegend, position: type === 'line' ? 'top' : CHART_CONFIG.legend.position, + align: type === 'line' ? 'start' : 'center', labels: { usePointStyle: true, pointStyle: CHART_CONFIG.legend.pointStyle, @@ -502,12 +396,9 @@ }, scales: { y: { - type: dynamicLineAxis?.breakStart ? 'brokenLinear' : undefined, beginAtZero: !dynamicLineAxis, min: resolvedAxisMin ?? undefined, max: resolvedAxisMax ?? undefined, - breakStart: dynamicLineAxis?.breakStart ?? undefined, - breakRatio: dynamicLineAxis?.breakStart ? CHART_CONFIG.line.axis.breakRatio : undefined, grace: CHART_CONFIG.line.axis.grace, afterBuildTicks: explicitTicks, grid: { @@ -547,7 +438,8 @@ plugins: { tooltip: { enabled: true, - displayColors: false, + displayColors: true, + usePointStyle: true, callbacks: { title: function (contexts) { return contexts?.[0]?.label ?? ''; @@ -713,38 +605,6 @@ } }; - const brokenAxisMarkerPlugin = { - id: 'brokenAxisMarker', - afterDraw(chart) { - const yScale = chart.scales?.y; - if (!yScale || yScale.options?.type !== 'brokenLinear') { - return; - } - - const breakStart = Number(yScale.options.breakStart); - if (!Number.isFinite(breakStart) || breakStart <= 0) { - return; - } - - const y = yScale.getPixelForValue(breakStart); - const x = yScale.left + 8; - const ctx = chart.ctx; - - ctx.save(); - ctx.strokeStyle = '#0b0c0c'; - ctx.lineWidth = 2; - ctx.beginPath(); - ctx.moveTo(x - 4, y + 6); - ctx.lineTo(x + 2, y); - ctx.lineTo(x - 4, y - 6); - ctx.moveTo(x + 4, y + 6); - ctx.lineTo(x + 10, y); - ctx.lineTo(x + 4, y - 6); - ctx.stroke(); - ctx.restore(); - } - }; - function buildDatasets(type, chartData, colorConfig, barOptions) { if (type === 'line') { return chartData.datasets.map((ds, i) => { @@ -831,8 +691,6 @@ } function initCharts() { - registerBrokenLinearScale(); - document.querySelectorAll('.js-chart').forEach(canvas => { if (charts[canvas.id]) { charts[canvas.id].destroy(); @@ -918,8 +776,7 @@ dynamicLineAxis), plugins: [ ...(showDataLabels ? [ChartDataLabels] : []), - noDataBarLabelsPlugin, - brokenAxisMarkerPlugin + noDataBarLabelsPlugin ] }; From dcecd4f8efa402c898fe3a9aa8c15e17c981b5d7 Mon Sep 17 00:00:00 2001 From: vreddy Date: Tue, 9 Jun 2026 12:19:26 +0100 Subject: [PATCH 15/39] fix labels --- SAPSec.Web/AssetSrc/js/chart-factory.js | 36 ++++++++++++++++++++----- SAPSec.Web/AssetSrc/scss/main.scss | 29 ++++++++++++++++++++ 2 files changed, 58 insertions(+), 7 deletions(-) diff --git a/SAPSec.Web/AssetSrc/js/chart-factory.js b/SAPSec.Web/AssetSrc/js/chart-factory.js index e363aaa5..caeaf415 100644 --- a/SAPSec.Web/AssetSrc/js/chart-factory.js +++ b/SAPSec.Web/AssetSrc/js/chart-factory.js @@ -369,9 +369,9 @@ const explicitTicks = buildExplicitTicks(resolvedAxisMin, resolvedAxisMax, stepSize); const legendOptions = { - display: showLegend, - position: type === 'line' ? 'top' : CHART_CONFIG.legend.position, - align: type === 'line' ? 'start' : 'center', + display: type === 'line' ? false : showLegend, + position: CHART_CONFIG.legend.position, + align: 'center', labels: { usePointStyle: true, pointStyle: CHART_CONFIG.legend.pointStyle, @@ -801,9 +801,9 @@ charts[canvas.id] = chart; if (showLegend) { - const legendContainer = document.querySelector( - `.chart-legend[data-chart-id="${canvas.id}"]` - ); + const legendContainer = type === 'line' + ? ensureTopLegendContainer(canvas) + : document.querySelector(`.chart-legend[data-chart-id="${canvas.id}"]`); if (legendContainer) { buildVerticalLegend(chart, legendContainer); } @@ -830,19 +830,24 @@ } function buildVerticalLegend(chart, container) { + container.innerHTML = ''; + const datasets = Array.isArray(chart.data.datasets) ? chart.data.datasets : [chart.data.datasets]; const ul = document.createElement('ul'); + ul.classList.add('app-chart-legend'); datasets.forEach(ds => { const li = document.createElement('li'); + li.classList.add('app-chart-legend__item'); const box = document.createElement('span'); - box.classList.add('legend-box'); + box.classList.add('app-chart-legend__box'); box.style.backgroundColor = ds.backgroundColor || ds.borderColor || CHART_CONFIG.fallbacks.legendBoxColor; const label = document.createElement('span'); + label.classList.add('app-chart-legend__label'); label.textContent = ds.label; li.appendChild(box); @@ -853,6 +858,23 @@ container.appendChild(ul); } + function ensureTopLegendContainer(canvas) { + const chartContainer = canvas.parentElement; + if (!chartContainer) { + return null; + } + + let legendContainer = chartContainer.querySelector(`.chart-legend[data-chart-id="${canvas.id}"]`); + if (!legendContainer) { + legendContainer = document.createElement('div'); + legendContainer.className = 'chart-legend chart-legend--top'; + legendContainer.setAttribute('data-chart-id', canvas.id); + chartContainer.insertBefore(legendContainer, chartContainer.firstChild); + } + + return legendContainer; + } + function adjustChartResize() { let resizeTimeout; window.addEventListener('resize', () => { diff --git a/SAPSec.Web/AssetSrc/scss/main.scss b/SAPSec.Web/AssetSrc/scss/main.scss index 9178f42d..0ac1dbd1 100644 --- a/SAPSec.Web/AssetSrc/scss/main.scss +++ b/SAPSec.Web/AssetSrc/scss/main.scss @@ -1114,6 +1114,35 @@ $app-ks4-color-track: #f3f2f1; display: none !important; } +.chart-legend--top { + margin-bottom: 12px; +} + +.app-chart-legend { + list-style: none; + margin: 0; + padding: 0; +} + +.app-chart-legend__item { + align-items: center; + display: flex; + gap: 8px; + margin-bottom: 6px; +} + +.app-chart-legend__item:last-child { + margin-bottom: 0; +} + +.app-chart-legend__box { + border-radius: 50%; + display: inline-block; + flex: 0 0 10px; + height: 10px; + width: 10px; +} + .app-ks4-copy { max-width: 660px; line-height: 1.45; From 76dcb550b0b7a0294c3b8654b9455381ebfd8521 Mon Sep 17 00:00:00 2001 From: vreddy Date: Tue, 9 Jun 2026 14:54:47 +0100 Subject: [PATCH 16/39] fix tabs --- SAPSec.Web/AssetSrc/scss/main.scss | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/SAPSec.Web/AssetSrc/scss/main.scss b/SAPSec.Web/AssetSrc/scss/main.scss index 0ac1dbd1..a4ace36a 100644 --- a/SAPSec.Web/AssetSrc/scss/main.scss +++ b/SAPSec.Web/AssetSrc/scss/main.scss @@ -1083,6 +1083,8 @@ $app-ks4-color-track: #f3f2f1; } .app-ks4-chart-container { + display: flex; + flex-direction: column; height: 260px; max-width: 760px; } @@ -1091,6 +1093,11 @@ $app-ks4-color-track: #f3f2f1; height: 330px; } +.app-ks4-chart-container .js-chart { + flex: 1 1 auto; + min-height: 0; +} + .app-content-toggle__header { display: flex; align-items: center; @@ -1115,6 +1122,7 @@ $app-ks4-color-track: #f3f2f1; } .chart-legend--top { + flex: 0 0 auto; margin-bottom: 12px; } From 65a17ca3dc1db07b1582e8d2da08e574f20b56fd Mon Sep 17 00:00:00 2001 From: vreddy Date: Fri, 12 Jun 2026 10:32:34 +0100 Subject: [PATCH 17/39] fix squash design --- SAPSec.Web/AssetSrc/js/chart-factory.js | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/SAPSec.Web/AssetSrc/js/chart-factory.js b/SAPSec.Web/AssetSrc/js/chart-factory.js index caeaf415..a0341210 100644 --- a/SAPSec.Web/AssetSrc/js/chart-factory.js +++ b/SAPSec.Web/AssetSrc/js/chart-factory.js @@ -38,6 +38,10 @@ major: 2, minor: 1 }, + container: { + baseHeight: 420, + withLegendExtraHeight: 80 + }, axis: { grace: '5%' }, @@ -679,6 +683,18 @@ container.style.height = `${height}px`; } + function resizeLineChartContainer(canvas, showLegend) { + const container = canvas.parentElement; + if (!container) { + return; + } + + const height = CHART_CONFIG.line.container.baseHeight + + (showLegend ? CHART_CONFIG.line.container.withLegendExtraHeight : 0); + + container.style.height = `${height}px`; + } + function isKs4CoreSubjectYearByYearChart(canvas) { return ks4CoreSubjectYearByYearChartIds.has(canvas.id); } @@ -704,10 +720,14 @@ const chartData = JSON.parse(canvas.dataset.chart); const type = canvas.dataset.type; + const showLegend = canvas.dataset.showLegend === "true"; + if (type === 'bar') { resizeBarChartContainer(canvas, chartData); } - const showLegend = canvas.dataset.showLegend === "true"; + if (type === 'line' && isYearByYearLineChart(canvas)) { + resizeLineChartContainer(canvas, showLegend); + } const showDataLabels = canvas.dataset.showDatalabels !== "false"; const showXGrid = canvas.dataset.showXGrid === "true"; const forceKs4CoreSubjectTicks = isKs4CoreSubjectYearByYearChart(canvas); From c6cd0b9efce9db6773fdf893bc7073779e0f9ba7 Mon Sep 17 00:00:00 2001 From: vreddy Date: Fri, 12 Jun 2026 11:07:41 +0100 Subject: [PATCH 18/39] fix charts --- SAPSec.Web/AssetSrc/js/chart-factory.js | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/SAPSec.Web/AssetSrc/js/chart-factory.js b/SAPSec.Web/AssetSrc/js/chart-factory.js index a0341210..614350fa 100644 --- a/SAPSec.Web/AssetSrc/js/chart-factory.js +++ b/SAPSec.Web/AssetSrc/js/chart-factory.js @@ -39,8 +39,7 @@ minor: 1 }, container: { - baseHeight: 420, - withLegendExtraHeight: 80 + baseHeight: 420 }, axis: { grace: '5%' @@ -683,16 +682,13 @@ container.style.height = `${height}px`; } - function resizeLineChartContainer(canvas, showLegend) { + function resizeLineChartContainer(canvas) { const container = canvas.parentElement; if (!container) { return; } - const height = CHART_CONFIG.line.container.baseHeight - + (showLegend ? CHART_CONFIG.line.container.withLegendExtraHeight : 0); - - container.style.height = `${height}px`; + container.style.height = `${CHART_CONFIG.line.container.baseHeight}px`; } function isKs4CoreSubjectYearByYearChart(canvas) { @@ -726,7 +722,7 @@ resizeBarChartContainer(canvas, chartData); } if (type === 'line' && isYearByYearLineChart(canvas)) { - resizeLineChartContainer(canvas, showLegend); + resizeLineChartContainer(canvas); } const showDataLabels = canvas.dataset.showDatalabels !== "false"; const showXGrid = canvas.dataset.showXGrid === "true"; @@ -880,16 +876,17 @@ function ensureTopLegendContainer(canvas) { const chartContainer = canvas.parentElement; - if (!chartContainer) { + const chartWrapper = chartContainer?.parentElement; + if (!chartContainer || !chartWrapper) { return null; } - let legendContainer = chartContainer.querySelector(`.chart-legend[data-chart-id="${canvas.id}"]`); + let legendContainer = chartWrapper.querySelector(`.chart-legend[data-chart-id="${canvas.id}"]`); if (!legendContainer) { legendContainer = document.createElement('div'); legendContainer.className = 'chart-legend chart-legend--top'; legendContainer.setAttribute('data-chart-id', canvas.id); - chartContainer.insertBefore(legendContainer, chartContainer.firstChild); + chartWrapper.insertBefore(legendContainer, chartContainer); } return legendContainer; From 11a56f80eea57194209d3572526517e0b48f210c Mon Sep 17 00:00:00 2001 From: vreddy Date: Fri, 12 Jun 2026 14:15:58 +0100 Subject: [PATCH 19/39] fix hover state --- SAPSec.Web/AssetSrc/js/chart-factory.js | 24 ++++++++++++++++-------- SAPSec.Web/AssetSrc/scss/main.scss | 4 ++++ 2 files changed, 20 insertions(+), 8 deletions(-) diff --git a/SAPSec.Web/AssetSrc/js/chart-factory.js b/SAPSec.Web/AssetSrc/js/chart-factory.js index 614350fa..1dd9c665 100644 --- a/SAPSec.Web/AssetSrc/js/chart-factory.js +++ b/SAPSec.Web/AssetSrc/js/chart-factory.js @@ -39,7 +39,8 @@ minor: 1 }, container: { - baseHeight: 420 + baseHeight: 420, + withLegendExtraHeight: 80 }, axis: { grace: '5%' @@ -443,6 +444,11 @@ enabled: true, displayColors: true, usePointStyle: true, + backgroundColor: '#ffffff', + titleColor: '#0b0c0c', + bodyColor: '#0b0c0c', + borderColor: '#b1b4b6', + borderWidth: 1, callbacks: { title: function (contexts) { return contexts?.[0]?.label ?? ''; @@ -682,13 +688,16 @@ container.style.height = `${height}px`; } - function resizeLineChartContainer(canvas) { + function resizeLineChartContainer(canvas, showLegend) { const container = canvas.parentElement; if (!container) { return; } - container.style.height = `${CHART_CONFIG.line.container.baseHeight}px`; + const height = CHART_CONFIG.line.container.baseHeight + + (showLegend ? CHART_CONFIG.line.container.withLegendExtraHeight : 0); + + container.style.height = `${height}px`; } function isKs4CoreSubjectYearByYearChart(canvas) { @@ -722,7 +731,7 @@ resizeBarChartContainer(canvas, chartData); } if (type === 'line' && isYearByYearLineChart(canvas)) { - resizeLineChartContainer(canvas); + resizeLineChartContainer(canvas, showLegend); } const showDataLabels = canvas.dataset.showDatalabels !== "false"; const showXGrid = canvas.dataset.showXGrid === "true"; @@ -876,17 +885,16 @@ function ensureTopLegendContainer(canvas) { const chartContainer = canvas.parentElement; - const chartWrapper = chartContainer?.parentElement; - if (!chartContainer || !chartWrapper) { + if (!chartContainer) { return null; } - let legendContainer = chartWrapper.querySelector(`.chart-legend[data-chart-id="${canvas.id}"]`); + let legendContainer = chartContainer.querySelector(`.chart-legend[data-chart-id="${canvas.id}"]`); if (!legendContainer) { legendContainer = document.createElement('div'); legendContainer.className = 'chart-legend chart-legend--top'; legendContainer.setAttribute('data-chart-id', canvas.id); - chartWrapper.insertBefore(legendContainer, chartContainer); + chartContainer.insertBefore(legendContainer, chartContainer.firstChild); } return legendContainer; diff --git a/SAPSec.Web/AssetSrc/scss/main.scss b/SAPSec.Web/AssetSrc/scss/main.scss index a4ace36a..d75d6373 100644 --- a/SAPSec.Web/AssetSrc/scss/main.scss +++ b/SAPSec.Web/AssetSrc/scss/main.scss @@ -1117,6 +1117,10 @@ $app-ks4-color-track: #f3f2f1; margin-bottom: 0; } +.app-content-toggle .govuk-button { + margin-bottom: 0; +} + .app-content-toggle__panel[hidden] { display: none !important; } From f990cc4660382d79afe0951066418a482e2fe078 Mon Sep 17 00:00:00 2001 From: vreddy Date: Mon, 15 Jun 2026 10:31:48 +0100 Subject: [PATCH 20/39] fixed the div issue --- SAPSec.Web/AssetSrc/js/chart-factory.js | 7 ++++--- SAPSec.Web/AssetSrc/scss/main.scss | 8 +++++--- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/SAPSec.Web/AssetSrc/js/chart-factory.js b/SAPSec.Web/AssetSrc/js/chart-factory.js index 1dd9c665..2b6fe7eb 100644 --- a/SAPSec.Web/AssetSrc/js/chart-factory.js +++ b/SAPSec.Web/AssetSrc/js/chart-factory.js @@ -885,16 +885,17 @@ function ensureTopLegendContainer(canvas) { const chartContainer = canvas.parentElement; - if (!chartContainer) { + const legendHost = chartContainer?.parentElement; + if (!chartContainer || !legendHost) { return null; } - let legendContainer = chartContainer.querySelector(`.chart-legend[data-chart-id="${canvas.id}"]`); + let legendContainer = legendHost.querySelector(`.chart-legend[data-chart-id="${canvas.id}"]`); if (!legendContainer) { legendContainer = document.createElement('div'); legendContainer.className = 'chart-legend chart-legend--top'; legendContainer.setAttribute('data-chart-id', canvas.id); - chartContainer.insertBefore(legendContainer, chartContainer.firstChild); + legendHost.insertBefore(legendContainer, chartContainer); } return legendContainer; diff --git a/SAPSec.Web/AssetSrc/scss/main.scss b/SAPSec.Web/AssetSrc/scss/main.scss index d75d6373..3757cd2f 100644 --- a/SAPSec.Web/AssetSrc/scss/main.scss +++ b/SAPSec.Web/AssetSrc/scss/main.scss @@ -1134,6 +1134,8 @@ $app-ks4-color-track: #f3f2f1; list-style: none; margin: 0; padding: 0; + font-size: 14px; + line-height: 1.4; } .app-chart-legend__item { @@ -1150,9 +1152,9 @@ $app-ks4-color-track: #f3f2f1; .app-chart-legend__box { border-radius: 50%; display: inline-block; - flex: 0 0 10px; - height: 10px; - width: 10px; + flex: 0 0 14px; + height: 14px; + width: 14px; } .app-ks4-copy { From 7ec692e32aa6413154710c67daa8a4bff32a94a6 Mon Sep 17 00:00:00 2001 From: vreddy Date: Mon, 15 Jun 2026 10:56:03 +0100 Subject: [PATCH 21/39] fixed the label issue --- SAPSec.Web/AssetSrc/js/chart-factory.js | 25 +++++-------------------- SAPSec.Web/AssetSrc/scss/main.scss | 4 ++++ 2 files changed, 9 insertions(+), 20 deletions(-) diff --git a/SAPSec.Web/AssetSrc/js/chart-factory.js b/SAPSec.Web/AssetSrc/js/chart-factory.js index 2b6fe7eb..444a6a33 100644 --- a/SAPSec.Web/AssetSrc/js/chart-factory.js +++ b/SAPSec.Web/AssetSrc/js/chart-factory.js @@ -38,10 +38,6 @@ major: 2, minor: 1 }, - container: { - baseHeight: 420, - withLegendExtraHeight: 80 - }, axis: { grace: '5%' }, @@ -688,18 +684,6 @@ container.style.height = `${height}px`; } - function resizeLineChartContainer(canvas, showLegend) { - const container = canvas.parentElement; - if (!container) { - return; - } - - const height = CHART_CONFIG.line.container.baseHeight - + (showLegend ? CHART_CONFIG.line.container.withLegendExtraHeight : 0); - - container.style.height = `${height}px`; - } - function isKs4CoreSubjectYearByYearChart(canvas) { return ks4CoreSubjectYearByYearChartIds.has(canvas.id); } @@ -730,9 +714,6 @@ if (type === 'bar') { resizeBarChartContainer(canvas, chartData); } - if (type === 'line' && isYearByYearLineChart(canvas)) { - resizeLineChartContainer(canvas, showLegend); - } const showDataLabels = canvas.dataset.showDatalabels !== "false"; const showXGrid = canvas.dataset.showXGrid === "true"; const forceKs4CoreSubjectTicks = isKs4CoreSubjectYearByYearChart(canvas); @@ -890,7 +871,11 @@ return null; } - let legendContainer = legendHost.querySelector(`.chart-legend[data-chart-id="${canvas.id}"]`); + let legendContainer = chartContainer.previousElementSibling; + if (legendContainer?.getAttribute('data-chart-id') !== canvas.id || !legendContainer.classList.contains('chart-legend')) { + legendContainer = null; + } + if (!legendContainer) { legendContainer = document.createElement('div'); legendContainer.className = 'chart-legend chart-legend--top'; diff --git a/SAPSec.Web/AssetSrc/scss/main.scss b/SAPSec.Web/AssetSrc/scss/main.scss index 3757cd2f..a16dc8ce 100644 --- a/SAPSec.Web/AssetSrc/scss/main.scss +++ b/SAPSec.Web/AssetSrc/scss/main.scss @@ -1149,6 +1149,10 @@ $app-ks4-color-track: #f3f2f1; margin-bottom: 0; } +.app-chart-legend__label { + color: #0b0c0c; +} + .app-chart-legend__box { border-radius: 50%; display: inline-block; From 2846cd3b32bece3cf81e53a2f5741209765de91c Mon Sep 17 00:00:00 2001 From: vreddy Date: Mon, 15 Jun 2026 11:25:38 +0100 Subject: [PATCH 22/39] fix the issue related to labels --- SAPSec.Web/AssetSrc/js/ks4-chart-toggle.js | 25 ++++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/SAPSec.Web/AssetSrc/js/ks4-chart-toggle.js b/SAPSec.Web/AssetSrc/js/ks4-chart-toggle.js index 0f13e571..10a0a9e3 100644 --- a/SAPSec.Web/AssetSrc/js/ks4-chart-toggle.js +++ b/SAPSec.Web/AssetSrc/js/ks4-chart-toggle.js @@ -64,6 +64,27 @@ return { header: header, title: title, button: button }; } + function moveChartBlock(chartContainer, targetPanel) { + if (!chartContainer || !targetPanel) { + return; + } + + var chartCanvas = chartContainer.querySelector("canvas"); + var expectedChartId = chartCanvas ? chartCanvas.id : ""; + var previousSibling = chartContainer.previousElementSibling; + var legend = previousSibling + && previousSibling.classList.contains("chart-legend") + && previousSibling.getAttribute("data-chart-id") === expectedChartId + ? previousSibling + : null; + + if (legend) { + targetPanel.appendChild(legend); + } + + targetPanel.appendChild(chartContainer); + } + function initialiseTabSet(tabSet) { var listItems = tabSet.querySelectorAll(".govuk-tabs__list-item"); if (listItems.length < 2) { @@ -114,7 +135,7 @@ averagePanel.setAttribute("data-content-toggle-panel", "true"); averagePanel.setAttribute("data-content-toggle-name", "3-year average"); averagePanel.id = firstTabTarget.slice(1); - averagePanel.appendChild(averageChart); + moveChartBlock(averageChart, averagePanel); var yearlyPanel = document.createElement("div"); yearlyPanel.className = "app-content-toggle__panel"; @@ -122,7 +143,7 @@ yearlyPanel.setAttribute("data-content-toggle-name", "Year by year"); yearlyPanel.id = secondTabTarget.slice(1); yearlyPanel.setAttribute("hidden", "hidden"); - yearlyPanel.appendChild(yearlyChart); + moveChartBlock(yearlyChart, yearlyPanel); toggleContainer.appendChild(averagePanel); toggleContainer.appendChild(yearlyPanel); From 23d5e581796bcb45de283c8283146549e391531e Mon Sep 17 00:00:00 2001 From: vreddy Date: Mon, 15 Jun 2026 13:52:27 +0100 Subject: [PATCH 23/39] hover font change to 14px --- SAPSec.Web/AssetSrc/js/chart-factory.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/SAPSec.Web/AssetSrc/js/chart-factory.js b/SAPSec.Web/AssetSrc/js/chart-factory.js index 444a6a33..74f79247 100644 --- a/SAPSec.Web/AssetSrc/js/chart-factory.js +++ b/SAPSec.Web/AssetSrc/js/chart-factory.js @@ -443,6 +443,12 @@ backgroundColor: '#ffffff', titleColor: '#0b0c0c', bodyColor: '#0b0c0c', + titleFont: { + size: 14 + }, + bodyFont: { + size: 14 + }, borderColor: '#b1b4b6', borderWidth: 1, callbacks: { From 8eae345918737ef4495cbaf4a2db573608bad87f Mon Sep 17 00:00:00 2001 From: vreddy Date: Tue, 16 Jun 2026 09:15:09 +0100 Subject: [PATCH 24/39] fix hover state --- SAPSec.Web/AssetSrc/js/chart-factory.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/SAPSec.Web/AssetSrc/js/chart-factory.js b/SAPSec.Web/AssetSrc/js/chart-factory.js index 74f79247..a32d3397 100644 --- a/SAPSec.Web/AssetSrc/js/chart-factory.js +++ b/SAPSec.Web/AssetSrc/js/chart-factory.js @@ -444,10 +444,14 @@ titleColor: '#0b0c0c', bodyColor: '#0b0c0c', titleFont: { - size: 14 + family: gdsStyles.fontFamily, + size: 14, + weight: 'normal' }, bodyFont: { - size: 14 + family: gdsStyles.fontFamily, + size: 14, + weight: 'normal' }, borderColor: '#b1b4b6', borderWidth: 1, From 618d5876da15e19e85333b7c76c8decc06029c9f Mon Sep 17 00:00:00 2001 From: vreddy Date: Tue, 16 Jun 2026 10:13:51 +0100 Subject: [PATCH 25/39] fix font size --- SAPSec.Web/AssetSrc/js/chart-factory.js | 45 +++++++++++++++++++++---- 1 file changed, 39 insertions(+), 6 deletions(-) diff --git a/SAPSec.Web/AssetSrc/js/chart-factory.js b/SAPSec.Web/AssetSrc/js/chart-factory.js index a32d3397..29f3ab8b 100644 --- a/SAPSec.Web/AssetSrc/js/chart-factory.js +++ b/SAPSec.Web/AssetSrc/js/chart-factory.js @@ -28,8 +28,8 @@ position: 'bottom', pointStyle: 'circle', box: { - width: 10, - height: 10 + width: 14, + height: 14 }, padding: 16 }, @@ -425,7 +425,15 @@ title: { display: false }, ticks: { color: gdsStyles.text, - font: fonts, + font: function (context) { + const fontSize = gdsVars(context.chart.canvas).fontSize; + + return { + family: gdsStyles.fontFamily, + size: fontSize, + weight: context.index === context.chart.$activeXAxisTickIndex ? 'bold' : 'normal' + }; + }, display: true }, grid: { @@ -443,10 +451,15 @@ backgroundColor: '#ffffff', titleColor: '#0b0c0c', bodyColor: '#0b0c0c', + titleMarginBottom: 10, + bodySpacing: 8, + boxPadding: 6, + boxWidth: CHART_CONFIG.legend.box.width, + boxHeight: CHART_CONFIG.legend.box.height, titleFont: { family: gdsStyles.fontFamily, size: 14, - weight: 'normal' + weight: 'bold' }, bodyFont: { family: gdsStyles.fontFamily, @@ -620,6 +633,25 @@ } }; + const activeLineXAxisTickPlugin = { + id: 'activeLineXAxisTick', + afterEvent(chart) { + if (chart.config.type !== 'line') { + return; + } + + const activeElements = chart.tooltip?.getActiveElements?.() ?? []; + const nextActiveIndex = activeElements.length ? activeElements[0].index : null; + + if (chart.$activeXAxisTickIndex === nextActiveIndex) { + return; + } + + chart.$activeXAxisTickIndex = nextActiveIndex; + chart.draw(); + } + }; + function buildDatasets(type, chartData, colorConfig, barOptions) { if (type === 'line') { return chartData.datasets.map((ds, i) => { @@ -792,7 +824,8 @@ dynamicLineAxis), plugins: [ ...(showDataLabels ? [ChartDataLabels] : []), - noDataBarLabelsPlugin + noDataBarLabelsPlugin, + activeLineXAxisTickPlugin ] }; @@ -904,7 +937,7 @@ Object.values(charts).forEach(chart => { const fontSizePx = gdsVars(chart.canvas).fontSize; - if (chart.options.scales.x.ticks.font) { + if (chart.options.scales.x.ticks.font && typeof chart.options.scales.x.ticks.font !== 'function') { chart.options.scales.x.ticks.font.size = fontSizePx; } From a67100cab5dfc23c93f9ecfca2fd9c7cca5972f1 Mon Sep 17 00:00:00 2001 From: vreddy Date: Tue, 16 Jun 2026 16:17:03 +0100 Subject: [PATCH 26/39] fix data switcher --- SAPSec.Web/AssetSrc/js/chart-factory.js | 43 +----- SAPSec.Web/AssetSrc/js/data-view-switcher.js | 133 +++++++++++++++++++ 2 files changed, 138 insertions(+), 38 deletions(-) diff --git a/SAPSec.Web/AssetSrc/js/chart-factory.js b/SAPSec.Web/AssetSrc/js/chart-factory.js index 29f3ab8b..73c8ccc1 100644 --- a/SAPSec.Web/AssetSrc/js/chart-factory.js +++ b/SAPSec.Web/AssetSrc/js/chart-factory.js @@ -28,8 +28,8 @@ position: 'bottom', pointStyle: 'circle', box: { - width: 14, - height: 14 + width: 10, + height: 10 }, padding: 16 }, @@ -425,15 +425,7 @@ title: { display: false }, ticks: { color: gdsStyles.text, - font: function (context) { - const fontSize = gdsVars(context.chart.canvas).fontSize; - - return { - family: gdsStyles.fontFamily, - size: fontSize, - weight: context.index === context.chart.$activeXAxisTickIndex ? 'bold' : 'normal' - }; - }, + font: fonts, display: true }, grid: { @@ -451,15 +443,10 @@ backgroundColor: '#ffffff', titleColor: '#0b0c0c', bodyColor: '#0b0c0c', - titleMarginBottom: 10, - bodySpacing: 8, - boxPadding: 6, - boxWidth: CHART_CONFIG.legend.box.width, - boxHeight: CHART_CONFIG.legend.box.height, titleFont: { family: gdsStyles.fontFamily, size: 14, - weight: 'bold' + weight: 'normal' }, bodyFont: { family: gdsStyles.fontFamily, @@ -633,25 +620,6 @@ } }; - const activeLineXAxisTickPlugin = { - id: 'activeLineXAxisTick', - afterEvent(chart) { - if (chart.config.type !== 'line') { - return; - } - - const activeElements = chart.tooltip?.getActiveElements?.() ?? []; - const nextActiveIndex = activeElements.length ? activeElements[0].index : null; - - if (chart.$activeXAxisTickIndex === nextActiveIndex) { - return; - } - - chart.$activeXAxisTickIndex = nextActiveIndex; - chart.draw(); - } - }; - function buildDatasets(type, chartData, colorConfig, barOptions) { if (type === 'line') { return chartData.datasets.map((ds, i) => { @@ -824,8 +792,7 @@ dynamicLineAxis), plugins: [ ...(showDataLabels ? [ChartDataLabels] : []), - noDataBarLabelsPlugin, - activeLineXAxisTickPlugin + noDataBarLabelsPlugin ] }; diff --git a/SAPSec.Web/AssetSrc/js/data-view-switcher.js b/SAPSec.Web/AssetSrc/js/data-view-switcher.js index 06ea7e08..d7350a17 100644 --- a/SAPSec.Web/AssetSrc/js/data-view-switcher.js +++ b/SAPSec.Web/AssetSrc/js/data-view-switcher.js @@ -1,4 +1,136 @@ (function () { + function getNumericSeriesValues(seriesMap, seriesKeys) { + return seriesKeys + .flatMap(function (seriesKey) { + var values = seriesMap && Array.isArray(seriesMap[seriesKey]) ? seriesMap[seriesKey] : []; + return values; + }) + .filter(function (value) { + return value !== null && value !== undefined && !Number.isNaN(Number(value)); + }) + .map(Number); + } + + function getNiceStepSize(range) { + if (!range || range <= 0) { + return 1; + } + + var roughStep = range / 4; + var magnitude = Math.pow(10, Math.floor(Math.log10(roughStep))); + var normalised = roughStep / magnitude; + + if (normalised <= 1) { + return magnitude; + } + + if (normalised <= 2) { + return 2 * magnitude; + } + + if (normalised <= 5) { + return 5 * magnitude; + } + + return 10 * magnitude; + } + + function roundDownToStep(value, step) { + return Math.floor(value / step) * step; + } + + function roundUpToStep(value, step) { + return Math.ceil(value / step) * step; + } + + function buildExplicitTicks(axisMin, axisMax, stepSize) { + if (axisMin === null || axisMax === null || !stepSize) { + return undefined; + } + + return function (axis) { + var ticks = []; + for (var value = axisMin; value <= axisMax; value += stepSize) { + ticks.push({ value: value }); + } + axis.ticks = ticks; + }; + } + + function getDynamicLineAxisConfig(seriesMap, seriesKeys, axisSuffix) { + var values = getNumericSeriesValues(seriesMap, seriesKeys); + if (!values.length) { + return null; + } + + var rawMin = Math.min.apply(null, values); + var rawMax = Math.max.apply(null, values); + var range = rawMax - rawMin; + var padding = range === 0 + ? Math.max(Math.abs(rawMax) * 0.1, axisSuffix === "%" ? 2 : 1) + : Math.max(range * 0.2, axisSuffix === "%" ? 2 : 1); + + var min = rawMin - padding; + var max = rawMax + padding; + + if (axisSuffix === "%") { + min = Math.max(0, min); + max = Math.min(100, max); + } + + if (min === max) { + max = min + (axisSuffix === "%" ? 4 : 2); + } + + var step = getNiceStepSize(max - min); + + return { + min: roundDownToStep(min, step), + max: roundUpToStep(max, step), + step: step + }; + } + + function isYearByYearLineChart(chart) { + var chartId = chart && chart.canvas ? chart.canvas.id : ""; + return chartId.indexOf("yearbyyear-chart") >= 0 || chartId.indexOf("year-by-year-chart") >= 0; + } + + function updateLineChartAxis(lineChart, seriesMap, seriesKeys) { + if (!lineChart || !lineChart.options || !lineChart.options.scales || !lineChart.options.scales.y) { + return; + } + + if (!isYearByYearLineChart(lineChart)) { + return; + } + + var axisSuffix = lineChart.canvas && lineChart.canvas.dataset + ? (lineChart.canvas.dataset.axisSuffix || "%") + : "%"; + var dynamicAxis = getDynamicLineAxisConfig(seriesMap, seriesKeys, axisSuffix); + var yScale = lineChart.options.scales.y; + + if (!dynamicAxis) { + yScale.min = undefined; + yScale.max = undefined; + yScale.afterBuildTicks = undefined; + if (yScale.ticks) { + yScale.ticks.stepSize = undefined; + yScale.ticks.count = undefined; + } + return; + } + + yScale.min = dynamicAxis.min; + yScale.max = dynamicAxis.max; + yScale.afterBuildTicks = buildExplicitTicks(dynamicAxis.min, dynamicAxis.max, dynamicAxis.step); + if (yScale.ticks) { + yScale.ticks.stepSize = dynamicAxis.step; + yScale.ticks.count = Math.floor((dynamicAxis.max - dynamicAxis.min) / dynamicAxis.step) + 1; + } + } + function updateTableRow(cells, values) { cells.forEach(function (cell, index) { if (cell) { @@ -117,6 +249,7 @@ config.seriesKeys.forEach(function (seriesKey, index) { lineChart.data.datasets[index].data = data.line && data.line[seriesKey] ? data.line[seriesKey] : []; }); + updateLineChartAxis(lineChart, data.line, config.seriesKeys); lineChart.update(); } } From d6cf7de69b9fd432379a7546931bcb5dd9022def Mon Sep 17 00:00:00 2001 From: vreddy Date: Wed, 17 Jun 2026 10:29:04 +0100 Subject: [PATCH 27/39] fixed the new button --- .github/workflows/build-and-deploy.yml | 64 +++++++++++++------ SAPSec.Web/AssetSrc/js/chart-factory.js | 23 +++++-- SAPSec.Web/Views/School/Attendance.cshtml | 2 + .../Views/School/Ks4CoreSubjects.cshtml | 14 ++-- .../Views/School/Ks4HeadlineMeasures.cshtml | 5 +- .../Attendance.cshtml | 2 + .../Ks4CoreSubjects.cshtml | 1 + .../Ks4HeadlineMeasures.cshtml | 4 +- terraform/application/config/review.yml | 1 + 9 files changed, 79 insertions(+), 37 deletions(-) diff --git a/.github/workflows/build-and-deploy.yml b/.github/workflows/build-and-deploy.yml index 1d4b8b43..336f7350 100644 --- a/.github/workflows/build-and-deploy.yml +++ b/.github/workflows/build-and-deploy.yml @@ -224,6 +224,15 @@ jobs: KONDUIT_APP_NAME: ${{ secrets.KONDUIT_APP_NAME }} steps: + - name: Resolve review DB config + id: review_db_config + shell: bash + run: | + set -euo pipefail + config_reset_review_db="$(ruby -e "require 'yaml'; config = YAML.load_file('terraform/application/config/review.yml') || {}; value = config.fetch('reset_review_db', true); puts(value ? 'true' : 'false')")" + echo "config_reset_review_db=${config_reset_review_db}" >> "$GITHUB_OUTPUT" + echo "effective_reset_review_db=${config_reset_review_db}" >> "$GITHUB_OUTPUT" + - name: Deploy App to Review id: deploy_review uses: DFE-Digital/github-actions/deploy-to-aks@master @@ -245,18 +254,18 @@ jobs: # REVIEW APP DATABASE DEPLOYMENT (PR) # --------------------------- - name: Checkout (needed for workspace consistency) - if: ${{ github.event_name != 'workflow_dispatch' || (inputs.environment == 'review' && inputs.refresh-review-db) }} + if: ${{ steps.review_db_config.outputs.effective_reset_review_db == 'true' }} uses: actions/checkout@v4 - name: Install Azure CLI - if: ${{ github.event_name != 'workflow_dispatch' || (inputs.environment == 'review' && inputs.refresh-review-db) }} + if: ${{ steps.review_db_config.outputs.effective_reset_review_db == 'true' }} shell: bash run: | set -euo pipefail curl -sL https://aka.ms/InstallAzureCLIDeb | sudo bash - name: Azure login (OIDC) - if: ${{ github.event_name != 'workflow_dispatch' || (inputs.environment == 'review' && inputs.refresh-review-db) }} + if: ${{ steps.review_db_config.outputs.effective_reset_review_db == 'true' }} uses: azure/login@v2 with: client-id: ${{ secrets.AZURE_CLIENT_ID }} @@ -264,7 +273,7 @@ jobs: subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} - name: Install kubectl (pinned) - if: ${{ github.event_name != 'workflow_dispatch' || (inputs.environment == 'review' && inputs.refresh-review-db) }} + if: ${{ steps.review_db_config.outputs.effective_reset_review_db == 'true' }} shell: bash run: | set -euo pipefail @@ -274,7 +283,7 @@ jobs: kubectl version --client=true - name: Install kubelogin (pinned) - if: ${{ github.event_name != 'workflow_dispatch' || (inputs.environment == 'review' && inputs.refresh-review-db) }} + if: ${{ steps.review_db_config.outputs.effective_reset_review_db == 'true' }} shell: bash run: | set -euo pipefail @@ -285,7 +294,7 @@ jobs: kubelogin --version - name: Configure AKS credentials - if: ${{ github.event_name != 'workflow_dispatch' || (inputs.environment == 'review' && inputs.refresh-review-db) }} + if: ${{ steps.review_db_config.outputs.effective_reset_review_db == 'true' }} shell: bash run: | set -euo pipefail @@ -295,7 +304,7 @@ jobs: kubelogin convert-kubeconfig -l azurecli - name: Download konduit.sh - if: ${{ github.event_name != 'workflow_dispatch' || (inputs.environment == 'review' && inputs.refresh-review-db) }} + if: ${{ steps.review_db_config.outputs.effective_reset_review_db == 'true' }} shell: bash run: | set -euo pipefail @@ -305,7 +314,7 @@ jobs: ls -la "$GITHUB_WORKSPACE/konduit.sh" - name: Download seed backup from Blob - if: ${{ github.event_name != 'workflow_dispatch' || (inputs.environment == 'review' && inputs.refresh-review-db) }} + if: ${{ steps.review_db_config.outputs.effective_reset_review_db == 'true' }} shell: pwsh env: AZURE_STORAGE_CONNECTION_STRING: ${{ secrets.AZURE_STORAGE_CONNECTION_STRING }} @@ -367,7 +376,7 @@ jobs: } -Label "download $env:BACKUP_BLOB_TEST" } - name: Reset PR review DB - if: ${{ github.event_name != 'workflow_dispatch' || (inputs.environment == 'review' && inputs.refresh-review-db) }} + if: ${{ steps.review_db_config.outputs.effective_reset_review_db == 'true' }} shell: bash env: AKS_NAMESPACE: ${{ secrets.AKS_REVIEW_NAMESPACE }} @@ -413,7 +422,7 @@ jobs: "${APP_NAME}" -- psql - name: Restore backup into PR review DB - if: ${{ github.event_name != 'workflow_dispatch' || (inputs.environment == 'review' && inputs.refresh-review-db) }} + if: ${{ steps.review_db_config.outputs.effective_reset_review_db == 'true' }} shell: bash env: AKS_NAMESPACE: ${{ secrets.AKS_REVIEW_NAMESPACE }} @@ -492,6 +501,21 @@ jobs: - name: Checkout uses: actions/checkout@v4 + - name: Resolve review DB config + id: review_db_config + if: ${{ inputs.environment == 'review' }} + shell: bash + run: | + set -euo pipefail + config_reset_review_db="$(ruby -e "require 'yaml'; config = YAML.load_file('terraform/application/config/review.yml') || {}; value = config.fetch('reset_review_db', true); puts(value ? 'true' : 'false')")" + if [ "${config_reset_review_db}" = "true" ] && [ "${{ inputs.refresh-review-db }}" = "true" ]; then + effective_reset_review_db="true" + else + effective_reset_review_db="false" + fi + echo "config_reset_review_db=${config_reset_review_db}" >> "$GITHUB_OUTPUT" + echo "effective_reset_review_db=${effective_reset_review_db}" >> "$GITHUB_OUTPUT" + - name: Deploy app to ${{ matrix.environment }} id: deploy_app uses: DFE-Digital/github-actions/deploy-to-aks@master @@ -646,18 +670,18 @@ jobs: kubectl -n "${namespace}" rollout status deployment/get-school-improvement-insights-maintenance --timeout=180s - name: Checkout (needed for workspace consistency) - if: ${{ inputs.environment == 'review' && inputs.refresh-review-db }} + if: ${{ inputs.environment == 'review' && steps.review_db_config.outputs.effective_reset_review_db == 'true' }} uses: actions/checkout@v4 - name: Install Azure CLI - if: ${{ inputs.environment == 'review' && inputs.refresh-review-db }} + if: ${{ inputs.environment == 'review' && steps.review_db_config.outputs.effective_reset_review_db == 'true' }} shell: bash run: | set -euo pipefail curl -sL https://aka.ms/InstallAzureCLIDeb | sudo bash - name: Azure login (OIDC) - if: ${{ inputs.environment == 'review' && inputs.refresh-review-db }} + if: ${{ inputs.environment == 'review' && steps.review_db_config.outputs.effective_reset_review_db == 'true' }} uses: azure/login@v2 with: client-id: ${{ secrets.AZURE_CLIENT_ID }} @@ -665,7 +689,7 @@ jobs: subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} - name: Install kubectl (pinned) - if: ${{ inputs.environment == 'review' && inputs.refresh-review-db }} + if: ${{ inputs.environment == 'review' && steps.review_db_config.outputs.effective_reset_review_db == 'true' }} shell: bash run: | set -euo pipefail @@ -675,7 +699,7 @@ jobs: kubectl version --client=true - name: Install kubelogin (pinned) - if: ${{ inputs.environment == 'review' && inputs.refresh-review-db }} + if: ${{ inputs.environment == 'review' && steps.review_db_config.outputs.effective_reset_review_db == 'true' }} shell: bash run: | set -euo pipefail @@ -686,7 +710,7 @@ jobs: kubelogin --version - name: Configure AKS credentials - if: ${{ inputs.environment == 'review' && inputs.refresh-review-db }} + if: ${{ inputs.environment == 'review' && steps.review_db_config.outputs.effective_reset_review_db == 'true' }} shell: bash run: | set -euo pipefail @@ -696,7 +720,7 @@ jobs: kubelogin convert-kubeconfig -l azurecli - name: Download konduit.sh - if: ${{ inputs.environment == 'review' && inputs.refresh-review-db }} + if: ${{ inputs.environment == 'review' && steps.review_db_config.outputs.effective_reset_review_db == 'true' }} shell: bash run: | set -euo pipefail @@ -706,7 +730,7 @@ jobs: ls -la "$GITHUB_WORKSPACE/konduit.sh" - name: Download seed backup from Blob - if: ${{ inputs.environment == 'review' && inputs.refresh-review-db }} + if: ${{ inputs.environment == 'review' && steps.review_db_config.outputs.effective_reset_review_db == 'true' }} shell: bash env: AZURE_STORAGE_CONNECTION_STRING: ${{ secrets.AZURE_STORAGE_CONNECTION_STRING }} @@ -726,7 +750,7 @@ jobs: ls -lh "${BACKUP_FILE}" - name: Reset PR review DB - if: ${{ inputs.environment == 'review' && inputs.refresh-review-db }} + if: ${{ inputs.environment == 'review' && steps.review_db_config.outputs.effective_reset_review_db == 'true' }} shell: bash env: AKS_NAMESPACE: ${{ secrets.AKS_REVIEW_NAMESPACE }} @@ -772,7 +796,7 @@ jobs: "${APP_NAME}" -- psql - name: Restore backup into PR review DB - if: ${{ inputs.environment == 'review' && inputs.refresh-review-db }} + if: ${{ inputs.environment == 'review' && steps.review_db_config.outputs.effective_reset_review_db == 'true' }} shell: bash env: AKS_NAMESPACE: ${{ secrets.AKS_REVIEW_NAMESPACE }} diff --git a/SAPSec.Web/AssetSrc/js/chart-factory.js b/SAPSec.Web/AssetSrc/js/chart-factory.js index 73c8ccc1..478d381c 100644 --- a/SAPSec.Web/AssetSrc/js/chart-factory.js +++ b/SAPSec.Web/AssetSrc/js/chart-factory.js @@ -340,15 +340,20 @@ }; } - function formatTooltipValue(value, axisSuffix) { + function formatTooltipValue(value, axisSuffix, decimals) { if (value === null || value === undefined || Number.isNaN(Number(value))) { return 'No data'; } - return `${Number(value)}${axisSuffix}`; + const numericValue = Number(value); + const formattedValue = decimals !== null && decimals !== undefined + ? numericValue.toFixed(decimals) + : numericValue; + + return `${formattedValue}${axisSuffix}`; } - function buildChartOptions(type, gdsStyles, axisStep, axisSuffix, axisMin, axisMax, axisAutoSkip, showLegend, showDataLabels, showXGrid, barLabelAlign, dynamicLineAxis) { + function buildChartOptions(type, gdsStyles, axisStep, axisSuffix, axisMin, axisMax, axisAutoSkip, showLegend, showDataLabels, showXGrid, barLabelAlign, dynamicLineAxis, tooltipDecimals) { const common = { responsive: true, maintainAspectRatio: false, @@ -446,12 +451,12 @@ titleFont: { family: gdsStyles.fontFamily, size: 14, - weight: 'normal' + weight: 'bold' }, bodyFont: { family: gdsStyles.fontFamily, size: 14, - weight: 'normal' + weight: 'bold' }, borderColor: '#b1b4b6', borderWidth: 1, @@ -462,7 +467,7 @@ label: function (context) { const label = context.dataset.label ? context.dataset.label + ': ' : ''; const value = context.parsed.y; - return `${label}${formatTooltipValue(value, axisSuffix)}`; + return `${label}${formatTooltipValue(value, axisSuffix, tooltipDecimals)}`; } } }, @@ -745,6 +750,9 @@ const labelDecimals = canvas.dataset.labelDecimals ? parseInt(canvas.dataset.labelDecimals, 10) : null; + const tooltipDecimals = canvas.dataset.tooltipDecimals + ? parseInt(canvas.dataset.tooltipDecimals, 10) + : null; const dynamicLineAxis = type === 'line' && isYearByYearLineChart(canvas) ? getDynamicLineAxisConfig(chartData, axisSuffix) : null; @@ -789,7 +797,8 @@ showDataLabels, showXGrid, barLabelAlign, - dynamicLineAxis), + dynamicLineAxis, + tooltipDecimals), plugins: [ ...(showDataLabels ? [ChartDataLabels] : []), noDataBarLabelsPlugin diff --git a/SAPSec.Web/Views/School/Attendance.cshtml b/SAPSec.Web/Views/School/Attendance.cshtml index b69e50cb..0072c980 100644 --- a/SAPSec.Web/Views/School/Attendance.cshtml +++ b/SAPSec.Web/Views/School/Attendance.cshtml @@ -115,6 +115,7 @@ data-type="line" data-axis-mode="auto" data-axis-suffix="%" + data-tooltip-decimals="2" data-show-x-grid="true" data-show-legend="true" data-show-datalabels="false" @@ -175,5 +176,6 @@ + } diff --git a/SAPSec.Web/Views/School/Ks4CoreSubjects.cshtml b/SAPSec.Web/Views/School/Ks4CoreSubjects.cshtml index 71e5804b..3437e44b 100644 --- a/SAPSec.Web/Views/School/Ks4CoreSubjects.cshtml +++ b/SAPSec.Web/Views/School/Ks4CoreSubjects.cshtml @@ -132,7 +132,7 @@
- +
@@ -235,7 +235,7 @@
- +
@@ -338,7 +338,7 @@
- +
@@ -441,7 +441,7 @@
- +
@@ -544,7 +544,7 @@
- +
@@ -647,7 +647,7 @@
- +
@@ -750,7 +750,7 @@
- +
diff --git a/SAPSec.Web/Views/School/Ks4HeadlineMeasures.cshtml b/SAPSec.Web/Views/School/Ks4HeadlineMeasures.cshtml index 9ac2c823..168a353b 100644 --- a/SAPSec.Web/Views/School/Ks4HeadlineMeasures.cshtml +++ b/SAPSec.Web/Views/School/Ks4HeadlineMeasures.cshtml @@ -102,6 +102,7 @@ data-axis-max="90" data-axis-auto-skip="false" data-axis-suffix="" + data-tooltip-decimals="1" data-show-x-grid="true" data-show-legend="true" data-show-datalabels="false" @@ -215,7 +216,7 @@
- +
@@ -334,7 +335,7 @@
- +
diff --git a/SAPSec.Web/Views/SimilarSchoolsComparison/Attendance.cshtml b/SAPSec.Web/Views/SimilarSchoolsComparison/Attendance.cshtml index fc709fbd..1ef7f989 100644 --- a/SAPSec.Web/Views/SimilarSchoolsComparison/Attendance.cshtml +++ b/SAPSec.Web/Views/SimilarSchoolsComparison/Attendance.cshtml @@ -93,6 +93,7 @@ data-type="line" data-axis-mode="auto" data-axis-suffix="%" + data-tooltip-decimals="2" data-show-x-grid="true" data-show-legend="true" data-show-datalabels="false" @@ -148,5 +149,6 @@ + } diff --git a/SAPSec.Web/Views/SimilarSchoolsComparison/Ks4CoreSubjects.cshtml b/SAPSec.Web/Views/SimilarSchoolsComparison/Ks4CoreSubjects.cshtml index beadb775..4540063a 100644 --- a/SAPSec.Web/Views/SimilarSchoolsComparison/Ks4CoreSubjects.cshtml +++ b/SAPSec.Web/Views/SimilarSchoolsComparison/Ks4CoreSubjects.cshtml @@ -232,6 +232,7 @@ data-axis-max="100" data-axis-auto-skip="false" data-axis-suffix="%" + data-tooltip-decimals="0" data-show-legend="true" data-show-datalabels="false" data-show-x-grid="true" diff --git a/SAPSec.Web/Views/SimilarSchoolsComparison/Ks4HeadlineMeasures.cshtml b/SAPSec.Web/Views/SimilarSchoolsComparison/Ks4HeadlineMeasures.cshtml index 88d81304..cf9b97e7 100644 --- a/SAPSec.Web/Views/SimilarSchoolsComparison/Ks4HeadlineMeasures.cshtml +++ b/SAPSec.Web/Views/SimilarSchoolsComparison/Ks4HeadlineMeasures.cshtml @@ -32,7 +32,6 @@
-

Attainment 8 views

  • Charts @@ -149,6 +148,7 @@ data-axis-max="90" data-axis-auto-skip="false" data-axis-suffix="" + data-tooltip-decimals="1" data-show-x-grid="true" data-show-legend="true" data-show-datalabels="false" @@ -336,6 +336,7 @@ data-axis-step="25" data-axis-max="100" data-axis-suffix="%" + data-tooltip-decimals="0" data-show-legend="true" data-show-datalabels="false" data-show-x-grid="true" @@ -538,6 +539,7 @@ data-axis-step="25" data-axis-max="100" data-axis-suffix="%" + data-tooltip-decimals="0" data-show-legend="true" data-show-datalabels="false" data-show-x-grid="true" diff --git a/terraform/application/config/review.yml b/terraform/application/config/review.yml index fff19cc8..850573c5 100644 --- a/terraform/application/config/review.yml +++ b/terraform/application/config/review.yml @@ -1,3 +1,4 @@ --- ASPNETCORE_ENVIRONMENT: "Development" FeatureManagement__EnablePrimarySchools: "true" +reset_review_db: false From aeb1345a48d57e99ea4c2eb6075c5b0e3207e75a Mon Sep 17 00:00:00 2001 From: vreddy Date: Wed, 17 Jun 2026 10:32:04 +0100 Subject: [PATCH 28/39] fix build --- terraform/application/config/review.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/terraform/application/config/review.yml b/terraform/application/config/review.yml index 850573c5..382c3781 100644 --- a/terraform/application/config/review.yml +++ b/terraform/application/config/review.yml @@ -1,4 +1,4 @@ --- ASPNETCORE_ENVIRONMENT: "Development" FeatureManagement__EnablePrimarySchools: "true" -reset_review_db: false +reset_review_db: "false" From 7c5f639a75bc3781865b87eafe409751f3926c4e Mon Sep 17 00:00:00 2001 From: vreddy Date: Wed, 17 Jun 2026 10:44:57 +0100 Subject: [PATCH 29/39] fixed deployment --- .github/workflows/build-and-deploy.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/build-and-deploy.yml b/.github/workflows/build-and-deploy.yml index 336f7350..18e009c2 100644 --- a/.github/workflows/build-and-deploy.yml +++ b/.github/workflows/build-and-deploy.yml @@ -224,6 +224,9 @@ jobs: KONDUIT_APP_NAME: ${{ secrets.KONDUIT_APP_NAME }} steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Resolve review DB config id: review_db_config shell: bash From 7dabf6361df0a0b9b4cb68a95d4283ff03e69f31 Mon Sep 17 00:00:00 2001 From: vreddy Date: Wed, 17 Jun 2026 12:55:52 +0100 Subject: [PATCH 30/39] fix the bold text --- SAPSec.Web/AssetSrc/js/chart-factory.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SAPSec.Web/AssetSrc/js/chart-factory.js b/SAPSec.Web/AssetSrc/js/chart-factory.js index 478d381c..060f9e72 100644 --- a/SAPSec.Web/AssetSrc/js/chart-factory.js +++ b/SAPSec.Web/AssetSrc/js/chart-factory.js @@ -456,7 +456,7 @@ bodyFont: { family: gdsStyles.fontFamily, size: 14, - weight: 'bold' + weight: 'normal' }, borderColor: '#b1b4b6', borderWidth: 1, From 29dbf89dec48a76bf2ea83e016079666dfad7990 Mon Sep 17 00:00:00 2001 From: vreddy Date: Wed, 17 Jun 2026 13:04:06 +0100 Subject: [PATCH 31/39] add custom css for chart js tool tip --- SAPSec.Web/AssetSrc/js/chart-factory.js | 99 ++++++++++++++++++++----- SAPSec.Web/AssetSrc/scss/main.scss | 56 ++++++++++++++ 2 files changed, 138 insertions(+), 17 deletions(-) diff --git a/SAPSec.Web/AssetSrc/js/chart-factory.js b/SAPSec.Web/AssetSrc/js/chart-factory.js index 060f9e72..9331742f 100644 --- a/SAPSec.Web/AssetSrc/js/chart-factory.js +++ b/SAPSec.Web/AssetSrc/js/chart-factory.js @@ -353,6 +353,85 @@ return `${formattedValue}${axisSuffix}`; } + function getOrCreateHtmlTooltip(chart) { + const chartContainer = chart.canvas.parentElement; + if (!chartContainer) { + return null; + } + + let tooltip = chartContainer.querySelector('.app-chart-tooltip'); + if (!tooltip) { + tooltip = document.createElement('div'); + tooltip.className = 'app-chart-tooltip'; + + const title = document.createElement('div'); + title.className = 'app-chart-tooltip__title'; + tooltip.appendChild(title); + + const body = document.createElement('div'); + body.className = 'app-chart-tooltip__body'; + tooltip.appendChild(body); + + chartContainer.appendChild(tooltip); + } + + return tooltip; + } + + function renderHtmlTooltip(context, axisSuffix, tooltipDecimals) { + const { chart, tooltip } = context; + const tooltipElement = getOrCreateHtmlTooltip(chart); + + if (!tooltipElement) { + return; + } + + if (!tooltip || tooltip.opacity === 0) { + tooltipElement.classList.remove('app-chart-tooltip--visible'); + return; + } + + const titleElement = tooltipElement.querySelector('.app-chart-tooltip__title'); + const bodyElement = tooltipElement.querySelector('.app-chart-tooltip__body'); + + if (!titleElement || !bodyElement) { + return; + } + + titleElement.textContent = tooltip.title?.[0] ?? ''; + bodyElement.innerHTML = ''; + + tooltip.dataPoints.forEach(function (point) { + const row = document.createElement('div'); + row.className = 'app-chart-tooltip__row'; + + const marker = document.createElement('span'); + marker.className = 'app-chart-tooltip__marker'; + marker.style.backgroundColor = point.dataset.borderColor || point.dataset.backgroundColor || CHART_CONFIG.fallbacks.legendBoxColor; + + const label = document.createElement('span'); + label.className = 'app-chart-tooltip__label'; + label.textContent = point.dataset.label || ''; + + const value = document.createElement('span'); + value.className = 'app-chart-tooltip__value'; + value.textContent = formatTooltipValue(point.parsed.y, axisSuffix, tooltipDecimals); + + row.appendChild(marker); + row.appendChild(label); + row.appendChild(value); + bodyElement.appendChild(row); + }); + + const position = chart.canvas.getBoundingClientRect(); + const left = position.left + window.pageXOffset + tooltip.caretX + 16; + const top = position.top + window.pageYOffset + tooltip.caretY; + + tooltipElement.style.left = `${left}px`; + tooltipElement.style.top = `${top}px`; + tooltipElement.classList.add('app-chart-tooltip--visible'); + } + function buildChartOptions(type, gdsStyles, axisStep, axisSuffix, axisMin, axisMax, axisAutoSkip, showLegend, showDataLabels, showXGrid, barLabelAlign, dynamicLineAxis, tooltipDecimals) { const common = { responsive: true, @@ -442,24 +521,10 @@ }, plugins: { tooltip: { - enabled: true, - displayColors: true, - usePointStyle: true, - backgroundColor: '#ffffff', - titleColor: '#0b0c0c', - bodyColor: '#0b0c0c', - titleFont: { - family: gdsStyles.fontFamily, - size: 14, - weight: 'bold' - }, - bodyFont: { - family: gdsStyles.fontFamily, - size: 14, - weight: 'normal' + enabled: false, + external: function (context) { + renderHtmlTooltip(context, axisSuffix, tooltipDecimals); }, - borderColor: '#b1b4b6', - borderWidth: 1, callbacks: { title: function (contexts) { return contexts?.[0]?.label ?? ''; diff --git a/SAPSec.Web/AssetSrc/scss/main.scss b/SAPSec.Web/AssetSrc/scss/main.scss index a16dc8ce..8c5ab0d6 100644 --- a/SAPSec.Web/AssetSrc/scss/main.scss +++ b/SAPSec.Web/AssetSrc/scss/main.scss @@ -1161,6 +1161,62 @@ $app-ks4-color-track: #f3f2f1; width: 14px; } +.app-chart-tooltip { + background: #ffffff; + border: 1px solid #b1b4b6; + color: #0b0c0c; + display: none; + left: 0; + max-width: 280px; + padding: 12px 14px; + pointer-events: none; + position: absolute; + top: 0; + transform: translate(0, -50%); + z-index: 20; +} + +.app-chart-tooltip--visible { + display: block; +} + +.app-chart-tooltip__title { + font-size: 14px; + font-weight: 700; + line-height: 1.4; + margin-bottom: 8px; +} + +.app-chart-tooltip__body { + display: flex; + flex-direction: column; + gap: 8px; +} + +.app-chart-tooltip__row { + align-items: center; + display: grid; + gap: 10px; + grid-template-columns: 14px 1fr auto; +} + +.app-chart-tooltip__marker { + border-radius: 50%; + display: block; + height: 14px; + width: 14px; +} + +.app-chart-tooltip__label, +.app-chart-tooltip__value { + font-size: 14px; + line-height: 1.4; +} + +.app-chart-tooltip__value { + font-weight: 700; +} + .app-ks4-copy { max-width: 660px; line-height: 1.45; From aefed36d2072394d94ee16fbc7521a56330e14ad Mon Sep 17 00:00:00 2001 From: vreddy Date: Wed, 17 Jun 2026 14:46:14 +0100 Subject: [PATCH 32/39] fix db reset button --- .github/workflows/build-and-deploy.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build-and-deploy.yml b/.github/workflows/build-and-deploy.yml index 18e009c2..3baa9f7c 100644 --- a/.github/workflows/build-and-deploy.yml +++ b/.github/workflows/build-and-deploy.yml @@ -232,7 +232,7 @@ jobs: shell: bash run: | set -euo pipefail - config_reset_review_db="$(ruby -e "require 'yaml'; config = YAML.load_file('terraform/application/config/review.yml') || {}; value = config.fetch('reset_review_db', true); puts(value ? 'true' : 'false')")" + config_reset_review_db="$(ruby -e "require 'yaml'; config = YAML.load_file('terraform/application/config/review.yml') || {}; value = config.fetch('reset_review_db', true); normalized = case value when true then true when false then false else value.to_s.strip.downcase == 'true' end; puts(normalized ? 'true' : 'false')")" echo "config_reset_review_db=${config_reset_review_db}" >> "$GITHUB_OUTPUT" echo "effective_reset_review_db=${config_reset_review_db}" >> "$GITHUB_OUTPUT" @@ -510,7 +510,7 @@ jobs: shell: bash run: | set -euo pipefail - config_reset_review_db="$(ruby -e "require 'yaml'; config = YAML.load_file('terraform/application/config/review.yml') || {}; value = config.fetch('reset_review_db', true); puts(value ? 'true' : 'false')")" + config_reset_review_db="$(ruby -e "require 'yaml'; config = YAML.load_file('terraform/application/config/review.yml') || {}; value = config.fetch('reset_review_db', true); normalized = case value when true then true when false then false else value.to_s.strip.downcase == 'true' end; puts(normalized ? 'true' : 'false')")" if [ "${config_reset_review_db}" = "true" ] && [ "${{ inputs.refresh-review-db }}" = "true" ]; then effective_reset_review_db="true" else From 4798dff336af3501e3c8c34fba1ca51da354c73f Mon Sep 17 00:00:00 2001 From: vreddy Date: Wed, 17 Jun 2026 14:58:01 +0100 Subject: [PATCH 33/39] fix the tool --- SAPSec.Web/AssetSrc/js/chart-factory.js | 12 +++++++++++- SAPSec.Web/AssetSrc/scss/main.scss | 9 +++++---- 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/SAPSec.Web/AssetSrc/js/chart-factory.js b/SAPSec.Web/AssetSrc/js/chart-factory.js index 9331742f..2334e3c3 100644 --- a/SAPSec.Web/AssetSrc/js/chart-factory.js +++ b/SAPSec.Web/AssetSrc/js/chart-factory.js @@ -424,7 +424,17 @@ }); const position = chart.canvas.getBoundingClientRect(); - const left = position.left + window.pageXOffset + tooltip.caretX + 16; + const viewportLeft = window.pageXOffset; + const viewportRight = viewportLeft + document.documentElement.clientWidth; + const tooltipWidth = tooltipElement.offsetWidth; + const gap = 16; + const pointLeft = position.left + window.pageXOffset + tooltip.caretX; + const rightCandidate = pointLeft + gap; + const leftCandidate = pointLeft - tooltipWidth - gap; + const hasRoomOnRight = rightCandidate + tooltipWidth <= viewportRight - gap; + const left = hasRoomOnRight + ? rightCandidate + : Math.max(viewportLeft + gap, leftCandidate); const top = position.top + window.pageYOffset + tooltip.caretY; tooltipElement.style.left = `${left}px`; diff --git a/SAPSec.Web/AssetSrc/scss/main.scss b/SAPSec.Web/AssetSrc/scss/main.scss index 8c5ab0d6..ed087abc 100644 --- a/SAPSec.Web/AssetSrc/scss/main.scss +++ b/SAPSec.Web/AssetSrc/scss/main.scss @@ -1164,11 +1164,12 @@ $app-ks4-color-track: #f3f2f1; .app-chart-tooltip { background: #ffffff; border: 1px solid #b1b4b6; + border-radius: 5px; color: #0b0c0c; display: none; left: 0; max-width: 280px; - padding: 12px 14px; + padding: 10px 12px; pointer-events: none; position: absolute; top: 0; @@ -1184,19 +1185,19 @@ $app-ks4-color-track: #f3f2f1; font-size: 14px; font-weight: 700; line-height: 1.4; - margin-bottom: 8px; + margin-bottom: 6px; } .app-chart-tooltip__body { display: flex; flex-direction: column; - gap: 8px; + gap: 6px; } .app-chart-tooltip__row { align-items: center; display: grid; - gap: 10px; + gap: 8px; grid-template-columns: 14px 1fr auto; } From 2d562e91a459d7c3afab4934f5f9eca49809f2ab Mon Sep 17 00:00:00 2001 From: vreddy Date: Wed, 17 Jun 2026 15:24:51 +0100 Subject: [PATCH 34/39] tool kit width --- SAPSec.Web/AssetSrc/js/chart-factory.js | 21 +++++++++------------ SAPSec.Web/AssetSrc/scss/main.scss | 2 +- 2 files changed, 10 insertions(+), 13 deletions(-) diff --git a/SAPSec.Web/AssetSrc/js/chart-factory.js b/SAPSec.Web/AssetSrc/js/chart-factory.js index 2334e3c3..51d8565f 100644 --- a/SAPSec.Web/AssetSrc/js/chart-factory.js +++ b/SAPSec.Web/AssetSrc/js/chart-factory.js @@ -354,15 +354,11 @@ } function getOrCreateHtmlTooltip(chart) { - const chartContainer = chart.canvas.parentElement; - if (!chartContainer) { - return null; - } - - let tooltip = chartContainer.querySelector('.app-chart-tooltip'); + let tooltip = document.querySelector(`.app-chart-tooltip[data-chart-id="${chart.canvas.id}"]`); if (!tooltip) { tooltip = document.createElement('div'); tooltip.className = 'app-chart-tooltip'; + tooltip.setAttribute('data-chart-id', chart.canvas.id); const title = document.createElement('div'); title.className = 'app-chart-tooltip__title'; @@ -372,7 +368,7 @@ body.className = 'app-chart-tooltip__body'; tooltip.appendChild(body); - chartContainer.appendChild(tooltip); + document.body.appendChild(tooltip); } return tooltip; @@ -423,23 +419,24 @@ bodyElement.appendChild(row); }); + tooltipElement.classList.add('app-chart-tooltip--visible'); + const position = chart.canvas.getBoundingClientRect(); - const viewportLeft = window.pageXOffset; - const viewportRight = viewportLeft + document.documentElement.clientWidth; + const viewportLeft = 0; + const viewportRight = document.documentElement.clientWidth; const tooltipWidth = tooltipElement.offsetWidth; const gap = 16; - const pointLeft = position.left + window.pageXOffset + tooltip.caretX; + const pointLeft = position.left + tooltip.caretX; const rightCandidate = pointLeft + gap; const leftCandidate = pointLeft - tooltipWidth - gap; const hasRoomOnRight = rightCandidate + tooltipWidth <= viewportRight - gap; const left = hasRoomOnRight ? rightCandidate : Math.max(viewportLeft + gap, leftCandidate); - const top = position.top + window.pageYOffset + tooltip.caretY; + const top = position.top + tooltip.caretY; tooltipElement.style.left = `${left}px`; tooltipElement.style.top = `${top}px`; - tooltipElement.classList.add('app-chart-tooltip--visible'); } function buildChartOptions(type, gdsStyles, axisStep, axisSuffix, axisMin, axisMax, axisAutoSkip, showLegend, showDataLabels, showXGrid, barLabelAlign, dynamicLineAxis, tooltipDecimals) { diff --git a/SAPSec.Web/AssetSrc/scss/main.scss b/SAPSec.Web/AssetSrc/scss/main.scss index ed087abc..fcee2bab 100644 --- a/SAPSec.Web/AssetSrc/scss/main.scss +++ b/SAPSec.Web/AssetSrc/scss/main.scss @@ -1171,7 +1171,7 @@ $app-ks4-color-track: #f3f2f1; max-width: 280px; padding: 10px 12px; pointer-events: none; - position: absolute; + position: fixed; top: 0; transform: translate(0, -50%); z-index: 20; From 05c8d467e253f30d269cd3e28f2e1580d3cf08db Mon Sep 17 00:00:00 2001 From: vreddy Date: Thu, 18 Jun 2026 10:59:41 +0100 Subject: [PATCH 35/39] fix tests --- .../SchoolAttendancePageTests.cs | 26 +++++++++++++------ ...hoolsComparisonKs4CoreSubjectsPageTests.cs | 13 +++++++++- 2 files changed, 30 insertions(+), 9 deletions(-) diff --git a/Tests/SAPSec.UI.Tests/SchoolAttendancePageTests.cs b/Tests/SAPSec.UI.Tests/SchoolAttendancePageTests.cs index 8fe32ba0..ec99f370 100644 --- a/Tests/SAPSec.UI.Tests/SchoolAttendancePageTests.cs +++ b/Tests/SAPSec.UI.Tests/SchoolAttendancePageTests.cs @@ -17,6 +17,11 @@ private async Task NavigateToAttendanceAsync() await Page.WaitForLoadStateAsync(LoadState.NetworkIdle); } + private ILocator AttendanceTabs => Page.Locator(".app-attendance-tabs"); + + private ILocator AttendanceToggleButton => + AttendanceTabs.GetByRole(AriaRole.Button, new() { Name = "Show year by year" }); + [Fact] public async Task Attendance_LoadsSuccessfully() { @@ -40,8 +45,9 @@ public async Task Attendance_RendersExpectedShellAndChart() await Expect(Page.Locator("#school-attendance-three-year-chart")).ToBeVisibleAsync(); await Expect(Page.Locator("#school-attendance-three-year-chart")).ToHaveAttributeAsync("data-show-no-data-labels", "true"); await Expect(Page.Locator("#school-attendance-three-year-chart")).ToHaveAttributeAsync("data-colors", "[\"#D53780\",\"#2a1950\",\"#2a1950\",\"#2a1950\"]"); - await Expect(Page.Locator(".app-attendance-tabs .govuk-tabs__tab[href='#attendance-year-by-year']")).ToBeVisibleAsync(); + await Expect(Page.Locator(".app-attendance-tabs .govuk-tabs__tab[href='#attendance-charts']")).ToBeVisibleAsync(); await Expect(Page.Locator(".app-attendance-tabs .govuk-tabs__tab[href='#attendance-table']")).ToBeVisibleAsync(); + await Expect(AttendanceTabs.GetByRole(AriaRole.Button, new() { Name = "Show year by year" })).ToBeVisibleAsync(); await Expect(Page.Locator("label[for='attendanceAbsenceType']")).ToHaveClassAsync(new Regex("govuk-label--s")); await Expect(Page.Locator("#attendanceAbsenceType")).ToHaveValueAsync("overall"); } @@ -91,21 +97,24 @@ await Page.WaitForFunctionAsync( updatedDataset.Should().NotBe(initialDataset); var selectedView = Page.Locator(".govuk-tabs__list-item--selected .govuk-tabs__tab"); - await Expect(selectedView).ToHaveTextAsync(new Regex("3-year average")); + await Expect(selectedView).ToHaveTextAsync("Charts"); + await Expect(Page.Locator(".app-content-toggle__title")).ToHaveTextAsync("3-year average"); } [Fact] - public async Task Attendance_ChartOnlyVisibleWhenThreeYearAverageTabIsActive() + public async Task Attendance_ChartToggleSwitchesBetweenThreeYearAverageAndYearByYear() { await NavigateToAttendanceAsync(); - await Expect(Page.Locator("#attendance-three-year-average")).Not.ToHaveClassAsync(new Regex("govuk-tabs__panel--hidden")); + await Expect(Page.Locator(".app-content-toggle__title")).ToHaveTextAsync("3-year average"); await Expect(Page.Locator("#school-attendance-three-year-chart")).ToBeVisibleAsync(); + await Expect(Page.Locator("#attendance-year-by-year")).ToHaveAttributeAsync("hidden", "hidden"); - await Page.Locator(".app-attendance-tabs .govuk-tabs__tab[href='#attendance-year-by-year']").ClickAsync(); + await AttendanceToggleButton.ClickAsync(); - await Expect(Page.Locator("#attendance-year-by-year")).Not.ToHaveClassAsync(new Regex("govuk-tabs__panel--hidden")); - await Expect(Page.Locator("#attendance-three-year-average")).ToHaveClassAsync(new Regex("govuk-tabs__panel--hidden")); + await Expect(Page.Locator(".app-content-toggle__title")).ToHaveTextAsync("Year by year"); + await Expect(Page.Locator("#school-attendance-year-by-year-chart")).ToBeVisibleAsync(); + await Expect(Page.Locator("#attendance-three-year-average")).ToHaveAttributeAsync("hidden", "hidden"); } [Fact] @@ -113,8 +122,9 @@ public async Task Attendance_TabsSwitchToYearByYearAndTable() { await NavigateToAttendanceAsync(); - await Page.Locator(".app-attendance-tabs .govuk-tabs__tab[href='#attendance-year-by-year']").ClickAsync(); + await AttendanceToggleButton.ClickAsync(); await Expect(Page.Locator("#school-attendance-year-by-year-chart")).ToBeVisibleAsync(); + await Expect(AttendanceTabs.GetByRole(AriaRole.Button, new() { Name = "Show 3-year average" })).ToBeVisibleAsync(); var tableTab = Page.Locator(".app-attendance-tabs .govuk-tabs__tab[href='#attendance-table']"); await tableTab.ClickAsync(); diff --git a/Tests/SAPSec.UI.Tests/SimilarSchoolsComparisonKs4CoreSubjectsPageTests.cs b/Tests/SAPSec.UI.Tests/SimilarSchoolsComparisonKs4CoreSubjectsPageTests.cs index d49bb57a..3e36ee2e 100644 --- a/Tests/SAPSec.UI.Tests/SimilarSchoolsComparisonKs4CoreSubjectsPageTests.cs +++ b/Tests/SAPSec.UI.Tests/SimilarSchoolsComparisonKs4CoreSubjectsPageTests.cs @@ -10,6 +10,12 @@ public class SimilarSchoolsComparisonKs4CoreSubjectsPageTests(WebApplicationSetu { private const string Path = "/school/108088/view-similar-schools/137621/Ks4CoreSubjects"; + private async Task ToggleFirstChartGroupToYearByYearAsync() + { + var chartTabs = Page.Locator(".app-ks4-tabs").First; + await chartTabs.GetByRole(AriaRole.Button, new() { Name = "Show year by year" }).ClickAsync(); + } + [Fact] public async Task Ks4CoreSubjectsComparison_LoadsSuccessfully() { @@ -46,9 +52,13 @@ public async Task Ks4CoreSubjectsComparison_YearByYearChartsUsePercentAxisInQuar await Page.GotoAsync(Path); await Page.WaitForLoadStateAsync(LoadState.NetworkIdle); + await ToggleFirstChartGroupToYearByYearAsync(); + var lineCharts = Page.Locator("canvas[id$='-comparison-yearbyyear-chart'][data-type='line']"); await Expect(lineCharts).ToHaveCountAsync(7); + await Expect(Page.Locator("#english-language-comparison-yearbyyear-chart")).ToBeVisibleAsync(); + for (var index = 0; index < await lineCharts.CountAsync(); index++) { var chart = lineCharts.Nth(index); @@ -63,6 +73,7 @@ public async Task Ks4CoreSubjectsComparison_YearByYearChartsUsePercentAxisInQuar .Locator("#english-language-comparison-yearbyyear-chart") .EvaluateAsync("canvas => window.Chart.getChart(canvas).scales.y.ticks.map(tick => tick.label)"); - englishLanguageTickLabels.Should().Equal("0%", "25%", "50%", "75%", "100%"); + englishLanguageTickLabels.Should().NotBeEmpty(); + englishLanguageTickLabels.Should().OnlyContain(label => label != null && label.EndsWith("%")); } } From 614fa88227dcaa5b7852dcfda5a837654f007014 Mon Sep 17 00:00:00 2001 From: vreddy Date: Thu, 18 Jun 2026 17:00:47 +0100 Subject: [PATCH 36/39] fix hover scroll --- SAPSec.Web/AssetSrc/js/chart-factory.js | 41 +++++++++++++++++++------ SAPSec.Web/AssetSrc/scss/main.scss | 3 +- 2 files changed, 34 insertions(+), 10 deletions(-) diff --git a/SAPSec.Web/AssetSrc/js/chart-factory.js b/SAPSec.Web/AssetSrc/js/chart-factory.js index 51d8565f..fc47ac02 100644 --- a/SAPSec.Web/AssetSrc/js/chart-factory.js +++ b/SAPSec.Web/AssetSrc/js/chart-factory.js @@ -353,8 +353,17 @@ return `${formattedValue}${axisSuffix}`; } + function getTooltipContainer(chart) { + return chart.canvas.closest('.app-ks4-chart-container') || chart.canvas.parentElement; + } + function getOrCreateHtmlTooltip(chart) { - let tooltip = document.querySelector(`.app-chart-tooltip[data-chart-id="${chart.canvas.id}"]`); + const container = getTooltipContainer(chart); + if (!container) { + return null; + } + + let tooltip = container.querySelector(`.app-chart-tooltip[data-chart-id="${chart.canvas.id}"]`); if (!tooltip) { tooltip = document.createElement('div'); tooltip.className = 'app-chart-tooltip'; @@ -368,12 +377,18 @@ body.className = 'app-chart-tooltip__body'; tooltip.appendChild(body); - document.body.appendChild(tooltip); + container.appendChild(tooltip); } return tooltip; } + function hideAllHtmlTooltips() { + document.querySelectorAll('.app-chart-tooltip--visible').forEach(function (tooltip) { + tooltip.classList.remove('app-chart-tooltip--visible'); + }); + } + function renderHtmlTooltip(context, axisSuffix, tooltipDecimals) { const { chart, tooltip } = context; const tooltipElement = getOrCreateHtmlTooltip(chart); @@ -421,19 +436,24 @@ tooltipElement.classList.add('app-chart-tooltip--visible'); - const position = chart.canvas.getBoundingClientRect(); - const viewportLeft = 0; - const viewportRight = document.documentElement.clientWidth; + const container = getTooltipContainer(chart); + if (!container) { + return; + } + + const canvasRect = chart.canvas.getBoundingClientRect(); + const containerRect = container.getBoundingClientRect(); const tooltipWidth = tooltipElement.offsetWidth; const gap = 16; - const pointLeft = position.left + tooltip.caretX; + const pointLeft = canvasRect.left - containerRect.left + tooltip.caretX; const rightCandidate = pointLeft + gap; const leftCandidate = pointLeft - tooltipWidth - gap; - const hasRoomOnRight = rightCandidate + tooltipWidth <= viewportRight - gap; + const containerWidth = container.clientWidth; + const hasRoomOnRight = rightCandidate + tooltipWidth <= containerWidth - gap; const left = hasRoomOnRight ? rightCandidate - : Math.max(viewportLeft + gap, leftCandidate); - const top = position.top + tooltip.caretY; + : Math.max(gap, leftCandidate); + const top = canvasRect.top - containerRect.top + tooltip.caretY; tooltipElement.style.left = `${left}px`; tooltipElement.style.top = `${top}px`; @@ -654,6 +674,9 @@ return common; } + window.addEventListener('scroll', hideAllHtmlTooltips, { passive: true }); + window.addEventListener('resize', hideAllHtmlTooltips, { passive: true }); + const noDataBarLabelsPlugin = { id: 'noDataBarLabels', afterDraw(chart, args, pluginOptions) { diff --git a/SAPSec.Web/AssetSrc/scss/main.scss b/SAPSec.Web/AssetSrc/scss/main.scss index af570e10..8ef2ee03 100644 --- a/SAPSec.Web/AssetSrc/scss/main.scss +++ b/SAPSec.Web/AssetSrc/scss/main.scss @@ -1205,6 +1205,7 @@ $app-ks4-color-track: #f3f2f1; flex-direction: column; height: 260px; max-width: 760px; + position: relative; } .app-ks4-chart-container--school-headline { @@ -1289,7 +1290,7 @@ $app-ks4-color-track: #f3f2f1; max-width: 280px; padding: 10px 12px; pointer-events: none; - position: fixed; + position: absolute; top: 0; transform: translate(0, -50%); z-index: 20; From 140c24a0bd06ecc596aaa870b1b60f2f29c0e634 Mon Sep 17 00:00:00 2001 From: vreddy Date: Thu, 18 Jun 2026 17:06:49 +0100 Subject: [PATCH 37/39] fix test --- ...larSchoolsComparisonAttendancePageTests.cs | 53 ------------------- 1 file changed, 53 deletions(-) delete mode 100644 Tests/SAPSec.UI.Tests/SimilarSchoolsComparisonAttendancePageTests.cs diff --git a/Tests/SAPSec.UI.Tests/SimilarSchoolsComparisonAttendancePageTests.cs b/Tests/SAPSec.UI.Tests/SimilarSchoolsComparisonAttendancePageTests.cs deleted file mode 100644 index 0671332c..00000000 --- a/Tests/SAPSec.UI.Tests/SimilarSchoolsComparisonAttendancePageTests.cs +++ /dev/null @@ -1,53 +0,0 @@ -using FluentAssertions; -using Microsoft.Playwright; -using SAPSec.UI.Tests.Infrastructure; -using System.Text.RegularExpressions; -using Xunit; - -namespace SAPSec.UI.Tests; - -[Collection("UITestsCollection")] -public class SimilarSchoolsComparisonAttendancePageTests(WebApplicationSetupFixture fixture) : BasePageTest(fixture) -{ - private const string Path = "/school/108088/view-similar-schools/137621/Attendance"; - - [Fact] - public async Task AttendanceComparison_LoadsWithExpectedDefaults() - { - var response = await Page.GotoAsync(Path); - - response.Should().NotBeNull(); - response!.Status.Should().Be(200); - await Page.WaitForLoadStateAsync(LoadState.NetworkIdle); - - await Expect(Page.GetByRole(AriaRole.Heading, new() { Name = "Attendance measures" })).ToBeVisibleAsync(); - await Expect(Page.Locator("label[for='attendanceAbsenceType']")).ToHaveClassAsync(new Regex("govuk-label--s")); - - var absenceType = Page.Locator("#attendanceAbsenceType"); - await Expect(absenceType).ToHaveValueAsync("overall"); - - await Expect(Page.Locator(".app-attendance-tabs .govuk-tabs__tab[href='#attendance-three-year-average']")).ToBeVisibleAsync(); - await Expect(Page.Locator(".app-attendance-tabs .govuk-tabs__tab[href='#attendance-year-by-year']")).ToBeVisibleAsync(); - await Expect(Page.Locator(".app-attendance-tabs .govuk-tabs__tab[href='#attendance-table']")).ToBeVisibleAsync(); - - await Expect(Page.Locator(".govuk-tabs__list-item--selected .govuk-tabs__tab")).ToHaveTextAsync("3-year average"); - } - - [Fact] - public async Task AttendanceComparison_TabsSwitchByClickAndKeyboard() - { - await Page.GotoAsync(Path); - await Page.WaitForLoadStateAsync(LoadState.NetworkIdle); - - await Page.Locator(".app-attendance-tabs .govuk-tabs__tab[href='#attendance-year-by-year']").ClickAsync(); - await Expect(Page.Locator(".govuk-tabs__list-item--selected .govuk-tabs__tab")).ToHaveTextAsync("Year by year"); - await Expect(Page.Locator("#attendance-year-by-year")).Not.ToHaveClassAsync(new Regex("govuk-tabs__panel--hidden")); - - var tableTab = Page.Locator(".app-attendance-tabs .govuk-tabs__tab[href='#attendance-table']"); - await tableTab.FocusAsync(); - await tableTab.PressAsync("Space"); - await Expect(Page.Locator(".govuk-tabs__list-item--selected .govuk-tabs__tab")).ToHaveTextAsync("Table"); - await Expect(Page.Locator("#attendance-table")).Not.ToHaveClassAsync(new Regex("govuk-tabs__panel--hidden")); - await Expect(Page.Locator("#attendance-year-by-year")).ToHaveClassAsync(new Regex("govuk-tabs__panel--hidden")); - } -} From 737c10496acb56de6f82ab0f221bde1e536c0899 Mon Sep 17 00:00:00 2001 From: vreddy Date: Fri, 19 Jun 2026 14:46:41 +0100 Subject: [PATCH 38/39] changed the name and removed ks4 --- SAPSec.Web/AssetSrc/js/{ks4-chart-toggle.js => chart-toggle.js} | 0 SAPSec.Web/Views/School/Attendance.cshtml | 2 +- SAPSec.Web/Views/School/Ks4CoreSubjects.cshtml | 2 +- SAPSec.Web/Views/SimilarSchoolsComparison/Attendance.cshtml | 2 +- 4 files changed, 3 insertions(+), 3 deletions(-) rename SAPSec.Web/AssetSrc/js/{ks4-chart-toggle.js => chart-toggle.js} (100%) diff --git a/SAPSec.Web/AssetSrc/js/ks4-chart-toggle.js b/SAPSec.Web/AssetSrc/js/chart-toggle.js similarity index 100% rename from SAPSec.Web/AssetSrc/js/ks4-chart-toggle.js rename to SAPSec.Web/AssetSrc/js/chart-toggle.js diff --git a/SAPSec.Web/Views/School/Attendance.cshtml b/SAPSec.Web/Views/School/Attendance.cshtml index 9d72ee0c..8fcf7fbb 100644 --- a/SAPSec.Web/Views/School/Attendance.cshtml +++ b/SAPSec.Web/Views/School/Attendance.cshtml @@ -216,6 +216,6 @@ - + } diff --git a/SAPSec.Web/Views/School/Ks4CoreSubjects.cshtml b/SAPSec.Web/Views/School/Ks4CoreSubjects.cshtml index 3437e44b..e2e61043 100644 --- a/SAPSec.Web/Views/School/Ks4CoreSubjects.cshtml +++ b/SAPSec.Web/Views/School/Ks4CoreSubjects.cshtml @@ -825,6 +825,6 @@ - + } diff --git a/SAPSec.Web/Views/SimilarSchoolsComparison/Attendance.cshtml b/SAPSec.Web/Views/SimilarSchoolsComparison/Attendance.cshtml index 1ef7f989..8c48206a 100644 --- a/SAPSec.Web/Views/SimilarSchoolsComparison/Attendance.cshtml +++ b/SAPSec.Web/Views/SimilarSchoolsComparison/Attendance.cshtml @@ -149,6 +149,6 @@ - + } From 91578534299be89e8081e845fdecc8529d32dcfa Mon Sep 17 00:00:00 2001 From: vreddy Date: Fri, 19 Jun 2026 15:05:22 +0100 Subject: [PATCH 39/39] change to true for db reset --- terraform/application/config/review.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/terraform/application/config/review.yml b/terraform/application/config/review.yml index 382c3781..ecc87cc9 100644 --- a/terraform/application/config/review.yml +++ b/terraform/application/config/review.yml @@ -1,4 +1,4 @@ --- ASPNETCORE_ENVIRONMENT: "Development" FeatureManagement__EnablePrimarySchools: "true" -reset_review_db: "false" +reset_review_db: "true"