Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Debounce observablehq events #5

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@ Embed an Observable notebook into the Streamlit app. If any cells are passed int
- `observe`: An optional list of cell names to observe. When those cells are updated in the Observable notebook, the new values will be sent back to Streamlit as part of the return value. Keep in mind, there is a serialization process from going from Observable notebook JavaScript -> Streamlit Python (JSON serializing).
- `redefine`: An optional dict of cell names and values used to redefine in the embeded notebook. Keep in mind, there is a serialization process from going from Streamlit Python -> Observable notebook JavaScript (JSON serializing).
- `hide`: An optional list of cell names that will not be rendered in the DOM of the embed. Useful for side-effect logic cells, like `mouse` in https://observablehq.com/@mbostock/eyes.
- `debounce`: An optional delay in milliseconds. Observed values don't change until a delay after the last update is less than the option value.

## Caveats

Expand All @@ -138,4 +139,4 @@ I haven't tried this, but I expect that if you try loading 1GB+ of data into a b

### You'll need to fork a lot

Most Observable notebooks are built with only other Observable users in mind. Meaning, a lot of cells are exposed as custom Objects, Dates, functions, or classes, all of which you can't control very well in Python land. So, you may need to fork the notebook you want in Observable, make changes to make it a little friendlier, then publish/enable link-sharing to access in Streamlit. Thankfully, this is pretty quick to do on Observable once you get the hang of it, but it does take extra time.
Most Observable notebooks are built with only other Observable users in mind. Meaning, a lot of cells are exposed as custom Objects, Dates, functions, or classes, all of which you can't control very well in Python land. So, you may need to fork the notebook you want in Observable, make changes to make it a little friendlier, then publish/enable link-sharing to access in Streamlit. Thankfully, this is pretty quick to do on Observable once you get the hang of it, but it does take extra time.
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ def get_long_description():

setuptools.setup(
name="streamlit-observable",
version="0.0.8",
version="0.1.0",
author="Alex Garcia",
author_email="[email protected]",
description="A Streamlit component for embedding Observable notebooks in Streamlit Apps",
Expand Down
11 changes: 8 additions & 3 deletions streamlit_observable/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
_component_func = components.declare_component("observable", path=build_dir)


def observable(key, notebook, targets=None, redefine={}, observe=[], hide=[]):
def observable(key, notebook, targets=None, redefine={}, observe=[], hide=[], debounce=None):
"""Create a new instance of "observable".

Parameters
Expand All @@ -37,8 +37,12 @@ def observable(key, notebook, targets=None, redefine={}, observe=[], hide=[]):
redefine, the values are what they will be redefined as. Keep in mind,
there is a serialization process from Streamlit Python -> frontend JavaScript.
hide: list or None
An option list of strings that are the names of cells that will be embeded,
An optional list of strings that are the names of cells that will be embeded,
but won't be rendered to the DOM.
debounce: float or None
An optional delay in milliseconds.
Observed values don't change
until a delay after the last update is less than the option value.
Returns
-------
dict
Expand All @@ -54,7 +58,8 @@ def observable(key, notebook, targets=None, redefine={}, observe=[], hide=[]):
redefine=redefine,
hide=hide,
key=key,
name=key
name=key,
debounce=debounce
)

if component_value is None:
Expand Down
17 changes: 5 additions & 12 deletions streamlit_observable/frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,23 +4,16 @@
"private": true,
"dependencies": {
"@observablehq/runtime": "^4.7.2",
"@testing-library/jest-dom": "^4.2.4",
"@testing-library/react": "^9.3.2",
"@testing-library/user-event": "^7.1.2",
"@types/hoist-non-react-statics": "^3.3.1",
"@types/jest": "^24.0.0",
"@types/node": "^12.0.0",
"@types/react": "^16.9.0",
"@types/react-dom": "^16.9.0",
"apache-arrow": "^0.17.0",
"bootstrap": "^4.4.1",
"event-target-shim": "^5.0.1",
"hoist-non-react-statics": "^3.3.2",
"react": "^16.13.1",
"react-dom": "^16.13.1",
"react-scripts": "3.4.1",
"streamlit-component-lib": "^1.4.0",
"typescript": "~3.7.2"
},
"devDependencies": {
"@types/react": "^16.9.0",
"@types/react-dom": "^16.9.0"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
Expand Down
4 changes: 2 additions & 2 deletions streamlit_observable/frontend/public/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
href="https://cdn.jsdelivr.net/npm/@observablehq/inspector@3/dist/inspector.css" />
</head>

<body>
<body style="margin:0">
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<!--
Expand All @@ -26,4 +26,4 @@
-->
</body>

</html>
</html>
48 changes: 35 additions & 13 deletions streamlit_observable/frontend/src/Observable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,16 @@ import {
withStreamlitConnection,
StreamlitComponentBase,
Streamlit,
} from "./streamlit"
} from "streamlit-component-lib"
import { Runtime, Inspector } from "@observablehq/runtime";
import debounceExecution from "./debounceExecution"

class Observable extends StreamlitComponentBase<{}> {
public observeValue = {};
private notebookRef = React.createRef<HTMLDivElement>();
private runtime: any = null;
private main: any = null;
private debounceUpdate: any = null;

componentWillUnmount() {
this.runtime?.dispose();
Expand All @@ -21,27 +23,32 @@ class Observable extends StreamlitComponentBase<{}> {
if (prevArgs.notebook !== this.props.args.notebook) {
// TODO handle new notebook
}
this.redefineCells(this.main, this.props.args.redefine);
if (prevArgs.debounce !== this.props.args.debounce) {
this.setupDebounceUpdate(this.props.args.debounce)
}
if (this.main) {
this.redefineCells(this.main, this.props.args.redefine);
}
}

async embedNotebook(notebook: string, targets: string[], observe: string[], hide:string[]) {
async embedNotebook(notebook: string, targets: string[], observe: string[], hide: string[], debounce: number) {
if (this.runtime) {
this.runtime.dispose();
}
this.setupDebounceUpdate(debounce)
const targetSet = new Set(targets);
const observeSet = new Set(observe);
const hideSet = new Set(hide);
this.runtime = new Runtime();
const { default: define } = await eval(`import("https://api.observablehq.com/${notebook}.js?v=3")`);
const { default: define } = await eval(`import("https://api.observablehq.com/${notebook}.js?v=3")`); // eslint-disable-line no-eval
this.main = this.runtime.module(define, (name: string) => {
if (observeSet.has(name) && !targetSet.has(name)) {
const observeValue = this.observeValue;
return {
fulfilled: (value: any) => {
//@ts-ignore
observeValue[name] = value;
//@ts-ignore
Streamlit.setComponentValue(observeValue);
this.debounceUpdate()
}
}
}
Expand All @@ -51,7 +58,15 @@ class Observable extends StreamlitComponentBase<{}> {
this.notebookRef.current?.appendChild(el);

const i = new Inspector(el);
el.addEventListener('input', e => {

const ResizeObserver = (window as any).ResizeObserver
if (ResizeObserver) {
const resizeObserver = new ResizeObserver(() => {
Streamlit.setFrameHeight();
})
resizeObserver.observe(el)
}
el.addEventListener('input', () => {
Streamlit.setFrameHeight();
})
return {
Expand All @@ -74,22 +89,29 @@ class Observable extends StreamlitComponentBase<{}> {
for (const [name, value] of initial) {
// @ts-ignore
this.observeValue[name] = value
};
Streamlit.setComponentValue(this.observeValue);
}
this.debounceUpdate()
})
}
}

private setupDebounceUpdate(debounce: any) {
if (debounce) {
this.debounceUpdate = debounceExecution(() => Streamlit.setComponentValue(this.observeValue), debounce)
} else {
this.debounceUpdate = () => Streamlit.setComponentValue(this.observeValue)
}
}

redefineCells(main: any, redefine = {}) {
for (let cell in redefine) {
//@ts-ignore
main.redefine(cell, redefine[cell]);
}
}
componentDidMount() {
const { notebook, targets = [], observe = [], redefine = {} , hide=[]} = this.props.args;
Streamlit.setComponentValue(this.observeValue);
this.embedNotebook(notebook, targets, observe, hide).then(() => {
const { notebook, targets = [], observe = [], redefine = {} , hide = [], debounce = 0 } = this.props.args;
this.embedNotebook(notebook, targets, observe, hide, debounce).then(() => {
this.redefineCells(this.main, redefine);
});

Expand All @@ -113,7 +135,7 @@ class Observable extends StreamlitComponentBase<{}> {
}}>
<div style={{textAlign:"left"}}>{this.props.args.name}</div>
<div style={{textAlign:"right"}}>
<a href={`https://observablehq.com/${this.props.args.notebook}`} style={{ color: '#666', }}>{this.props.args.notebook}</a>
<a target="_blank" rel="noopener noreferrer" href={`https://observablehq.com/${this.props.args.notebook}`} style={{ color: '#666', }}>{this.props.args.notebook}</a>
</div>
</div>
</div>
Expand Down
13 changes: 13 additions & 0 deletions streamlit_observable/frontend/src/debounceExecution.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
export default function debounceExecution(func: any, wait: any) {
let timeout: any

return function executedFunction(...args: any[]) {
const later = () => {
clearTimeout(timeout)
func(...args)
}

clearTimeout(timeout)
timeout = setTimeout(later, wait)
}
}
Loading