Go back Hotwire / Stimulus / Turbo ...

D3 JS & Stimulus








// app/javascript/controllers/chart_controller.js
import {Controller} from "@hotwired/stimulus"
import * as d3 from "d3";

// Connects to data-controller="chart"
export default class extends Controller {
  static targets = [ "chart", "sale" ]
  connect() {
    this.sales = this.saleTargets
      .map(sale => sale.dataset)
      .map((value) => {
        return { date: new Date(value.date), name: value.name, amount: +value.amount }
      });

    console.log(this.sales);

    this.draw()
  }

  draw() {
    const margin = { top: 70, right: 30, bottom: 40, left: 80 };
    const width = 1200 - margin.left - margin.right;
    const height = 500 - margin.top - margin.bottom;

    // Set up the x and y scales

    const x = d3.scaleTime()
      .range([0, width]);

    const y = d3.scaleLinear()
      .range([height, 0]);

    // Create the SVG element and append it to the chart container

    const svg = d3.select(this.chartTarget)
      .append("svg")
      .attr("width", width + margin.left + margin.right)
      .attr("height", height + margin.top + margin.bottom)
      .append("g")
      .attr("transform", `translate(${margin.left},${margin.top})`);

    // Define the x and y domains

    x.domain(d3.extent(this.sales, d => d.date));
    y.domain([0, d3.max(this.sales, d => d.amount)]);

    // Add the x-axis

    svg.append("g")
      .attr("transform", `translate(0,${height})`)
      .call(d3.axisBottom(x)
        .ticks(d3.timeMonth.every(1))
        .tickFormat(d3.timeFormat("%b %Y")));

    // Add the y-axis

    svg.append("g")
      .call(d3.axisLeft(y))

    // Create the line generator

    const line = d3.line()
      .x(d => x(d.date))
      .y(d => y(d.amount));

    // Add the line to the chart

    svg.append("path")
      .datum(this.sales)
      .attr("fill", "none")
      .attr("stroke", "steelblue")
      .attr("stroke-width", 1.5)
      .attr("d", line);
  }
}

<!-- a/v/pages/index.html -->
<div data-controller="chart">
  <table class="w-full lg:w-2/3 border-collapse border border-slate-500 bg-red-50 table-fixed ">
    <caption class="text-4xl mb-2">Best sales</caption>
    <thead>
      <tr class="hidden sm:table-row">
        <th class="border border-gray-800 text-left px-2 py-1 bg-red-100 w-2/4">Date</th>
        <th class="border border-gray-800 text-left px-2 py-1 bg-red-100">Name</th>
        <th class="border border-gray-800 text-left px-2 py-1 bg-red-100">Amount</th>
      </tr>
    </thead>
    <tbody>
      <% @sales.each do |sale| %>
        <%= tag.tr nil, class: "block mb-8 sm:table-row", data: { chart_target: "sale", **sale.as_json } do %>
          <td class="block sm:table-cell sm:border border-gray-800 px-2 py-1 font-bold"><%= sale.date %></td>
          <td class="block sm:table-cell sm:border border-gray-800 px-2 py-1"><%= sale.name %></td>
          <td class="block sm:table-cell sm:border border-gray-800 px-2 py-1"><%= number_to_currency sale.amount, unit: "€" %></td>
        <% end %>
      <% end %>
    </tbody>
  </table>

  <div data-chart-target="chart"></div>
</div>

# a/c/pages_controller
class PagesController < ApplicationController
  class Sale
    attr_reader :date, :name, :amount

    def initialize(date:, name:, amount:)
      @date = date
      @name = name
      @amount = amount
    end

    def as_json
      {
        date: date,
        name: name,
        amount: amount
      }
    end
  end

  def index
    @sales = [
      Sale.new(date: Date.parse("2024-01-02"), name: "John", amount: 100),
      Sale.new(date: Date.parse("2024-02-02"), name: "Sally", amount: 2000),
      Sale.new(date: Date.parse("2024-03-02"), name: "Bill", amount: 300),
      Sale.new(date: Date.parse("2024-04-02"), name: "Jane", amount: 6000)
    ]
  end
end