#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 = SupportedCombinationRecipe.Create("vertical-secondary", mode);
var context = new VisualRenderContext(
new VisualLayoutOptions(new VisualSize(800, 460)), theme, mode,
new VisualFrame("Orders by location", "Grouped categories, stable observations and independent configuration", showLegend: true, 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
/// Retained series handles share categories while keeping their own configuration, identities and scales.
public static class SupportedCombinationRecipe {
/// Builds a grouped secondary-axis, horizontal bar/line or mixed radial chart, optionally after a data update.
public static Chart Create(string variant = "wide", VisualThemeMode mode = VisualThemeMode.Light) {
var updated = variant.StartsWith("updated-", StringComparison.Ordinal);
var layout = updated ? variant.Substring(8) : variant;
if (layout is "wide" or "compact") layout = "vertical-secondary";
if (layout is not ("vertical-secondary" or "horizontal" or "mixed-radial"))
throw new ArgumentOutOfRangeException(nameof(variant));
var radial = layout == "mixed-radial";
var horizontal = layout == "horizontal";
var labels = new[] {
new ChartAxisLabel(1, "Central", "North"), new ChartAxisLabel(2, "Remote", "North"),
new ChartAxisLabel(3, "Central", "South"), new ChartAxisLabel(4, "Remote", "South")
};
var observed = new ChartSeries(new ChartSeriesId("observed"), "Completed orders",
radial ? ChartSeriesKind.RadialColumn : horizontal ? ChartSeriesKind.HorizontalBar : ChartSeriesKind.Bar,
new[] { new ChartPoint(1, 28), new ChartPoint(3, 41), new ChartPoint(4, 0) })
.WithObservationIds(Ids("north-central", "south-central", "south-remote"));
ChartSeries reference;
if (layout == "vertical-secondary") {
reference = ChartSeries.CreateBubble(new ChartSeriesId("reference"), "On-time share",
new[] { new ChartBubble(1, .72, 8), new ChartBubble(2, .86, 12), new ChartBubble(3, .79, 10), new ChartBubble(4, .91, 14) });
} else {
reference = new ChartSeries(new ChartSeriesId("reference"), "Target",
radial ? ChartSeriesKind.Radar : ChartSeriesKind.Line,
new[] { new ChartPoint(1, 45), new ChartPoint(2, 50), new ChartPoint(3, 55), new ChartPoint(4, 60) });
reference.ConfigureMarkers(markers => { markers.Enabled = true; markers.Radius = 4; });
if (radial) reference.ConfigureRadar(options => options.Form = ChartLineAreaForm.Line);
}
reference.WithObservationIds(Ids("north-central", "north-remote", "south-central", "south-remote"));
var chart = Chart.Create().WithSize(800, 460).WithTheme(VisualTheme.Graphite(), mode)
.WithTitle("Orders by location").WithSubtitle("Source identities survive updates; an absent order count stays absent")
.WithLegend().WithDataLabels(false)
.AddSeries(observed).AddSeries(reference);
if (horizontal) {
chart.Options.LineOrientation = ChartOrientation.Horizontal;
chart.WithXAxis("Orders").WithYAxis("Location");
chart.ConfigureXAxis(axis => { axis.Minimum = 0; axis.Maximum = 70; });
chart.ConfigureYAxis(axis => { foreach (var label in labels) axis.Labels.Add(label); axis.Reversed = true; });
} else {
chart.WithXLabels(radial ? labels.Select(label => new ChartAxisLabel(label.Value, string.Join(" / ", label.Groups) + " / " + label.Text)) : labels)
.WithYAxis("Orders");
chart.ConfigureYAxis(axis => { axis.Minimum = 0; axis.Maximum = 70; });
if (!radial) {
reference.UseSecondaryValueAxis();
reference.ConfigureMarkers(markers => markers.Shape = ChartMarkerShape.Diamond);
chart.WithSecondaryYAxis("On-time share", value => value.ToString("0%", CultureInfo.InvariantCulture));
chart.ConfigureSecondaryYAxis(axis => { axis.Minimum = 0; axis.Maximum = 1; });
}
}
if (updated) {
observed.Points[0] = new ChartPoint(1, 34); // Indexed numeric updates retain this observation's identity.
if (layout == "vertical-secondary") {
// Full typed replacement clears observation metadata: reassign IDs in the new source order.
reference.ReplaceBubbles(new[] { new ChartBubble(3, .83, 11), new ChartBubble(1, .94, 16), new ChartBubble(4, .88, 13) })
.WithObservationIds(Ids("south-central", "north-central", "south-remote"));
} else reference.Points[0] = new ChartPoint(1, 48);
reference.WithName(layout == "vertical-secondary" ? "Updated on-time share" : "Updated target");
chart.MoveSeries(reference, 0);
}
return chart;
}
private static IEnumerable Ids(params string[] ids) => ids.Select(id => new ChartObservationId(id));
}