Search code examples
javascriptruby-on-railsd3.jsruby-on-rails-7

Pass div id to javascript function to show multiple d3 charts on a page


I am trying to display multiple d3 charts on a page using Rails 7 using different div (or svg) ids:

view.html.erb

<%= content_tag "svg", class: 'chart', id: raw(chart_type)+'_'+raw(chart_num), data: {chart_type: chart_type, chart_num: chart_num} do %>
<script>
    line('#line_1');
</script>
<% end %>

javascript\component\charts\line.js

import * as d3 from "d3";

function line(chart_id) {

   const svg = d3.select('#line_1')
        .attr('width', 400)
        .attr('height', 400)
        .style('background-color', 'black');

    svg.append(chart_id)
        .style("stroke", "lightgreen")
        .style("stroke-width", 10)
        .attr("x1", 0)
        .attr("y1", 0)
        .attr("x2", 200)
        .attr("y2", 200);
}

importmap. pin_all_from "app/javascript/components", under: "components"

application.js import "components/charts/line"

I have tried the suggestions in this posts: How to get a Rails variable into JavaScript file (D3) mainly because the div/svg id is the data that needs to be passed. Also, I know how to do this with in-line javascript but I can't figure out how to pass the div id as a variable to the chart as a function.

Any help would be greatly appreciated.


Solution

  • I'm not exactly clear what you're asking here, but I see a couple of obvious issues. Because we're working with modules, line function is not global, it has to be exported/imported where needed or it has to be explicitly made global.

    import * as d3 from "d3";
    
    function line (chart_id) {
      const svg = d3.select(chart_id)
        .attr('width', 400)
        .attr('height', 400)
        .style('background-color', 'black');
    
      svg.append("line")
        .style("stroke", "lightgreen")
        .style("stroke-width", 10)
        .attr("x1", 0)
        .attr("y1", 0)
        .attr("x2", 200)
        .attr("y2", 200);
    }
    
    window.line = line;
    
    // or like this 
    //
    // window.line = function (chart_id) { 
    //   ...
    // }
    

    Another issue is plain script tags get loaded before module scripts, so line function would not be defined yet:

    <script type="module">
      line("#line_1");
    </script>
    

    You might want to consider making a Stimulus controller instead of making inline scripts and adding global functions:
    https://stimulus.hotwired.dev/

    $ bin/rails stimulus chart
    
    // 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 {
      // https://stimulus.hotwired.dev/reference/values
      static values = {
        type: String,
        num: Number,
      }
    
      connect() {
        // this.typeValue
        // this.numValue
    
        this.line();
      }
    
      line() {
        const svg = d3.select(this.element)
          .attr('width', 400)
          .attr('height', 400)
          .style('background-color', 'black');
    
        svg.append("line")
          .style("stroke", "lightgreen")
          .style("stroke-width", 10)
          .attr("x1", 0)
          .attr("y1", 0)
          .attr("x2", 200)
          .attr("y2", 200);
      }
    }
    
    # app/views/home/index.html.erb
    
    <%= tag.svg data: {
      controller: "chart",
      chart_type_value: "line",
      chart_num_value: 1
    } %>