{"id":31423,"date":"2025-06-17T15:24:24","date_gmt":"2025-06-17T22:24:24","guid":{"rendered":"https:\/\/digilent.com\/blog\/?p=31423"},"modified":"2025-06-17T15:24:24","modified_gmt":"2025-06-17T22:24:24","slug":"dash-python-create-interactive-web-applications-without-javascript-or-html","status":"publish","type":"post","link":"https:\/\/digilent.com\/blog\/dash-python-create-interactive-web-applications-without-javascript-or-html\/","title":{"rendered":"Dash &#038; Python: Create Interactive Web Applications Without JavaScript or HTML"},"content":{"rendered":"<p>Consider Dash if you&#8217;ve ever wanted to create a web browser application using Python. With Dash, you don\u2019t need to be good at using JavaScript, great at Cascading Style Sheets (CSS), and excellent at the Hypertext Markup Language (HTML). An application is structured like HTML, but with Python. Interaction with the outside world is handled with callback functions. In the truest sense, the application is stateless, meaning no data is passed between functions or maintained locally. Each time it runs, it has no memory of previous runs.\u00a0 Stateless applications tend to be simpler in design, have faster execution, and use fewer resources.<\/p>\n<p>In this article, I will discuss how a stateless web application can be used as a front end to a program, how callbacks work, and what triggers them. To understand all this,\u00a0 I&#8217;ve simplified the web server Python example included with the <a href=\"https:\/\/digilent.com\/shop\/raspberry-pi\/\">MCC Daqhat<\/a> data acquisition boards.\u00a0 I&#8217;ve converted the example to show simulated data as sine waves instead of vibration data from an MCC 172 or sensor data from an MCC 128.<\/p>\n<p><img loading=\"lazy\" decoding=\"async\" src=\"https:\/\/digilent.com\/blog\/wp-content\/uploads\/2025\/05\/dash_interface-600x456.png\" alt=\"\" width=\"600\" height=\"456\" class=\"alignnone size-medium wp-image-31424\" srcset=\"https:\/\/digilent.com\/blog\/wp-content\/uploads\/2025\/05\/dash_interface-600x456.png 600w, https:\/\/digilent.com\/blog\/wp-content\/uploads\/2025\/05\/dash_interface-1024x779.png 1024w, https:\/\/digilent.com\/blog\/wp-content\/uploads\/2025\/05\/dash_interface-290x220.png 290w, https:\/\/digilent.com\/blog\/wp-content\/uploads\/2025\/05\/dash_interface.png 1061w\" sizes=\"auto, (max-width: 600px) 100vw, 600px\" \/><\/p>\n<p>The program consists of two textboxes, a list of checkboxes, two buttons, and a single graph to display the data. There is also a hidden message field to display an error message if a mistake is made, like not checking at least one waveform box. The two text boxes are for Sample Rate (Hz) and Number of Samples. The Number of Samples divided by the Sample Rate (Hz) sets the duration of the X-axis, in seconds. These two parameters are also used to calculate the tick interval of a timer. The timer, when running, triggers certain callbacks to update the graph and give the appearance of a continuous sine wave.<\/p>\n<p>The program begins by launching an HTML web page. Instead of using local variables and lists, it\u00a0<span style=\"margin: 0px; padding: 0px;\">stores data using HTML elements. For instance, the\u00a0<em>chartData<\/em>\u00a0container stores the data in the graph. It is initialized using the default\u00a0<em>Number of Samples<\/em>\u00a0and<em>\u00a0Sample Rate<\/em> to have 1000 null values and<\/span>\u00a0an X-axis time of one second.<\/p>\n<div class=\"hcb_wrap\">\n<pre class=\"prism line-numbers lang-plain\" data-lang=\"Plain Text\"><code>html.Div(\r\n    id='chartData',\r\n    style={'display': 'none'},\r\n    children=<strong>init_chart_data(1, 1000, 1000)<\/strong>\r\n), <\/code><\/pre>\n<\/div>\n<p>Like a state machine, this program uses four states for control: <em>idle, configured, running, and error, which<\/em> are stored in the <em>status<\/em> element.<\/p>\n<div class=\"hcb_wrap\">\n<pre class=\"prism line-numbers lang-plain\" data-lang=\"Plain Text\"><code>html.Div(\r\n    id='<strong>status<\/strong>',\r\n    style={'display': 'none'} \r\n),<\/code><\/pre>\n<\/div>\n<p>Pressing the <em>Configuration<\/em> button updates the <em>status<\/em> element, which triggers any callback using <em>status<\/em> as an <em>Input<\/em>() in the callback function signature. One of those callbacks is called <em>disable_channel_checkboxes. <\/em>It cycles through all the checkboxes to\u00a0<span style=\"margin: 0px; padding: 0px;\">disable and grey them out. The\u00a0<em>Output()<\/em> in the function signature is its return variable.\u00a0<\/span>It returns the <em>channelSelections<\/em> list, which updates the display.<\/p>\n<div class=\"hcb_wrap\">\n<pre class=\"prism line-numbers lang-plain\" data-lang=\"Plain Text\"><code>\r\n@app.callback(\r\n    Output('channelSelections', 'options'),\r\n    [Input('<strong>status<\/strong>', 'children')]\r\n)\r\ndef disable_channel_checkboxes(acq_state):\r\n\r\n    num_of_checkboxes = 8\r\n    frequencies = [1.0, 2.0, 8.0, 16.0, 32.0, 64.0, 128.0, 256.0]  # Hz\r\n    options = []\r\n    for channel in range(num_of_checkboxes):\r\n        label = str(frequencies[channel]) +' Hz sine wave ' \r\n        disabled = False\r\n        if acq_state == 'configured' or acq_state == 'running':\r\n            disabled = True\r\n        options.append({'label': label, 'value': channel, 'disabled': disabled})\r\n    return options<\/code><\/pre>\n<\/div>\n<p>The\u00a0 <em>disable_channel_checkboxes<\/em> callback inputs <em>status<\/em> and is triggered by a <em>status<\/em> change. <em>State()<\/em> can be used to avoid triggering unnecessary callbacks.\u00a0 In the function<em> start_stop_click<\/em>, <em>Input()<\/em> is used only for the two buttons. The rest of the function parameters use<em> State()<\/em> to avoid triggering unnecessary callbacks. For more on callback, please refer to:\u00a0<a href=\"https:\/\/dash.plotly.com\/basic-callbacks\">https:\/\/dash.plotly.com\/basic-callbacks<\/a><\/p>\n<div class=\"hcb_wrap\">\n<pre class=\"prism line-numbers lang-plain\" data-lang=\"Plain Text\"><code>\r\n@app.callback(\r\n    Output('status', 'children'),\r\n    [Input('startButton', 'n_clicks'),\r\n     Input('stopButton', 'n_clicks')],\r\n    [State('startButton', 'children'),\r\n     State('channelSelections', 'value'),\r\n     State('sampleRate', 'value'),\r\n     State('numberOfSamples', 'value')]\r\n)\r\ndef start_stop_click(btn1, btn2, btn1_label, active_channels,\r\n                     sample_rate, number_of_samples):\r\n\r\n    button_clicked = ctx.triggered_id\r\n    output = 'idle'\r\n    if btn1 is not None and btn1 &gt; 0:\r\n        if button_clicked == 'startButton' and btn1_label == 'Configure':\r\n            if number_of_samples is not None and sample_rate is not None and active_channels:\r\n                if (999 &lt; number_of_samples &lt;= 10000) and (999 &lt; sample_rate &lt;= 10000): \r\n                     output = 'configured' else: output = 'error' else: output = 'error' \r\n                elif button_clicked == 'startButton' and btn1_label == 'Start': \r\n                    output = 'running' if btn2 is not None and btn2 &gt; 0:\r\n        if button_clicked == 'stopButton':\r\n            output = 'idle'\r\n\r\n    return output\r\n<\/code><\/pre>\n<p>&nbsp;<\/p>\n<p>The timer element is the heartbeat. For instance, the <em>update_strip_chart_data<\/em> callback inputs it to refresh the <em>chartData<\/em> element continuously on each tick of the timer.\u00a0 When<em> idle<\/em>, an interval of 1 day is used to disable the timer. When <em>running<\/em>,\u00a0 <em>start_stop_click<\/em> artificially restricts<span style=\"margin: 0px; padding: 0px;\">\u00a0<em>Number of Samples<\/em>\u00a0and<em>\u00a0Sample Rate<\/em> to a range between 1000 and 10000, or a timer interval between 0.1 and 10.0 seconds<\/span>. When<em>\u00a0update_timer_interval<\/em> detects\u00a0<em>running<\/em> as the status, it recalculates the timer interval based on the <em>Number of Samples<\/em> and <em>Sample Rate<\/em>.<\/p>\n<div class=\"hcb_wrap\">\n<pre class=\"prism line-numbers lang-plain\" data-lang=\"Plain Text\"><code>dcc.Interval(\r\n      id='timer',\r\n      interval=1000 * 60 * 60 * 24, \r\n      n_intervals=0\r\n),<\/code><\/pre>\n<p>&nbsp;<\/p>\n<p>When data is available, the <em>update_strip_chart_data<\/em> callback loads the new data into <em>chartData<\/em>, not the graph. However, changing <em>chartData<\/em> triggers the <em>update_strip_chart<\/em> callback, which transfers the <em>chartData<\/em> data into the graph you see. Because the timer is essentially turned off at start, <em>update_strip_chart_data<\/em> must input the <em>status<\/em> so that it can be called when the user presses the Start button, which changes the <em>status<\/em>. From there, the timer takes over.<\/p>\n<div class=\"hcb_wrap\">\n<pre class=\"prism line-numbers lang-plain\" data-lang=\"Plain Text\"><code>@app.callback(\r\n    Output('chartData', 'children'),\r\n    [Input('timer', 'n_intervals'),\r\n     Input('status', 'children')],\r\n    [State('chartData', 'children'),\r\n     State('numberOfSamples', 'value'),\r\n     State('sampleRate', 'value'),\r\n     State('channelSelections', 'value'),\r\n     State('sumChannels', 'value')],\r\n    #prevent_initial_call=True\r\n)\r\ndef update_strip_chart_data(_n_intervals, acq_state, chart_data_json_str,\r\n                            number_of_samples, sample_rate, active_channels, make_complex):\r\n\r\n    updated_chart_data = chart_data_json_str\r\n    samples_to_display = int(number_of_samples)\r\n    num_channels = len(active_channels)\r\n\r\n    if acq_state == 'running':\r\n        chart_data = json.loads(chart_data_json_str)\r\n\r\n        frequencies = [1.0, 2.0, 8.0, 16.0, 32.0, 64.0, 128.0, 256.0]  # Hz\r\n\r\n        duration = float(number_of_samples \/ sample_rate)\r\n        start_time = duration * _n_intervals\r\n\r\n        t = np.linspace(start_time, start_time + duration, int(sample_rate * duration), endpoint=False)\r\n\r\n        sine_waves = np.array([np.sin(2 * np.pi * f * t) for f in frequencies])\r\n\r\n        data = sine_waves[active_channels]if len(make_complex) &gt; 1:\r\n            temp = sum(data)\r\n            num_channels = 1\r\n            sample_count = add_samples_to_data(samples_to_display, num_channels,\r\n                                               chart_data, temp.flatten(), sample_rate)\r\n        else:\r\n\r\n            sample_count = add_samples_to_data(samples_to_display, num_channels,\r\n                                               chart_data, data.T.flatten(), sample_rate)\r\n\r\n        # Update the total sample count.\r\n        chart_data['sample_count'] = sample_count\r\n\r\n        updated_chart_data = json.dumps(chart_data)\r\n\r\n    elif acq_state == 'configured':\r\n        # Clear the data in the strip chart when Ready is clicked.\r\n        updated_chart_data = init_chart_data(num_channels, int(number_of_samples), sample_rate)\r\n\r\n    return updated_chart_data\r\n\r\n\r\n<\/code><\/pre>\n<\/div>\n<p>When the <em>status<\/em> equals &#8216;<em>running&#8217;<\/em>, <em>update_strip_chart_data<\/em> and <em>update_strip_chart<\/em> are called one after the other..<\/p>\n<div class=\"hcb_wrap\">\n<div class=\"hcb_wrap\">\n<pre class=\"prism line-numbers lang-plain\" data-lang=\"Plain Text\"><code>\r\n@app.callback(\r\n    Output('stripChart', 'figure'),\r\n    [Input('chartData', 'children')],\r\n    [State('channelSelections', 'value')]\r\n)\r\ndef update_strip_chart(chart_data_json_str, active_channels):\r\n\r\n    data = []\r\n    xaxis_range = [0, 1000]\r\n    chart_data = json.loads(chart_data_json_str)\r\n    if 'samples' in chart_data and chart_data['samples']:\r\n        xaxis_range = [min(chart_data['samples']), max(chart_data['samples'])]\r\n\r\n    if 'data' in chart_data:\r\n        data = chart_data['data']\r\n\r\n    plot_data = []\r\n    colors = ['#DD3222', '#FFC000', '#3482CB', '#FF6A00',\r\n              '#75B54A', '#808080', '#6E1911', '#806000']\r\n    # Update the serie data for each active channel.\r\n    for chan_idx, channel in enumerate(active_channels):\r\n        scatter_serie = go.Scatter(\r\n            x=list(chart_data['samples']),\r\n            y=list(data[chan_idx]),\r\n            name='Waveform {0:d}'.format(channel),\r\n            marker={'color': colors[channel]}\r\n        )\r\n        plot_data.append(scatter_serie)\r\n\r\n    figure = {\r\n        'data': plot_data,\r\n        'layout': go.Layout(\r\n            xaxis=dict(title='Time (s)', range=xaxis_range),\r\n            yaxis=dict(title='Voltage (V)'),\r\n            margin={'l': 40, 'r': 40, 't': 50, 'b': 40, 'pad': 0},\r\n            showlegend=True,\r\n            title='Strip Chart'\r\n        )\r\n    }\r\n\r\n    return figure\r\n<\/code><\/pre>\n<p>&nbsp;<\/p>\n<p>This example is available for download:<a href=\"https:\/\/github.com\/JRYS\/DashServer\">https:\/\/github.com\/JRYS\/DashServer<\/a><\/p>\n<p>To run the example, you must have Dash installed. Fortunately, installing it could not be easier. In a terminal, enter the following:<\/p>\n<p><em>pip install dash<\/em><\/p>\n<p>This also adds the Plotly graph objects library.<\/p>\n<p>The example, when run in Jupyter Notebook, should start automatically. However, in a Python IDE, you must use the provided IP address (127.0.0.1:8050) entered into your browser&#8217;s address bar to start it. Most of the time, you can click the IP shown in the debug window to launch it.<\/p>\n<\/div>\n<\/div>\n<\/div>\n<\/div>\n<div class='watch-action'><div class='watch-position align-left'><div class='action-like'><a class='lbg-style6 like-31423 jlk' data-task='like' data-post_id='31423' data-nonce='d8c4d58d14' rel='nofollow'><img src='https:\/\/digilent.com\/blog\/wp-content\/plugins\/wti-like-post-pro\/images\/pixel.gif' title='Like' \/><span class='lc-31423 lc'>0<\/span><\/a><\/div><div class='action-unlike'><a class='unlbg-style6 unlike-31423 jlk' data-task='unlike' data-post_id='31423' data-nonce='d8c4d58d14' rel='nofollow'><img src='https:\/\/digilent.com\/blog\/wp-content\/plugins\/wti-like-post-pro\/images\/pixel.gif' title='Unlike' \/><span class='unlc-31423 unlc'>0<\/span><\/a><\/div><\/div> <div class='status-31423 status align-left'>Be the 1st to vote.<\/div><\/div><div class='wti-clear'><\/div>","protected":false},"excerpt":{"rendered":"<p>Consider Dash if you&#8217;ve ever wanted to create a web browser application using Python. With Dash, you don\u2019t need to be good at using JavaScript, great at Cascading Style Sheets &hellip; <\/p>\n","protected":false},"author":60,"featured_media":31424,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"_monsterinsights_skip_tracking":false,"_monsterinsights_sitenote_active":false,"_monsterinsights_sitenote_note":"","_monsterinsights_sitenote_category":0,"_jetpack_memberships_contains_paid_content":false,"footnotes":""},"categories":[4323,1561],"tags":[5173,5171,5176,5170,4364,5172,4419,4358,5177,5106,4626,5132,5174,5175],"ppma_author":[4461],"class_list":["post-31423","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-software","category-applications","tag-callback","tag-css","tag-daqhats","tag-dash","tag-data-acquisition","tag-html","tag-jupyter-notebook","tag-mcc","tag-mcc-128","tag-mcc-172","tag-python","tag-state-machine","tag-timer","tag-web-server"],"jetpack_featured_media_url":"https:\/\/digilent.com\/blog\/wp-content\/uploads\/2025\/05\/dash_interface.png","jetpack_sharing_enabled":true,"authors":[{"term_id":4461,"user_id":60,"is_guest":0,"slug":"jrys","display_name":"John Rys","avatar_url":"https:\/\/secure.gravatar.com\/avatar\/b5f589ceec35aa72d93bdb885888fe28157d060d3a9bd5484c6388d3f10fc51d?s=96&d=mm&r=g","1":"","2":"","3":"","4":"","5":"","6":"","7":"","8":"","9":"","10":""}],"post_mailing_queue_ids":[],"_links":{"self":[{"href":"https:\/\/digilent.com\/blog\/wp-json\/wp\/v2\/posts\/31423","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/digilent.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/digilent.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/digilent.com\/blog\/wp-json\/wp\/v2\/users\/60"}],"replies":[{"embeddable":true,"href":"https:\/\/digilent.com\/blog\/wp-json\/wp\/v2\/comments?post=31423"}],"version-history":[{"count":57,"href":"https:\/\/digilent.com\/blog\/wp-json\/wp\/v2\/posts\/31423\/revisions"}],"predecessor-version":[{"id":31660,"href":"https:\/\/digilent.com\/blog\/wp-json\/wp\/v2\/posts\/31423\/revisions\/31660"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/digilent.com\/blog\/wp-json\/wp\/v2\/media\/31424"}],"wp:attachment":[{"href":"https:\/\/digilent.com\/blog\/wp-json\/wp\/v2\/media?parent=31423"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/digilent.com\/blog\/wp-json\/wp\/v2\/categories?post=31423"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/digilent.com\/blog\/wp-json\/wp\/v2\/tags?post=31423"},{"taxonomy":"author","embeddable":true,"href":"https:\/\/digilent.com\/blog\/wp-json\/wp\/v2\/ppma_author?post=31423"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}