#region Setup
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using ChartForgeX.Core;
using ChartForgeX.Primitives;
using ChartForgeX.Rendering;
using ChartForgeX.Themes;
using ChartForgeX.Topology;
using ChartForgeX.Typography;
using ChartForgeX.VisualArtifacts;
using ChartForgeX.VisualBlocks;
var mode = VisualThemeMode.Light;
var theme = VisualTheme.Graphite();
var model = AlignedDifferenceRecipe.Create("connect", mode);
var context = new VisualRenderContext(
new VisualLayoutOptions(new VisualSize(800, 520)), theme, mode,
new VisualFrame("Independent source comparison", "Exact shared coordinates retain both complete inputs and nonlinear native crossing geometry", showLegend: null, showSurface: false));
var prepared = model.Prepare(context);
File.WriteAllText("visual.svg", prepared.ToSvg());
File.WriteAllBytes("visual.png", prepared.ToPng());
File.WriteAllText("visual.html", prepared.ToArtifact("example", VisualArtifactKind.Chart).ToHtmlPage());
#endregion
/// Compares exact shared coordinates while retaining both independent raw sources and selected nonlinear scales.
public static class AlignedDifferenceRecipe {
/// Creates a complete comparison. Exact documentation typography needs Carlito regular/bold installed or registered; other hosts use normal font fallback.
public static Chart Create(string variant = "wide", VisualThemeMode mode = VisualThemeMode.Light) {
if (variant is not ("wide" or "compact" or "negative-log" or "compact-negative-log" or "sym-log" or "compact-sym-log" or "connect" or "compact-connect"))
throw new ArgumentOutOfRangeException(nameof(variant));
var compact = variant.StartsWith("compact", StringComparison.Ordinal);
var negative = variant.EndsWith("negative-log", StringComparison.Ordinal);
var symmetric = variant.EndsWith("sym-log", StringComparison.Ordinal);
var connect = variant.EndsWith("connect", StringComparison.Ordinal);
var actualValues = symmetric ? new[] { -40d, -20, 30, -10, 80 } : negative ? new[] { -120d, -90, -4, -80, -3 } : new[] { 2d, 4, 80, 5, 120 };
var forecastValues = symmetric ? new[] { -25d, -10, 20, 50, -5 } : negative ? new[] { -30d, -75, -8, -10, -50 } : new[] { 20d, 18, 10, 50, 8 };
var actual = ChartSeries.CreateLine(new ChartSeriesId("actual-source"), "Actual", new[] {
new ChartPoint(1, actualValues[0]), new ChartPoint(2, actualValues[1]), new ChartPoint(4, actualValues[2]),
new ChartPoint(8, actualValues[3]), new ChartPoint(16, actualValues[4]) })
.WithObservationIds(Enumerable.Range(0, 5).Select(index => new ChartObservationId("actual-" + index)));
var forecast = ChartSeries.CreateLine(new ChartSeriesId("forecast-source"), "Forecast", new[] {
new ChartPoint(1, forecastValues[0]), new ChartPoint(3, forecastValues[1]), new ChartPoint(4, forecastValues[2]),
new ChartPoint(8, forecastValues[3]), new ChartPoint(16, forecastValues[4]) })
.WithObservationIds(Enumerable.Range(0, 5).Select(index => new ChartObservationId("forecast-" + index)));
var alignment = ChartDifferencePoints.Align(actual, forecast, connect ? ChartMissingDataPolicy.Connect : ChartMissingDataPolicy.Gap);
var comparison = ChartSeries.CreateAlignedDifferenceArea(new ChartSeriesId("comparison"), "Actual against forecast", alignment)
// Reuse actual authored first-source IDs explicitly; Align never invents a destination key.
.WithObservationIds(alignment.FirstSourceIndices.Select(index => alignment.FirstObservationIds[index]))
.ConfigureMarkers(options => options.Radius = 3)
.ConfigureDifferenceArea(options => {
options.PositiveFill = ChartColor.FromHex("#2F8199"); options.NegativeFill = ChartColor.FromHex("#C7703C");
options.PositiveFillOpacity = .38; options.NegativeFillOpacity = .38;
});
var chart = Chart.Create().WithSize(compact ? 390 : 800, compact ? 500 : 520).WithTheme(VisualTheme.Graphite(), mode)
.WithTitle("Observed and forecast at shared coordinates")
.WithSubtitle(connect ? "Unmatched source slots remain captured; surviving pairs connect" : "Unmatched source slots remain captured and break the filled path")
.WithXAxis("Authored coordinate").WithYAxis("Requests").AddSeries(comparison);
if (symmetric) { chart.Options.YAxis.Scale = ChartScaleKind.SymmetricLogarithmic; chart.Options.YAxis.SymmetricLogarithmThreshold = 1; }
else chart.Options.YAxis.WithLogarithmicScale(domain: negative ? ChartLogarithmicDomain.Negative : ChartLogarithmicDomain.Positive);
return chart;
}
}