Python Notes: FFT web via CGI

Edit: 2026-08-29

A simple FFT demo for python3 (based upon numpy)

introduction:
  1. my demo starts with this web page: fft_demo.html
  2. that page loads two JavaScript files via these lines:
    <script src="/js/fft_demo.js"></script>
    <script src="https://cdn.plot.ly/plotly-3.7.0.min.js"></script>          
    1) the first file contains my JS glue which used to send HTML messages to python, and receive the response
    2) the second file contains the plotly JS used to render graphics within HTML
  3. My JS file tells Apache to invoke a Linux file: /var/www/html/cgi-bin/fft_demo
  4. fft_demo is just a python loader (invokes a JIT compiler to ensure the latest version of fft_demo_101.py is compiled to a pyc file)

1) file: fft_demo.html (the original HTML file)

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta http-equiv="content-type" content="text/html; charset=UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>FFT Demo (via Python)</title>
    <link href="css/nsr-20170909.css" rel="stylesheet" type="text/css">
    <script src="/js/fft_demo.js"></script>
    <script src="https://cdn.plot.ly/plotly-3.7.0.min.js"></script>
    <!--
    <script src="https://d3js.org/d3.v5.js"></script>
    <script src="https://mpld3.github.io/js/mpld3.v0.5.10.js"></script>
    -->
    <style>
      body {font-family: monospace; font-weight:bold;border:1px solid gray;padding:4px;}
      .r {color:red}
    </style>
  </head>
  <body>
    <h4>FFT Web Demo (Python via CGI)</h4>
    <form id="form1">
      <div style="width:400px;text-align:right">
        Frequency&nbsp;&nbsp;&nbsp;Amplitude&nbsp;&nbsp;<br>
        Wave 1: <input id="w1" type="checkbox">
        <input id="f1" size="12" maxlength="12" type="text">
        <input id="a1" size="12" maxlength="12" type="text"><br>
        Wave 2: <input id="w2" type="checkbox">
        <input id="f2" size="12" maxlength="12" type="text">
        <input id="a2" size="12" maxlength="12" type="text"><br>
        Wave 3: <input id="w3" type="checkbox">
        <input id="f3" size="12" maxlength="12" type="text">
        <input id="a3" size="12" maxlength="12" type="text"><br>
        <button type="button" id="but1" onclick="setDefaults1();">Defaults 1</button>
        <button type="button" id="but2" onclick="setDefaults2();">Defaults 2</button>
        <button type="button" id="but3" onclick="setDefaults3();">Defaults 3</button>
        <button type="button" id="but4" onclick="runDemo();">Run Demo</button>
        <button type="button" id="but5" onclick="doReset();">Reset</button>
      </div>
      <hr>interface message:<br>
      <div id="msg" style="width:396px;height:40px;padding:2px;border:1px solid gray;display:inline-block;">&nbsp;</div>
    </form>
    <div id="pix_buffer1"></div>
    <hr>
    <div style="font-family:Calibri, Arial, sans-serif;font-weight:normal">
      <p>Neil Rieck<br>
        Waterloo, Ontario, Canada.<br>
        <a href="https://neilrieck.net" target="_blank">https://neilrieck.net</a></p>
      <p><span class="r">Caveat:</span> This demo occasionally breaks at this hobbyist site. If you notice any problems
        then please send me an email here: <a href="MAILTO:n.rieck@bell.net">n.rieck@bell.net</a></p>
      <hr>
      <div style="font-weight:700">Additional information for inquiring minds</div>
      <ul>
        <li>Overview
          <ul>
            <li>The FFT (Fast Fourier Transform) is an algorithm for converting signals from the time-domain
              (think oscilloscope) to the frequency-domain</li>
            <li>The <a href="https://en.wikipedia.org/wiki/Fourier_series" target="_blank">Fourier series</a> was first
              published by <a href="https://en.wikipedia.org/wiki/Joseph_Fourier"
                target="_blank">Joseph Fourier</a> in 1822 (original work purportedly done in Egypt while serving under
              Napoleon)<br>
              Note: Daniel Bernoulli and Leonhard Euler worked on this branch of mathematics before Fourier, but did not
              publish (or properly publish)</li>
            <li>The <a href="https://en.wikipedia.org/wiki/Fast_Fourier_transform" target="_blank">Fast Fourier Transform</a>
              was first published in 1965 by  J. W. Cooley and John Tukey while working at the research division of IBM<br>
              Note: While working on a theory for planetary orbits, Carl Gauss proposed a solution very close to the 1965
              FFT publication.</li>
          </ul>
        </li>
        <li>Source Code:
          <ul>
            <li><a href="https://neilrieck.net/docs/python_notes.html" target="_blank">python: notes</a> - for people new
              to python programming</li>
          </ul>
        </li>
      </ul>
    </div>
  </body>
</html>

2) file: fft_demo.js (my JavaScript glue)

// ===========================================================================
// title  : fft_demo.js
// author : Neil Rieck
// created: 2025-05-12
// edit   : 2025-08-29
// notes  :
// 1) this SPA (single page application) empoys AJAX (Asynchronous Javascript And XML)
// 2) this demo employs plain JavaScript in order to make things clear to you
// 3) a real world application would employ jQuery with either ReactJS or AngularJS
// 4) this demo employs HTTP GET. A real world application would use HTTP POST
// ===========================================================================
var state=0;                                // ajax state
var timeOutId1=0;                           // timer ID
var debugFlag=1;                            // enable to develop
var f1="";                                  // frequency
var f2="";                                  //
var f3="";                                  //
var a1="";                                  // amplitude
var a2="";                                  //
var a3="";                                  //
var w1=false;                               // wave
var w2=false;                               //
var w3=false;                               //
var SECS=15;                                //
var MS=SECS*1000;                           //
var nsr_domain=window.location.hostname;    // server address
var nsr_protocol=window.location.protocol;  // http or https
//
//      called by the RESET button
//
function doReset(){
    state=0;
    if (timeOutId1!=0){
        clearTimeout(timeOutId1);
        timeOutId1=0;
    }
        for (i=1; i<=3; i++){
                document.getElementById("f"+i).value="";
                document.getElementById("a"+i).value="";
                document.getElementById("w"+i).checked=false;
        }
    clearPixBuffer();
    writeMsg("Reset Done");
}
function clearPixBuffer(){
        try{
                document.getElementById("pix_buffer1").innerHTML = "";
        }catch(e){
                console.log("clearPixBuffer-e-",e);
        }
}
function writeMsg(x){
    console.log("writeMsg:",x);
        // document.getElementById("msg").value=x;      // if element is INPUT
        document.getElementById("msg").innerHTML=x;     // if element is DIV
        console.log("writeMsg: done");
}
//
//      called by the "SET DEFAULTS 1" button
//
function setDefaults1() {
    send_ajax("setDefaults1");
}
//
//      called by the "SET DEFAULTS 2" button
//
function setDefaults2() {
    send_ajax("setDefaults2");
}
//
//      called by the "SET DEFAULTS 3" button
//
function setDefaults3() {
    send_ajax("setDefaults3");
}
//
//      read form fields
//
function readFields(){
    f1 = document.getElementById("f1").value;
        f2 = document.getElementById("f2").value;
        f3 = document.getElementById("f3").value;
        a1 = document.getElementById("a1").value;
        a2 = document.getElementById("a2").value;
        a3 = document.getElementById("a3").value;
    w1 = document.getElementById("w1").checked;
    w2 = document.getElementById("w2").checked;
    w3 = document.getElementById("w3").checked;
}
//
//      called by the RUN DEMO button
//
function runDemo() {
        score=0;
    readFields();
        try{
                for (i=1; i<=3; i++){
                        if (w1===true) score++;
                        if (w2===true) score++;
                        if (w3===true) score++;
                }
        }catch{
                score=-1;
        }
    if (score<0){
                writeMsg("error, bad data detected");
                return;
        }
        if (score==0){
                writeMsg("error, no waves enabled:");
                return;
        }
        clearPixBuffer();
    send_ajax("runDemo");
}
//
//      send_ajax (send an HTTP message via AJAX)
//
function send_ajax(action){
    switch(state){
    case 0:
        writeMsg("Connecting");
        var msg = "";                               // init
        switch(action){
            case "runDemo":
                msg = (nsr_protocol +"//"+ nsr_domain+ "/cgi-bin/fft_demo?op="+action+
                    "&f1="+f1+"&f2="+f2+"&f3="+f3+
                    "&a1="+a1+"&a2="+a2+"&a3="+a3+
                    "&w1="+w1+"&w2="+w2+"&w3="+w3)
                console.log("msg:"+ msg);
                start_ajax(msg);
                break;
            case "setDefaults1":
            case "setDefaults2":
            case "setDefaults3":
                msg=nsr_protocol+"//"+nsr_domain+"/cgi-bin/fft_demo?op="+ action;
                start_ajax(msg);
                break;
            default:
                writeMsg("programmer error (001)");
                break;
        }
    default:
        writeMsg("Please wait up to "+SECS+" seconds (state:"+state+")");
        break;
    }
}
//
//      start_ajax (start an AJAX transaction)
//
function start_ajax(cmd){
    response=null;
    if (typeof XMLHttpRequest == "undefined")
                XMLHttpRequest=function(){
                        try { return new ActiveXObject("Msxml2.XMLHTTP.6.0")    } catch (e) {}
                        try { return new ActiveXObject("Msxml2.XMLHTTP.3.0")    } catch (e) {}
                        try { return new ActiveXObject("Msxml2.XMLHTTP")        } catch (e) {}
                        try { return new ActiveXObject("Microsoft.XMLHTTP")     } catch (e) {}
                        throw new Error("This browser does not support XMLHttpRequest or XMLHTTP.")
                }
        if (cmd != ""){
                //
                //      code to block AJAX caching
                //
                var dt          = new Date();               // today's local date time on YOUR machine
                var stamp       = ""+ dt.getTime();         // millisecs since 1970-01-01
                // console.log("time:",stamp);
                //
                //      original code continues
                //
                response=new XMLHttpRequest();
                if (response != null){
                        cmd += "&timestamp=" + stamp;       // to foil browser caching
                        console.log("cmd:",cmd);
                        response.onreadystatechange=ajax_event_handler;
                        response.open("GET", cmd, true);    // async=true
                        response.send(null);                // not null for POST
                }
                state = 1;
                init_timer1();
        }
}
//
//      ajax_event_handler (only executed if something is received)
//
function ajax_event_handler(){
    if (response.readyState == 4){
            state=0;
            if (timeOutId1!=0){
                clearTimeout(timeOutId1);
                timeOutId1=0;
        }
        if (response.status == 500) {
            writeMsg("Server Error 500")
        }
        if (response.status == 200) {
            writeMsg("Ready");
                var resp$=response.responseText;
                if (debugFlag==1) {
                        console.log("raw data:"+htmlEncode(resp$));
            }
                if (window.DOMParser) {
                        parser=new DOMParser();
                        gXmlDoc=parser.parseFromString(resp$,"text/xml");
                }else{
                        gXmlDoc=new ActiveXObject("Microsoft.XMLDOM");
                        gXmlDoc.async=false;
                        gXmlDoc.loadXML(resp$);
                }
                    var f1  = get_xml_data("f1");
                        var f2  = get_xml_data("f2");
                        var f3  = get_xml_data("f3");
                    var a1  = get_xml_data("a1");
                        var a2  = get_xml_data("a2");
                        var a3  = get_xml_data("a3");
                var w1  = get_xml_data("w1");
                        var w2  = get_xml_data("w2");
                        var w3  = get_xml_data("w3");
                        var msg = get_xml_data("msg");
                        var gra = get_xml_data("gra");
                        var status= get_xml_data("status");
                        clearPixBuffer();
                // var status = 1;
                        if ((status == 1)||(status == 2)) {
//                      document.getElementById("msg").innerHTML="raw data:"+htmlEncode(resp$);
                                if (f1 !=null) document.getElementById("f1").value=f1;
                                if (f2 !=null) document.getElementById("f2").value=f2;
                                if (f3 !=null) document.getElementById("f3").value=f3;
                                if (a1 !=null) document.getElementById("a1").value=a1;
                                if (a2 !=null) document.getElementById("a2").value=a2;
                                if (a3 !=null) document.getElementById("a3").value=a3;
                                if (w1 !=null) changeChecked("w1",w1);
                                if (w2 !=null) changeChecked("w2",w2);
                                if (w3 !=null) changeChecked("w3",w3);
                                if (msg!=null) writeMsg(msg);
                                if (gra!=null){
                    if (1==0){
                        console.log("========== gra is not null ==========");
                        console.log("len:", gra.length);
                        console.log("data:", gra);
                    }
                    try{
                        var tmp = gra;
                        console.log("outputting");
                        if (1==0){
                            // this injects properly, but no scripts are run (security restriction by all browsers)
                            document.getElementById("pix_buffer1").innerHTML = gra;
                        }else{
                            // this alternative works properly
                            // first we create an html object
                            myDiv = document.getElementById("pix_buffer1");
                            // now we inject the html string
                            myDiv.innerHTML = gra;
                            // now we will parse (and run) the scripts within
                            const scripts = myDiv.getElementsByTagName('script');
                            for (let script of scripts) {
                                const freshScript = document.createElement('script');
                                if (script.src) {
                                    freshScript.src = script.src;
                                } else {
                                    freshScript.textContent = script.textContent;
                                }
                            document.body.appendChild(freshScript);
                            }
                        }
                    }catch(e){
                        console.log("error during data extract:",e);
                    }
                }
            }else{
                writeMsg("error:"+status);
            }
        }
    }
}
function changeChecked(id, state){
        console.log("changeChecked1: ",id,state,typeof(state));
        if (typeof(state) == "string"){
                if (state.toLowerCase() == "true"){
                        state = true;
                }else{
                        state = false;
                }
        }
        console.log("changeChecked2: ",id,state,typeof(state));
        try{
                document.getElementById(id).checked=state;
                }
        catch{
                console.log("checked error: ",id,state);
        }
}
//
//      the server must answer back within 15 seconds
//
function init_timer1(){
    if (timeOutId1==0){                             // if available
        timeOutId1=setTimeout("timer_job1();",MS);  // arm timer
    }
}
//
//      this code is ONLY executed on TIMEOUT
//
function timer_job1(){
    state=0;                                        // reset state
    if (timeOutId1!=0){
        clearTimeout(timeOutId1);
        timeOutId1=0;
    }
    var txt="Error: the timer has expired after "+SECS+" secs";
    document.getElementById("msg").innerHTML=txt;
}
//
//      does string 'x' represent a positive integer?
//
function isStrInt(x){
    x=x.replace(/\s/g,'');
    if (x=="") return false;
    try{
        y = parseInt(x);
    }catch(e){
        y = -1;
    }
    if(y>0){
        return true;
    }else{
        return false;
    }
}
function htmlEncode(str) {
    return String(str).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
}
// new way
function htmlDecode(htmlString) {
    const doc = new DOMParser().parseFromString(htmlString, 'text/html');
    return doc.documentElement.textContent;
}
// old way (IE11, etc.)
function htmlDecode9(text) {
    var entities = [
        ['amp', '&'],
        ['apos', '\''],
        ['#x27', '\''],
        ['#x2F', '/'],
        ['#39', '\''],
        ['#47', '/'],
        ['lt', '<'],
        ['gt', '>'],
        ['nbsp', ' '],
        ['quot', '"']
    ];

    for (var i = 0, max = entities.length; i < max; ++i)
        text = text.replace(new RegExp('&' + entities[i][0] + ';', 'g'), entities[i][1]);

    return text;
}
//
//      get_xml_data (look for one item)
//
function get_xml_data(tag){
    var x;
    try{
        x = gXmlDoc.getElementsByTagName(tag)[0].childNodes[0].nodeValue;
    }catch(e){
        // console.log("t:"+tag+" e:"+e);
        x = null;
    }
    return(x);
}
//
//      get_xml_data2 (look for numerous similar items)
//
function get_xml_data2(xmlObj, tag){
    var x;
    try{
        x = xmlObj.getElementsByTagName(tag)[0].childNodes[0].nodeValue;
    }catch(e){
        // console.log("t:"+tag+" e:"+e);
        x = null;
    }
    return(x);
}
function convertToJson() {
    let form = document.getElementById("dataForm");
    let formData = {};
    for (let i = 0; i < form.elements.length; i++) {
        let element = form.elements[i];
        if (element.type !== "submit") {
            formData[element.name] = element.value;
        }
    }
    let jsonData = JSON.stringify(formData);
    let jsonOutput = document.getElementById("jsonOutput");
    jsonOutput.innerHTML = "<pre>" + jsonData + "</pre>";
}

3) file: fft_demo (just a server-side python loader)

#!/usr/bin/python3.9
# ---------------------------------------
# title  : /var/www/html/cgi-bin/fft_demo
# author : Neil Rieck
# created: 2025-05-12
# purpose: ensure use of auto-compiled python (.pyc)
# ver who when
# 100 NSR 2025-05-12
# 101 NSR 2026-08-28
# ---------------------------------------
import fft_demo_101  # invoke JIT auto-compile on this module
fft_demo_101.main()  # call main (only if it exists)
#

4) file: fft_demo_101.py (the server-side python program)

#!/usr/bin/python3.9
# ===================================================================
# Title   : fft_demo_main_101.py
# Author  : Neil Rieck
# created : 2025-05-12
# 1) a server-side program (not for interactive use)
# 2) v100 employs mpld3 to render matplotlib graphs in your browser
#    v101 employs pltly to render matplotlib graphs in your browser
# who when       what
# 100 NSR 2025-05-12 original effort using mpld3 (AlmaLinux-8)
# 101 NSR 2026-08-19 debug after moving to AlmaLinux-9
#                    replaced mpld3 with plotly
#                    replaced html.escape() with markupsafe.escape()
# ===================================================================
#
import math, sys, cgi, cgitb, os, datetime, markupsafe
import numpy as np
from numpy.fft import fft, ifft

#
#   constants
#
FNAME = "fft_demo_101"
PATH = "fft-log/"

#
#   globals (ugh!)
#
n_samples = 0   # number of samples (init)
ts = 0          # time slots
t = 0           # time data (an array, after processing)
signal = "init" # composite signal data (an array, after processing)
debug = 0       #
dvlp = 0        # 0=off 1=trace 2=data

#
#   generate one analog signal
#
def generateSignal(freq=1.0, ampl=1.0):
    global n_samples, ts, t
    if n_samples == 0:                          # if first time (if init)
        n_samples = 10000                       # set number of samples
        ts = 1.0/n_samples                      # sampling interval (time slots)
        # note:  1.0 / 1000 = 0.001

        # t = np.arange(0, 1, ts)                 # time
        t = np.linspace(0, (n_samples-1)*ts, n_samples)
    signal = ampl * np.sin(2*np.pi*freq*t)      # generate signal data (a numpy array)
    return signal                               #

#
#   text to int (called by processData)
#
def textToInt(data, label, wc):
    msg = ""
    try:
        tmp = int(data)
    except Exception:
        tmp = 0
    if (tmp > 999):
        msg = f"Skipped Wave-{wc} because {label} > 999"
    if (tmp < 1):
        msg = f"Skipped Wave-{wc}. Bad value for {label}"
    return tmp, msg

#
#   text to real (called by processData)
#
def textToFloat(data, label, wc):
    msg = ""
    try:
        tmp = float(data)
    except Exception:
        tmp = 0.0
    if (tmp > 99.9):
        msg = f"Skipped Wave-{wc} because {label} > 99.9"
    if tmp < 0.1:
        msg = f"Skipped Wave-{wc}. Bad value for {label}"
    return tmp, msg

#
#   process data (single data set)
#   note: this may be called numerous times (or once)
#
def processData(w, f, a, wc, sc, msg9):
    global sr, ts, t, signal
    wc += 1                                         # update 'wave count'
    if (w == "true"):                               # if checked
        freq, msgF = textToInt(f, "Freq", wc)
        if msgF != "":
            if msg9 != "":
                msg9 += "\n"
            msg9 += msgF
        ampl, msgA = textToFloat(a, "Ampl", wc)
        if msgA != "":
            if msg9 != "":
                msg9 += "\n"
            msg9 += msgA
        if (msgF == "") and (msgA == ""):           # if both are okay
            sc += 1                                 # update 'signal count'
            x = generateSignal(freq, ampl)          #
            if dvlp >= 2:
                logIt(f"{wc} {sc} {type(x)} {len(x)} {x}")
            if (type(signal) == str) and (signal == "init"):    # if first time
                logIt("copy")
                signal = x.copy()                               # copy data as-is
            else:                                               # else
                logIt("merge")
                signal += x.copy()                              # add the two data items
            if dvlp >= 2:
                logIt(f"9 9 {type(x)} {len(x)} {x}")
    return wc, sc, msg9

#
#   generate Graphical Data (only come here when we need to generate a graphic)
#
def generateGraphicalData():
    global signal, n_samples, t
    logIt("-i-generateGraphicalData()")
    #
    #   call matplotlib etc.
    #
    os.environ['MPLCONFIGDIR'] = '/tmp'
    if debug >= 1:
        logIt("trace-1")
    import matplotlib.pyplot as plt
    #
    # developer's recipe:
    #  1- 9: mpld3
    # 10-10: plotly
    #
    recipe = 16                                     # DEVELOPER CHOICE
    logIt(f"recipe: {recipe}")
    if recipe >= 10:                                # new way (python3.9 on AlmaLinux-9)
        import plotly.graph_objects as go
        import plotly.express as px
        import plotly.io as pio
    else:                                           # old way (python3.9 on AlmaLinux-8)
        import mpld3
    #
    #   use: plotly
    #   notes: this code replaces mpld3 (see below) which stopped working after a server move
    #
    if recipe == 10:                                # DVLP: really simple demo (Plotly Express)
        fig = px.line(x=[1, 2], y=[3, 4], title="Demo-10")
        html_str = fig.to_html(full_html=False, include_plotlyjs=False, div_id="pix_buffer9")
    '''
    if recipe == 11:                                # DVLP: a simple demo (Plotly Graph Objects)
        fig = go.Figure()
        fig.add_trace(go.Scatter(
            x=[1, 2, 3, 4, 5],
            y=[10, 15, 13, 17, 22],
            mode='lines+markers',
            name='Sales'
        ))
        fig.update_layout(
            title='CGI Generated Line Graph',
            xaxis_title='X Axis',
            yaxis_title='Y Axis'
        )
        html_str = pio.to_html(fig, full_html=False, include_plotlyjs=False, div_id="pix_buffer9")
    if recipe == 12:                                # DVLP (no FFT here)
        import pandas as pd
        # Generate x values from 0 to 4*pi (output to an array)
        x = np.linspace(0, 4 * np.pi, 500)          # <class 'numpy.ndarray'="">
        # Compute 4 different sine waves (output to an array)
        y1 = np.sin(x)                              # <class 'numpy.ndarray'="">
        y2 = np.sin(x + np.pi / 4)
        y3 = np.sin(2 * x)
        y4 = np.sin(2 * x + np.pi / 2)
        # Combine 4 wave into a pandas DataFrame
        df = pd.DataFrame({
            'x': x,
            'Sine Wave 1': y1,
            'Sine Wave 2 (Phase Shift)': y2,
            'Sine Wave 3 (2x Freq)': y3,
            'Sine Wave 4 (Shift & Freq)': y4
        })
        # Pass the DataFrame to Plotly Express for a line plot
        fig = px.line(df, x='x', y=['Sine Wave 1', 'Sine Wave 2 (Phase Shift)',
                      'Sine Wave 3 (2x Freq)', 'Sine Wave 4 (Shift & Freq)'],
                      title='Demo-12: 4 Different Sine Waves')
        html_str = pio.to_html(fig, full_html=False, include_plotlyjs=False, div_id="pix_buffer9")
    if recipe == 13:                                # DVLP (no FFT here)
        from plotly.subplots import make_subplots
        # Create a figure with 1 row and 2 columns
        fig = make_subplots(rows=1, cols=2, subplot_titles=("Plot 1", "Plot 2"))
        # Add a scatter plot to the first subplot
        fig.add_trace(go.Scatter(x=[1, 2, 3], y=[4, 5, 6], mode='lines+markers'), row=1, col=1)
        # Add a bar chart to the second subplot
        fig.add_trace(go.Bar(x=[1, 2, 3], y=[2, 3, 5]), row=1, col=2)
        # Update layout and show
        fig.update_layout(title_text="Side-by-Side Subplots Demo", showlegend=False)
        html_str = pio.to_html(fig, full_html=False, include_plotlyjs=False, div_id="pix_buffer9")
    if recipe == 14:                                # DVLP (no FFT here)
        from plotly.subplots import make_subplots
        from plotly import tools
        # import plotly.graph_objects as go
        #
        # Create a figure with 1 row and 2 columns
        fig = make_subplots(rows=1, cols=2, subplot_titles=("First Plot", "Second Plot"))
        # Add trace to the first subplot (row 1, col 1)
        fig.add_trace(go.Scatter(x=[1, 2, 3], y=[4, 5, 6], name="Group A"), row=1, col=1)
        # Add trace to the second subplot (row 1, col 2)
        fig.add_trace(go.Scatter(x=[20, 30, 40], y=[50, 60, 70], name="Group B"), row=1, col=2)
        # Update overall layout size and title
        fig.update_layout(height=500, width=800, title_text="Integrated Subplots Demo")
        html_str = pio.to_html(fig, full_html=False, include_plotlyjs=False, div_id="pix_buffer9")
        '''
    if recipe == 15:                                # DVLP (FFT based upon static data)
        from numpy import fft
        from plotly.subplots import make_subplots
        # 1. Generate a noisy composite signal
        n_samples = 1000
        dt = 0.001
        t = np.linspace(0, (n_samples - 1) * dt, n_samples)
        # Signal = 50 Hz sine wave + 120 Hz sine wave + random noise
        signal = (
            5 * np.sin(2 * np.pi * 50 * t)
            + 10 * np.sin(2 * np.pi * 120 * t)
            + np.random.normal(0, 2, n_samples)
        )

        # 2. Compute FFT
        fft_values = np.fft.fft(signal)
        fft_freq = np.fft.fftfreq(n_samples, dt)

        # Keep only the positive frequencies
        pos_mask = fft_freq >= 0
        frequencies = fft_freq[pos_mask]
        magnitudes = (2 / n_samples) * np.abs(fft_values[pos_mask])

        # 3. Create interactive subplots with Plotly
        fig = make_subplots(
            rows=2, cols=1, subplot_titles=("Time Domain Signal", "Frequency Domain (FFT)")
        )

        # Time domain trace
        fig.add_trace(
            go.Scatter(x=t, y=signal, mode="lines", name="Signal", line=dict(color="blue")),
            row=1,
            col=1,
        )

        # Frequency domain trace
        fig.add_trace(
            go.Scatter(
                x=frequencies,
                y=magnitudes,
                mode="lines",
                name="FFT Magnitude",
                line=dict(color="red"),
            ),
            row=2,
            col=1,
        )

        # Update layout
        fig.update_xaxes(title_text="Time (s)", row=1, col=1)
        fig.update_yaxes(title_text="Amplitude", row=1, col=1)
        fig.update_xaxes(title_text="Frequency (Hz)", row=2, col=1)
        fig.update_yaxes(title_text="Magnitude", row=2, col=1)
        fig.update_layout(
            height=600,
            title_text="Interactive FFT Demo with Plotly",
            showlegend=False,
        )
        html_str = pio.to_html(fig, full_html=False, include_plotlyjs=False, div_id="pix_buffer9")
    if recipe == 16:                                # PROD (works like 15 but with variable data)
        from numpy import fft
        from plotly.subplots import make_subplots
        # 1. Generate a noisy composite signal
        '''
        n_samples = 1000
        dt = 0.001              # note: 1.0 seconds / 1000 = 0.001
        t = np.linspace(0, (n_samples - 1) * dt, n_samples)
        # Signal = 50 Hz sine wave + 120 Hz sine wave + random noise
        signal = (
            5 * np.sin(2 * np.pi * 50 * t)
            + 10 * np.sin(2 * np.pi * 120 * t)
            + np.random.normal(0, 2, n_samples)
        )
        '''
        dt = 1                          # temp fix
        # 2. Compute FFT
        fft_values = np.fft.fft(signal)
        fft_freq = np.fft.fftfreq(n_samples, dt)

        # Keep only the positive frequencies
        pos_mask = fft_freq >= 0
        frequencies = fft_freq[pos_mask]
        magnitudes = (2 / n_samples) * np.abs(fft_values[pos_mask])

        # 3. Create interactive subplots with Plotly
        fig = make_subplots(
            rows=2, cols=1, subplot_titles=("Time Domain Signal", "Frequency Domain (FFT)")
        )

        # Time domain trace
        fig.add_trace(
            go.Scatter(x=t, y=signal, mode="lines", name="Signal", line=dict(color="blue")),
            row=1,
            col=1,
        )

        # Frequency domain trace
        fig.add_trace(
            go.Scatter(
                x=frequencies,
                y=magnitudes,
                mode="lines",
                name="FFT Magnitude",
                line=dict(color="red"),
            ),
            row=2,
            col=1,
        )

        # Update layout
        fig.update_xaxes(title_text="Time (s)", row=1, col=1)
        fig.update_yaxes(title_text="Amplitude", row=1, col=1)
        fig.update_xaxes(title_text="Frequency (Hz)", row=2, col=1)
        fig.update_yaxes(title_text="Magnitude", row=2, col=1)
        fig.update_layout(
            height=600,
            title_text="Interactive FFT Demo with Plotly",
            showlegend=False,
        )
        html_str = pio.to_html(fig, full_html=False, include_plotlyjs=False, div_id="pix_buffer9")

    #
    #   use: mpld3
    #   notes: these routines worked properly with mpdl3 (0.5.10) via python3.9 on AlmaLinux-8.
    #   After the move to AlmaLinux-9, I installed mpdl3 (0.5.12) which is now throwing errors
    #   with the lastest matplotlib (3.9.4). Rather than attempting to install older libraries,
    #   I abandonded this code (for now) then switched to plotly.
    #
    if recipe == 1:                                 # DVLP: really simple demo
        fig = plt.figure()
        obj = plt.plot([3,1,4,1,5])
        html_str = mpld3.fig_to_html(fig, figid='pix_buffer9', include_libraries=False)
    if recipe == 2:                                 # DVLP: two plots (one figure)
        x = np.array([0, 1, 2, 3])
        y = np.array([3, 8, 1, 10])
        fig, ax = plt.subplots(1, 2)                # create figure and axis objects
        ax[0].plot(x,y)
        ax[0].set_title('Plot 1')
        ax[1].plot(x,y,'r')
        ax[1].set_title('Plot 2')
        html_str = mpld3.fig_to_html(fig, figid='pix_buffer9', include_libraries=False)
    if recipe == 3:                                 # DVLP: simple
        fig = plt.figure()
        obj = plt.plot(t, x)
        html_str = mpld3.fig_to_html(fig, figid='pix_buffer9', include_libraries=False)
    if recipe == 4:                                 # DVLP: FFT prep
        x9 = np.array([0, 1, 2, 3])
        y9 = np.array([3, 8, 1, 10])
        fig, ax = plt.subplots(1, 2)                # create figure and axis objects
        ax[0].plot(t, x)
        ax[0].set_title('Signal (time domain)')
        ax[1].plot(x9, y9)
        ax[1].set_title('FFT (frequenecy domain)')
        html_str = mpld3.fig_to_html(fig, figid='pix_buffer9', include_libraries=False)
    return html_str                                 #

#
#   logIt (a simple logger)
#   note: SELinux needs to allow this
#
def logIt(log):
    stamp14 = datetime.datetime.now().strftime('%Y%m%d%H%M%S')
    try:
        stamp8 = stamp14[:8]
        fn=f"{PATH}{FNAME}_{stamp8}.txt"
        f = open(fn,"a")
        f.write(f"-i-event: {stamp14}\n"
                f"-i-txt: {log}\n=====\n")
        f.close()
    except Exception as e:
        if debug > 0:               # only interactive mode
            print(f"-e-error: {e}") #
    if debug > 0:                   #
        print(f"-i-txt: {log}")     #

#
#   the name says it all
#
def main():
    global debug
    #
    #   declare/init a few variables
    #
    w1 = f1 = a1 = ""   # wave, freq, amplitude
    w2 = f2 = a2 = ""
    w3 = f3 = a3 = ""
    wc = 0              # wave count
    sc = 0              # signal data
    #
    #   declare a few program variables
    #
    method = os.environ.get('REQUEST_METHOD', "BLANK")
    qs = os.environ.get('QUERY_STRING', "BLANK")
    #
    #   a little hook for interactive testing
    #
    if method == "BLANK":
        debug = 1
        print("-i-interactive debug mode")
        print(f"-i-QUERY_STRING: {qs} (from your shell)")
    else:
        debug = 0
    #
    #   extract CGI field data
    #
    fld  = cgi.FieldStorage(keep_blank_values=True)     # grab all CGI data
    a1 = fld.getvalue('a1', '')
    a2 = fld.getvalue('a2', '')
    a3 = fld.getvalue('a3', '')
    f1 = fld.getvalue('f1', '')
    f2 = fld.getvalue('f2', '')
    f3 = fld.getvalue('f3', '')
    w1 = fld.getvalue('w1', '')
    w2 = fld.getvalue('w2', '')
    w3 = fld.getvalue('w3', '')
    op = fld.getvalue('op', 'setDefaults1')
    msg = ""
    gra = ""
    #
    log = (f"qs: '{qs}'\n"
           f"w1: '{w1}' f1: '{f1}' a1: '{a1}'\n"
           f"w2: '{w2}' f2: '{f2}' a2: '{a2}'\n"
           f"w3: '{w3}' f3: '{f3}' a3: '{a3}'\n"
           f"m : {method}")
    logIt(log)
    #
    #   send a response back to Apache
    #   note: some popular return types include text/html, text/xml, text/plain
    #
    resp = ('Content-Type: text/xml; charset=windows-1252\n'    # we're returning an XML response
            '\n'                                                # this marks the end of the HTTP header
            '<?xml version="1.0" encoding="iso-8859-1"?>\n'     # start of XML content
            '\n')                                       # user-defined by me (see JavaScript)
    if (op=="setDefaults1"):                                    # user hit button-1
        resp += ('1\n'
                 '1009true\n'
                 '    false\n'
                 '    false\n'
                 'Program Defaults #1\n')
    elif op=="setDefaults2":                                    # user hit button-2
        resp += ('1\n'
                 '1009true\n'
                 '506true\n'
                 '   false\n'
                 'Program Defaults #2\n')
    elif op=="setDefaults3":                                    # user hit button-3
        resp += ('1\n'
                 '1009true\n'
                 '506true\n'
                 '253true\n'
                 'Program Defaults #3\n')
    elif op=="runDemo":                                         # user hit button-4
        try:
            wc, sc, msg = processData(w1, f1, a1, wc, sc, msg)
            wc, sc, msg = processData(w2, f2, a2, wc, sc, msg)
            wc, sc, msg = processData(w3, f3, a3, wc, sc, msg)
            if msg == "":
                msg += "\n"
                msg += f"\nSignals generated: {sc}"
            if (sc > 0):                                        # if we generated at least one wave
                status = 1
                gra = generateGraphicalData()
            else:
                status = 90
            log = (f"status: {status}\n"
                   f"msg: {msg}")
            logIt(log)
        except Exception as e:
            status = 95
            msg = f"{e}"
            logIt(msg)
        #
        #       since we're sending back XML it might be wise to escape the data
        #
        # logIt(gra)
        # logIt(gra2)
        gra2 = markupsafe.escape(gra)
        msg2 = markupsafe.escape(msg)
        resp += (''+ str(status) +'\n'
                 f'{msg2}\n'
                 f'{gra2}\n')
    else:                                               # this should never happen
        resp = ('1\n'
                'Program Defaults #1\n')
    resp += ('\n')
    # logIt(resp)
    print(resp)                                         # this goes to Apache
    sys.exit()                                          # no more work to do

#
#   boilerplate
#
if __name__ == '__main__':
    main()
#
# this is the last line
 

 Back to Home
 Neil Rieck
 Waterloo, Ontario, Canada.