@using MeterVault.App.AnalysisPage @using MeterVault.Core.Analysis @using MeterVault.Core.Analysis.Costing @using MeterVault.Infrastructure.Analysis @inject NavigationManager Nav @* Everything the Analysis page shows for one committed load (brief §7.4, §4.3): an explanation instead of figures when the selection cannot be one quantity, the attention items, the empty and pending states, the period figures with their change, which dates are compared, the chart (a click drills into a bucket, D-51) and the table with its drill-down links. Everything comes from the one view, so a title never sits above another selection's chart. *@ @if (View.Selection.Refusal != AnalysisPageRefusal.None) { } else if (View.ReaderRefusal == AnalysisRefusal.TooManySeries) { @Loc.F(S.Analysis_TooManyMeters, AnalysisLimits.MaxSeries) } else if (View.ReaderRefusal == AnalysisRefusal.TooManyPoints) { @* The toolbar says which interval would work and offers it. *@ @S.Analysis_ChooseCoarserBucket } else { @if (View.NotYetOccurred) { } else if (View.IsPending) { } else if (View.HasNoData) { @if (View.Availability is null) { @* Nothing at all yet: where data comes from. With older data, the dates and "Go to latest data" say it. *@ @S.Analysis_NoDataYetHint @S.Nav_Import } } else { @ChartTitle @if (HiddenOverlays) { @Loc.F(S.Analysis_ComparisonInTable, AnalysisPageLoader.MaxOverlaidSeries) } @S.Analysis_TableTitle @if (View.Series is [{ Basis: SeriesBasis.Virtual or SeriesBasis.LegacyVirtual } only]) { } } } @code { [Parameter, EditorRequired] public AnalysisPageView View { get; set; } = null!; [Parameter, EditorRequired] public AnalysisPageOptions Options { get; set; } = AnalysisPageOptions.Empty; /// The address as shown when the view was read: drill-downs and links build on it. [Parameter, EditorRequired] public AnalysisQuery Shown { get; set; } = null!; /// The page defaults, which an address leaves out. [Parameter, EditorRequired] public AnalysisDefaults Defaults { get; set; } = null!; /// Shows another state (an action of an explanation), replacing the address. [Parameter] public EventCallback OnShow { get; set; } /// Reads the same state again (analysis being prepared: check whether it is ready). [Parameter] public EventCallback OnRefresh { get; set; } private AttentionNames _names = new(); private EventCallback _onBucketClick; private Func? _drillHref; protected override void OnParametersSet() { _names = new AttentionNames(Options.MeterNames, Options.TypeNames, Options.Categories.ToDictionary(c => c.Id, c => c.Name)); // Buckets are clickable, and the table has its drill column, only when some bucket leads somewhere (D-51): a // comparison of monthly imports has nothing finer to open. var drills = View.Plan is { } plan && plan.Buckets.Any(b => DrillHref(b) is not null); _onBucketClick = drills ? EventCallback.Factory.Create(this, DrillAsync) : default; _drillHref = drills ? DrillHref : null; } /// "Consumption — Strom", "Cost — All energy types". private string ChartTitle { get { var selection = View.Selection; var what = selection.Metric?.Display() ?? View.Series.FirstOrDefault()?.Kind.Display() ?? string.Empty; var scope = selection.ScopeName ?? selection.Scope.Kind.Display(); return what.Length == 0 ? scope : what + " — " + scope; } } private bool HiddenOverlays => View.Pairs is not null && (View.Kind == AnalysisPageViewKind.Cost ? View.Costs.Count : View.Series.Count) > AnalysisPageLoader.MaxOverlaidSeries; private string? LatestHref => AnalysisNavigation.LatestData(Shown, View.Availability) is { } latest ? AnalysisNavigation.UriFor(Nav, latest, Defaults) : null; /// /// Where a bucket leads (D-51): the same view over the bucket in the next finer size the data resolves; for one meter /// whose data is too coarse for that, its records of the bucket; otherwise nowhere. /// private string? DrillHref(AnalysisBucket bucket) { if (AnalysisNavigation.DrillInto(Shown, bucket, View.Resolution) is { } next) { return AnalysisNavigation.UriFor(Nav, next, Defaults); } if (View.Selection.Scope.Kind == QueryScopeKind.Meter && View.Selection.Scope.Id is { } meterId) { var (first, last) = AnalysisNavigation.DaysOf(bucket); return Options.Meter(meterId) is { IsVirtual: true } ? MeterLinks.Analysis(meterId, PeriodResolver.IsValidCustomRange(first, last) ? Shown.WithCustomRange(first, last) : Shown) : AnalysisNavigation.NormalizedData(meterId, Shown, bucket); } return null; } private Task DrillAsync(AnalysisBucket bucket) { // A drill-down is a new history entry (D-46): Back returns to the coarser view. if (DrillHref(bucket) is { } href) { Nav.NavigateTo(href); } return Task.CompletedTask; } }