add component code
Browse files- streamlit_observable/frontend/src/Observable.tsx +141 -0
- streamlit_observable/frontend/src/index.tsx +10 -0
- streamlit_observable/frontend/src/react-app-env.d.ts +1 -0
- streamlit_observable/frontend/src/streamlit/ArrowTable.ts +224 -0
- streamlit_observable/frontend/src/streamlit/StreamlitReact.tsx +150 -0
- streamlit_observable/frontend/src/streamlit/index.tsx +30 -0
- streamlit_observable/frontend/src/streamlit/streamlit.ts +198 -0
- streamlit_observable/frontend/src/types.d.ts +1 -0
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';
|