ci / build-test (push) Successful in 2m41s
Three things a reported Heizoel page got wrong at once. Its tank is dipped a few times a year and its burner read every few months, which is exactly the shape the coverage rules had not been walked through. "No data" for data that exists. A tank books nothing until the next dipstick closes the interval, so the stretch after the last dipstick is covered by no run at all, and a bucket no run covers was reported missing. The burner, whose run reaches into the window, said "only coarser data" -- the honest answer -- so one card claimed there was nothing while the coverage panel beside it listed years of data. A bucket that no run covers, no gap overlaps and no opening balance explains now reports the meter's resolution when its preceding coverage is within one interval of its own class: it is not silent, it is read rarely. A meter that does book its own buckets and stops -- a dead hourly source, a sheet asked about a later month -- still reads missing. Auto answering twelve months with one bar. Coarse only means "longer than a month", so a dipstick taken each autumn straddles a New Year as surely as a month start: coarsening the chart to years bought nothing and cost every point. The planning resolution now caps coarse at month when a run crosses a local year edge, and a series that cannot resolve the natural size no longer coarsens the whole chart -- it is drawn at that size with its buckets marked, which the chart and table already explain. A page contradicting itself. The comparison line above the ranking was fed the leading measure's matched coverage but worded as if it spoke for the page, directly above a burner row that did compare. It now names the figure it is about. Alongside: the "largest changes" ranking no longer drops a meter whose change is not comparable. It ranks what can be ranked, then lists the rest with their values and the reason -- the tank had been vanishing from its own energy type. And every dated table now reads newest first, as lists are read; charts stay chronological left to right, and the CSV export stays ascending for spreadsheets. A-41 to A-43 in the note record the three rules.
134 lines
6.1 KiB
Plaintext
134 lines
6.1 KiB
Plaintext
@* The accessible alternative to the chart (brief §7.2): one row per bucket with the period label, each series' value
|
|
with its unit, the status in words (availability, reason, provenance), the optional cost and price coverage, the
|
|
optional comparison (with the bucket it is compared with) and the change, a total row, and a drill-down per row.
|
|
A wide table scrolls inside its own container, never the page.
|
|
The rows read newest first with the total above them (A-42); the chart beside it stays chronological. *@
|
|
|
|
@if (_model is not null && _model.Rows.Count > 0)
|
|
{
|
|
<div class="mv-table-scroll" role="region" aria-label="@(Caption ?? S.AnalysisTable_Label)" tabindex="0">
|
|
<MudSimpleTable Dense="true" Hover="true" Class="mv-analysis-table">
|
|
@if (!string.IsNullOrWhiteSpace(Caption))
|
|
{
|
|
<caption class="mv-sr-only">@Caption</caption>
|
|
}
|
|
<thead>
|
|
<tr>
|
|
<th scope="col">@S.AnalysisTable_Period</th>
|
|
@foreach (var column in _model.Columns)
|
|
{
|
|
<th scope="col" class="@(column.IsNumeric ? "mv-num" : null)">
|
|
@column.Header
|
|
@if (column.SubHeader is not null)
|
|
{
|
|
<div class="mv-cell-secondary">@column.SubHeader</div>
|
|
}
|
|
</th>
|
|
}
|
|
@if (HasDrill)
|
|
{
|
|
<th scope="col"><span class="mv-sr-only">@S.Common_Actions</span></th>
|
|
}
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
@foreach (var row in _model.Rows)
|
|
{
|
|
<tr class="@RowClass(row)">
|
|
<th scope="row" class="mv-row-label">@row.Label</th>
|
|
@for (var i = 0; i < row.Cells.Count; i++)
|
|
{
|
|
var cell = row.Cells[i];
|
|
var numeric = _model.Columns[i].IsNumeric;
|
|
<td class="@CellClass(cell, numeric)">
|
|
@cell.Text
|
|
@if (cell.Secondary is not null)
|
|
{
|
|
<div class="mv-cell-secondary">@cell.Secondary</div>
|
|
}
|
|
</td>
|
|
}
|
|
@if (HasDrill)
|
|
{
|
|
<td class="mv-drill">
|
|
@if (row.Bucket is { } bucket)
|
|
{
|
|
var href = DrillHref?.Invoke(bucket);
|
|
var label = Loc.F(S.AnalysisTable_DrillLabel, row.Label);
|
|
if (href is not null)
|
|
{
|
|
<MudIconButton Href="@href" Icon="@Icons.Material.Filled.ZoomIn" Size="Size.Small"
|
|
aria-label="@label" title="@label" />
|
|
}
|
|
else if (OnDrill.HasDelegate)
|
|
{
|
|
<MudIconButton OnClick="() => OnDrill.InvokeAsync(bucket)" Icon="@Icons.Material.Filled.ZoomIn"
|
|
Size="Size.Small" aria-label="@label" title="@label" />
|
|
}
|
|
}
|
|
</td>
|
|
}
|
|
</tr>
|
|
}
|
|
</tbody>
|
|
</MudSimpleTable>
|
|
</div>
|
|
@if (Series.Any(s => !s.IsAdditive))
|
|
{
|
|
<MudText Typo="Typo.caption" Class="mv-muted mt-1">@S.AnalysisTable_NotAdditive</MudText>
|
|
}
|
|
}
|
|
|
|
@code {
|
|
/// <summary>The buckets of the plan, oldest first; the table turns them round itself (A-42).</summary>
|
|
[Parameter, EditorRequired]
|
|
public IReadOnlyList<AnalysisBucket> Buckets { get; set; } = [];
|
|
|
|
/// <summary>The series (<see cref="AnalysisTableSeries.ForSeries"/>, <see cref="AnalysisTableSeries.ForCosts"/>, …).</summary>
|
|
[Parameter, EditorRequired]
|
|
public IReadOnlyList<AnalysisTableSeries> Series { get; set; } = [];
|
|
|
|
/// <summary>The comparison buckets paired with <see cref="Buckets"/>: each row names the bucket it is compared with.</summary>
|
|
[Parameter]
|
|
public IReadOnlyList<BucketPair>? ComparisonPairs { get; set; }
|
|
|
|
[Parameter]
|
|
public bool IncludeTotal { get; set; } = true;
|
|
|
|
/// <summary>The drill-down address of a row (<see cref="AnalysisNavigation.DrillInto"/> written as a URL); null for none.</summary>
|
|
[Parameter]
|
|
public Func<AnalysisBucket, string?>? DrillHref { get; set; }
|
|
|
|
/// <summary>A row's drill-down as an action, when no address is given.</summary>
|
|
[Parameter]
|
|
public EventCallback<AnalysisBucket> OnDrill { get; set; }
|
|
|
|
/// <summary>The table's accessible name (also its caption for screen readers).</summary>
|
|
[Parameter]
|
|
public string? Caption { get; set; }
|
|
|
|
private AnalysisTableModel? _model;
|
|
private object? _builtFrom;
|
|
|
|
private bool HasDrill => DrillHref is not null || OnDrill.HasDelegate;
|
|
|
|
protected override void OnParametersSet()
|
|
{
|
|
var source = (Buckets, Series, ComparisonPairs, IncludeTotal, System.Globalization.CultureInfo.CurrentCulture.Name);
|
|
if (!Equals(_builtFrom, source))
|
|
{
|
|
_builtFrom = source;
|
|
_model = AnalysisTableModel.Build(Buckets, Series, ComparisonPairs, IncludeTotal);
|
|
}
|
|
}
|
|
|
|
private static string? RowClass(AnalysisTableRow row) =>
|
|
row.IsTotal ? "mv-row-total" : row.IsQualified ? "mv-row-qualified" : null;
|
|
|
|
private static string? CellClass(AnalysisTableCell cell, bool numeric)
|
|
{
|
|
var classes = string.Join(' ', new[] { numeric ? "mv-num" : null, cell.CssClass, cell.IsUnknown ? "mv-unknown" : null }.Where(c => c is not null));
|
|
return classes.Length > 0 ? classes : null;
|
|
}
|
|
}
|