Deliverable for #128 — Review Prism theme behavior across diagram types.
Audit date: 2026-03-23. Reviewed by @copilot.
Theme.CreatePrismTheme() defines Prism with:
NodePalette = ["#FFFFFF", "#FFFFFF", …]— all eight entries are pure whiteUseGradients = false— no fill gradients anywhereUseBorderGradients = true+BorderGradientStops = ["#2563EB", "#7C3AED", "#DB2777", "#F59E0B"]— 4-stop blue→purple→pink→amber rainbow borderNodeFillColor = "#FFFFFF",BackgroundColor = "#FFFFFF"— white canvas
The visual identity is white fills, expressive rainbow gradient borders, borders carry all the emphasis.
Nodes that pass through SvgRenderSupport.AppendGradientDefs receive this correctly. Nodes that bypass it — or that use NodePalette as direct fill colors on a white background — lose Prism's identity entirely.
For each diagram type:
- Read the
.expected.svgfixture (or trace the rendering code path) for default, Dracula, and Prism. - Confirmed which SVG attributes (
fill,stroke, gradient<defs>) are present. - Traced the code path in
SvgNodeWriter.AppendNode,SvgRenderSupport.AppendGradientDefs, and diagram-specific layout files.
These types route through the standard AppendGradientDefs path and receive white fills and the 4-stop rainbow border gradient.
| Diagram type | Verification |
|---|---|
| Mermaid flowchart | Confirmed by mermaid-flowchart-prism.expected.svg and mermaid-theme-prism.expected.svg: all nodes have fill="#FFFFFF" and stroke="url(#node-N-stroke-gradient)" with 4-stop stops. Groups also use the gradient border. |
| Mermaid subgraph | Same standard path; subgraph group border uses AppendGradientDefs for stroke. |
| Mermaid mindmap | Standard ellipse/rect shape routing — no custom renderer. |
| Mermaid sequence / state / class / architecture / block / venn / timeline | All standard shape routing. AppendClassNode for UML class still consumes fill/stroke from AppendGradientDefs. |
| Conceptual chevrons | AppendChevronNode receives fill/stroke from the standard gradient path — white polygon, rainbow border. |
| Conceptual funnel | AppendFunnelSegmentNode — same pattern. |
| Conceptual pyramid | AppendPyramidSegmentNode — same pattern. |
| Conceptual cycle | Standard ellipse routing. |
| Conceptual radial (spoke nodes) | Standard rect routing for leaf spokes. |
| Conceptual tree | Standard rect routing. |
Root: Prism works wherever a node reaches SvgRenderSupport.AppendGradientDefs without a render:noGradient override and without its FillColor set to an explicit non-white value by the layout.
What renders: Bar fills are accent-derived series colors. For Prism (AccentColor = "#2563EB"), bars are shades of blue. Border gradients on the bars are the 4-stop Prism rainbow.
What is muted: Prism's "white fills, borders carry emphasis" principle is bypassed. Bars look like any other colorful bar chart.
Root cause: SvgRenderSupport.TryResolveXyChartColors / GetXyChartSeriesColor derives 8 accent tints from theme.AccentColor, bypassing theme.NodePalette and theme.NodeFillColor entirely. Because UseGradients = false in Prism, the bar fill is the raw accent-derived solid color. The 4-stop border gradient is present, which partially expresses Prism, but the colored bar fills undermine the restrained-fill language.
Severity: Medium. Bar charts need series colors for data legibility; this is partly a design question. A follow-up should decide whether Prism should intentionally degrade series colors, apply a lighter tint, or explicitly opt xycharts out of the white-fill rule.
What renders: Ring nodes get solid palette-derived colors from ThemePaletteResolver.BuildRingColors. The center node gets a PrimaryColor/TextColor blend. Card sidebar nodes get background/surface blends with solid single-color strokes.
Root cause (code references):
DefaultLayoutEngine.Target.csline 107:centerNode.Metadata["render:noGradient"] = trueDefaultLayoutEngine.Target.csline 139:ringNode.Metadata["render:noGradient"] = trueAppendTargetRingNode(SvgNodeWriter.cs:352–371): receives pre-computedstrokeas a plain string and draws a circle with that solid stroke — never usestheme.BorderGradientStops.AppendTargetCardNode(SvgNodeWriter.cs:373–419): renders the inner accent border using the pre-computedstrokeas a solid color.ThemePaletteResolver.BuildRingColorsderives vivid semantic colors fromtheme.AccentColor,theme.SecondaryColor, etc. These are vivid in every theme — the Prism all-whiteNodePalettehas no effect because ring colors are derived from semantic (non-palette) sources.
Severity: High. Target is a prominent conceptual type. The diagram is theme-aware (semantic colors react to Prism values) but expresses zero Prism-specific visual identity (no white fills, no rainbow border gradients).
What renders: The snake tube gradient and the circular node fills both use theme.NodePalette as direct fill colors. In Prism every NodePalette entry is #FFFFFF.
- Node circles:
FillColor = palette[i]=#FFFFFF→ white circles on white background — invisible. - Snake path:
segmentColors = palette[i]=#FFFFFF→ white tube on white background — the entire serpentine path disappears. - Outline stroke is
theme.BackgroundColor(#FFFFFF) — no contrast at all.
Root cause (code references):
DefaultLayoutEngine.Snake.cslines 145–151:string[] palette = theme.NodePalette is { Count: > 0 } ? [.. theme.NodePalette] : [theme.NodeFillColor]- Line 170:
node.FillColor = palette[i] - Lines 213–220:
segmentColors.Add(palette[i % palette.Length]) - No check for whether every palette entry matches the background color.
Severity: Critical. A Prism snake diagram is a blank white rectangle.
What renders: All three tablist variants call render:noGradient = true and set their accent/tab/bar fills from ColorUtils.Vibrant(palette[i], 2.x). In Prism palette[i] = "#FFFFFF" and Vibrant of pure white (zero saturation) returns white unchanged.
| Variant | Element | Prism fill value | Visual result |
|---|---|---|---|
| Cards | Accent block (left column) | Vibrant("#FFFFFF", 2.5) = #FFFFFF |
Invisible on white |
| Cards | Content card area | Lighten("#FFFFFF", 0.88) = #FFFFFF |
Invisible on white |
| Stacked | Number tab block | Vibrant("#FFFFFF", 2.5) = #FFFFFF |
Invisible on white |
| Stacked | Content bar | Blend(Vibrant("#FFFFFF", 1.8), "#FFFFFF", 0.35) = #FFFFFF |
Invisible on white |
| Flat | Vertical accent line | tablist:accentColor = Vibrant(palette[0], 2.5) = #FFFFFF |
Invisible on white |
| Flat | Title bar | Vibrant("#FFFFFF", 2.0) = #FFFFFF |
Invisible on white |
Additionally, all three variants set render:noGradient = true (cards: TabList.cs line 140, stacked: line 248, flat: line 357), which bypasses even the border-gradient path — the 4-stop rainbow gradient is also suppressed.
Root cause (code references):
DefaultLayoutEngine.TabList.cslines 113–115, 217–219, 327–329: all three layout methods usetheme.NodePaletteas base palette.- All three call
ColorUtils.Vibrant(catFill, 2.x)wherecatFill = palette[i] = "#FFFFFF". - All three set
render:noGradient = true.
Severity: Critical. A Prism tablist of any variant is entirely invisible — no structural chrome, no readable sections.
| Diagram type | Prism fidelity | Root cause |
|---|---|---|
| Mermaid flowchart | A ✅ | Standard gradient path |
| Mermaid subgraph | A ✅ | Standard gradient path |
| Mermaid mindmap | A ✅ | Standard shape routing |
| Mermaid sequence / state / class / architecture / block / venn | A ✅ | Standard shape routing |
| Conceptual chevrons / funnel / pyramid | A ✅ | Custom polygon shape; uses standard fill/stroke |
| Conceptual cycle / radial / tree | A ✅ | Standard routing |
| Mermaid xychart | B |
TryResolveXyChartColors uses accent-tint palette, not white fills; border gradient present |
| Conceptual target | C 🚨 | render:noGradient=true + BuildRingColors bypasses Prism entirely |
| Conceptual snake | C 🚨 | NodePalette all-white → invisible tube + invisible circles |
| Conceptual tablist (all 3 variants) | C 🚨 | render:noGradient=true + Vibrant(white) = white on white |
Which diagram types already show Prism's intended white-plus-gradient-border language?
All Mermaid types (flowchart, subgraph, mindmap, class, sequence, state, architecture, block, venn, timeline) and the simpler conceptual types (chevrons, funnel, pyramid, cycle, radial spokes, tree). These all route through AppendGradientDefs unchanged.
Which diagram types only react to Prism through semantic color changes while suppressing Prism's distinctive styling?
Conceptual target — it uses Prism's semantic colors (AccentColor, PrimaryColor, etc.) to derive ring/card colors, so it changes appearance between themes, but the white-fill and rainbow-gradient-border features are entirely absent.
Are there categories of diagrams where Prism should intentionally degrade?
Possibly: xychart bars arguably need series color differentiation to encode data. A deliberate opt-out could be documented as intentional. Wireframe diagrams are intentionally theme-agnostic by design and are a justified exception. All other current Category C cases look like unintentional regressions.
Where should DiagramForge standardize theme participation versus allowing diagram-specific exceptions?
render:noGradient=trueis a valid exception mechanism for nodes that need saturated fills (e.g., pillars title nodes in non-Prism themes), but it should not be applied to nodes whose fill is palette-derived without a fallback for monochrome palettes.- Snake / tablist should detect when NodePalette is monochrome (all entries the same color, or all matching the background) and substitute meaningful accent colors.
- Target ring nodes should offer a Prism-native rendering mode that uses
theme.BorderGradientStopsfor the ring stroke gradient.
The following issues should be filed as enhancement with the listed root cause and fix direction. They are ordered by severity.
Root cause: DefaultLayoutEngine.Snake.cs assigns node.FillColor = palette[i] and segmentColors.Add(palette[i]) from theme.NodePalette. In Prism every palette entry is #FFFFFF, making both node circles and the snake tube invisible on the white background.
Fix direction:
- In the snake layout, detect when the resolved palette is monochrome or matches
theme.BackgroundColor. - When detected, fall back to
theme.BorderGradientStopsas segment colors (mapping evenly across nodes), or derive vibrant colors fromtheme.AccentColor/theme.SecondaryColorviaThemePaletteResolver. - Ensure the snake path outline stroke uses a dark neutral rather than always
theme.BackgroundColorwhen the background and path colors match.
Root cause: All three layout variants (DefaultLayoutEngine.TabList.cs) derive accent/tab/bar fills from ColorUtils.Vibrant(palette[i], 2.x) where palette[i] = "#FFFFFF" in Prism. Vibrant of pure white has no effect. Additionally, all three set render:noGradient = true, suppressing even the border-gradient path.
Fix direction:
- Detect the monochrome-palette case in the tablist layout (or in
ColorUtils.Vibrant): when the input color is achromatic (S ≈ 0), substitute the firsttheme.BorderGradientStop(ortheme.AccentColor) as the accent base. - Alternatively, replace the direct
palette[i]lookup with a dedicatedThemePaletteResolverhelper that handles monochrome palettes gracefully. - The
render:noGradient = trueflag is applied to prevent double-gradient on an already-filled accent block; consider whether it should be replaced with a more surgical per-element gradient opt-in.
Root cause: Ring and center nodes use render:noGradient = true with explicitly set FillColor/StrokeColor derived by ThemePaletteResolver.BuildRingColors. AppendTargetRingNode draws a plain circle with a solid stroke color; AppendTargetCardNode uses stroke as a solid color for the inner accent border. Neither path consults theme.BorderGradientStops.
Fix direction:
AppendTargetRingNode: whentheme.UseBorderGradients && theme.BorderGradientStops is { Count: > 1 }, emit a<linearGradient>for the ring stroke using the gradient stops and reference it asstroke="url(#...)".AppendTargetCardNode: similarly, use a gradient for the inner accent border rect stroke.- The center node's solid fill is intentional (it needs contrast against the rings), so keep
render:noGradient = truethere but consider using the firstBorderGradientStopas the fill color for Prism.
Root cause: SvgRenderSupport.GetXyChartSeriesColor derives an 8-color palette from theme.AccentColor tints, completely bypassing theme.NodePalette. In Prism, bars receive vivid blue fills (accent-derived) rather than white, undermining Prism's restrained-fill identity.
Fix direction:
- Decide the policy: should xychart bars in Prism be white (consistent with node fills) or should they use lighter tints of
BorderGradientStopsfor series encoding? - A practical middle ground: when
theme.UseGradients == false && theme.UseBorderGradients == true && theme.BorderGradientStops is { Count: > 1 }, build the series palette fromtheme.BorderGradientStopswith appropriate lightening rather than from the accent tint ladder. This preserves data encoding while using Prism's characteristic colors. - Lower bar fill opacity (already at 0.88 for light backgrounds) could also help soften the filled appearance for Prism.