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

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 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 file generates flow diagrams that visualize money movement between categories, implementing the D3.js charting system's approach to complex network layouts.

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 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

<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

<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

<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, sankey_chart_controller.js, and 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 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 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 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.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →