How to use dash app in Angular application without using Iframes

Hello, I want to use dash application in Angular application, currently i am using it through iframes but its not a reliable way to do it as we are offering many customizations for charts and tables of Dash and everytime iframe needs to refresh.
We can also go through microfrontend route if thats possible as we are already using it for Angular.

1 Like

Hello @akash1,

With the addition of dash_clientside.set_props in Dash 2.16.1, it became possible to send information back into the dash ecosystem from a regular JS callback.

I havent messed with Angular/React combination, but if you can mix them it could be possible to use this as a bridge.

1 Like

I figured out one way around, we can keep iframes as it is but we can use postMessage method to send and receive data from Dash to angular and vice versa.

In below example I have added a button in angular which sends random data to dash using postmessage and dash receives that data using JS setup and stores it in dcc.store and uses it to re-render the graph.

Also I have added one button in dash layout, which sends back that random data to angular using clientside_callback()

Steps i followed are like this =>

1. Dash Setup
Data Store: Used a dcc.Store component to hold the data sent from the Angular app.
Graph Component: A dcc.Graph component to display the pie chart.
Interval Component: A dcc.Interval component to periodically check for updates from the global variable.
Clients-side Callback: A client-side callback to update the dcc.Store with data from the global variable.
Python Callback: A Python callback to update the graph based on the data in the dcc.Store.

import dash
from dash import Dash, html, dcc, Input, Output, State
import plotly.express as px
import pandas as pd
import json


app = Dash(__name__)

# Sample data for initial plotting

# Initial data frame
df = pd.DataFrame({
    "species": ["setosa", "versicolor", "virginica"],
    "count": [10, 15, 13,]
})

# Empty data frame
# df = pd.DataFrame(columns=["species", "count"])

# Create a pie chart
if df is not None:
    fig = px.pie(df, names='species', values='count')

# Define the layout of the app
app.layout = html.Div([
    html.Button('Send graph data to Angular', id='send-data-button', n_clicks=0, className='simple-button'),
    dcc.Graph(id='pie-chart', figure=fig),
    dcc.Interval(id='interval-component', interval=1*1000, n_intervals=0),
    dcc.Store(id='angular-data-store', data=''),
    html.Div(id='output'),
    dcc.Store(id='store-data')
])


# Send data from Dash to Angular
app.clientside_callback(
    """
    function(n_clicks, data) {
        if (n_clicks > 0) {
        if(data) {
            window.parent.postMessage(JSON.parse(data), '*');
        
            }
        }
        return window.dash_clientside.no_update;
    }
    """,
    Output('send-data-button', 'n_clicks'),
    Input('send-data-button', 'n_clicks'),
    State('angular-data-store', 'data')
)


#  Get data from Angular and store it to dcc store
app.clientside_callback(
    """
    function(n_intervals) {
        if (window.angularDataStore && window.angularDataStore !== '') {
            var data = window.angularDataStore;
            window.angularDataStore = '';  // Reset after reading
            return data;
        }
        return window.dash_clientside.no_update;
    }
    """,
    Output('angular-data-store', 'data'),
    Input('interval-component', 'n_intervals')
)

@app.callback(
    Output('pie-chart', 'figure'),
    Input('angular-data-store', 'data')
)
def update_figure(data):
    if not data or data == '':
        raise dash.exceptions.PreventUpdate

    angular_data = json.loads(data)
    # print(angular_data)
    df = pd.DataFrame(angular_data["graphData"])
    fig = px.pie(df, names='species', values='count', hole=angular_data["hole"] if "hole" in angular_data else 0)
    return fig


# Define callback to handle clicks
@app.callback(
    Output('store-data', 'data'),
    Input('pie-chart', 'clickData')
)
def display_click_data(clickData):
    if clickData:
        point = clickData['points'][0]
        species = point['label']
        count = point['value']
        return {'species': species, 'count': count}
    return {}

# Clientside callback to execute JavaScript in the browser
app.clientside_callback(
    """
    function(data) {
        if (data) {
            window.parent.postMessage(data, '*');
            console.log('Sent data to parent:', data);
        }
        return '';
    }
    """,
    Output('output', 'children'),
    Input('store-data', 'data')
)



if __name__ == '__main__':
    app.run_server(debug=True, host='0.0.0.0', port=8050)

2. JS setup in dash
Global Variable: Used a global JavaScript variable to store the data received via postMessage.
Event Listener: Listened for postMessage events and updated the global variable.
Periodic Check: The Dash app periodically checked the global variable and updated the dcc.Store.

window.angularDataStore = "";

window.addEventListener("message", function (event) {
  if (event.origin !== "http://localhost:4201") {
    return;
  }
  console.log("Received message from Angular:", event.data);
  if (event.data.type === "ANGULAR_TO_DASH") {
    window.angularDataStore = JSON.stringify({ graphData: event.data.data, hole: event.data.hole });
  }
});

3. Angular setup
PostMessage: Sent data from the Angular app to the Dash app using postMessage.

export class AppComponent implements OnInit {
  constructor(private renderer: Renderer2) {}

  counter = 0;
  dataFromDash: any;
  options: number[] = [0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1];
  clickedSection: any;

  ngOnInit(): void {
    this.renderer.listen('window', 'message', (event) => {
      if (event.origin === 'http://localhost:8050') {
        console.log('Message from Dash app:', event.data);
        this.dataFromDash = event.data;
      }
    });

    window.addEventListener('message', this.receiveMessage.bind(this), false);
  }

  sendMessageToDash(holeValue?: number) {
    this.counter++;
    const iframe = document.getElementById('dashIframe') as HTMLIFrameElement;
    const message: any = {
      data: [
        { species: 'setosa', count: Math.floor(Math.random() * 20) + 1 },
        { species: 'versicolor', count: Math.floor(Math.random() * 20) + 1 },
        { species: 'virginica', count: Math.floor(Math.random() * 20) + 1 },
      ],
      type: 'ANGULAR_TO_DASH',
      counter: this.counter,
    };
    if (holeValue) {
      message['hole'] = holeValue;
    }
    if (iframe && iframe.contentWindow) {
      iframe.contentWindow.postMessage(message, 'http://localhost:8050');
    }
  }

  onOptionSelected(event: Event): void {
    const selectElement = event.target as HTMLSelectElement;
    const selectedValue = selectElement.value;
    console.log('Selected option:', selectedValue);
    this.sendMessageToDash(
      selectedValue ? parseFloat(selectedValue) : undefined
    );
  }

  receiveMessage(event: MessageEvent) {
    // Check the origin of the message to ensure it's from the Dash app
    if (event.origin !== 'http://localhost:8050') {
      return;
    }
    const data = event.data;
    if (data?.species && data?.count) {
      this.clickedSection = data;
    }
  }
}


HTML =>
<iframe
      id="dashIframe"
      [frameBorder]="0"
      src="http://localhost:8050"
    ></iframe>

Also i think this solution can be used in any web framework, i tried it in angular.

1 Like

@jinnyzor do you think this solution can be productise ?

Hello @akash1,

This very much works, there are a couple of things that could help:

window.angularDataStore = "";

window.addEventListener("message", function (event) {
  if (event.origin !== "http://localhost:4201") {
    return;
  }
  console.log("Received message from Angular:", event.data);
  if (event.data.type === "ANGULAR_TO_DASH") {
    window.angularDataStore = JSON.stringify({ graphData: event.data.data, hole: event.data.hole });
  }
});

With dash 2.16+, you can just do this:

dash_clientside.set_props('angular-data-store', {data: JSON.stringify({ graphData: event.data.data, hole: event.data.hole})

This would remove this callback:

#  Get data from Angular and store it to dcc store
app.clientside_callback(
    """
    function(n_intervals) {
        if (window.angularDataStore && window.angularDataStore !== '') {
            var data = window.angularDataStore;
            window.angularDataStore = '';  // Reset after reading
            return data;
        }
        return window.dash_clientside.no_update;
    }
    """,
    Output('angular-data-store', 'data'),
    Input('interval-component', 'n_intervals')
)

Yes, this would work in most frameworks as this is supported by the browser.

Also, just an observation on your postMessages, you are sending them to different urls or all urls. This may or may not become an issue based upon your usage.

2 Likes