SaulLu commited on
Commit
3fc2d14
1 Parent(s): cd8b3e4

add component code

Browse files
streamlit_observable/frontend/src/Observable.tsx ADDED
@@ -0,0 +1,141 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { ReactNode } from "react"
2
+ import {
3
+ withStreamlitConnection,
4
+ StreamlitComponentBase,
5
+ Streamlit,
6
+ } from "./streamlit"
7
+ import { Runtime, Inspector } from "@observablehq/runtime";
8
+
9
+ class Observable extends StreamlitComponentBase<{}> {
10
+ public observeValue = {};
11
+ private notebookRef = React.createRef<HTMLDivElement>();
12
+ private runtime: any = null;
13
+ private main: any = null;
14
+
15
+ componentWillUnmount() {
16
+ this.runtime?.dispose();
17
+ }
18
+ // @ts-ignore
19
+ public componentDidUpdate(prevProps: any) {
20
+ const { args: prevArgs } = prevProps;
21
+ if (prevArgs.notebook !== this.props.args.notebook) {
22
+ // TODO handle new notebook
23
+ }
24
+ console.log('this.props.args.redefine: ', this.props.args.redefine);
25
+ this.redefineCells(this.main, this.props.args.redefine);
26
+ }
27
+
28
+ async embedNotebook(notebook: string, targets: string[], observe: string[], hide:string[]) {
29
+ if (this.runtime) {
30
+ this.runtime.dispose();
31
+ }
32
+
33
+ console.log('Console says hi!');
34
+
35
+ const targetSet = new Set(targets);
36
+ const observeSet = new Set(observe);
37
+ const hideSet = new Set(hide);
38
+ this.runtime = new Runtime();
39
+ const { default: define } = await eval(`import("https://api.observablehq.com/${notebook}.js?v=3")`);
40
+
41
+ this.main = this.runtime.module(define, (name: string) => {
42
+ console.log('name: ', name);
43
+ console.log('observeSet.has(name: ', observeSet.has(name));
44
+ console.log('targetSet.has(name): ', targetSet.has(name));
45
+ if (observeSet.has(name) && !targetSet.has(name)) {
46
+ const observeValue = this.observeValue;
47
+
48
+ console.log('observeValue: ', observeValue);
49
+
50
+ return {
51
+ fulfilled: (value: any) => {
52
+ //@ts-ignore
53
+ observeValue[name] = value;
54
+ //@ts-ignore
55
+ Streamlit.setComponentValue(observeValue);
56
+ }
57
+ }
58
+ }
59
+ if (targetSet.size > 0 && !targetSet.has(name)) return;
60
+ if(hideSet.has(name)) return true;
61
+ const el = document.createElement('div');
62
+ this.notebookRef.current?.appendChild(el);
63
+
64
+ const i = new Inspector(el);
65
+ el.addEventListener('input', e => {
66
+ Streamlit.setFrameHeight();
67
+ })
68
+ return {
69
+ pending() {
70
+ i.pending();
71
+ Streamlit.setFrameHeight();
72
+ },
73
+ fulfilled(value: any) {
74
+ i.fulfilled(value);
75
+ Streamlit.setFrameHeight();
76
+ },
77
+ rejected(error: any) {
78
+ i.rejected(error);
79
+ Streamlit.setFrameHeight();
80
+ },
81
+ };
82
+ });
83
+ if (observeSet.size > 0) {
84
+ Promise.all(Array.from(observeSet).map(async name => [name, await this.main.value(name)])).then(initial => {
85
+ for (const [name, value] of initial) {
86
+ // @ts-ignore
87
+ this.observeValue[name] = value
88
+ };
89
+ Streamlit.setComponentValue(this.observeValue);
90
+ })
91
+ }
92
+ }
93
+
94
+ redefineCells(main: any, redefine = {}) {
95
+
96
+ console.log('Console says hi 2 !');
97
+
98
+ for (let cell in redefine) {
99
+ //@ts-ignore
100
+ main.redefine(cell, redefine[cell]);
101
+ }
102
+ }
103
+ componentDidMount() {
104
+ const { notebook, targets = [], observe = [], redefine = {} , hide=[]} = this.props.args;
105
+ Streamlit.setComponentValue(this.observeValue);
106
+ this.embedNotebook(notebook, targets, observe, hide).then(() => {
107
+ this.redefineCells(this.main, redefine);
108
+ });
109
+
110
+ }
111
+
112
+ public render = (): ReactNode => {
113
+
114
+ console.log('Fucking Console says hi 3 please!');
115
+ return (
116
+ <div style={{ border: '1px solid gray', borderRadius: '4px' }}>
117
+ <div style={{ padding: '9px 12px' }}>
118
+ <div ref={this.notebookRef}></div>
119
+ </div>
120
+ <div style={{ marginTop: '4px' }}>
121
+
122
+ <div style={{
123
+ backgroundColor: '#ddd',
124
+ fontWeight: 700,
125
+ padding: ".25rem .5rem",
126
+ borderRadius: '0 0 4px 4px',
127
+ gridTemplateColumns: "auto auto",
128
+ display:"grid"
129
+ }}>
130
+ <div style={{textAlign:"left"}}>{this.props.args.name}</div>
131
+ <div style={{textAlign:"right"}}>
132
+ <a href={`https://observablehq.com/${this.props.args.notebook}`} style={{ color: '#666', }}></a>
133
+ </div>
134
+ </div>
135
+ </div>
136
+ </div >
137
+ )
138
+ }
139
+ }
140
+
141
+ export default withStreamlitConnection(Observable)
streamlit_observable/frontend/src/index.tsx ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from "react"
2
+ import ReactDOM from "react-dom"
3
+ import Observable from "./Observable"
4
+
5
+ ReactDOM.render(
6
+ <React.StrictMode>
7
+ <Observable />
8
+ </React.StrictMode>,
9
+ document.getElementById("root")
10
+ )
streamlit_observable/frontend/src/react-app-env.d.ts ADDED
@@ -0,0 +1 @@
 
 
1
+ /// <reference types="react-scripts" />
streamlit_observable/frontend/src/streamlit/ArrowTable.ts ADDED
@@ -0,0 +1,224 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * @license
3
+ * Copyright 2018-2019 Streamlit Inc.
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */
17
+
18
+ import { Table, Type } from "apache-arrow"
19
+
20
+ type CellType = "blank" | "index" | "columns" | "data"
21
+
22
+ export interface ArrowDataframeProto {
23
+ data: ArrowTableProto
24
+ height: string
25
+ width: string
26
+ }
27
+
28
+ export interface ArrowTableProto {
29
+ data: Uint8Array
30
+ index: Uint8Array
31
+ columns: Uint8Array
32
+ styler: Styler
33
+ }
34
+
35
+ interface Cell {
36
+ classNames: string
37
+ content: string
38
+ id?: string
39
+ type: CellType
40
+ }
41
+
42
+ interface Styler {
43
+ caption?: string
44
+ displayValuesTable: Table
45
+ styles?: string
46
+ uuid: string
47
+ }
48
+
49
+ export class ArrowTable {
50
+ private readonly dataTable: Table
51
+ private readonly indexTable: Table
52
+ private readonly columnsTable: Table
53
+ private readonly styler?: Styler
54
+
55
+ constructor(
56
+ dataBuffer: Uint8Array,
57
+ indexBuffer: Uint8Array,
58
+ columnsBuffer: Uint8Array,
59
+ styler?: any
60
+ ) {
61
+ this.dataTable = Table.from(dataBuffer)
62
+ this.indexTable = Table.from(indexBuffer)
63
+ this.columnsTable = Table.from(columnsBuffer)
64
+ this.styler = styler
65
+ ? {
66
+ caption: styler.get("caption"),
67
+ displayValuesTable: Table.from(styler.get("displayValues")),
68
+ styles: styler.get("styles"),
69
+ uuid: styler.get("uuid"),
70
+ }
71
+ : undefined
72
+ }
73
+
74
+ get rows(): number {
75
+ return this.indexTable.length + this.columnsTable.numCols
76
+ }
77
+
78
+ get columns(): number {
79
+ return this.indexTable.numCols + this.columnsTable.length
80
+ }
81
+
82
+ get headerRows(): number {
83
+ return this.rows - this.dataRows
84
+ }
85
+
86
+ get headerColumns(): number {
87
+ return this.columns - this.dataColumns
88
+ }
89
+
90
+ get dataRows(): number {
91
+ return this.dataTable.length
92
+ }
93
+
94
+ get dataColumns(): number {
95
+ return this.dataTable.numCols
96
+ }
97
+
98
+ get uuid(): string | undefined {
99
+ return this.styler && this.styler.uuid
100
+ }
101
+
102
+ get caption(): string | undefined {
103
+ return this.styler && this.styler.caption
104
+ }
105
+
106
+ get styles(): string | undefined {
107
+ return this.styler && this.styler.styles
108
+ }
109
+
110
+ get table(): Table {
111
+ return this.dataTable
112
+ }
113
+
114
+ get index(): Table {
115
+ return this.indexTable
116
+ }
117
+
118
+ get columnTable(): Table {
119
+ return this.columnsTable
120
+ }
121
+
122
+ public getCell = (rowIndex: number, columnIndex: number): Cell => {
123
+ const isBlankCell =
124
+ rowIndex < this.headerRows && columnIndex < this.headerColumns
125
+ const isIndexCell =
126
+ rowIndex >= this.headerRows && columnIndex < this.headerColumns
127
+ const isColumnsCell =
128
+ rowIndex < this.headerRows && columnIndex >= this.headerColumns
129
+
130
+ if (isBlankCell) {
131
+ const classNames = ["blank"]
132
+ if (columnIndex > 0) {
133
+ classNames.push("level" + rowIndex)
134
+ }
135
+
136
+ return {
137
+ type: "blank",
138
+ classNames: classNames.join(" "),
139
+ content: "",
140
+ }
141
+ } else if (isColumnsCell) {
142
+ const dataColumnIndex = columnIndex - this.headerColumns
143
+ const classNames = [
144
+ "col_heading",
145
+ "level" + rowIndex,
146
+ "col" + dataColumnIndex,
147
+ ]
148
+
149
+ return {
150
+ type: "columns",
151
+ classNames: classNames.join(" "),
152
+ content: this.getContent(this.columnsTable, dataColumnIndex, rowIndex),
153
+ }
154
+ } else if (isIndexCell) {
155
+ const dataRowIndex = rowIndex - this.headerRows
156
+ const classNames = [
157
+ "row_heading",
158
+ "level" + columnIndex,
159
+ "row" + dataRowIndex,
160
+ ]
161
+
162
+ return {
163
+ type: "index",
164
+ id: `T_${this.uuid}level${columnIndex}_row${dataRowIndex}`,
165
+ classNames: classNames.join(" "),
166
+ content: this.getContent(this.indexTable, dataRowIndex, columnIndex),
167
+ }
168
+ } else {
169
+ const dataRowIndex = rowIndex - this.headerRows
170
+ const dataColumnIndex = columnIndex - this.headerColumns
171
+ const classNames = [
172
+ "data",
173
+ "row" + dataRowIndex,
174
+ "col" + dataColumnIndex,
175
+ ]
176
+ const content = this.styler
177
+ ? this.getContent(
178
+ this.styler.displayValuesTable,
179
+ dataRowIndex,
180
+ dataColumnIndex
181
+ )
182
+ : this.getContent(this.dataTable, dataRowIndex, dataColumnIndex)
183
+
184
+ return {
185
+ type: "data",
186
+ id: `T_${this.uuid}row${dataRowIndex}_col${dataColumnIndex}`,
187
+ classNames: classNames.join(" "),
188
+ content,
189
+ }
190
+ }
191
+ }
192
+
193
+ public getContent = (
194
+ table: Table,
195
+ rowIndex: number,
196
+ columnIndex: number
197
+ ): any => {
198
+ const column = table.getColumnAt(columnIndex)
199
+ if (column === null) {
200
+ return ""
201
+ }
202
+
203
+ const columnTypeId = this.getColumnTypeId(table, columnIndex)
204
+ switch (columnTypeId) {
205
+ case Type.Timestamp: {
206
+ return this.nanosToDate(column.get(rowIndex))
207
+ }
208
+ default: {
209
+ return column.get(rowIndex)
210
+ }
211
+ }
212
+ }
213
+
214
+ /**
215
+ * Returns apache-arrow specific typeId of column.
216
+ */
217
+ private getColumnTypeId(table: Table, columnIndex: number): Type {
218
+ return table.schema.fields[columnIndex].type.typeId
219
+ }
220
+
221
+ private nanosToDate(nanos: number): Date {
222
+ return new Date(nanos / 1e6)
223
+ }
224
+ }
streamlit_observable/frontend/src/streamlit/StreamlitReact.tsx ADDED
@@ -0,0 +1,150 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import hoistNonReactStatics from "hoist-non-react-statics"
2
+ import React, { ReactNode } from "react"
3
+ import { RenderData, Streamlit } from "./streamlit"
4
+
5
+ /**
6
+ * Props passed to custom Streamlit components.
7
+ */
8
+ export interface ComponentProps {
9
+ /** Named dictionary of arguments passed from Python. */
10
+ args: any
11
+
12
+ /** The component's width. */
13
+ width: number
14
+
15
+ /**
16
+ * True if the component should be disabled.
17
+ * All components get disabled while the app is being re-run,
18
+ * and become re-enabled when the re-run has finished.
19
+ */
20
+ disabled: boolean
21
+ }
22
+
23
+ /**
24
+ * Optional Streamlit React-based component base class.
25
+ *
26
+ * You are not required to extend this base class to create a Streamlit
27
+ * component. If you decide not to extend it, you should implement the
28
+ * `componentDidMount` and `componentDidUpdate` functions in your own class,
29
+ * so that your plugin properly resizes.
30
+ */
31
+ export class StreamlitComponentBase<S = {}> extends React.PureComponent<
32
+ ComponentProps,
33
+ S
34
+ > {
35
+ public componentDidMount(): void {
36
+ // After we're rendered for the first time, tell Streamlit that our height
37
+ // has changed.
38
+ Streamlit.setFrameHeight()
39
+ }
40
+
41
+ public componentDidUpdate(): void {
42
+ // After we're updated, tell Streamlit that our height may have changed.
43
+ Streamlit.setFrameHeight()
44
+ }
45
+ }
46
+
47
+ /**
48
+ * Wrapper for React-based Streamlit components.
49
+ *
50
+ * Bootstraps the communication interface between Streamlit and the component.
51
+ */
52
+ export function withStreamlitConnection(
53
+ WrappedComponent: React.ComponentType<ComponentProps>
54
+ ): React.ComponentType {
55
+ interface WrapperProps { }
56
+
57
+ interface WrapperState {
58
+ renderData?: RenderData
59
+ componentError?: Error
60
+ }
61
+
62
+ class ComponentWrapper extends React.PureComponent<
63
+ WrapperProps,
64
+ WrapperState
65
+ > {
66
+ public constructor(props: WrapperProps) {
67
+ super(props)
68
+ this.state = {
69
+ renderData: undefined,
70
+ componentError: undefined,
71
+ }
72
+ }
73
+
74
+ /**
75
+ * Error boundary function. This will be called if our wrapped
76
+ * component throws an error. We store the caught error in our state,
77
+ * and display it in the next render().
78
+ */
79
+ public static getDerivedStateFromError = (
80
+ error: Error
81
+ ): Partial<WrapperState> => {
82
+ return { componentError: error }
83
+ }
84
+
85
+ public componentDidMount = (): void => {
86
+ // Set up event listeners, and signal to Streamlit that we're ready.
87
+ // We won't render the component until we receive the first RENDER_EVENT.
88
+ Streamlit.events.addEventListener(
89
+ Streamlit.RENDER_EVENT,
90
+ this.onRenderEvent
91
+ )
92
+ Streamlit.setComponentReady()
93
+ }
94
+
95
+ public componentDidUpdate = (prevProps: any): void => {
96
+ // If our child threw an error, we display it in render(). In this
97
+ // case, the child won't be mounted and therefore won't call
98
+ // `setFrameHeight` on its own. We do it here so that the rendered
99
+ // error will be visible.
100
+ if (this.state.componentError != null) {
101
+ Streamlit.setFrameHeight()
102
+ }
103
+ }
104
+
105
+ public componentWillUnmount = (): void => {
106
+ Streamlit.events.removeEventListener(
107
+ Streamlit.RENDER_EVENT,
108
+ this.onRenderEvent
109
+ )
110
+ }
111
+
112
+ /**
113
+ * Streamlit is telling this component to redraw.
114
+ * We save the render data in State, so that it can be passed to the
115
+ * component in our own render() function.
116
+ */
117
+ private onRenderEvent = (event: Event): void => {
118
+ // Update our state with the newest render data
119
+ const renderEvent = event as CustomEvent<RenderData>
120
+ this.setState({ renderData: renderEvent.detail })
121
+ }
122
+
123
+ public render = (): ReactNode => {
124
+ // If our wrapped component threw an error, display it.
125
+ if (this.state.componentError != null) {
126
+ return (
127
+ <div>
128
+ <h1>Component Error</h1>
129
+ <span>{this.state.componentError.message}</span>
130
+ </div>
131
+ )
132
+ }
133
+
134
+ // Don't render until we've gotten our first RENDER_EVENT from Streamlit.
135
+ if (this.state.renderData == null) {
136
+ return null
137
+ }
138
+
139
+ return (
140
+ <WrappedComponent
141
+ width={window.innerWidth}
142
+ disabled={this.state.renderData.disabled}
143
+ args={this.state.renderData.args}
144
+ />
145
+ )
146
+ }
147
+ }
148
+
149
+ return hoistNonReactStatics(ComponentWrapper, WrappedComponent)
150
+ }
streamlit_observable/frontend/src/streamlit/index.tsx ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * @license
3
+ * Copyright 2018-2020 Streamlit Inc.
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */
17
+
18
+ // Workaround for type-only exports:
19
+ // https://stackoverflow.com/questions/53728230/cannot-re-export-a-type-when-using-the-isolatedmodules-with-ts-3-2-2
20
+ import { ComponentProps as ComponentProps_ } from "./StreamlitReact"
21
+ import { RenderData as RenderData_ } from "./streamlit"
22
+
23
+ export {
24
+ StreamlitComponentBase,
25
+ withStreamlitConnection,
26
+ } from "./StreamlitReact"
27
+ export { ArrowTable } from "./ArrowTable"
28
+ export { Streamlit } from "./streamlit"
29
+ export type ComponentProps = ComponentProps_
30
+ export type RenderData = RenderData_
streamlit_observable/frontend/src/streamlit/streamlit.ts ADDED
@@ -0,0 +1,198 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * @license
3
+ * Copyright 2018-2020 Streamlit Inc.
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */
17
+
18
+ // Safari doesn't support the EventTarget class, so we use a shim.
19
+ import { EventTarget } from "event-target-shim"
20
+ import { ArrowDataframeProto, ArrowTable } from "./ArrowTable"
21
+
22
+ /** Data sent in the custom Streamlit render event. */
23
+ export interface RenderData {
24
+ args: any
25
+ disabled: boolean
26
+ }
27
+
28
+ /** Messages from Component -> Streamlit */
29
+ enum ComponentMessageType {
30
+ // A component sends this message when it's ready to receive messages
31
+ // from Streamlit. Streamlit won't send any messages until it gets this.
32
+ // Data: { apiVersion: number }
33
+ COMPONENT_READY = "streamlit:componentReady",
34
+
35
+ // The component has a new widget value. Send it back to Streamlit, which
36
+ // will then re-run the app.
37
+ // Data: { value: any }
38
+ SET_COMPONENT_VALUE = "streamlit:setComponentValue",
39
+
40
+ // The component has a new height for its iframe.
41
+ // Data: { height: number }
42
+ SET_FRAME_HEIGHT = "streamlit:setFrameHeight",
43
+ }
44
+
45
+ /**
46
+ * Streamlit communication API.
47
+ *
48
+ * Components can send data to Streamlit via the functions defined here,
49
+ * and receive data from Streamlit via the `events` property.
50
+ */
51
+ export class Streamlit {
52
+ /**
53
+ * The Streamlit component API version we're targetting.
54
+ * There's currently only 1!
55
+ */
56
+ public static readonly API_VERSION = 1
57
+
58
+ public static readonly RENDER_EVENT = "streamlit:render"
59
+
60
+ /** Dispatches events received from Streamlit. */
61
+ public static readonly events = new EventTarget()
62
+
63
+ private static registeredMessageListener = false
64
+ private static lastFrameHeight?: number
65
+
66
+ /**
67
+ * Tell Streamlit that the component is ready to start receiving data.
68
+ * Streamlit will defer emitting RENDER events until it receives the
69
+ * COMPONENT_READY message.
70
+ */
71
+ public static setComponentReady = (): void => {
72
+ if (!Streamlit.registeredMessageListener) {
73
+ // Register for message events if we haven't already
74
+ window.addEventListener("message", Streamlit.onMessageEvent)
75
+ Streamlit.registeredMessageListener = true
76
+ }
77
+
78
+ Streamlit.sendBackMsg(ComponentMessageType.COMPONENT_READY, {
79
+ apiVersion: Streamlit.API_VERSION,
80
+ })
81
+ }
82
+
83
+ /**
84
+ * Report the component's height to Streamlit.
85
+ * This should be called every time the component changes its DOM - that is,
86
+ * when it's first loaded, and any time it updates.
87
+ */
88
+ public static setFrameHeight = (height?: number): void => {
89
+ if (height === undefined) {
90
+ // `height` is optional. If undefined, it defaults to scrollHeight,
91
+ // which is the entire height of the element minus its border,
92
+ // scrollbar, and margin.
93
+ height = document.body.scrollHeight + 10;
94
+ }
95
+
96
+ if (height === Streamlit.lastFrameHeight) {
97
+ // Don't bother updating if our height hasn't changed.
98
+ return
99
+ }
100
+
101
+ Streamlit.lastFrameHeight = height
102
+ Streamlit.sendBackMsg(ComponentMessageType.SET_FRAME_HEIGHT, { height })
103
+ }
104
+
105
+ /**
106
+ * Set the component's value. This value will be returned to the Python
107
+ * script, and the script will be re-run.
108
+ *
109
+ * For example:
110
+ *
111
+ * JavaScript:
112
+ * Streamlit.setComponentValue("ahoy!")
113
+ *
114
+ * Python:
115
+ * value = st.my_component(...)
116
+ * st.write(value) # -> "ahoy!"
117
+ *
118
+ * The value must be serializable into JSON.
119
+ */
120
+ public static setComponentValue = (value: any): void => {
121
+ Streamlit.sendBackMsg(ComponentMessageType.SET_COMPONENT_VALUE, { value })
122
+ }
123
+
124
+ /** Receive a ForwardMsg from the Streamlit app */
125
+ private static onMessageEvent = (event: MessageEvent): void => {
126
+ const type = event.data["type"]
127
+ switch (type) {
128
+ case Streamlit.RENDER_EVENT:
129
+ Streamlit.onRenderMessage(event.data)
130
+ break
131
+ }
132
+ }
133
+
134
+ /**
135
+ * Handle an untyped Streamlit render event and redispatch it as a
136
+ * StreamlitRenderEvent.
137
+ */
138
+ private static onRenderMessage = (data: any): void => {
139
+ let args = data["args"]
140
+ if (args == null) {
141
+ console.error(
142
+ `Got null args in onRenderMessage. This should never happen`
143
+ )
144
+ args = {}
145
+ }
146
+
147
+ // Parse our dataframe arguments with arrow, and merge them into our args dict
148
+ const dataframeArgs =
149
+ data["dfs"] && data["dfs"].length > 0
150
+ ? Streamlit.argsDataframeToObject(data["dfs"])
151
+ : {}
152
+
153
+ args = {
154
+ ...args,
155
+ ...dataframeArgs,
156
+ }
157
+
158
+ const disabled = Boolean(data["disabled"])
159
+
160
+ // Dispatch a render event!
161
+ const eventData = { disabled, args }
162
+ const event = new CustomEvent<RenderData>(Streamlit.RENDER_EVENT, {
163
+ detail: eventData,
164
+ })
165
+ Streamlit.events.dispatchEvent(event)
166
+ }
167
+
168
+ private static argsDataframeToObject = (
169
+ argsDataframe: ArgsDataframe[]
170
+ ): object => {
171
+ const argsDataframeArrow = argsDataframe.map(
172
+ ({ key, value }: ArgsDataframe) => [key, Streamlit.toArrowTable(value)]
173
+ )
174
+ return Object.fromEntries(argsDataframeArrow)
175
+ }
176
+
177
+ private static toArrowTable = (df: ArrowDataframeProto): ArrowTable => {
178
+ const { data, index, columns } = df.data
179
+ return new ArrowTable(data, index, columns)
180
+ }
181
+
182
+ /** Post a message to the Streamlit app. */
183
+ private static sendBackMsg = (type: string, data?: any): void => {
184
+ window.parent.postMessage(
185
+ {
186
+ isStreamlitMessage: true,
187
+ type: type,
188
+ ...data,
189
+ },
190
+ "*"
191
+ )
192
+ }
193
+ }
194
+
195
+ interface ArgsDataframe {
196
+ key: string
197
+ value: ArrowDataframeProto
198
+ }
streamlit_observable/frontend/src/types.d.ts ADDED
@@ -0,0 +1 @@
 
 
1
+ declare module '@observablehq/runtime';