#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.Dark; var theme = VisualTheme.Graphite(); var model = ComparisonRecipe.Create("compact", mode); var context = new VisualRenderContext( new VisualLayoutOptions(new VisualSize(360, 360)), theme, mode, new VisualFrame("Checks over the reporting period", "Observed checks against the forecast and available capacity", 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 /// Bars, a forecast area and an observed line share daily categories, including explicit missing and empty states. public static class ComparisonRecipe { public static Chart Create(string variant = "wide", VisualThemeMode mode = VisualThemeMode.Light) { var count = variant == "empty" ? 0 : variant == "dense" ? 24 : 12; var names = variant == "wrapped-legend" ? new[] { "Observed checks across all locations", "Expected coverage of monitored services", "Capacity available for the next assessment" } : new[] { "Observed", "Expected", "Capacity" }; // Bars sit behind the forecast area and the observed line, so the series are added back to front. var capacity = ChartSeries.CreateBar(new ChartSeriesId("capacity"), names[2], Points(count, variant, 2, false)); var expected = ChartSeries.CreateArea(new ChartSeriesId("expected"), names[1], Points(count, variant, 1)); var observed = ChartSeries.CreateLine(new ChartSeriesId("observed"), names[0], Points(count, variant, 0)); if (variant is "explicit-status" or "explicit-series") { var colors = VisualTheme.Graphite().Resolve(mode); var selected = variant == "explicit-status" ? new[] { colors.Status.Pass.Fill, colors.Status.Medium.Fill, colors.Status.Critical.Fill } : colors.Palette.Take(3).ToArray(); observed.WithColor(selected[0]); expected.WithColor(selected[1]); capacity.WithColor(selected[2]); } return Chart.Create().WithXLabels(Enumerable.Range(1, count).Select(index => "Day " + index).ToArray()) .AddSeries(capacity).AddSeries(expected).AddSeries(observed); } private static IEnumerable Points(int count, string variant, int series, bool allowBreaks = true) { for (var index = 0; index < count; index++) { if (variant == "missing" && (index == 2 || index == 3)) continue; // Capacity grows in steps, the forecast follows it and observed checks swing around the forecast. var value = variant == "zero" ? 0.0 : series switch { 2 => 64 + index / 3 * 5 + index % 3, 1 => 46 + index * 1.6, _ => 44 + index * 1.7 + Math.Round(Math.Sin(index * 1.3) * 7) }; yield return new ChartPoint(index + 1, Math.Round(value), allowBreaks && variant == "missing" && index == 4); } } }