# How the D3.js Charting System Powers Financial Visualizations in Maybe Finance

> Discover how Maybe Finance uses D3.js charting for responsive, interactive financial visualizations built with Stimulus controllers on the client side.

- Repository: [Maybe/maybe](https://github.com/maybe-finance/maybe)
- Tags: internals
- Published: 2026-03-07

---

**Maybe Finance implements its D3.js charting system through dedicated Stimulus controllers that render responsive, interactive SVG visualizations entirely on the client side.**

The open-source personal finance application Maybe leverages a modular D3.js charting system to display time-series trends, cash flow diagrams, and budget breakdowns without server-side image generation. By encapsulating D3 logic within Stimulus controllers, the application maintains clean separation between Rails backend data and frontend rendering while delivering sophisticated financial visualizations that respond to user interactions.

## Architectural Overview of the D3.js Charting System

The charting architecture centers on three specialized Stimulus controllers, each handling a distinct visualization type. This modular approach allows the D3.js charting system to scale with the application's financial reporting needs while maintaining consistent interaction patterns across different chart types.

### Stimulus Integration Pattern

Each controller extends Stimulus's base `Controller` class and exposes `static values` for data injection from Rails views. When an element connects to the DOM, the controller initializes D3 selections via `d3.select(this.element)` and constructs SVG containers dynamically. Event listeners for `mousemove` and `mouseleave` attach directly to SVG elements or transparent overlays, enabling hover states without interfering with underlying page interactions.

### Data Flow from Rails to D3

The D3.js charting system receives normalized JSON data through `data-*` attributes. For time-series visualizations, Rails passes arrays of objects containing ISO date strings and numeric values. The Sankey controller expects node and link definitions with monetary values, while the donut chart receives segment arrays with color codes and amounts. This data contract ensures type safety while allowing D3's transformation pipelines to handle the heavy lifting of scale normalization and layout calculation.

## Time-Series Line Charts with Interactive Gradients

The [`time_series_chart_controller.js`](https://github.com/maybe-finance/maybe/blob/main/time_series_chart_controller.js) file implements the most complex visualization in the D3.js charting system, handling trend lines with dynamic gradient fills and sophisticated tooltip interactions.

### Data Normalization and Scale Configuration

The controller parses ISO date strings using `d3.timeParse("%Y-%m-%d")` and extracts numeric values through a private `_extractNumericValue` method. It constructs a **time scale** for the X-axis (`d3.scaleTime`) and a **linear scale** for the Y-axis (`d3.scaleLinear`). The Y-scale includes padding logic to prevent trend lines from touching container edges, maintaining visual breathing room even when data ranges are narrow.

### Split-Gradient Trend Lines and Tooltips

The visualization implements a split-gradient effect where the line fill changes at the cursor position. The controller creates a linear gradient definition in SVG `<defs>` and updates stop offsets via `_setTrendlineSplitAt` during mouse movement. A transparent overlay rectangle captures `mousemove` events, allowing the controller to bisect the data array and identify the nearest data point. The `_drawTooltip` method renders a floating HTML div containing the formatted date and value, while a vertical guideline and circle markers provide precise visual anchoring.

## Sankey Flow Diagrams for Cash Flow Visualization

The [`sankey_chart_controller.js`](https://github.com/maybe-finance/maybe/blob/main/sankey_chart_controller.js) file generates flow diagrams that visualize money movement between categories, implementing the D3.js charting system's approach to complex network layouts.

### Layout Generation and Per-Link Gradients

The controller initializes a `d3.sankey()` generator configured with `nodeWidth`, `nodePadding`, and extent boundaries (`[16, 16]` to `[width-16, height-16]`). It processes nodes and links through the Sankey layout algorithm, which calculates horizontal positioning and vertical spacing to minimize link crossings. Each link receives a unique linear gradient that transitions from the source node's color to the target node's color, created dynamically in SVG definitions and referenced via `url(#gradient-id)`.

### Node Styling and Value Tooltips

Nodes render as rounded rectangles with configurable corner radii. The controller assigns colors based on the data attributes passed from Rails, ensuring consistency with the application's Tailwind color system. Monetary values appear via native SVG `<title>` elements, providing browser-native tooltips that display formatted currency amounts when users hover over nodes or links.

## Donut Charts with Hover-Driven Content Swapping

The [`donut_chart_controller.js`](https://github.com/maybe-finance/maybe/blob/main/donut_chart_controller.js) file implements circular statistical displays with interactive segment highlighting, completing the D3.js charting system's coverage of common financial visualization patterns.

### Pie Layout and Segment Management

The controller computes a pie layout using `d3.pie()` with sorting disabled to preserve data order. It enforces a minimum angular size (`#minSegmentAngle`) to prevent tiny segments from becoming invisible, aggregating small values into "other" categories when necessary. The `d3.arc()` generator creates paths with configurable inner and outer radii, producing the characteristic donut shape with hollow centers suitable for overlaying summary text.

### Interactive Hover States and Template Injection

Hover interactions trigger content swapping in the chart's center area. When users mouse over a segment, the controller applies a `setTimeout` delay to prevent flickering, then fades non-hovered segments using the `segmentOpacityValue` parameter. Simultaneously, it locates a corresponding `<template>` element by ID and injects its content into the center container, displaying segment-specific details such as category names and amounts. Mouse-out events restore original colors and revert to the default center content.

## Responsive Design and Styling Architecture

The D3.js charting system implements comprehensive responsive behavior through modern browser APIs and CSS custom properties. All three controllers attach **ResizeObserver** instances to their container elements, triggering redraws when layout dimensions change without requiring page reloads. This ensures charts adapt to sidebar collapses, window resizing, or mobile orientation changes.

Styling relies entirely on CSS custom properties (e.g., `var(--color-gray-300)`) rather than hard-coded values, allowing the charts to inherit themes from Maybe's Tailwind configuration. The SVG elements respect utility classes for typography and color, ensuring visual consistency with the surrounding Rails application while maintaining the performance benefits of direct DOM manipulation through D3.

## Implementation Examples

### Time-Series Chart Implementation

```erb
<div data-controller="time-series-chart"
     data-time-series-chart-data-value="<%= @balance_series.to_json %>"
     data-time-series-chart-stroke-width-value="3"
     data-time-series-chart-use-labels-value="true"
     data-time-series-chart-use-tooltip-value="true">
</div>

```

The `@balance_series` array must contain objects with `date`, `date_formatted`, `value`, and `trend` properties. The controller renders a responsive line chart with gradient fills, X-axis labels, and interactive tooltips.

### Sankey Flow Diagram Implementation

```erb
<div data-controller="sankey-chart"
     data-sankey-chart-data-value="<%= @cash_flow.to_json %>"
     data-sankey-chart-node-width-value="20"
     data-sankey-chart-node-padding-value="15"
     data-sankey-chart-currency-symbol-value="$">
</div>

```

The `@cash_flow` object requires `nodes` (with `id`, `name`, and `color`) and `links` (with `source`, `target`, `value`, and `percentage`). This produces a flow diagram showing money movement between financial categories.

### Donut Chart Implementation

```erb
<div data-controller="donut-chart"
     data-donut-chart-segments-value="<%= @budget_segments.to_json %>"
     data-donut-chart-segment-height-value="4"
     data-donut-chart-segment-opacity-value="0.8">
  <div data-donut-chart-target="chartContainer"></div>

  <div data-donut-chart-target="defaultContent">
    <p>Total Budget: <%= @total_budget %></p>
  </div>

  <% @budget_segments.each do |segment| %>
    <template id="segment_<%= segment[:id] %>">
      <p><%= segment[:name] %>: <%= number_to_currency(segment[:amount]) %></p>
    </template>
  <% end %>
</div>

```

The `@budget_segments` array should contain `id`, `amount`, `color`, and `name` properties. Hovering segments triggers template swapping in the center area while fading non-active slices.

## Summary

- Maybe Finance implements a **modular D3.js charting system** through three specialized Stimulus controllers: [`time_series_chart_controller.js`](https://github.com/maybe-finance/maybe/blob/main/time_series_chart_controller.js), [`sankey_chart_controller.js`](https://github.com/maybe-finance/maybe/blob/main/sankey_chart_controller.js), and [`donut_chart_controller.js`](https://github.com/maybe-finance/maybe/blob/main/donut_chart_controller.js).
- The architecture leverages **Stimulus values** to pass JSON data from Rails views directly into D3 visualization logic, maintaining type safety and clean separation of concerns.
- **Interactive features** include split-gradient trend lines, per-link color gradients in Sankey diagrams, and hover-driven content swapping in donut charts.
- **Responsive behavior** is achieved through ResizeObserver implementations that redraw SVGs when container dimensions change, ensuring charts adapt to mobile and desktop layouts.
- **Styling integration** relies on CSS custom properties and Tailwind utility classes rather than hard-coded values, allowing the D3.js charting system to inherit application themes seamlessly.

## Frequently Asked Questions

### How does Maybe Finance handle responsive chart resizing without page reloads?

Each Stimulus controller in the D3.js charting system attaches a **ResizeObserver** to its container element. When the browser detects dimension changes—such as sidebar collapses, window resizing, or orientation shifts—the observer triggers a redraw method that recalculates scales, extents, and SVG paths using the new dimensions. This approach eliminates the need for page reloads while maintaining crisp rendering across device sizes.

### What data format does the time-series chart controller expect from Rails?

The [`time_series_chart_controller.js`](https://github.com/maybe-finance/maybe/blob/main/time_series_chart_controller.js) expects an array of objects containing `date` (ISO string), `date_formatted` (display string), `value` (numeric), and optionally `trend` properties. The controller uses `d3.timeParse("%Y-%m-%d")` to parse dates and `_extractNumericValue` to normalize monetary values. This JSON structure is passed via the `data-time-series-chart-data-value` attribute in the Rails ERB template.

### How are color gradients implemented in the Sankey flow diagrams?

The [`sankey_chart_controller.js`](https://github.com/maybe-finance/maybe/blob/main/sankey_chart_controller.js) generates unique **linear gradients** for each link in the SVG `<defs>` section. These gradients transition from the source node's color to the target node's color using `defs.append("linearGradient")` with calculated stop offsets. Each link element references its specific gradient via `url(#gradient-id)`, creating the visual effect of money flowing from one category to another with smooth color interpolation.

### Can the donut chart display custom content when hovering over segments?

Yes, the [`donut_chart_controller.js`](https://github.com/maybe-finance/maybe/blob/main/donut_chart_controller.js) implements **template-based content swapping** for hover interactions. The controller looks for `<template>` elements with IDs matching segment identifiers (e.g., `segment_food`). When a user hovers over a segment, the controller clones the template content and injects it into a designated center container, replacing the default content. This allows dynamic display of segment-specific details such as category names, amounts, and percentages without JavaScript string concatenation.