JavaScript in Shiny for Python

I am trying to import a JavaScript file to apply custom modifications to my Shiny App but it appears that the JavaScript code is being run before the Shiny app is loaded. As an example, I have placed the following JavaScript code in a www folder:

document.addEventListener("DOMContentLoaded", function(){
    // Your JavaScript code to initialize sliders
    const allTableCells = document.querySelectorAll('td');  
    console.log(allTableCells);
    console.log('hello')});

My (Python) Shiny app is specified below:

from shiny import App, render, ui 
import numpy as np
import pandas as pd
from sklearn.datasets import load_iris

# Load the Iris dataset
iris_data = load_iris()

# Create a Pandas DataFrame from the data
iris_df = pd.DataFrame(data=iris_data.data, 
                       columns=iris_data.feature_names).iloc[1:10]

js_path =  "www/js_script.js"
  
app_ui = ui.page_fluid(
  
    # Benchmarking section 
    ui.div({"class": "table_section"}, 
  
        # add custom JavaScript 
        ui.output_data_frame(id='ex_table'), 
        ui.include_js(path=js_path))
    )

def server(input, output, session):

    @output
    @render.data_frame()
    def ex_table():

      return iris_df

app = App(app_ui, server)

Given that the data_frame element outputted from ex_table() has <td> elements, the JavaScript code should catch these elements and return them. When I inspect the console, however, the returned object of allTableCells is empty:


Note that I've added a log of 'hello' as a check that the JavaScript code is being run.

Not sure if I missed something, but I would ideally want the JavaScript code to apply modifications to specific <td> elements. Given that no <td> elements are being identified in the current setup, I can't proceed with any of my modifications.

Hi,

Have you tried using load instead of DOMContentLoaded?

Source:

I tried using

window.addEventListener("load", function(){

    // Your JavaScript code to initialize sliders
    const allTableCells = document.querySelectorAll('td');  
    console.log(allTableCells);
})

but nothing changed. Admittedly, JavaScript is not my strength, so let me know if I misunderstood your point.

That is what I meant. I got your reprex running, and didn't work for me either. :frowning: Sums up why I do not like working on the frontend! I got it working with this js_script.js:

// function will loop until it finds `td` elements, then it will 
// call the function that gets passed in as a callback
function Checker(callback) {
    let interval = setInterval(function() {
        if (document.querySelectorAll('td').length > 0) {
            clearInterval(interval);
            callback();
            return;
        }
    }, 100);
    return true;
}

function WhatWeWantToDo() {
    const allTableCells = document.querySelectorAll('td');  
    console.log(allTableCells);
    console.log('hello');
}

Checker(WhatWeWantToDo);

Ok perfect! This works, however, I am encountering issues trying to modify specific td elements. Basically, I want to insert sliders within specific table cells that have innerHTML equivalent to 'sliders'. Unfortunately, although the sliders do appear in the correct cells, many of them disappear and the 'sliders' text reappears when I click anywhere on the table.

I can submit this question as a new topic if that is better. Otherwise, here is my full reprex.
I built on the code you provided and have this in my js_script.js file:

function Checker(callback) {

    let interval = setInterval(function() {

        // check if at least one td element contains 'sliders'. If so, then run the function 
        // passed in for callback 
        const tdElements = document.querySelectorAll('td');
        const slidersFound = Array.from(tdElements).some(td => td.innerHTML === 'sliders');

        if (slidersFound) {
            clearInterval(interval);  // reset interval 
            callback();  // run function 
            return;
        }
    }, 100);
    return true;
}

// function will render sliders that are added after the page is processed 
function initializeSliders() {
    const sliders = document.querySelectorAll('.slider');
    
    sliders.forEach(slider => {
        // Initialize sliders (you may need to use a slider library)
        noUiSlider.create(slider, {
            start: [50],
            range: {
                'min': 0,
                'max': 100
            }
        });
    });
}
 

function WhatWeWantToDo() {
    const td_elements = document.querySelectorAll('td');
    
    td_elements.forEach(td => {
        if (td.innerHTML.trim() === 'sliders') {
            td.innerHTML = '<div class="slidecontainer">' +
                           '  <input type="range" min="0" max="100" value="50" class="slider" id="myRange">' +
                           '</div>';
        }
    });

    // Initialize sliders after modifying the HTML
    initializeSliders();
}

Checker(WhatWeWantToDo);

Within my Python script for the Shiny app, I have added a path to the nouislider library;

  
from shiny import App, render, ui 
import numpy as np
import pandas as pd
from sklearn.datasets import load_iris

# Load the Iris dataset
iris_data = load_iris()

# Create a Pandas DataFrame from the data
iris_df = pd.DataFrame(data=iris_data.data, 
                       columns=iris_data.feature_names).iloc[1:10]

iris_df['Sliders'] = 'sliders'

from pathlib import Path

js_path =  "www/js_script.js"
js_slider = """
<script src="https://cdnjs.cloudflare.com/ajax/libs/noUiSlider/14.6.3/nouislider.min.js"></script>
"""

  
app_ui = ui.page_fluid(
  
    # Benchmarking section 
    ui.div({"class": "table_section"}, 
  
        # add custom JavaScript 
        ui.output_data_frame(id='ex_table'), 
        ui.include_js(path=js_path))
    )

def server(input, output, session):

    @output
    @render.data_frame()
    def ex_table():

      return iris_df

app = App(app_ui, server)

So just a quick observation, it doesn't look like you actually import the js_slider. What happens when you add this before the import of your file?

        ui.include_js(path=js_slider))

Ahh, good catch! Forgot to bring it over. This weird behavior still occurs even when I load the nouislider library. Just an FYI that I could only seem to import the library by inserting it as HTML code. Here is the new section of the Shiny app:


js_path =  "www/js_script.js"
js_slider = """
<script src="https://cdnjs.cloudflare.com/ajax/libs/noUiSlider/14.6.3/nouislider.min.js"></script>
"""

 

app_ui = ui.page_fluid(
  
    ui.head_content( ui.include_js(js_path), 
                  ui.HTML(js_slider)),  # add noUiSlider library 

    # Benchmarking section 
    ui.div({"class": "table_section"}, 
  
        # add custom JavaScript 
        ui.output_data_frame(id='ex_table'))
)

I was able to see the sliders slide with your example:

They didn't show up for all of them, but you can’t interact with them at all?

Ok nice! I can move the sliders, but I find it odd that, once I click anywhere on the table, some of the sliders disappear and the 'sliders' text reappears. Ideally, I want to replace each 'sliders' with actual HTML slider and not have the 'sliders' text reappear. Once I can get this working, I want to then use the sliders to change values of specific columns in the table. If there is an easier way to accomplish this, let me know!

I think you might have better success if you server side render those sliders and they would pair up easier with the data, but I am not sure on that. Seems to me that you'll have an easier time building the table with sliders on the server- without the use of ui.output_data_frame- than rendering and then updating with JS.

Might be time for a new topic that is focused on that end goal because I just don't know if there is a nice way to reference those table elements from JS and get them matched up.

Ok sounds good. I think I'll start a new topic then. Just a quick question: By the 'server side', do you mean by adding slider elements within the user interface section?

Yeah, maybe something like

js_slider = """
<div class="slideContainer">
  <input type="range" min="0" max="100" value="50" class="slider" id="slider-for-sepal-length-row-3">
</div>
"""
ui.HTML(js_slider)

would make it easier to update the data frame?

That's an interesting idea. Not sure exactly what you are thinking, but I tried simply inserting it into the dataframe column for the sliders but it does not seem to render.

slider_html_code = """
<div class="slideContainer">
  <input type="range" min="0" max="100" value="50" class="slider" id="slider-for-sepal-length-row-3">
</div>
"""
iris_df['Sliders'] = HTML(slider_html_code)

app_ui = ui.page_fluid(
  
    ui.head_content( ui.include_js(js_path), 
                  ui.HTML(js_slider)),  # add noUiSlider library 

    # Benchmarking section 
    ui.div({"class": "table_section"}, 
  
        # add custom JavaScript 
        ui.output_data_frame(id='ex_table'))
)

def server(input, output, session):

    @output
    @render.data_frame()
    def ex_table():

      return iris_df

app = App(app_ui, server)

Edit: Also tried using the ui.HTML() to render the code.

Something like in the example here:

Hmm made several attempts and couldn't get much going. I'll look into making a custom javascript component.

I realize I never asked: do the sliders need to be a part of the table? I think you might be able to do some CSS so multiple sliders are manageable.

Source:

Last few paragraphs of the details section and first example to get to:

from shiny import App, reactive, render, ui
import numpy as np
import pandas as pd
from sklearn.datasets import load_iris

iris_data = load_iris()

iris_df = pd.DataFrame(data=iris_data.data,
                       columns=iris_data.feature_names).iloc[1:4]

app_ui = ui.page_fluid(
    ui.input_slider("n", "Sepal width, row 2", min=1, max=100, value=1),
    ui.output_data_frame(id='ex_table')
)

def server(input, output, session):

    @output
    @render.data_frame()
    def ex_table():
      iris_df.iloc[1][1] = input.n()
      return iris_df
    
app = App(app_ui, server)

Ideally, the sliders do need to be within the table. I could probably wrangle some CSS and HTML to get the sliders aligned with the correct table cells. The challenge is that I also need to place additional columns after the columns that I want to contain the sliders. So I would basically have to output two tables (and get them aligned) and the sliders in between them. If I can't get anything to work, I might have to go down this path.

1 Like

Hmm, realizing that the cause of several issues I am encountering lies in the HTML dependencies that are loaded. Is it fairly easy to modify the dependencies loaded into output_data_frame(). For instance, I want to disable the table_summary.tsx. I would also want to disable the column sorting capability (along with the arrows that pop up) because it does not seem to play well with the sliders and I also have no need for it.

Or do I have to completely redefine output_data_frame() locally to accomplish this?

As far as I know, you would have to redefine it except maybe for minor stuff. For instance, you might be able to disable the column sorting by overwriting it with CSS/JS locally. The component itself I think gets bundled up and sent from the server after being built/transpiled. So you would have to undo that somehow.

Hi,

Thought this might be a fun project. This is my result:

Server side is R and not python, but should be translate-able.