#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 = MultiRingCalloutsRecipe.Create("automatic", mode);
var context = new VisualRenderContext(
new VisualLayoutOptions(new VisualSize(900, 560)), theme, mode,
new VisualFrame("Independent periods with inner captions", "Exact displaced sectors share one frame; source-owned overlay leaders retain every ring association", 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
/// Independent periods retain source-owned inner callouts and exact exploded native sectors.
public static class MultiRingCalloutsRecipe {
/// Creates explicit or automatically allocated rings with shared outside caption lanes.
/// Shifted sectors may overlap; haloed caption leaders overlay enclosing rings. Exact typography requires registered or installed Carlito regular/bold.
public static Chart Create(string variant = "wide", VisualThemeMode mode = VisualThemeMode.Light) {
if (variant is not ("wide" or "compact" or "automatic" or "compact-automatic" or "vertical" or "compact-vertical" or "mixed" or "compact-mixed"))
throw new ArgumentOutOfRangeException(nameof(variant));
var compact = variant.StartsWith("compact", StringComparison.Ordinal);
var chart = Chart.Create().WithSize(compact ? 390 : 900, compact ? 580 : 560)
.WithTitle("Service intake across two periods").WithSubtitle("Caption leaders retain their own ring and observation")
.WithTheme(VisualTheme.Graphite(), mode).WithXLabels("Consulting", "Subscriptions")
.WithDataLabels().WithDonutCenterLabel(false).WithPieSliceLabelContent(ChartPieSliceLabelContent.LabelAndPercent);
var automatic = variant.EndsWith("automatic", StringComparison.Ordinal);
if (automatic) chart.WithAutomaticRadialBands(new ChartRadialBandAllocation(.18, .14));
for (var index = 0; index < 2; index++) {
var values = index == 0 ? new[] { 60d, 30 } : new[] { 40d, 80 };
var series = new ChartSeries(new ChartSeriesId(index == 0 ? "prior" : "current"), index == 0 ? "Prior period" : "Current period", ChartSeriesKind.Donut,
values.Select((value, category) => new ChartPoint(category + 1, value)))
.WithObservationIds(new[] { new ChartObservationId("consulting"), new ChartObservationId("subscriptions") })
.WithPointSliceOffset(index, index == 0 ? .16 : .1);
if (!automatic) series.WithRadialBand(index == 0 ? new ChartRadialBand(.18, .48) : new ChartRadialBand(.62, 1));
series.DataLabelPlacement = variant.EndsWith("vertical", StringComparison.Ordinal) ? ChartDataLabelPlacement.Above
: variant.EndsWith("mixed", StringComparison.Ordinal) && index == 1 ? ChartDataLabelPlacement.Below : ChartDataLabelPlacement.Outside;
chart.AddSeries(series);
}
return chart;
}
}