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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 7 additions & 7 deletions SAPSec.Core/Features/Measures/Measure.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,13 @@ internal static Measure ForSchool<T>(
IEnumerable<SchoolData<T>> similarSchools,
MeasureFieldSelector<T> fieldSelector)
{
return new Measure(
key,
name,
dataType,
availableFilters.ToList(),
MeasureSeries.ForSchool(currentSchool, similarSchools, fieldSelector),
TopPerformer.BuildTopPerformers(currentSchool, similarSchools, fieldSelector));
return new Measure(
key,
name,
dataType,
availableFilters.ToList(),
MeasureSeries.ForSchool(currentSchool, similarSchools, fieldSelector),
TopPerformer.BuildTopPerformers(currentSchool, similarSchools, fieldSelector, dataType));
}

internal static Measure ForSchoolComparison<T>(
Expand Down
57 changes: 32 additions & 25 deletions SAPSec.Core/Features/Measures/TopPerformer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,15 @@ public record TopPerformer(
decimal? Value,
bool IsCurrentSchool = false)
{
internal static IReadOnlyCollection<TopPerformer> BuildTopPerformers<T>(
SchoolData<T> currentSchool,
IEnumerable<SchoolData<T>> similarSchools,
MeasureFieldSelector<T> fieldSelector)
{
return similarSchools
// Include current school in list
.Append(currentSchool)
internal static IReadOnlyCollection<TopPerformer> BuildTopPerformers<T>(
SchoolData<T> currentSchool,
IEnumerable<SchoolData<T>> similarSchools,
MeasureFieldSelector<T> fieldSelector,
MeasureDataType dataType)
{
return similarSchools
// Include current school in list
.Append(currentSchool)
.Select(x => new TopPerformerCandidate(
x.SchoolInfo.Urn,
x.SchoolInfo.Name,
Expand All @@ -27,22 +28,28 @@ internal static IReadOnlyCollection<TopPerformer> BuildTopPerformers<T>(
.Where(x => x.Value.HasValue)

// Remove duplicates if school with the same URN already appears in the list,
// prioritising current school
// TODO: not sure this will ever happen?
.GroupBy(x => x.Urn, StringComparer.Ordinal)
.Select(x => x.OrderByDescending(candidate => candidate.IsCurrentSchool).First())

.OrderByDescending(x => x.Value)
.ThenBy(x => x.Name, StringComparer.OrdinalIgnoreCase)
.Take(3)
.Select((x, index) => new TopPerformer(index + 1, x.Urn, x.Name, x.Value, x.IsCurrentSchool))
.ToList()
.AsReadOnly();
}

private sealed record TopPerformerCandidate(
string Urn,
string Name,
decimal? Value,
// prioritising current school
// TODO: not sure this will ever happen?
.GroupBy(x => x.Urn, StringComparer.Ordinal)
.Select(x => x.OrderByDescending(candidate => candidate.IsCurrentSchool).First())

.OrderByDescending(x => TopPerformerSortValue(x.Value, dataType))
.ThenBy(x => x.Name, StringComparer.OrdinalIgnoreCase)
.Take(3)
.Select((x, index) => new TopPerformer(index + 1, x.Urn, x.Name, x.Value, x.IsCurrentSchool))
.ToList()
.AsReadOnly();
}

private static decimal TopPerformerSortValue(decimal? value, MeasureDataType dataType) =>
Math.Round(value!.Value, DisplayDecimalPlaces(dataType), MidpointRounding.AwayFromZero);

private static int DisplayDecimalPlaces(MeasureDataType dataType) =>
dataType is MeasureDataType.Score or MeasureDataType.ScaledScore ? 1 : 0;

private sealed record TopPerformerCandidate(
string Urn,
string Name,
decimal? Value,
bool IsCurrentSchool);
}
Original file line number Diff line number Diff line change
Expand Up @@ -74,8 +74,11 @@
</summary>
<div class="govuk-details__text">
<p class="govuk-body-s govuk-!-margin-bottom-2">
This shows the average reading scaled score for pupils at the end of key stage 2.
Scaled scores typically range from 80 to 120, and a score of 100 represents the expected standard.
This score shows how well pupils performed in the key stage 2 reading test. It is the
mean scaled score of all pupils who took the test and achieved a scaled score.
</p>
<p class="govuk-body-s govuk-!-margin-bottom-2">
The expected standard is 100 or more. The higher standard is 110 or more.
</p>
</div>
</details>
Expand All @@ -92,8 +95,11 @@
</summary>
<div class="govuk-details__text">
<p class="govuk-body-s govuk-!-margin-bottom-2">
This shows the average maths scaled score for pupils at the end of key stage 2.
Scaled scores typically range from 80 to 120, and a score of 100 represents the expected standard.
This score shows how well pupils performed in the key stage 2 maths test. It is the mean
scaled score of all pupils who took the test and achieved a scaled score.
</p>
<p class="govuk-body-s govuk-!-margin-bottom-2">
The expected standard is 100 or more. The higher standard is 110 or more.
</p>
</div>
</details>
Expand All @@ -111,7 +117,7 @@
<div class="govuk-details__text">
<p class="govuk-body-s govuk-!-margin-bottom-2">
Pupils achieve the expected standard when they achieve a scaled score of 100 or more
in the grammar, punctuation and spelling test.
in their key stage 2 grammar, punctuation and spelling test.
</p>
</div>
</details>
Expand All @@ -129,7 +135,7 @@
<div class="govuk-details__text">
<p class="govuk-body-s govuk-!-margin-bottom-2">
Pupils achieve the higher standard when they achieve a scaled score of 110 or more
in the grammar, punctuation and spelling test.
in their key stage 2 grammar, punctuation and spelling test.
</p>
</div>
</details>
Expand Down
27 changes: 21 additions & 6 deletions SAPSec.Web/AssetSrc/js/chart-factory.js
Original file line number Diff line number Diff line change
Expand Up @@ -64,10 +64,11 @@ const CHART_CONFIG = {
},
datalabels: {
anchor: 'end',
smallValueAlign: 'end',
smallValueAlign: 'right',
defaultAlign: 'start',
mobileInsideThresholdRatio: 0.4,
offset: 10,
defaultOffset: 10,
smallValueOffset: 6,
fontWeight: 'bold'
},
noData: {
Expand Down Expand Up @@ -175,15 +176,20 @@ function canBarFitLabel(ctx, axisSuffix) {
return true;
}

const barLength = Math.abs(xScale.getPixelForValue(value) - xScale.getPixelForValue(0));
const axisMin = typeof xScale.min === 'number' ? xScale.min : 0;
const axisMax = typeof xScale.max === 'number' ? xScale.max : 0;
const baseValue = axisMin > 0 && value >= axisMin && axisMax > axisMin
? axisMin
: 0;
const barLength = Math.abs(xScale.getPixelForValue(value) - xScale.getPixelForValue(baseValue));
const font = Chart.helpers.toFont(ctx.chart.options?.plugins?.datalabels?.font);

canvasContext.save();
canvasContext.font = font.string;
const labelWidth = canvasContext.measureText(getBarLabelText(value, axisSuffix)).width;
canvasContext.restore();

return barLength >= labelWidth + (CHART_CONFIG.bar.datalabels.offset * 2);
return barLength >= labelWidth + (CHART_CONFIG.bar.datalabels.defaultOffset * 2);
}

function isMobileViewport() {
Expand Down Expand Up @@ -230,6 +236,13 @@ function getBarLabelColor(ctx, gdsStyles, axisSuffix, barLabelAlign) {
: gdsStyles.text;
}

function getBarLabelOffset(ctx, axisSuffix, barLabelAlign) {
const align = getBarLabelAlignment(ctx, axisSuffix, barLabelAlign);
return align === CHART_CONFIG.bar.datalabels.defaultAlign
? CHART_CONFIG.bar.datalabels.defaultOffset
: CHART_CONFIG.bar.datalabels.smallValueOffset;
}

function buildExplicitTicks(axisMin, axisMax, stepSize) {
if (axisMin === null || axisMax === null || !stepSize) {
return undefined;
Expand Down Expand Up @@ -636,7 +649,9 @@ function buildChartOptions(type, gdsStyles, axisStep, axisSuffix, axisMin, axisM
align: function (ctx) {
return getBarLabelAlignment(ctx, axisSuffix, barLabelAlign);
},
offset: CHART_CONFIG.bar.datalabels.offset,
offset: function (ctx) {
return getBarLabelOffset(ctx, axisSuffix, barLabelAlign);
},
color: function (ctx) {
return getBarLabelColor(ctx, gdsStyles, axisSuffix, barLabelAlign);
},
Expand Down Expand Up @@ -1017,7 +1032,7 @@ function initAll() {
return;
}

init(document);
document.querySelectorAll('.js-chart').forEach(initCharts);

window.addEventListener('scroll', hideAllHtmlTooltips, { passive: true });
window.addEventListener('resize', hideAllHtmlTooltips, { passive: true });
Expand Down
2 changes: 1 addition & 1 deletion SAPSec.Web/Views/Shared/Measures/_CurrentYearChart.cshtml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
var (axisMin, axisStep, axisMax) = Model.MeasureInfo.DataType switch
{
MeasureDataType.Score => (0, 30, 90),
MeasureDataType.ScaledScore => (0, 20, 120),
MeasureDataType.ScaledScore => (80, 20, 120),
_ => (0, 25, 100)
};
}
Expand Down
2 changes: 1 addition & 1 deletion SAPSec.Web/Views/Shared/Measures/_YearByYearChart.cshtml
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
var (axisMin, axisStep, axisMax) = Model.MeasureInfo.DataType switch
{
MeasureDataType.Score => (0, 30, 90),
MeasureDataType.ScaledScore => (80, 5, 120),
MeasureDataType.ScaledScore => (80, 20, 120),
_ => (0, 25, 100)
};
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1931,6 +1931,32 @@ public async Task AchievedHigherStandardGps_TopPerfomers_LimitedToTop3_AndTiedVa
]);
}

[Fact]
public async Task AchievedHigherStandardGps_TopPerfomers_WhenDisplayedValuesTie_SortsAlphabetically()
{
_establishmentRepo.SetupEstablishments(
Build.Establishment("100001", "Test School 1", x => x.Primary()),
Build.Establishment("100002", "Thoresby Primary School", x => x.Primary()),
Build.Establishment("100003", "Manor Park Primary Academy", x => x.Primary()),
Build.Establishment("100004", "Montem Academy", x => x.Primary()));

_similarSchoolsRepo.SetupGroups(
Build.PrimaryGroup("100001", ["100002", "100003", "100004"]));

_performanceRepo.SetupEstablishmentPerformance(
Build.Ks2Performance.Establishment("100001", x => x.WithGpsHigher(current: "18", prev: "", prev2: "")),
Build.Ks2Performance.Establishment("100002", x => x.WithGpsHigher(current: "96.6", prev: "", prev2: "")),
Build.Ks2Performance.Establishment("100003", x => x.WithGpsHigher(current: "96.5", prev: "", prev2: "")),
Build.Ks2Performance.Establishment("100004", x => x.WithGpsHigher(current: "91.4", prev: "", prev2: "")));

var response = await _sut.Execute(Request("100001"));

response.AchievedHigherStandardGps.TopPerformers.Should().Equal(
new TopPerformer(1, "100003", "Manor Park Primary Academy", 96.5m, IsCurrentSchool: false),
new TopPerformer(2, "100002", "Thoresby Primary School", 96.6m, IsCurrentSchool: false),
new TopPerformer(3, "100004", "Montem Academy", 91.4m, IsCurrentSchool: false));
}

[Fact]
public async Task AverageScaledScoreReading_ShouldContainExpectedMeasureSeries()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -618,6 +618,50 @@ public async Task AverageScaledScoreMaths_TableView_ShouldShowCorrectValues()
["Schools in England average", "106.8", "107.6", "108.4"]);
}

[Fact]
public async Task AverageScaledScoreCharts_ShouldStartAxisAt80()
{
Fixture.EstablishmentRepository.SetupEstablishments(
Build.Establishment("100001", "Test School 1", x => x.Open().Primary().InLA("001")),
Build.Establishment("100002", "Test School 2", x => x.Open().Primary().InLA("002")),
Build.Establishment("100003", "Test School 3", x => x.Open().Primary().InLA("003")));

Fixture.SimilarSchoolsPrimaryRepository.SetupGroups(
Build.PrimaryGroup("100001", ["100002", "100003"]));

Fixture.Ks2PerformanceRepository.SetupEnglandPerformance(
Build.Ks2Performance.England(x => x
.WithReadingScaledScore(current: "108.4", prev: "107.6", prev2: "106.8")
.WithMathsScaledScore(current: "108.4", prev: "107.6", prev2: "106.8")));

Fixture.Ks2PerformanceRepository.SetupLAPerformance(
Build.Ks2Performance.LA("001", x => x
.WithReadingScaledScore(current: "105.5", prev: "104.5", prev2: "103.5")
.WithMathsScaledScore(current: "105.5", prev: "104.5", prev2: "103.5")));

Fixture.Ks2PerformanceRepository.SetupEstablishmentPerformance(
Build.Ks2Performance.Establishment("100001", x => x
.WithReadingScaledScore(current: "102.4", prev: "101.4", prev2: "100.4")
.WithMathsScaledScore(current: "102.4", prev: "101.4", prev2: "100.4")),
Build.Ks2Performance.Establishment("100002", x => x
.WithReadingScaledScore(current: "104.2", prev: "103.2", prev2: "102.2")
.WithMathsScaledScore(current: "104.2", prev: "103.2", prev2: "102.2")),
Build.Ks2Performance.Establishment("100003", x => x
.WithReadingScaledScore(current: "106.2", prev: "105.2", prev2: "104.2")
.WithMathsScaledScore(current: "106.2", prev: "105.2", prev2: "104.2")));

var page = await Fixture.RequestPageAsync(Routes.PrimarySchool("100001").KS2, HttpStatusCode.OK);

foreach (var chartId in new[] { "reading-score-school-chart", "maths-score-school-chart" })
{
var chart = page.QuerySelector($"#{chartId}");
chart.Should().NotBeNull();
chart.GetAttribute("data-axis-min").Should().Be("80");
chart.GetAttribute("data-axis-step").Should().Be("5");
chart.GetAttribute("data-axis-max").Should().Be("120");
}
}

[Fact]
public async Task AverageScaledScoreMaths_TopPerformers_ShouldShowCorrectValues()
{
Expand Down Expand Up @@ -849,19 +893,19 @@ public async Task AchievedHigherStandardGps_TopPerformers_ShouldShowCorrectValue
{
Fixture.EstablishmentRepository.SetupEstablishments(
Build.Establishment("100001", "Test School 1", x => x.Primary()),
Build.Establishment("100002", "Test School 2", x => x.Primary()),
Build.Establishment("100003", "Test School 3", x => x.Primary()),
Build.Establishment("100004", "Test School 4", x => x.Primary()),
Build.Establishment("100002", "Thoresby Primary School", x => x.Primary()),
Build.Establishment("100003", "Manor Park Primary Academy", x => x.Primary()),
Build.Establishment("100004", "Montem Academy", x => x.Primary()),
Build.Establishment("100005", "Test School 5", x => x.Primary()));

Fixture.SimilarSchoolsPrimaryRepository.SetupGroups(
Build.PrimaryGroup("100001", ["100002", "100003", "100004", "100005"]));

Fixture.Ks2PerformanceRepository.SetupEstablishmentPerformance(
Build.Ks2Performance.Establishment("100001", x => x.WithGpsHigher(current: "18", prev: "17", prev2: "16")),
Build.Ks2Performance.Establishment("100002", x => x.WithGpsHigher(current: "24", prev: "23", prev2: "22")),
Build.Ks2Performance.Establishment("100003", x => x.WithGpsHigher(current: "24", prev: "22", prev2: "21")),
Build.Ks2Performance.Establishment("100004", x => x.WithGpsHigher(current: "23", prev: "21", prev2: "20")),
Build.Ks2Performance.Establishment("100002", x => x.WithGpsHigher(current: "96.6", prev: "23", prev2: "22")),
Build.Ks2Performance.Establishment("100003", x => x.WithGpsHigher(current: "96.5", prev: "22", prev2: "21")),
Build.Ks2Performance.Establishment("100004", x => x.WithGpsHigher(current: "91.4", prev: "21", prev2: "20")),
Build.Ks2Performance.Establishment("100005", x => x.WithGpsHigher(current: "19", prev: "18", prev2: "17")));

var page = await Fixture.RequestPageAsync(Routes.PrimarySchool("100001").KS2, HttpStatusCode.OK);
Expand All @@ -870,9 +914,9 @@ public async Task AchievedHigherStandardGps_TopPerformers_ShouldShowCorrectValue

table.ShouldHaveRows(
["Rank", "School", "2024 to 2025"],
["1", "Test School 2", "24%"],
["2", "Test School 3", "24%"],
["3", "Test School 4", "23%"]);
["1", "Manor Park Primary Academy", "97%"],
["2", "Thoresby Primary School", "97%"],
["3", "Montem Academy", "91%"]);
}

[Fact]
Expand Down
Loading