{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"pygments_lexer":"ipython3","nbconvert_exporter":"python","version":"3.6.4","file_extension":".py","codemirror_mode":{"name":"ipython","version":3},"name":"python","mimetype":"text/x-python"}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"<div style=\"color:white;\n            display:fill;\n            border-radius:15px;\n            border: solid;\n            border-color:#6E3A25;\n            background-color:#E4CEA7;\n            font-size:75%;\n            letter-spacing:1px\">\n    <h1 style='padding: 20px;\n              color:#804C24;\n              text-align:center;'>\n        Notebook Description 📜\n    </h1>\n</div>\n\n\n\n\n\n\n\n","metadata":{}},{"cell_type":"code","source":"import IPython.display as py_display\npy_display.Image(\"/kaggle/input/student-performance-room-img/main.jpg\")","metadata":{"_kg_hide-input":true,"execution":{"iopub.status.busy":"2023-02-20T20:01:37.502835Z","iopub.execute_input":"2023-02-20T20:01:37.503258Z","iopub.status.idle":"2023-02-20T20:01:37.574499Z","shell.execute_reply.started":"2023-02-20T20:01:37.503172Z","shell.execute_reply":"2023-02-20T20:01:37.573487Z"},"jupyter":{"source_hidden":true},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"\n\n<h2>This notebook was created to have fun. 🎲</h2>\n<div class=\"alert alert-block alert-warning\" style=\"width:65%\">\n    <b>Info:</b> If you want to see statistics for click events and where most clicks are done just take a look at this:<br>\n    <b>notebook:</b> <a href=\"https://www.kaggle.com/code/cdeotte/game-room-click-eda\">Game Room Click EDA</a><br>\n    and <br>\n    <b>discussion:</b> <a href=\"https://www.kaggle.com/competitions/predict-student-performance-from-game-play/discussion/387864\">How The Game Begins - EDA</a><br>\n</div>\n\nBut if you want to see how the events of a specific session look like.\n\n<h3 style='padding: 20px;\ncolor:#B1723D;\ntext-align:center;'>\nYou are welcome 👋\n</h3>\n\nIn this notebook, you'll have a chance to select the session and take a look.\n\n<h3 style='padding: 20px;\ncolor:#B1723D;\ntext-align:center;'>\nWhat was clicking the user of the session during each stage of the game?\n</h3>\n   \n<b>If you like the notebook:</b>    \n<h2 style='padding: 20px;\n       color:#B1723D;\n       text-align:center;'>\n    Upvote 🫶 🧡 👍\n</h2>\n \n\n\n","metadata":{}},{"cell_type":"markdown","source":"<div style=\"color:white;\n            display:fill;\n            border-radius:15px;\n            border: solid;\n            border-color:#6E3A25;\n            background-color:#E4CEA7;\n            font-size:75%;\n            letter-spacing:1px\">\n    <h1 style='padding: 20px;\n              color:#804C24;\n              text-align:center;'>\n        Import Libraries\n    </h1>\n</div>\n\n\n\n\n\n\n","metadata":{}},{"cell_type":"code","source":"import numpy as np\nimport pandas as pd \n\nfrom string import Template\nfrom IPython.core.display import display, HTML, Javascript\nimport imageio\nfrom PIL import Image\nimport os","metadata":{"_kg_hide-input":false,"_kg_hide-output":true,"execution":{"iopub.status.busy":"2023-02-20T20:01:37.576045Z","iopub.execute_input":"2023-02-20T20:01:37.576721Z","iopub.status.idle":"2023-02-20T20:01:37.684834Z","shell.execute_reply.started":"2023-02-20T20:01:37.576682Z","shell.execute_reply":"2023-02-20T20:01:37.683087Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"%%javascript\nrequire.config({\n    paths: { \n        d3: 'https://d3js.org/d3.v6.min'\n    }\n});","metadata":{"_kg_hide-input":true,"_kg_hide-output":true,"execution":{"iopub.status.busy":"2023-02-20T20:01:37.686795Z","iopub.execute_input":"2023-02-20T20:01:37.687543Z","iopub.status.idle":"2023-02-20T20:01:37.696130Z","shell.execute_reply.started":"2023-02-20T20:01:37.687499Z","shell.execute_reply":"2023-02-20T20:01:37.694957Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"* save room images","metadata":{}},{"cell_type":"code","source":"img_dir = '/kaggle/input/student-performance-room-img/'\nfor img in os.listdir(img_dir):\n    img_path = os.path.join(img_dir, img)\n    im = imageio.imread(img_path)\n    Image.fromarray(im).save(img)","metadata":{"_kg_hide-input":false,"_kg_hide-output":true,"execution":{"iopub.status.busy":"2023-02-20T20:01:37.698616Z","iopub.execute_input":"2023-02-20T20:01:37.699148Z","iopub.status.idle":"2023-02-20T20:01:44.650602Z","shell.execute_reply.started":"2023-02-20T20:01:37.699107Z","shell.execute_reply":"2023-02-20T20:01:44.649549Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"<div style=\"color:white;\n            display:fill;\n            border-radius:15px;\n            border: solid;\n            border-color:#6E3A25;\n            background-color:#E4CEA7;\n            font-size:75%;\n            letter-spacing:1px\">\n    <h1 style='padding: 20px;\n              color:#804C24;\n              text-align:center;'>\n        Load Train Dataset\n    </h1>\n</div>\n\n\n\n\n","metadata":{}},{"cell_type":"code","source":"train_df = pd.read_csv('/kaggle/input/predict-student-performance-from-game-play/train.csv')\ntrain_df.head()","metadata":{"_kg_hide-input":true,"execution":{"iopub.status.busy":"2023-02-20T20:01:44.652123Z","iopub.execute_input":"2023-02-20T20:01:44.652594Z","iopub.status.idle":"2023-02-20T20:02:55.663420Z","shell.execute_reply.started":"2023-02-20T20:01:44.652561Z","shell.execute_reply":"2023-02-20T20:02:55.662422Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"<div style=\"color:white;\n            display:fill;\n            border-radius:15px;\n            border: solid;\n            border-color:#6E3A25;\n            background-color:#E4CEA7;\n            font-size:75%;\n            letter-spacing:1px\">\n    <h1 style='padding: 20px;\n              color:#804C24;\n              text-align:center;'>\n        Prepare Data\n    </h1>\n</div>\n\n\n","metadata":{}},{"cell_type":"markdown","source":"* unique rooms","metadata":{}},{"cell_type":"code","source":"unique_rooms = train_df.room_fqid.unique()\nprint(\"Number of unique rooms - \", len(unique_rooms))\nprint(unique_rooms)","metadata":{"_kg_hide-input":true,"execution":{"iopub.status.busy":"2023-02-20T20:02:55.664734Z","iopub.execute_input":"2023-02-20T20:02:55.665257Z","iopub.status.idle":"2023-02-20T20:02:56.918966Z","shell.execute_reply.started":"2023-02-20T20:02:55.665224Z","shell.execute_reply":"2023-02-20T20:02:56.917000Z"},"jupyter":{"source_hidden":true},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"* prepare data","metadata":{}},{"cell_type":"code","source":"# Find the size of each room\nroom_bounds = {room:\n{\"x_min\": train_df.loc[train_df.room_fqid==room, 'room_coor_x'].min(),\n \"x_max\": train_df.loc[train_df.room_fqid==room,\"room_coor_x\"].max(),\n  \"y_min\": train_df.loc[train_df.room_fqid==room,\"room_coor_y\"].min(),\n  \"y_max\": train_df.loc[train_df.room_fqid==room,\"room_coor_y\"].max()\n  } for room in unique_rooms}","metadata":{"execution":{"iopub.status.busy":"2023-02-20T20:02:56.920222Z","iopub.execute_input":"2023-02-20T20:02:56.920629Z","iopub.status.idle":"2023-02-20T20:04:01.378435Z","shell.execute_reply.started":"2023-02-20T20:02:56.920600Z","shell.execute_reply":"2023-02-20T20:04:01.377482Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"# Set room sizes for each record\ntrain_df['room_x_max'] = train_df.room_fqid.map(lambda x: room_bounds[x]['x_max'])\ntrain_df['room_x_min'] = train_df.room_fqid.map(lambda x: room_bounds[x]['x_min'])\ntrain_df['room_y_max'] = train_df.room_fqid.map(lambda x: room_bounds[x]['y_max'])\ntrain_df['room_y_min'] = train_df.room_fqid.map(lambda x: room_bounds[x]['y_min'])","metadata":{"execution":{"iopub.status.busy":"2023-02-20T20:04:01.379792Z","iopub.execute_input":"2023-02-20T20:04:01.380679Z","iopub.status.idle":"2023-02-20T20:04:20.857039Z","shell.execute_reply.started":"2023-02-20T20:04:01.380638Z","shell.execute_reply":"2023-02-20T20:04:20.855799Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"* unique event types","metadata":{}},{"cell_type":"code","source":"print(\"Unique events - \", train_df.event_name.unique())\n# Select only click events\nclick_events = [event for event in train_df.event_name.unique() if 'click' in event]","metadata":{"_kg_hide-input":false,"execution":{"iopub.status.busy":"2023-02-20T20:04:20.858490Z","iopub.execute_input":"2023-02-20T20:04:20.859061Z","iopub.status.idle":"2023-02-20T20:04:22.959826Z","shell.execute_reply.started":"2023-02-20T20:04:20.859020Z","shell.execute_reply":"2023-02-20T20:04:22.958693Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"<div style=\"color:white;\n            display:fill;\n            border-radius:15px;\n            border: solid;\n            border-color:#6E3A25;\n            background-color:#E4CEA7;\n            font-size:75%;\n            letter-spacing:1px\">\n    <h2 style='padding: 20px;\n              color:#804C24;\n              text-align:center;'>\n        Save Session Events\n    </h2>\n</div>\n\n","metadata":{}},{"cell_type":"code","source":"# The list of columns that we want to save. \n# Only the info related to clicks\ncolumns=['room_fqid','elapsed_time', 'event_name', 'room_coor_x', 'room_coor_y', 'screen_coor_x', 'screen_coor_y' , 'room_x_max', 'room_x_min', 'room_y_max', 'room_y_min']","metadata":{"execution":{"iopub.status.busy":"2023-02-20T20:04:22.965548Z","iopub.execute_input":"2023-02-20T20:04:22.965899Z","iopub.status.idle":"2023-02-20T20:04:22.970440Z","shell.execute_reply.started":"2023-02-20T20:04:22.965869Z","shell.execute_reply":"2023-02-20T20:04:22.969634Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"<div class=\"alert alert-block alert-info\" style=\"background-color:#E6D77C; color:#642A1F; border-color:#3B2E38;\">\n    <b>Info:</b> If you want to see events for your own selected session just change the <b>session_id</b> below 👇🏾\n</div>\n","metadata":{}},{"cell_type":"code","source":"# select session which you want to display\nsession_id = 20090312431273200\none_session_events = train_df.loc[(train_df.session_id==session_id)&(train_df.event_name.isin(click_events))]\none_session_events.head()\n# save session \none_session_events.sort_values(by='elapsed_time').loc[:,columns].to_csv('one_session_events.csv', index=False)","metadata":{"execution":{"iopub.status.busy":"2023-02-20T20:04:22.971744Z","iopub.execute_input":"2023-02-20T20:04:22.972280Z","iopub.status.idle":"2023-02-20T20:04:24.767941Z","shell.execute_reply.started":"2023-02-20T20:04:22.972247Z","shell.execute_reply":"2023-02-20T20:04:24.767021Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"<div style=\"color:white;\n            display:fill;\n            border-radius:15px;\n            border: solid;\n            border-color:#6E3A25;\n            background-color:#E4CEA7;\n            font-size:75%;\n            letter-spacing:1px\">\n    <h1 style='padding: 20px;\n              color:#804C24;\n              text-align:center;'>\n        Session Events Animation 🕹️\n    </h1>\n</div>\n\n","metadata":{}},{"cell_type":"code","source":"css_text = Template('''\n#functionality {\n    display: flex;\n    justify-content: center;\n    flex-wrap: wrap;\n    padding-top: 5px;\n}\n\n#scene {\n    padding-top: 10px;\n}\n\nselect {\n    font-size: 15px;\n    margin-left: 10px;\n}\n\nbutton {\n    font-size: 15px;\n    margin-left: 10px;\n}\n#all{\n    background-color:#D0CABC;\n}\n''')","metadata":{"_kg_hide-input":true,"execution":{"iopub.status.busy":"2023-02-20T20:12:29.064519Z","iopub.execute_input":"2023-02-20T20:12:29.064950Z","iopub.status.idle":"2023-02-20T20:12:29.070479Z","shell.execute_reply.started":"2023-02-20T20:12:29.064915Z","shell.execute_reply":"2023-02-20T20:12:29.069423Z"},"jupyter":{"source_hidden":true},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"htmlt = Template('''\n<!-- Style -->\n<style>\n$css_file\n</style>\n<img id=\"baseimg\" src=\"tunic.historicalsociety.closet.jpg\" style=\"display:none\" />\n<div id=\"all\">\n    <div id=\"functionality\">\n            <select id=\"room\">\n                <option value=\"tunic.historicalsociety.closet\" selected>tunic.historicalsociety.closet</option>\n                <option value=\"tunic.historicalsociety.basement\">tunic.historicalsociety.basement</option>\n                <option value=\"tunic.historicalsociety.entry\">tunic.historicalsociety.entry</option>\n                <option value=\"tunic.historicalsociety.collection\">tunic.historicalsociety.collection</option>\n                <option value=\"tunic.historicalsociety.stacks\">tunic.historicalsociety.stacks</option>\n                <option value=\"tunic.kohlcenter.halloffame\">tunic.kohlcenter.halloffame</option>\n                <option value=\"tunic.capitol_0.hall\">tunic.capitol_0.hall</option>\n                <option value=\"tunic.historicalsociety.closet_dirty\">tunic.historicalsociety.closet_dirty</option>\n                <option value=\"tunic.historicalsociety.frontdesk\">tunic.historicalsociety.frontdesk</option>\n                <option value=\"tunic.humanecology.frontdesk\">tunic.humanecology.frontdesk</option>\n                <option value=\"tunic.drycleaner.frontdesk\">tunic.drycleaner.frontdesk</option>\n                <option value=\"tunic.library.frontdesk\">tunic.library.frontdesk</option>\n                <option value=\"tunic.library.microfiche\">tunic.library.microfiche</option>\n                <option value=\"tunic.capitol_1.hall\">tunic.capitol_1.hall</option>\n                <option value=\"tunic.historicalsociety.cage\">tunic.historicalsociety.cage</option>\n                <option value=\"tunic.historicalsociety.collection_flag\">tunic.historicalsociety.collection_flag</option>\n                <option value=\"tunic.wildlife.center\">tunic.wildlife.center</option>\n                <option value=\"tunic.flaghouse.entry\">tunic.flaghouse.entry</option>\n                <option value=\"tunic.capitol_2.hall\">tunic.capitol_2.hall</option>\n            </select>\n            <select id=\"event_type\">\n                <option value=\"all\" selected>All</option>\n                <option value=\"cutscene_click\">cutscene_click</option>\n                <option value=\"person_click\">person_click</option>\n                <option value=\"navigate_click\">navigate_click</option>\n                <option value=\"observation_click\">observation_click</option>\n                <option value=\"notification_click\">notification_click</option>\n                <option value=\"object_click\">object_click</option>\n                <option value=\"map_click\">map_click</option>\n                <option value=\"notebook_click\">notebook_click</option>\n            </select>\n            <button id=\"start_stop\">START</button>\n            <button id=\"reset\">RESET</button>\n    </div>\n    <div id=\"scene\"></div>\n    <div id=\"legend\"></div>\n</div>\n''')","metadata":{"_kg_hide-input":true,"jupyter":{"source_hidden":true},"execution":{"iopub.status.busy":"2023-02-20T20:04:24.775984Z","iopub.execute_input":"2023-02-20T20:04:24.776427Z","iopub.status.idle":"2023-02-20T20:04:24.791095Z","shell.execute_reply.started":"2023-02-20T20:04:24.776366Z","shell.execute_reply":"2023-02-20T20:04:24.789769Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"js_t = Template('''\nrequire([\"d3\"], function(d3) {\n\n    // Get Assets URL set by Kaggle\n    const baseAssetsUrl = document.getElementById('baseimg').src.replace(/tunic.historicalsociety.closet.jpg.*$/, '');\n\n    function draw_scene(room=document.getElementById(\"room\").value) {\n    \n        d3.csv(baseAssetsUrl + \"one_session_events.csv\").then(\n            function (dataset) {\n            \n               dataset =  dataset.filter(d => d.room_fqid == room)\n               \n               const xAccessor = d => +d.room_coor_x\n                const yAccessor = d => +d.room_coor_y\n                const userXCoor = d => +d.screen_coor_x\n                const userYCoor = d => +d.screen_coor_y\n                const eventName = d => d.event_name\n\n                const xMin = +dataset[0].room_x_min\n                const xMax = +dataset[0].room_x_max\n                const yMin = +dataset[0].room_y_min\n                const yMax = +dataset[0].room_y_max\n\n                const dimensions = {\n                    width: 850,\n                    height: 400,\n                    margins: 50,\n                    userWidth: 380,\n                    userHeight: 360\n                }\n\n                const xScale = d3.scaleLinear()\n                    .domain([xMin, xMax])\n                    .range([0, dimensions.width])\n\n                const yScale = d3.scaleLinear()\n                    .domain([yMin, yMax])\n                    .range([dimensions.height, 0])\n\n                const eventColors = d3.scaleOrdinal()\n                    .domain(dataset.map(d => d.event_name))\n                    .range(d3.schemeCategory10)\n\n                // Draw scene\n                const svg = d3.select(\"#scene\")\n                    .append(\"svg\")\n                    .attr(\"width\", dimensions.width)\n                    .attr(\"height\", dimensions.height)\n                    .style(\"background-image\", `url(\"${baseAssetsUrl}${room}.jpg\")`)\n                    .style(\"background-size\", \"100% 100%\")\n                    \n                //Draw legend\n                const legendContainer = d3.select(\"#legend\").append(\"svg\")\n                    .attr(\"width\", dimensions.width)\n                    .attr(\"height\", 75)\n\n                legendContainer.append('rect')\n                    .attr(\"x\", 0)\n                    .attr(\"y\", 0)\n                    .attr(\"width\", dimensions.width)\n                    .attr(\"height\", 70)\n                    .attr(\"fill\", \"none\")\n                    .attr(\"stroke\", \"black\")\n                    .attr(\"stroke-width\", 3)\n                    .attr(\"stroke-dasharray\", \"10 10\")\n\n                legendContainer.selectAll(\"circle\")\n                    .data(eventColors.domain())\n                    .join(\"circle\")\n                    .attr(\"cx\", (d, i) => 100 * (i+1) )\n                    .attr(\"cy\", 25)\n                    .attr(\"r\", 7)\n                    .attr(\"fill\", \"none\")\n                    .attr(\"stroke\", d => eventColors(d))\n                    .attr(\"stroke-width\", 5)\n                    \n\n                legendContainer.append('rect')\n                    .attr(\"x\", 100 * (eventColors.domain().length+1))\n                    .attr(\"y\", 15)\n                    .attr(\"width\", 14)\n                    .attr(\"height\", 14)\n                    .attr(\"fill\", \"none\")\n                    .attr(\"stroke\", \"red\")\n                    .attr(\"stroke-width\", 5)\n\n                legendContainer.selectAll(\"text\")\n                    .data(eventColors.domain())\n                    .join(\"text\")\n                    .attr(\"x\", (d, i) => 100 * (i+0.7))\n                    .attr(\"y\", 50)\n                    .attr(\"font-size\", 11)\n                    .attr(\"font-weight\", \"bold\")\n                    .text(d => d)\n\n                legendContainer.append(\"text\")\n                    .attr(\"x\", 100 * (eventColors.domain().length+0.7))\n                    .attr(\"y\", 50)\n                    .attr(\"font-size\", 11)\n                    .attr(\"font-weight\", \"bold\")\n                    .text(\"user screen\")\n                    \n                    \n                function draw_touches(event_type) {\n                    let data;\n                    if (event_type != \"all\") {\n                        data = dataset.filter(d => d.event_name === event_type)\n                    }else{\n                        data = dataset;\n                    }\n\n                    // Draw all touches and screens\n                    const touches = svg.selectAll(\"circle\")\n                        .data(data)\n                        .join(enter => enter.append(\"circle\")\n                            .attr(\"cx\", d => xScale(xAccessor(d)))\n                            .attr(\"cy\", d => yScale(yAccessor(d)))\n                            .attr(\"r\", 20)\n                            .attr(\"fill\", \"none\")\n                            .attr(\"stroke\", d => eventColors(eventName(d)))\n                            .attr(\"stroke-width\", 3)\n                            .attr('opacity', 0)\n                        )\n\n                    const screens = svg.selectAll(\"rect\")\n                        .data(data)\n                        .join(\n                            enter => enter.append(\"rect\")\n                                .attr(\"x\", d => xScale(xAccessor(d)) - (userXCoor(d)*0.42))\n                                .attr(\"y\", d => dimensions.height - dimensions.userHeight - (yScale(yAccessor(d)) - (userYCoor(d)*0.55)))\n                                .attr(\"width\", dimensions.userWidth)\n                                .attr(\"height\", dimensions.userHeight)\n                                .attr(\"fill\", \"none\") \n                                .attr(\"stroke\", \"red\")\n                                .attr(\"stroke-width\", 3)\n                                .attr('opacity', 0)\n                        )\n\n                    d3.select(\"#start_stop\").on(\"click\", function () {\n                        // Show touches by order\n                        touches\n                            .transition(500)\n                            .delay((d, i) => i * 1000)\n                            .attr('opacity', 1)\n                            .transition()\n                            .duration(1000)\n                            .attr(\"r\", 10)\n                            .attr(\"opacity\", 0.45)\n\n                        screens\n                            .transition()\n                            .attr('opacity', 1)\n                            .delay((d, i) => i * 1000)\n                            .transition()\n                            .duration(1050)\n                            .remove()\n                    });\n                }\n            draw_touches(document.querySelector(\"#event_type\").value)\n            \n            // Event listeners\n            d3.select(\"#reset\").on(\"click\", function () {\n                svg.selectAll(\"*\").remove()\n                draw_touches(document.querySelector(\"#event_type\").value)\n                })\n                \n            d3.select(\"#event_type\")\n                .on(\"change\", function (e) {\n                    e.preventDefault()\n                    svg.selectAll(\"*\").remove()\n                    draw_touches(this.value)\n                })\n    \n                    \n        })\n        d3.select(\"#room\")\n            .on(\"change\", function (e) {\n                e.preventDefault()\n                d3.select(\"#scene\").selectAll(\"*\").remove()\n                d3.select(\"#legend\").selectAll(\"*\").remove()\n                draw_scene(this.value)\n            })\n    }\n    draw_scene()  \n});\n'''\n)","metadata":{"_kg_hide-input":true,"execution":{"iopub.status.busy":"2023-02-20T20:28:56.105345Z","iopub.execute_input":"2023-02-20T20:28:56.105868Z","iopub.status.idle":"2023-02-20T20:28:56.116561Z","shell.execute_reply.started":"2023-02-20T20:28:56.105825Z","shell.execute_reply":"2023-02-20T20:28:56.115735Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"# Display game\nh = display(HTML(htmlt.safe_substitute(\n    {\"css_file\":css_text.substitute()})))\n\nj = py_display.Javascript(js_t.safe_substitute())\npy_display.display_javascript(j)","metadata":{"execution":{"iopub.status.busy":"2023-02-20T20:28:58.546788Z","iopub.execute_input":"2023-02-20T20:28:58.547173Z","iopub.status.idle":"2023-02-20T20:28:58.555045Z","shell.execute_reply.started":"2023-02-20T20:28:58.547141Z","shell.execute_reply":"2023-02-20T20:28:58.554319Z"},"trusted":true},"execution_count":null,"outputs":[]}]}