The convergence of Partas.Solid (web site) and D3.js represents a strategic opportunity to establish a new paradigm for data visualization in functional web development. This proposal outlines a principled architectural approach that would leverage the synergies between Solid.JS’s fine-grained reactivity and D3’s functional computation model.
π D3.js has long been misunderstood as a “DOM manipulation library.” In reality, its most powerful modules (d3-scale, d3-shape, d3-array, d3-interpolate) are pure functional computation engines that have no dependency on the DOM whatsoever. By separating computation from rendering, D3’s architecture naturally aligns with functional programming principles.
Why This Matters for F# Developers: Partas.Solid (GitHub repo) represents a more idiomatic F# approach to web development in the Fable ecosystem. Unlike Feliz, which leverages React’s model with hooks and components, Partas.Solid embraces Solid.JS’s “vanishing components” pattern where reactive primitives (signals) exist independently of component boundaries. This aligns perfectly with functional programming principles: pure functions, immutable data structures, and explicit data flow at boundaries. By building chart components as native Partas.Solid components that use D3 for computation, this approach would keep the entire visualization pipeline visible to the F# compiler, maximizing the number of design-time choices.
On Convergent Design
Through principled architectural exploration, this proposal has revealed an interesting property: the patterns that would naturally emerge from combining Partas.Solid’s reactivity model with D3’s functional approach closely mirror those used in the Fabulous framework. This convergence wasn’t a design goal. It emerged from independent, principled decisions about how to build functional UI for the web.
The shared patterns that would emerge include:
- MVU/Elmish architecture (pure update functions, message dispatch)
- Declarative composition (computation expressions for UI hierarchies)
- Fine-grained reactivity (Solid’s signals parallel Fabulous’s efficient diffing)
- Type-safe component trees (F#’s type system validates structure at compile time)
This convergence is significant because it suggests these patterns represent fundamental properties of functional UI design, not framework-specific choices. Whether targeting the web (proposed SPEC Stack approach), native mobile (Fabulous), or embedded systems (FidelityUI), applying F# and MVU principles consistently leads to similar architectural solutions.
The practical value: Developers familiar with Fabulous would find the proposed SPEC Stack patterns intuitive, and vice versa. This shared visual language would emerge as an organic property of functional design, not through deliberate imitation, making it easier to port UI concepts across domains without compromising each platform’s unique advantages.
Critically, this convergence would not require compromising Partas.Solid’s strengths. The similarity would arise naturally because both approaches share foundational principles (Elmish, computation expressions, fine-grained reactivity), while each remains optimized for its target platform.
Part I: The Partas.Solid Architecture
Partas.Solid Components] B[Fable Compiler
F# β JavaScript AST] C[Partas.Solid Plugin
AST Transformations] D[D3 Computation Layer
Pure Functions: scales, shapes, statistics] E[Solid.JS Reactivity
Signals & Fine-Grained DOM Updates] F[SVG Rendering
Direct DOM with Fluent UI Design Tokens] A -->|compile| B B -->|transform| C C -->|optimize| E A -->|imports| D E -->|reactive updates| F D -->|compute values| F style A fill:#e1f5ff style B fill:#fff3cd style C fill:#d4edda style D fill:#f8d7da style E fill:#e1f5ff style F fill:#fff3cd
The entire pipeline from F# source to rendered pixels is visible to the compiler, enabling whole-program optimizations.
How Partas.Solid Differs from Feliz
The Fable ecosystem has traditionally centered around Feliz, which provides F# bindings to React. While Feliz is powerful, it inherits React’s conceptual model: components as state machines that re-render on state changes, hooks for lifecycle management, and a virtual DOM for reconciliation. This is a long-standing proven model, but it carries some computational baggage that doesn’t burden SolidJS. So, distinct choices have been made to align to Partas.Solid’s model.
Feliz Pattern (React-Inspired):
// Feliz component with React hooks
open Feliz
[<ReactComponent>]
let Counter() =
let count, setCount = React.useState(0)
Html.div [
Html.button [
prop.text $"Count: {count}"
prop.onClick (fun _ -> setCount(count + 1))
]
]
In this pattern:
- Components are stateful entities that encapsulate behavior
- State lives inside component boundaries via
React.useState - Re-renders happen at the component level (entire subtree re-evaluates)
- Lifecycle is tied to mounting/unmounting components
Partas.Solid Pattern (Signal-Driven):
// Partas.Solid component with signals
open Partas.Solid
[<SolidComponent>]
let Counter() =
let count, setCount = createSignal(0)
div() {
button() {
text $"Count: {count()}"
onClick (fun _ -> setCount(count() + 1))
}
}
Notable differences:
Components Execute Once: The
Counterfunction runs exactly once when instantiated. It does not re-run on state changes. The component “vanishes” after initial execution, leaving behind a reactive graph of signals and effects.Signals Live Independently:
createSignalreturns an accessor function (count()) and a setter. These exist in closures, not component state. They can be passed anywhere, called from anywhere, and their lifecycle is independent of React-style mounting.Fine-Grained Reactivity: When
count()changes, only the specific DOM text node bound to that signal updates. There is no component re-render, no virtual DOM diffing, no reconciliation phase. Solid.JS tracks dependencies at the expression level and updates the minimal set of affected nodes. This reactivity model delivers 2-3x performance improvements over React as shown in independent benchmarks.Builder Syntax with Computation Expressions: Partas.Solid uses F#’s computation expression syntax (
div() { ... }) instead of list-based DSL (Html.div [ ... ]). This feels more natural in F# and leverages the language’s syntactic capabilities.Erased Types for Zero-Cost Abstraction: Partas.Solid extensively uses
[<Erase>]attributes to create F# types that compile to JavaScript with no runtime overhead. This enables rich type safety in F# while generating minimal JavaScript output.
Why This Matters for D3 Integration
The signal-driven, “vanishing component” model of Partas.Solid aligns perfectly with D3’s functional nature. Consider a scenario where we need to render a bar chart:
Feliz/React Approach (Suboptimal):
[<ReactComponent>]
let BarChart(data: ChartData[]) =
// Problem: This runs on EVERY render
// D3 scales get recreated, even if data hasn't changed
let xScale =
d3.scaleBand()
.domain(data |> Array.map (fun d -> d.label))
.range([| 0.0; 600.0 |])
let yScale =
d3.scaleLinear()
.domain([| 0.0; data |> Array.map (fun d -> d.value) |> Array.max |])
.range([| 400.0; 0.0 |])
Html.svg [
prop.width 600
prop.height 400
prop.children [
for d in data do
Html.rect [
prop.x (xScale.Invoke(d.label))
prop.y (yScale.Invoke(d.value))
// ...
]
]
]
Issues:
- Scales recalculated on every render (expensive for large datasets)
- Memoization required (
React.useMemo) to avoid unnecessary work - Component re-renders entire SVG tree when data changes
Partas.Solid Approach (Optimal):
[<SolidComponent>]
let BarChart(data: unit -> ChartData[]) =
// This closure runs ONCE
// Scales are functions that compute reactively when data() changes
let xScale() =
d3.scaleBand()
.domain(data() |> Array.map (fun d -> d.label))
.range([| 0.0; 600.0 |])
let yScale() =
d3.scaleLinear()
.domain([| 0.0; data() |> Array.map (fun d -> d.value) |> Array.max |])
.range([| 400.0; 0.0 |])
svg() {
width 600
height 400
For'(each'=data(), children=(fun (d, i) ->
rect() {
x (xScale().Invoke(d.label))
y (yScale().Invoke(d.value))
// ...
}
))
}
Benefits:
- Scales are computed accessors that only recalculate when
data()is accessed - Solid’s reactivity tracks that
xScale()depends ondata() - When data changes, only the specific rect attributes update, not the entire component
- No memoization needed; reactive graph handles optimization automatically
This pattern, D3 as pure computation wrapped in signal accessors, would form the foundation of this proposed architecture.
Data Flow Visualization
Only changed signals trigger recompute Note over D3: Pure functions:
Scales recomputed only when data() accessed
Solid’s fine-grained reactivity means only the specific SVG attributes that depend on changed signals are updated, no virtual DOM diffing or component re-renders.
Part II: D3 as Mathematics, Not DOM
The Misconception: D3 as a Rendering Library
D3.js (Data-Driven Documents) is often introduced with examples like this:
// Classic D3 "Hello World" - DOM manipulation
d3.select("body")
.append("svg")
.attr("width", 600)
.attr("height", 400)
.selectAll("circle")
.data([10, 20, 30])
.enter()
.append("circle")
.attr("cx", (d, i) => i * 50)
.attr("cy", 200)
.attr("r", d => d);
This code performs imperative DOM manipulation: selecting elements, appending nodes, setting attributes through chained method calls. Developers naturally assume D3 is a “rendering library” akin to React or Vue. This is incorrect.
D3’s Modular Architecture
D3 v4+, released in 2016, is a collection of independent modules documented at d3js.org:
Only d3-selection manipulates the DOM. All other modules are pure functional libraries with no DOM dependencies:
- d3-scale: Mathematical functions that map input domains to output ranges
- d3-shape: Path generators that produce SVG path strings from data
- d3-array: Statistical operations (max, min, mean, quantile, bins)
- d3-interpolate: Value interpolation for animations and color scales
- d3-color: Color space conversions and manipulations
- d3-time: Time interval calculations
- d3-format: Number and date formatting
These modules can run in Node.js, Web Workers, or any JavaScript environment, no DOM required.
Functional D3: Examples
Example 1: Scales as Pure Functions
open Fable.Core
open Fable.Core.JsInterop
// d3-scale bindings
[<ImportMember("d3-scale")>]
let scaleLinear<'Domain, 'Range>() : obj = jsNative
// Usage: Pure function composition
let createYScale (data: float[]) (height: float) =
let scale = scaleLinear()
scale?domain([| 0.0; Array.max data |]) |> ignore
scale?range([| height; 0.0 |]) |> ignore
scale
// Invoke scale as a function
let yPosition value scale =
scale $ value // scale(value) in JavaScript
This createYScale function:
- Takes data and height as inputs
- Returns a scale function
- Has no side effects
- Can be composed, memoized, tested in isolation
Example 2: Shape Generators as Path Computers
open Fable.Core.JsInterop
[<ImportMember("d3-shape")>]
let line<'T>() : obj = jsNative
type Point = { x: float; y: float }
let createLineGenerator (xScale: obj) (yScale: obj) =
let generator = line<Point>()
generator?x(fun (p: Point) -> xScale $ p.x) |> ignore
generator?y(fun (p: Point) -> yScale $ p.y) |> ignore
generator
// Usage: Pure transformation
let points = [| { x = 1.0; y = 10.0 }; { x = 2.0; y = 20.0 } |]
let pathData : string = generator $ points // Returns "M 10,100 L 20,80" (SVG path string)
The line generator:
- Accepts accessor functions (how to extract x/y from data)
- Returns a string (SVG path syntax)
- Never touches the DOM
- Is a pure function: same inputs β same outputs
Example 3: Array Statistics
[<ImportMember("d3-array")>]
let max<'T>(data: 'T[], accessor: 'T -> float) : float = jsNative
[<ImportMember("d3-array")>]
let extent<'T>(data: 'T[], accessor: 'T -> float) : float * float = jsNative
type SalesData = { month: string; revenue: float }
let salesData = [|
{ month = "Jan"; revenue = 100.0 }
{ month = "Feb"; revenue = 150.0 }
|]
// Pure functional operations
let maxRevenue = max(salesData, fun d -> d.revenue) // 150.0
let (minRev, maxRev) = extent(salesData, fun d -> d.revenue) // (100.0, 150.0)
No DOM, no side effects, just data transformation.
Reactive Scale Computation Lifecycle
Scales are reactive functions that only recompute when their dependencies (data, dimensions) change. Solid’s fine-grained reactivity ensures minimal work on updates.
Part III: Native Partas.Solid Chart Components
The Proposed Architecture: D3 + Solid.JS Reactivity
The architectural plum here is that Partas.Solid components with D3 computation would create a fully transparent compilation pipeline. Unlike approaches that wrap external components, everything in this proposal would be F# source code that flows through Fable.
Core Pattern:
[<SolidComponent>]
let BarChart(data: unit -> ChartDatum[]) =
// Scales as computed accessors (reactive)
let xScale() =
d3.scaleBand()
.domain(data() |> Array.map (fun d -> d.label))
.range([| 0.0; 600.0 |])
let yScale() =
d3.scaleLinear()
.domain([| 0.0; data() |> Array.map (fun d -> d.value) |> Array.max |])
.range([| 400.0; 0.0 |])
svg() {
width 600
height 400
For'(each'=data(), children=(fun (d, i) ->
rect() {
x (xScale().Invoke(d.label))
y (yScale().Invoke(d.value))
width (xScale().bandwidth())
height (400.0 - yScale().Invoke(d.value))
fill "var(--colorBrandBackground)"
}
))
}
What Would Make This Powerful:
- Component Runs Once: The
BarChartfunction would execute once, creating the reactive graph - Scales Are Reactive:
xScale()andyScale()would recompute only whendata()changes - Fine-Grained Updates: Solid would track that rect attributes depend on scales, which depend on data
- No Virtual DOM: Changes would flow directly to specific DOM attributes
- Compiler Visibility: The entire pipeline would be F# AST visible to Fable and Partas.Solid plugins
Design Precedent: FluentUI.Charts
Microsoft’s @fluentui/react-charting library (npm package) demonstrates one implementation of the D3 for computation, framework for rendering pattern. While this proposal follows similar principles, the architecture emerges from Fabulous-style functional patterns rather than React’s component model.
FluentUI.Charts Pattern (React):
class VerticalBarChart extends React.Component {
render() {
// D3 computes scales (pure functions)
const xScale = d3ScaleBand()
.domain(this.props.data.map(d => d.label))
.range([0, this.state.chartWidth]);
const yScale = d3ScaleLinear()
.domain([0, d3Max(this.props.data, d => d.value)])
.range([this.state.chartHeight, 0]);
// React renders declaratively
return (
<svg>
{this.props.data.map((d, i) => (
<rect
x={xScale(d.label)}
y={yScale(d.value)}
fill={this.props.colors[i]}
/>
))}
</svg>
);
}
}
Our Partas.Solid Pattern:
[<SolidComponent>]
let BarChart(data: unit -> ChartDatum[], colors: string[]) =
let xScale() =
scaleBand()
.domain(data() |> Array.map (fun d -> d.label))
.range([| 0.0; 600.0 |])
let yScale() =
scaleLinear()
.domain([| 0.0; data() |> Array.map (fun d -> d.value) |> Array.max |])
.range([| 400.0; 0.0 |])
svg() {
For'(each'=data(), children=(fun (d, i) ->
rect() {
x (xScale().Invoke(d.label))
y (yScale().Invoke(d.value))
fill colors.[i]
}
))
}
Points of Differentiation:
- FluentUI.Charts: Scales recalculated on every React render
- Proposed approach: Scales would be computed reactively only when dependencies change
- FluentUI.Charts: Entire component re-renders
- Proposed approach: Only changed attributes would update
Compilation Pipeline Visibility
Sees Entire Flow] G -.->|analyze| A G -.->|analyze| B G -.->|analyze| C G -.->|analyze| D G -.->|optimize| E style G fill:#d4edda,stroke:#28a745,stroke-width:3px style A fill:#e1f5ff style B fill:#e1f5ff style C fill:#e1f5ff style D fill:#f8d7da style E fill:#fff3cd style F fill:#fff3cd
Because everything is F# source code, the compiler can analyze data flow across the entire visualization pipeline and can apply optimizations at the AST level.
Common Chart Patterns
1. Responsive Sizing
Following established patterns (as seen in FluentUI.Charts and similar libraries), this proposal would use ResizeObserver for responsive behavior:
[<SolidComponent>]
let ResponsiveChart(data: unit -> ChartDatum[]) =
let containerRef, setContainerRef = createSignal<HTMLElement option>(None)
let dimensions, setDimensions = createSignal({ width = 600.0; height = 400.0 })
createEffect(fun () ->
match containerRef() with
| Some el ->
let observer = ResizeObserver(fun entries ->
let entry = entries.[0]
setDimensions({
width = entry.contentRect.width
height = entry.contentRect.height
})
)
observer.observe(el)
| None -> ()
)
div() {
ref setContainerRef
class' "chart-container"
svg() {
width (dimensions().width)
height (dimensions().height)
// ... chart content
}
}
2. Accessibility (ARIA)
Following accessibility best practices:
[<SolidComponent>]
let AccessibleBarChart(data: unit -> ChartDatum[], title: string) =
svg() {
role "img"
ariaLabel $"{title}: Bar chart with {data().Length} data points"
For'(each'=data(), children=(fun (d, i) ->
rect() {
role "graphics-symbol"
ariaLabel $"{d.label}: {d.value}"
// ... positioning
}
))
}
3. Design Token System
Following modern theming patterns with CSS custom properties:
// Chart uses CSS variables that reference Fluent tokens
rect() {
fill "var(--chart-bar-fill, var(--colorBrandBackground))"
stroke "var(--chart-bar-stroke, var(--colorNeutralStroke1))"
}
/* Design tokens */
:root {
--chart-bar-fill: var(--colorBrandBackground);
--chart-bar-stroke: var(--colorNeutralStroke1);
--chart-grid-color: var(--colorNeutralStroke2);
--chart-text-color: var(--colorNeutralForeground1);
}
Part IV: Design System Integration via Hugo Themes
This proposal integrates with your existing Hugo theme’s design system through Victor CLI, our planned tool for transforming Hugo templates into Partas.Solid components. Rather than introducing a separate design system, charts would inherit styling from your Hugo theme automaticallyβwhether that’s DaisyUI, Tailwind, or any other CSS framework your theme uses.
How Victor Bridges Hugo Themes and Partas.Solid Charts
Victor CLI transforms Hugo templates (with their embedded DaisyUI, Tailwind, or custom CSS) into Partas.Solid components. This transformation preserves the design tokens and theme variables from your Hugo theme, making them available to chart components automatically.
The key insight from FluentUI.Charts: They demonstrated that D3 can be used purely for computation (scales, shapes, statistics) while the framework handles rendering. We borrow this architectural patternβD3 for math, Partas.Solid for reactivity, your Hugo theme for design.
Hugo Theme (DaisyUI/Tailwind/Custom CSS)
β
Victor transforms templates β Partas.Solid components
β
Charts consume same CSS variables
β
Unified visual language across static and dynamic content
Theme Application Workflow
Theme changes happen via CSS custom property updates, no component re-renders needed. The browser’s CSS cascade handles the visual updates automatically.
Design Token Integration
Your Hugo theme already defines CSS custom properties for colors, typography, and spacing. Charts would consume these same variables, ensuring visual consistency across your entire site.
Example: DaisyUI Theme Variables (common in Hugo themes):
/* DaisyUI automatically generates these from your theme config */
:root {
--p: 259 94% 51%; /* primary color (HSL) */
--s: 314 100% 47%; /* secondary color */
--a: 174 60% 51%; /* accent color */
--n: 219 14% 28%; /* neutral color */
--b1: 0 0% 100%; /* base background */
--bc: 215 28% 17%; /* base content (text) */
/* Rounded corners */
--rounded-box: 1rem;
--rounded-btn: 0.5rem;
/* Animations */
--animation-btn: 0.25s;
--animation-input: 0.2s;
}
Chart-Specific Token Mapping:
Charts would reference theme tokens through simple CSS custom property mappings:
:root {
/* Bar charts use primary color from theme */
--chart-bar-fill: hsl(var(--p));
--chart-bar-stroke: hsl(var(--pf)); /* primary-focus */
--chart-bar-hover-opacity: 0.8;
/* Line charts */
--chart-line-stroke: hsl(var(--p));
--chart-line-stroke-width: 2px;
--chart-area-fill: hsl(var(--p));
--chart-area-opacity: 0.2;
/* Axes and grids use neutral colors */
--chart-axis-stroke: hsl(var(--n));
--chart-grid-stroke: hsl(var(--bc) / 0.1);
/* Text inherits base content color */
--chart-title-color: hsl(var(--bc));
--chart-label-color: hsl(var(--bc) / 0.7);
}
Usage in Partas.Solid Components:
[<SolidComponent>]
let ThemedBarChart(data: unit -> ChartDatum[]) =
svg() {
For'(each'=data(), children=(fun (d, i) ->
rect() {
fill "var(--chart-bar-fill)"
stroke "var(--chart-bar-stroke)"
class' "transition-opacity hover:opacity-80" // Tailwind utilities work too
// D3 positioning...
}
))
g() {
class' "axis"
// Axis inherits --chart-axis-stroke from theme
}
}
Charts can also use Tailwind utility classes directly when appropriate, blending seamlessly with your Hugo theme’s styling approach.
Color Palette Strategies
Charts would leverage your Hugo theme’s color system. For example, DaisyUI themes provide semantic color categories that adapt automatically to theme changes:
1. Using Theme Color Variables:
module ColorPalettes =
// DaisyUI semantic colors - automatically theme-aware
let themeColors = [|
"hsl(var(--p))" // Primary
"hsl(var(--s))" // Secondary
"hsl(var(--a))" // Accent
"hsl(var(--in))" // Info
"hsl(var(--su))" // Success
"hsl(var(--wa))" // Warning
"hsl(var(--er))" // Error
|]
// Generate sequential palette from theme primary color
let sequentialFromPrimary = [|
"hsl(var(--p) / 0.2)" // Lightest
"hsl(var(--p) / 0.4)"
"hsl(var(--p) / 0.6)"
"hsl(var(--p) / 0.8)"
"hsl(var(--p))" // Full opacity
"hsl(var(--pf))" // Primary-focus (darker)
|]
2. Using Tailwind Color Utilities (if your theme includes Tailwind):
// Tailwind classes can be used directly on SVG elements
let tailwindColorClasses = [|
"fill-blue-500"
"fill-green-500"
"fill-red-500"
"fill-orange-500"
"fill-purple-500"
"fill-teal-500"
|]
Using Palettes in Charts:
[<SolidComponent>]
let MultiSeriesChart(data: unit -> ChartDatum[][]) =
let colors = ColorPalettes.themeColors
svg() {
For'(each'=data(), children=(fun (series, seriesIdx) ->
For'(each'=series, children=(fun (d, i) ->
rect() {
fill colors.[seriesIdx % colors.Length]
// Automatically adapts when user switches theme
}
))
))
}
This approach ensures charts automatically adapt when users switch between light/dark themes or select different color schemesβno chart-specific code changes required.
Typography and Text Rendering
Charts would inherit typography from your Hugo theme, ensuring consistent text rendering across your site:
[<SolidComponent>]
let ChartWithLabels(data: unit -> ChartDatum[], title: string) =
svg() {
// Chart title inherits theme typography
text() {
x 0
y 20
class' "text-2xl font-bold fill-base-content" // Tailwind + DaisyUI classes
textContent title
}
// Axis labels
For'(each'=data(), children=(fun (d, i) ->
text() {
class' "text-sm fill-base-content/70"
textContent d.label
}
))
}
Alternatively, charts can reference CSS variables directly:
text() {
fill "hsl(var(--bc))" // Base content color from theme
fontSize "var(--chart-title-size)" // Custom chart token
fontFamily "inherit" // Inherit from theme
textContent title
}
Accessibility Patterns
The proposed charts would prioritize accessibility:
1. Semantic ARIA Roles:
[<SolidComponent>]
let AccessibleChart(data: unit -> ChartDatum[], title: string, description: string) =
svg() {
role "img"
ariaLabel title
ariaDescription description
For'(each'=data(), children=(fun (d, i) ->
rect() {
role "graphics-symbol"
ariaLabel $"{d.label}: {d.value}"
tabIndex 0 // Keyboard accessible
}
))
}
2. Keyboard Navigation:
[<SolidComponent>]
let KeyboardNavigableChart(data: unit -> ChartDatum[]) =
let focusedIndex, setFocusedIndex = createSignal(0)
createEffect(fun () ->
// Handle arrow keys for navigation
let handler = fun (e: KeyboardEvent) ->
match e.key with
| "ArrowRight" ->
setFocusedIndex(min (focusedIndex() + 1) (data().Length - 1))
e.preventDefault()
| "ArrowLeft" ->
setFocusedIndex(max (focusedIndex() - 1) 0)
e.preventDefault()
| _ -> ()
document.addEventListener("keydown", handler)
)
svg() {
For'(each'=data(), children=(fun (d, i) ->
rect() {
class' (if i = focusedIndex() then "focused" else "")
tabIndex (if i = focusedIndex() then 0 else -1)
}
))
}
3. High Contrast Mode Support:
/* Automatically adapts to Windows High Contrast mode */
@media (prefers-contrast: high) {
:root {
--chart-bar-fill: CanvasText;
--chart-bar-stroke: CanvasText;
--chart-grid-stroke: CanvasText;
}
}
Animation and Transitions
Charts would use your theme’s animation timing, or DaisyUI’s built-in animation tokens:
/* DaisyUI provides animation tokens */
:root {
--animation-btn: 0.25s;
--animation-input: 0.2s;
}
/* Or define chart-specific timing that matches your theme */
.bar {
transition:
height var(--animation-btn) ease-in-out,
fill 0.15s ease-in-out;
}
.bar:hover {
opacity: 0.8;
}
D3 Transitions with Theme Timing:
// Use D3 transitions with timing that matches your theme
let updateBars (data: ChartDatum[]) =
let bars = d3.selectAll(".bar").data(data)
bars.transition()
.duration(250) // Matches typical theme animations
.ease(d3.easeCubicInOut)
.attr("height", fun d -> yScale(d.value))
Tailwind utilities can also handle transitions:
rect() {
class' "transition-all duration-200 ease-in-out hover:opacity-80"
// ...
}
Responsive Behavior
Charts would adapt to container size:
[<SolidComponent>]
let ResponsiveChart(data: unit -> ChartDatum[]) =
let containerRef, setContainerRef = createSignal<HTMLElement option>(None)
let dimensions, setDimensions = createSignal({ width = 600.0; height = 400.0 })
createEffect(fun () ->
match containerRef() with
| Some container ->
let observer = ResizeObserver(fun entries ->
let rect = entries.[0].contentRect
setDimensions({ width = rect.width; height = rect.height })
)
observer.observe(container)
| None -> ()
)
div() {
ref setContainerRef
class' "chart-container"
style "width: 100%; height: 100%"
svg() {
width (dimensions().width)
height (dimensions().height)
viewBox $"0 0 {dimensions().width} {dimensions().height}"
// Chart content scales automatically
}
}
Theme Integration Pattern
Charts automatically inherit theming from your Hugo site via CSS custom properties. DaisyUI’s data-theme attribute handles theme switching at the document level:
<!-- Victor-generated HTML inherits theme from Hugo -->
<html data-theme="dark">
<body>
<article>
<!-- Static content -->
<div id="chart-mount"></div>
</article>
</body>
</html>
Charts automatically adapt to theme changes:
[<SolidComponent>]
let ThemedChart(data: unit -> ChartDatum[]) =
svg() {
rect() {
// These CSS variables update automatically when data-theme changes
fill "hsl(var(--p))"
stroke "hsl(var(--pf))"
}
}
If you need to override theme colors for specific charts, use Solid’s context:
module ChartOverrides =
type ChartColors = {
barColor: string option
lineColor: string option
}
let colorContext = createContext<ChartColors>()
[<SolidComponent>]
let CustomThemedChart(data: unit -> ChartDatum[]) =
let overrides = useContext(ChartOverrides.colorContext)
svg() {
rect() {
fill (overrides.barColor |> Option.defaultValue "hsl(var(--p))")
}
}
This keeps charts simple while allowing customization when needed.
Part V: Complete Implementation Example
Native Partas.Solid Line Chart
This example shows a complete, production-ready line chart component built as a native Partas.Solid component with D3 computation and FluentUI design alignment.
// Partas.Charts.D3/LineChart.fs
module Partas.Charts.D3.LineChart
open System
open Fable.Core
open Fable.Core.JsInterop
open Partas.Solid
open Browser.Types
// D3 bindings
[<ImportMember("d3-scale")>]
let scaleLinear() : obj = jsNative
[<ImportMember("d3-scale")>]
let scaleTime() : obj = jsNative
[<ImportMember("d3-shape")>]
let line() : obj = jsNative
[<ImportMember("d3-array")>]
let extent(data: 'T[], accessor: 'T -> 'U) : 'U[] = jsNative
[<ImportMember("d3-array")>]
let max(data: 'T[], accessor: 'T -> float) : float = jsNative
// Chart types
type LineChartDatum = {
x: obj // Date or number
y: float
}
type LineChartConfig = {
width: float
height: float
showPoints: bool
showGrid: bool
curveType: string
margin: {| top: float; right: float; bottom: float; left: float |}
}
// Native Partas.Solid line chart component
[<SolidComponent>]
let LineChart(
data: unit -> LineChartDatum[][],
config: LineChartConfig,
?onPointClick: LineChartDatum * int -> unit
) =
let chartWidth = config.width - config.margin.left - config.margin.right
let chartHeight = config.height - config.margin.top - config.margin.bottom
// Reactive D3 scales (recompute when data changes)
let xScale() =
let allPoints = data() |> Array.collect id
if allPoints.Length = 0 then jsNative
else
let firstX = allPoints.[0].x
let scale =
match firstX with
| :? DateTime -> scaleTime()
| _ -> scaleLinear()
let domain = extent(allPoints, fun d -> d.x)
scale?domain(domain) |> ignore
scale?range([| 0.0; chartWidth |]) |> ignore
scale
let yScale() =
let allPoints = data() |> Array.collect id
if allPoints.Length = 0 then jsNative
else
let scale = scaleLinear()
let maxY = max(allPoints, fun d -> d.y)
scale?domain([| 0.0; maxY |]) |> ignore
scale?range([| chartHeight; 0.0 |]) |> ignore
scale?nice() |> ignore
scale
let lineGenerator() =
let generator = line()
generator?x(fun (d: LineChartDatum) -> xScale() $ d.x) |> ignore
generator?y(fun (d: LineChartDatum) -> yScale() $ d.y) |> ignore
// Could add curve type here based on config
generator
svg() {
width config.width
height config.height
class' "line-chart"
role "img"
ariaLabel $"Line chart with {data().Length} series"
g() {
transform $"translate({config.margin.left},{config.margin.top})"
// Grid lines (if enabled)
if config.showGrid then
g() {
class' "grid"
// Grid implementation would go here
}
// Line paths for each series
For'(each'=data(), children=(fun (series, seriesIdx) ->
path() {
class' "line-series"
d (lineGenerator() $ series)
fill "none"
stroke $"var(--chart-line-stroke, #0078d4)"
strokeWidth 2
}
))
// Data points (if enabled)
if config.showPoints then
For'(each'=(data() |> Array.mapi (fun seriesIdx series ->
series |> Array.map (fun point -> (point, seriesIdx))
) |> Array.collect id), children=(fun ((point, seriesIdx), i) ->
circle() {
class' "data-point"
cx (xScale() $ point.x)
cy (yScale() $ point.y)
r 4
fill $"var(--chart-line-stroke, #0078d4)"
role "graphics-symbol"
ariaLabel $"{point.y}"
tabIndex 0
onClick (fun _ ->
onPointClick |> Option.iter (fun handler ->
handler (point, seriesIdx)
)
)
}
))
// Axes would be rendered here using D3 axis generators
// or custom SVG elements
}
}
// Usage example
[<SolidComponent>]
let TimeSeriesExample() =
let seriesData, setSeriesData = createSignal [|
[|
{ x = DateTime(2025, 1, 1) :> obj; y = 100.0 }
{ x = DateTime(2025, 2, 1) :> obj; y = 120.0 }
{ x = DateTime(2025, 3, 1) :> obj; y = 90.0 }
{ x = DateTime(2025, 4, 1) :> obj; y = 140.0 }
|]
[|
{ x = DateTime(2025, 1, 1) :> obj; y = 80.0 }
{ x = DateTime(2025, 2, 1) :> obj; y = 110.0 }
{ x = DateTime(2025, 3, 1) :> obj; y = 130.0 }
{ x = DateTime(2025, 4, 1) :> obj; y = 100.0 }
|]
|]
let selectedPoint, setSelectedPoint = createSignal<(LineChartDatum * int) option>(None)
let chartConfig = {
width = 800.0
height = 500.0
showPoints = true
showGrid = true
curveType = "smooth"
margin = {| top = 20.0; right = 20.0; bottom = 40.0; left = 50.0 |}
}
div() {
class' "p-4"
h2() {
class' "text-2xl font-bold mb-4"
text "Revenue Trends"
}
// Native Partas.Solid component
LineChart(
seriesData,
chartConfig,
onPointClick = (fun (point, seriesIdx) ->
setSelectedPoint(Some (point, seriesIdx))
)
)
// Selection display
Show'(
when'=(selectedPoint().IsSome),
children=(fun () ->
let point, series = selectedPoint().Value
div() {
class' "mt-4 p-2 bg-blue-100 rounded"
text $"Series {series}, Value: {point.y}"
}
)
)
// Controls
button() {
class' "mt-4 px-4 py-2 bg-blue-500 text-white rounded"
text "Add Random Month"
onClick (fun _ ->
let current = seriesData()
let nextMonth = DateTime(2025, current.[0].Length + 1, 1)
let newData = current |> Array.map (fun series ->
Array.append series [| {
x = nextMonth :> obj
y = Random().NextDouble() * 200.0
} |]
)
setSeriesData(newData)
)
}
}
Part VI: Summary and Implementation Roadmap
Foundational Patterns Established
1. D3 as Pure Computation
- D3’s scale, shape, and array modules are pure functions with no DOM dependencies
- Charts use D3 for mathematics (coordinate transformations, path generation, statistics)
- Rendering is handled by Solid.JS’s fine-grained reactivity
2. Native Partas.Solid Components
- Erased types for zero-cost abstractions
- Signal-driven reactivity for automatic recomputation
- Builder syntax provides natural F# DSL for component configuration
- Compiler visibility: Entire pipeline is F# AST visible to Fable
3. Following FluentUI Token Design
- CSS custom properties for themeable charts (framework-agnostic)
- Color palettes from established design research
- Accessibility patterns (ARIA roles, keyboard navigation)
- Responsive behavior using standard web APIs
- Subtle animations following modern motion design principles
Strategic Advantages
For F# Developers
- Functional-first approach: Pure functions, immutable data, explicit data flow
- Type safety: F# type system validates chart configurations at compile time
- Idiomatic syntax: Computation expressions and builder patterns feel natural
- Zero runtime overhead: Erased types compile to optimal JavaScript
- Compiler integration: Entire visualization pipeline visible to AST transformations
For SPEC Stack Optimization
- Fine-grained reactivity: Solid.JS updates only changed DOM attributes
- Compiler visibility: F# β Fable β Partas.Solid plugin can optimize entire flow
- Signal-based scales: D3 computations only when dependencies change
- Tree-shakeable: Import only needed D3 modules (d3-scale, d3-shape)
- No framework boundaries: Everything compiles through the same pipeline
For Maintainability
- Separation of concerns: D3 = math, Partas.Solid = reactivity, SVG = rendering
- Pure functions: D3 scales and generators are testable in isolation
- Modular design: Each chart type is independent F# module
- Design system alignment: FluentUI tokens ensure visual consistency
Filling the Ecosystem Gap
Current State:
- @fluentui/react-charting (npm): D3-powered charts for React only
- Feliz ecosystem (GitHub): React-style bindings, misaligned with functional patterns
- No F#-first charting: Existing solutions wrap JavaScript libraries
Proposed Contribution:
- Partas.Charts.D3: Native F# chart components following Fabulous patterns
- Functional D3 paradigm: Establish D3 as computation library, not DOM manipulator
- Professional design: Accessible charts with modern visual language
- SPEC-native: Optimized for the entire F# β Fable β Solid.JS pipeline
Implementation Roadmap
Phase 1: Core Foundation
- D3 F# bindings (d3-scale, d3-shape, d3-array, d3-interpolate)
- Victor CLI integration for theme token consumption
- Color palette utilities leveraging theme colors (sequential, categorical, diverging)
- Base chart utilities (margins, responsive sizing, accessibility helpers)
Phase 2: Essential Charts
- Bar Chart: Vertical, horizontal, grouped, stacked
- Line Chart: Single/multi-series, time-series, area variants
- Scatter Plot: With trend lines, zoom/pan
- Pie Chart: Pie, donut, with legends
Phase 3: Advanced Features
- Interactive tooltips styled with theme tokens
- Keyboard navigation support
- High contrast mode support
- Animation and transitions matching theme timing
- Export utilities (SVG, PNG)
Phase 4: Documentation & Ecosystem
- Interactive documentation site (built with Partas.Solid + Victor)
- Accessibility audit (WCAG 2.1 AA compliance)
- Performance benchmarks comparing to other charting libraries
- NuGet package publication
- Example dashboard applications showcasing Hugo theme integration
Integrating with Kobalte for Interactive UI
Kobalte provides headless, accessible UI primitives built natively for Solid.JS. These components integrate seamlessly with our chart architecture, maintaining compiler visibility throughout the stack.
Common UI Patterns with Charts:
open Kobalte
[<SolidComponent>]
let InteractiveChartDashboard() =
let chartData, setChartData = createSignal [| ... |]
let chartType, setChartType = createSignal "bar"
let selectedPoint, setSelectedPoint = createSignal None
div() {
class' "dashboard"
// Kobalte Select for chart type (native Solid.JS component)
Kobalte.Select.Root() {
value chartType
onChange setChartType
Kobalte.Select.Trigger() {
class' "chart-type-selector"
Kobalte.Select.Value() {
placeholder "Select Chart Type"
}
Kobalte.Select.Icon()
}
Kobalte.Select.Portal() {
Kobalte.Select.Content() {
Kobalte.Select.Listbox() {
Kobalte.Select.Item(value = "bar") {
Kobalte.Select.ItemLabel() { "Bar Chart" }
}
Kobalte.Select.Item(value = "line") {
Kobalte.Select.ItemLabel() { "Line Chart" }
}
}
}
}
}
// Native Partas.Solid chart
match chartType() with
| "bar" -> BarChart(chartData, onBarClick = fun point ->
setSelectedPoint(Some point))
| "line" -> LineChart(chartData, onPointClick = fun (point, _) ->
setSelectedPoint(Some point))
| _ -> div() {}
// Kobalte Popover for data point details
Show'(when' = selectedPoint().IsSome, children = fun () ->
let point = selectedPoint().Value
Kobalte.Popover.Root() {
Kobalte.Popover.Trigger() { "View Details" }
Kobalte.Popover.Portal() {
Kobalte.Popover.Content() {
class' "popover-content"
Kobalte.Popover.Title() { "Data Point" }
Kobalte.Popover.Description() {
$"Value: {point.y}"
}
}
}
}
)
}
Styled with Hugo Theme Tokens (DaisyUI example):
/* Kobalte components inherit theme styling */
.chart-type-selector {
background: hsl(var(--b1));
border: 1px solid hsl(var(--bc) / 0.2);
border-radius: var(--rounded-btn);
padding: 0.5rem 1rem;
font-size: 0.875rem;
}
.popover-content {
background: hsl(var(--b1));
border: 1px solid hsl(var(--bc) / 0.2);
box-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.1);
padding: 1rem;
}
/* Or use DaisyUI/Tailwind utility classes directly */
.chart-type-selector {
@apply bg-base-100 border border-base-300 rounded-btn px-4 py-2 text-sm;
}
Advantages:
- Full Compiler Visibility: Both charts and UI controls are native Solid.JS components
- Consistent Architecture: No framework boundaries between charts and UI
- Type Safety: F# types flow through the entire component tree
- Accessibility: Kobalte provides WCAG-compliant primitives out of the box
- Theme Coherence: CSS custom properties from your Hugo theme apply uniformly across all components
Conclusion: A Unified Architecture Proposal
This document has outlined a principled architecture for data visualization in the F# ecosystem that would combine:
- Architectural patterns from Fabulous (MVU, declarative composition, functional UI)
- Reactive foundation from Partas.Solid (signals, fine-grained updates)
- Computational core from D3 (pure functional transformations)
- UI primitives from Kobalte (accessible, headless components)
- Design system integration via Victor CLI (Hugo theme transformation)
- Architectural precedent from FluentUI.Charts (D3 as computation, not DOM manipulation)
The Proposed Stack
F# Source Code
ββ Partas.Solid (charts)
ββ Kobalte (UI controls)
ββ D3 (pure computation)
β
Fable Compiler
(full AST visibility)
β
Solid.JS Runtime
(fine-grained reactivity)
β
Rendered DOM
(styled with Hugo theme tokens via Victor)
Every layer would be compiler-visible F# code: this would mean no foreign components, no compilation boundaries, no framework impedance mismatches.
What Would Make This Architecture Compelling
- Unified Reactivity: Partas.Solid signals would drive both charts and UI controls through the same reactive graph
- Pure Functional Core: D3 would provide mathematical transformations without DOM coupling (insight from FluentUI.Charts)
- Native Integration: Kobalte components are Solid.JS natives, not foreign wrappers
- Design System Coherence: Your Hugo theme’s design tokens (DaisyUI, Tailwind, etc.) would style everything uniformly via CSS custom properties
- Compiler Optimization: F# β Fable would see the entire pipeline, enabling AST-level transformations
- Static Site Integration: Victor CLI would transform Hugo templates into Partas.Solid components, creating seamless static-to-dynamic experiences
For F# Developers
This proposal represents more than just a charting library. It demonstrates what becomes possible through principled functional design:
- No JavaScript interop complexity - Everything would compile from F# through one pipeline
- No virtual DOM overhead - Solid’s fine-grained reactivity would update only what changed
- No framework boundaries - Charts, UI controls, and application code would share the same reactive primitives
- No styling fragmentation - Your Hugo theme’s design system would govern the entire visual language
- No static vs. dynamic divide - Victor CLI would bridge Hugo templates and Partas.Solid components seamlessly
The result would be a charting library that feels native to F#, performs exceptionally with rich datasets, integrates seamlessly with the SPEC stack, and inherits professional, accessible styling from your existing Hugo themeβno additional design system required.
