diff --git a/frontend/app/block/block.tsx b/frontend/app/block/block.tsx index 135d3562c..3a99550e7 100644 --- a/frontend/app/block/block.tsx +++ b/frontend/app/block/block.tsx @@ -7,6 +7,7 @@ import { atoms, blockDataMap, removeBlockFromTab } from "@/store/global"; import { TerminalView } from "@/app/view/term"; import { PreviewView } from "@/app/view/preview"; +import { PlotView } from "@/app/view/plotview"; import { CenteredLoadingDiv } from "@/element/quickelems"; import "./block.less"; @@ -37,6 +38,8 @@ const Block = ({ tabId, blockId }: { tabId: string; blockId: string }) => { blockElem = ; } else if (blockData.view === "preview") { blockElem = ; + } else if (blockData.view === "plot") { + blockElem = ; } return (
diff --git a/frontend/app/element/modal.less b/frontend/app/element/modal.less new file mode 100644 index 000000000..eda7cc8a3 --- /dev/null +++ b/frontend/app/element/modal.less @@ -0,0 +1,55 @@ +.modal-container { + position: absolute; + top: 0; + left: 0; + width: 100vw; + height: 100vh; + z-index: 100; + background-color: rgba(21, 23, 21, 0.7); + + .modal { + display: flex; + flex-direction: column; + border-radius: 10px; + padding: 0; + width: 80vw; + height: 80vh; + margin-top: 25vh; + margin-left: auto; + margin-right: auto; + background: var(--modal-bg-color); + border: 1px solid var(--app-border-color); + + .modal-header { + display: flex; + flex-direction: column; + padding: 20px 20px 10px; + border-bottom: 1px solid var(--modal-header-bottom-border-color); + + .modal-title { + margin: 0 0 5px; + color: var(--app-text-primary-color); + font-size: var(--title-font-size); + } + + p { + margin: 0; + font-size: 0.8rem; + color: var(--app-text-secondary-color); + } + } + + .modal-content { + padding: 20px; + overflow: auto; + } + + .modal-footer { + display: flex; + flex-direction: row; + justify-content: flex-end; + padding: 15px 20px; + gap: 20px; + } + } +} diff --git a/frontend/app/element/modal.tsx b/frontend/app/element/modal.tsx new file mode 100644 index 000000000..d0a7a2268 --- /dev/null +++ b/frontend/app/element/modal.tsx @@ -0,0 +1,80 @@ +import React from "react"; +import { Button } from "@/element/button"; + +import "./modal.less"; + +interface ModalProps { + id?: string; + children: React.ReactNode; + onClickOut: () => void; +} + +function Modal({ children, onClickOut, id = "modal", ...otherProps }: ModalProps) { + const handleOutsideClick = (e: React.SyntheticEvent) => { + if (typeof onClickOut === "function" && (e.target as Element).className === "modal-container") { + onClickOut(); + } + }; + + return ( +
+ + {children} + +
+ ); +} + +interface ModalContentProps { + children: React.ReactNode; +} + +function ModalContent({ children }: ModalContentProps) { + return
{children}
; +} + +interface ModalHeaderProps { + title: React.ReactNode; + description?: string; +} + +function ModalHeader({ title, description }: ModalHeaderProps) { + return ( +
+ {typeof title === "string" ?

{title}

: title} + {description &&

{description}

} +
+ ); +} + +interface ModalFooterProps { + children: React.ReactNode; +} + +function ModalFooter({ children }: ModalFooterProps) { + return
{children}
; +} + +interface WaveModalProps { + title: string; + description?: string; + id?: string; + onSubmit: () => void; + onCancel: () => void; + buttonLabel?: string; + children: React.ReactNode; +} + +function WaveModal({ title, description, onSubmit, onCancel, buttonLabel = "Ok", children }: WaveModalProps) { + return ( + + + {children} + + + + + ); +} + +export { WaveModal }; diff --git a/frontend/app/view/plotview.less b/frontend/app/view/plotview.less new file mode 100644 index 000000000..616fd714d --- /dev/null +++ b/frontend/app/view/plotview.less @@ -0,0 +1,19 @@ +.plot-view { + width: 100%; + + .plot-window { + display: flex; + justify-content: center; + } + + .plot-config { + width: 100%; + margin: 0; + padding: 0; + resize: none; + height: 50vh; + background-color: var(--panel-bg-color); + color: var(--main-text-color); + font: var(--fixed-font); + } +} diff --git a/frontend/app/view/plotview.tsx b/frontend/app/view/plotview.tsx new file mode 100644 index 000000000..e6ea95ef7 --- /dev/null +++ b/frontend/app/view/plotview.tsx @@ -0,0 +1,133 @@ +import * as React from "react"; +import * as Plot from "@observablehq/plot"; +import * as d3 from "d3"; +import { Button } from "@/element/button"; +import { WaveModal } from "@/element/modal"; + +import "./plotview.less"; + +function PlotWindow() { + return
; +} + +function PlotConfig() { + return ; +} + +const AsyncFunction = Object.getPrototypeOf(async function () {}).constructor; + +function evalAsync(Plot: any, d3: any, funcText: string): Promise { + return new Promise((resolve, reject) => { + new AsyncFunction( + "resolve", + "reject", + "Plot", + "d3", + `try { await ${funcText}; resolve(); } catch(e) { reject(e); } }` + )(resolve, reject, Plot, d3); + }); +} + +function PlotView() { + const containerRef = React.useRef(); + const [plotDef, setPlotDef] = React.useState(); + const [savedDef, setSavedDef] = React.useState(); + const [modalUp, setModalUp] = React.useState(false); + /* + const [data, setData] = React.useState(); + + React.useEffect(() => { + d3.csv("/plotdata/congress.csv", d3.autoType).then(setData); + }, []); + */ + + React.useEffect(() => { + // replace start + /* + d3.csv("/plotdata/congress.csv", d3.autoType).then((out) => data = out); + return Plot.plot({ + aspectRatio: 1, + x: { label: "Age (years)" }, + y: { + grid: true, + label: "← Women · Men →", + labelAnchor: "center", + tickFormat: Math.abs, + }, + marks: [ + Plot.dot( + data, + Plot.stackY2({ + x: (d) => 2023 - d.birthday.getUTCFullYear(), + y: (d) => (d.gender === "M" ? 1 : -1), + fill: "gender", + title: "full_name", + }) + ), + Plot.ruleY([0]), + ], + }); + */ + // replace end + let plot; + let plotErr; + try { + console.log(plotDef); + plot = new Function("Plot", "d3", plotDef)(Plot, d3); + //plot = new Function("Plot", "data", "d3", plotDef)(Plot, data, d3); + //evalAsync(Plot, d3, plotDef).then((out) => (plot = out)); + } catch (e) { + plotErr = e; + console.log("error: ", e); + return; + } + console.log(plot); + + if (plot !== undefined) { + containerRef.current.append(plot); + } else { + // todo + } + + return () => { + if (plot !== undefined) { + plot.remove(); + } + }; + }, [plotDef]); + + const handleOpen = React.useCallback(() => { + setSavedDef(plotDef); + setModalUp(true); + }, []); + + const handleCancel = React.useCallback(() => { + setPlotDef(savedDef); + setModalUp(false); + }, []); + + const handleSave = React.useCallback(() => { + setModalUp(false); + }, []); + + return ( +
+ +
+ {modalUp && ( + +