2

I want to display a chart created using Python Plotly in my front-end (written in JavaScript). The chart is created in my back-end (running on Python Flask) and has the following structure:

#document                                // chartDoc
<div>                                    // outerDiv
   <div></div>                           // innerDiv
   <script> innerScriptText </script>    // innerScript
</div>

I send it as a string inside a JSON file: {"chart": my_chart_str}.

The problem: I receive the chart in my JS, I create a new <script> element, I fill it with the code to display the chart (otherwise the browser doesn't execute the script, only prints it as plaintext) and I get the following error:

Uncaught TypeError: Cannot read property 'setProperty' of undefined
    at Element.<anonymous> (plotly-latest.min.js:20)
    at plotly-latest.min.js:20
    at ut (plotly-latest.min.js:20)
    at Array.Y.each (plotly-latest.min.js:20)
    at Array.Y.style (plotly-latest.min.js:20)
    at lt (plotly-latest.min.js:61)
    at Object.r.plot (plotly-latest.min.js:61)
    at Object.r.newPlot (plotly-latest.min.js:61)
    at <anonymous>:1:210
    at code.js:38

which comes from the plotly.js library and is caused by a div component that evaluates this.style as undefined.

But if I take the received chart code and manually paste it inside an .html file, the chart is displayed fine.

Basically what I'm trying to do is automating the procedure described in this answer.

This is the minimal code to reproduce my error:

index.html

<html>
  <head>
    <script src="https://cdn.plot.ly/plotly-latest.min.js"></script>
    <script src="./code.js"></script>
  </head>
  <body>
    <div id="graph-container">

    </div>
  </body>
</html>

code.js

window.onload = function () {
  displayChart();
}

function displayChart() {
  fetch("http://127.0.0.1:5000/chart", {
    headers: { 'Accept': 'application/json', 'Content-Type': 'application/json' },
    method: "GET"
  })
    .then(response => response.json())
    .then(response => {
      let chartString = response.chart;
      let chartDoc = new DOMParser().parseFromString(chartString, "text/xml"); // #document

      // get elements from received graph
      let outerDiv = chartDoc.firstElementChild;
      let innerDiv = outerDiv.firstElementChild.cloneNode();
      let innerScriptText = outerDiv.lastElementChild.innerHTML;

      // recreate same structure with new component and old content
      let newOuterDiv = document.createElement("div");

      let newInnerScript = document.createElement("script");
      newInnerScript.setAttribute("type", "text/javascript");

      let newInnerScriptText = document.createTextNode(innerScriptText);

      newInnerScript.appendChild(newInnerScriptText);

      newOuterDiv.appendChild(innerDiv);
      newOuterDiv.appendChild(newInnerScript);

      // insert graph in the page
      document.getElementById("graph-container").appendChild(newOuterDiv);
    });
}

server.py

from flask import Flask
from flask_restful import Api, Resource
from flask_cors import CORS

app = Flask(__name__)
api = Api(app)
CORS(app)

class Chart(Resource):
    def get(self):
        my_chart_str = str(get_chart())
        return {"chart": my_chart_str}

def get_chart():
    # taken from dash "getting started" guide
    import plotly.graph_objs as go
    from plotly.offline import plot

    x  = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
    y1 = [9, 6, 2, 1, 5, 4, 6, 8, 1, 3]
    y2 = [19, 36, 12, 1, 35, 4, 6, 8, 1, 3]
    trace1 = go.Bar(x=x,
                    y=y1,
                    name='Boats')
    trace2 = go.Bar(x=x,
                    y=y2,
                    name='Cars')

    data = [trace1, trace2]
    layout = go.Layout(title='Title',
                    xaxis=dict(title='X axis',
                                tickfont=dict(size=14,
                                                color='rgb(107, 107, 107)'),
                                tickangle=-45),
                    yaxis=dict(title='Y axis',
                                titlefont=dict(size=16,
                                                color='rgb(107, 107, 107)'),
                                tickfont=dict(size=14,
                                                color='rgb(107, 107, 107)')),)

    fig = go.Figure(data=data, layout=layout)
    return plot(fig,
        include_plotlyjs=False,
        output_type='div')

api.add_resource(Chart, "/chart")

if __name__ == "__main__":
    app.run(debug=True, host="127.0.0.1", port=5000)

Start the server (I'm on Windows) with python server.py, open index.html in your browser (double click on it, not via localhost), open the developer console and you should see the error.

Any idea on how to solve it?

mcalabresi
  • 37
  • 6
  • 1
    Just a question. Why you do not use dash directly for this ? Asking to understand the actual use case – kakou Jun 18 '21 at 15:32
  • I don't have control over the technologies used in this project. Furthermore, it has to be managed by front end developers in JS and pyhton data scientists in the backend. – mcalabresi Jun 18 '21 at 15:36
  • I see. Imo this is not correct as approach then. Just use a FE library to draw charts like charts.js or high-charts or w/e and your BE should only send the data to fill the graphs. – kakou Jun 18 '21 at 15:38
  • I get your point, but graphs are a fixed size result of potentially millions of data-points, in which case sending the graph is more convenient than sending data-points. – mcalabresi Jun 18 '21 at 15:53

1 Answers1

0

I finally found a workaround.

Instead of sending the chart as a JSON string, I do the following:

  1. From index.html a JQuery function call the Server.
  2. The Server creates the chart (in the form of a div) and saves it in a file named chart.html.
  3. The Server returns to JQuery the chart.html file.
  4. JQuery receives the file and insert it into index.html (this answer helped me figurig out how to embed an html file inside another).

My working code:

index.html

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
    
    <script src="https://cdn.plot.ly/plotly-latest.min.js"></script>
    <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
    <script src="./index.js"></script>
  </head>
  <body>
      <p>CHART WILL BE SHOWN BELOW</p>
      <div id="chart-container"></div>
  </body>
</html>

index.js

(function () {
    window.onload = function () {
        getChart();
    }

    function getChart() {
        $("#chart-container").load("get_chart"); // chart will be inserted in "chart-container"
    }
})();

server.py

from flask import Flask, render_template
from flask_cors import CORS

app = Flask(__name__, static_url_path="", static_folder='', template_folder='')
CORS(app)

def create_chart():
    # sample graph
    import plotly.express as px

    fig = px.imshow([[1, 20, 30],
                    [20, 1, 60],
                    [30, 60, 1]])
    fig.write_html("./chart.html", include_plotlyjs=False, full_html=False)

@app.route("/")
def index():
    return render_template("/index.html") # return "index.html" without chart

@app.route("/get_chart")
def get_chart():
    create_chart() # create "chart.html" file
    return render_template("/chart.html") # return chart to jquery

if __name__ == "__main__":
    app.run(debug=True, host="127.0.0.1", port=5000)

To run it, first start the server (windows: python server.py) then open the browser and navigate to 127.0.0.1:5000.

mcalabresi
  • 37
  • 6