1

I have D3 + HTML code which I want to Embed in Plotly Dash . Can someone help ?

For Example if this the code that populates a scatter plot using D3 , how should I callback using plotly dash ? I already have a dashboard built using plotly dash . So when apply button is clicked this javascript should be called and wll display the scatter graph.


<svg width="500" height="350"></svg>
<script src="https://d3js.org/d3.v4.min.js"></script>
<script>

var svg = d3.select("svg"),
    margin = {top: 20, right: 20, bottom: 30, left: 50},
    width = +svg.attr("width") - margin.left - margin.right,
    height = +svg.attr("height") - margin.top - margin.bottom;

function make_x_axis() {
    return d3.axisBottom(x)
        // .scale(x)
        //  .orient("bottom")
         .ticks(5)
}

function make_y_axis() {
    return d3.axisLeft(y)
        // .scale(y)
        // .orient("left")
        .ticks(5)
}

let points = d3.range(1, 10).map(function(i) {
    return [i * width / 10, 50 + Math.random() * (height - 100)];
});

var x = d3.scaleLinear()
    .rangeRound([0, width]);

var y = d3.scaleLinear()
    .rangeRound([height, 0]);

var xAxis = d3.axisBottom(x),
    yAxis = d3.axisLeft(y);

var line = d3.line()
    .x(function(d) { return x(d[0]); })
    .y(function(d) { return y(d[1]); });



let drag = d3.drag()
        .on('start', dragstarted)
        .on('drag', dragged)
        .on('end', dragended);

svg.append('rect')
    .attr('class', 'zoom')
    .attr('cursor', 'move')
    .attr('fill', 'none')
    .attr('pointer-events', 'all')
    .attr('width', width)
    .attr('height', height)
    .attr('transform', 'translate(' + margin.left + ',' + margin.top + ')')



// svg.append("g")
//         .attr("class", "grid")
//         .attr('transform', 'translate(' + margin.left + ',' + margin.top + ')')
//         .call(make_x_axis()
//             .tickSize(-height, 0, 0)
//             .tickFormat("")
//         )
//
// svg.append("g")
//     .attr("class", "grid")
//     .attr('transform', 'translate(' + margin.left + ',' + margin.top + ')')
//     // .attr("transform", "translate(0," + (height + margin.top) + ")")
//     .call(make_y_axis()
//     .tickSize(-width, 0, 0)
//     .tickFormat("")
//          )
svg.append("g")
      .attr("class", "grid")
      .attr("transform", `translate(${margin.left}, ${height + margin.top})`)
      .call(make_x_axis()
          .tickSize(-height)
          .tickFormat("")
      )

  // add the Y gridlines
  svg.append("g")
      .attr("class", "grid")
      .attr("transform", `translate(${margin.left}, ${margin.top})`)
      .call(make_y_axis()
          .tickSize(-width)
          .tickFormat("")
      )

 var focus = svg.append("g")
                .attr("transform", "translate(" + margin.left + "," + margin.top + ")");

x.domain(d3.extent(points, function(d) { return d[0]; }));
y.domain(d3.extent(points, function(d) { return d[1]; }));

focus.append("path")
    .datum(points)
    .attr("fill", "none")
    .attr("stroke", "white")
    .attr("stroke-linejoin", "round")
    .attr("stroke-linecap", "round")
    .attr("stroke-width", 1.5)
    .attr("d", line);

focus.selectAll('circle')
    .data(points)
    .enter()
    .append('circle')
    .attr('r', 5.0)
    .attr('cx', function(d) { return x(d[0]);  })
    .attr('cy', function(d) { return y(d[1]); })
    .style('cursor', 'pointer')
    .style('fill', 'steelblue');

focus.selectAll('circle')
        .call(drag);

focus.append('g')
    .attr('class', 'axis axis--x')
    .attr('transform', 'translate(0,' + height + ')')
    .call(xAxis);


focus.append('g')
    .attr('class', 'axis axis--y')
    .call(yAxis);


function dragstarted(d) {
    d3.select(this).raise().classed('active', true);
}

function dragged(d) {
    //d[0] = x.invert(d3.event.x);
    d[1] = y.invert(d3.event.y);
    d3.select(this)
        //.attr('cx', x(d[0]))
        .attr('cy', y(d[1]))
    focus.select('path').attr('d', line);
}

function dragended(d) {
    d3.select(this).classed('active', false);
}

</script>

Usage.py

import dash
#import dash_alternative_viz as dav
import dash_html_components as html
from dash.dependencies import Input, Output
#import random

external_scripts = [
    "https://d3js.org/d3.v4.min.js"
    
]

app = dash.Dash(external_scripts=external_scripts)
app.layout = html.Div(
        [html.Div(id="content", className="app-header"), 
         html.Button(id="button", children="Button")]
)

@app.callback(
    Output("content", "style"),
    [dash.dependencies.Input("button", "n_clicks")],
)
def update_output(n_clicks):
    if n_clicks:
        return {"display": "block"}
    return {"display": "no graph"}
    
if __name__ == "__main__":
   app.run_server( port = 8052, debug=True)

header.css

app-header {
  height: 350px;
  width: 50%;
  background-color: powderblue;
}

Thanks, Meera

Meera S
  • 57
  • 5

2 Answers2

1

For embedding Javascript in Dash apps see this. The easiest way to include Javascript (and CSS) is to make an assets folder in the root directory of your app.

To execute the code in your example in a Dash app you could put your Javascript code in a .js file in the assets folder.

So you could have a script.js file that looks something like this:

window.addEventListener("load", function () {
  const svg = d3
    .select("#content")
    .append("svg")
    .attr("width", "500")
    .attr("height", "350");

  const margin = { top: 20, right: 20, bottom: 30, left: 50 };
  const width = +svg.attr("width") - margin.left - margin.right;
  const height = +svg.attr("height") - margin.top - margin.bottom;

  function make_x_axis() {
    return (
      d3
        .axisBottom(x)
        // .scale(x)
        //  .orient("bottom")
        .ticks(5)
    );
  }

  function make_y_axis() {
    return (
      d3
        .axisLeft(y)
        // .scale(y)
        // .orient("left")
        .ticks(5)
    );
  }

  let points = d3.range(1, 10).map(function (i) {
    return [(i * width) / 10, 50 + Math.random() * (height - 100)];
  });

  var x = d3.scaleLinear().rangeRound([0, width]);

  var y = d3.scaleLinear().rangeRound([height, 0]);

  var xAxis = d3.axisBottom(x),
    yAxis = d3.axisLeft(y);

  var line = d3
    .line()
    .x(function (d) {
      return x(d[0]);
    })
    .y(function (d) {
      return y(d[1]);
    });

  let drag = d3
    .drag()
    .on("start", dragstarted)
    .on("drag", dragged)
    .on("end", dragended);

  svg
    .append("rect")
    .attr("class", "zoom")
    .attr("cursor", "move")
    .attr("fill", "none")
    .attr("pointer-events", "all")
    .attr("width", width)
    .attr("height", height)
    .attr("transform", "translate(" + margin.left + "," + margin.top + ")");

  svg
    .append("g")
    .attr("class", "grid")
    .attr("transform", `translate(${margin.left}, ${height + margin.top})`)
    .call(make_x_axis().tickSize(-height).tickFormat(""));

  // add the Y gridlines
  svg
    .append("g")
    .attr("class", "grid")
    .attr("transform", `translate(${margin.left}, ${margin.top})`)
    .call(make_y_axis().tickSize(-width).tickFormat(""));

  var focus = svg
    .append("g")
    .attr("transform", "translate(" + margin.left + "," + margin.top + ")");

  x.domain(
    d3.extent(points, function (d) {
      return d[0];
    })
  );
  y.domain(
    d3.extent(points, function (d) {
      return d[1];
    })
  );

  focus
    .append("path")
    .datum(points)
    .attr("fill", "none")
    .attr("stroke", "white")
    .attr("stroke-linejoin", "round")
    .attr("stroke-linecap", "round")
    .attr("stroke-width", 1.5)
    .attr("d", line);

  focus
    .selectAll("circle")
    .data(points)
    .enter()
    .append("circle")
    .attr("r", 5.0)
    .attr("cx", function (d) {
      return x(d[0]);
    })
    .attr("cy", function (d) {
      return y(d[1]);
    })
    .style("cursor", "pointer")
    .style("fill", "steelblue");

  focus.selectAll("circle").call(drag);

  focus
    .append("g")
    .attr("class", "axis axis--x")
    .attr("transform", "translate(0," + height + ")")
    .call(xAxis);

  focus.append("g").attr("class", "axis axis--y").call(yAxis);

  function dragstarted(d) {
    d3.select(this).raise().classed("active", true);
  }

  function dragged(d) {
    //d[0] = x.invert(d3.event.x);
    d[1] = y.invert(d3.event.y);
    d3.select(this)
      //.attr('cx', x(d[0]))
      .attr("cy", y(d[1]));
    focus.select("path").attr("d", line);
  }

  function dragended(d) {
    d3.select(this).classed("active", false);
  }
  
  document.querySelector("#content").style.display = "none" // Hide if button content if button has not yet been clicked.
});

I put your code in a load event listener to make sure the Javascript only executes if the page has loaded.

The other adjustment I made was to add a container div to the Dash layout (which I gave an id of content) and append an svg element to this container. You could also select the body or any other element rendered from the Dash layout and append the svg to that.

As far as I know there isn't really a Dash svg component you could directly put in your Dash layout.

So the idea is to let d3 create the svg element for us instead of Dash.

A minimal Dash layout that shows the graph looks something like this:

external_scripts = [
    "https://d3js.org/d3.v4.min.js",
]

app = dash.Dash(external_scripts=external_scripts)
app.layout = html.Div(
        [html.Div(id="content"), html.Button(id="button", children="Button")]
)

d3 is included using the external_scripts parameter of Dash.

For actually showing the chart on button click I can think of two approaches (varying slightly).

You handle a button click in Python using a Dash callback:

app = dash.Dash(external_scripts=external_scripts)
app.layout = html.Div(
    [
        html.Div(id="content"),
        html.Button(id="button", children="Button"),
    ]
)


@app.callback(
    Output("content", "style"),
    [dash.dependencies.Input("button", "n_clicks")],
)
def update_output(n_clicks):
    if n_clicks:
        return {"display": "block"}
    return {}

If the button hasn't been clicked at least once the graph isn't shown. If the button is clicked the graph is shown. You could change the behavior to toggle the visibility of the graph or hide the button after it is clicked once, adjust according to your needs.

The other approach is to add a click listener in your Javascript script.

Be sure to also add a CSS file in the assets folder and add a width and a height to the div if you follow above example, or nothing is going to show up.


Another more involved approach would be to create a React component for your graph and convert this to a Dash component that you could use in your dash layout. See my answer here and the official documentation here to get an idea. If you plan on using a lot of these Javascript libraries to interact with Dash I think this approach is preferable. It gives you more control and you can interact with these Javascript libraries in a more declarative way.

5eb
  • 14,798
  • 5
  • 21
  • 65
  • Hi @Bas van der Linden , thank you so much for the detailed explanation . I have tried as you instructed by adding an assets folder and copied the script.js and the css file . Still im not able to get the graph . Can you please help . I have edited the code that i have added to the main question. – Meera S May 06 '21 at 09:15
  • @MeeraS Can you try changing `external_scripts` to what I have in my answer? Also try emptying cache and hard reloading (ctrl + shift + r or something similar if you're on mac). It could be that the css is not applied to the div. – 5eb May 06 '21 at 09:25
  • No I did not change any id , i just added a classname for the CSS – Meera S May 06 '21 at 10:02
  • @MeeraS My bad I read that wrong. If you remove the `display: none` styles in the dash layout do you see the graph? I think because of the way "display: none" was added, the script couldn't find the element when it was running. I've edited the code in my answer. – 5eb May 06 '21 at 10:30
  • No luck , :( this time too..Im also exploring to make to run .... – Meera S May 06 '21 at 10:40
  • @MeeraS I'll still try to reproduce your problem at a later time. Thanks for accepting my answer anyway. By the way since you've passed 15 reputation you're also able to upvote answers that you find helpful in addition to accepting one. – 5eb May 06 '21 at 13:57
0

D3 cannot select some elements were defined in app.py by Dash,to be honestly,i know next to nothing about this issues.But you can select "body" by D3 in .js,and append some elements.

.js

var body = d3.select('body');
var svg = body.append('svg').attr('width',200).attr('height',200);
svg.append("circle").attr("r","150px").attr("fill","gray");
aniliii
  • 1
  • 1