Spaces:
Running
Running
Download frontends/react/src/useAppLogic.ts from nhamid289/decision_boundary-dev: direct link, hf CLI and curl.
- Browser
- Download file 7.15 kB
-
https://huggingface.co/spaces/nhamid289/decision_boundary-dev/resolve/main/frontends/react/src/useAppLogic.ts
- Command line
-
hf download hf://spaces/nhamid289/decision_boundary-dev/frontends/react/src/useAppLogic.ts
-
curl -L -o useAppLogic.ts https://huggingface.co/spaces/nhamid289/decision_boundary-dev/resolve/main/frontends/react/src/useAppLogic.ts
7.15 kB
| import { useEffect, useState, useRef } from "react"; | |
| import { usePythonStatus, type Result } from "@elvis/pyodide"; | |
| import type { Dataset, ModelResults } from "@elvis/ui"; | |
| import type { ModelSettings, DataPoints, DecisionBoundarySettings, DecisionBoundaryResult } from "./types.ts"; | |
| import { LabelColors } from "./colors.ts"; | |
| import { python } from "./python.ts"; | |
| const DEFAULT_RESOLUTION = 500; | |
| const DEFAULT_X_RANGE: [number, number] = [-10, 10]; | |
| const DEFAULT_Y_RANGE: [number, number] = [-10, 10]; | |
| // With auto-fit on, wait this long after a model setting stops changing | |
| const AUTO_FIT_DELAY_MS = 500; | |
| // The axis range covering values, with 10% padding on each side | |
| function paddedRange(values: number[]): [number, number] { | |
| const min = Math.min(...values); | |
| const max = Math.max(...values); | |
| const pad = max > min ? 0.1 * (max - min) : 1; | |
| return [min - pad, max + pad]; | |
| } | |
| // Returns the call's data, or null on error. User errors go to showUserError | |
| // if given; everything else is logged. | |
| function dataOf<T>(result: Result<T>, showUserError?: (message: string) => void): T | null { | |
| if (result.ok) { | |
| return result.data; | |
| } | |
| if (result.error.kind === "user" && showUserError) { | |
| showUserError(result.error.message); | |
| } else { | |
| console.error(`Python ${result.error.kind} error: ${result.error.message}`); | |
| } | |
| return null; | |
| } | |
| export default function useAppLogic() { | |
| const pythonStatus = usePythonStatus(python); | |
| const [dataPoints, setDataPoints] = useState<DataPoints>({ | |
| xPoints: [], | |
| yPoints: [], | |
| labels: [], | |
| }); | |
| const labelColors = new LabelColors(dataPoints.labels); | |
| const [resolution, _] = useState<number>(DEFAULT_RESOLUTION); // todo user defined | |
| const xRangeRef = useRef<[number, number]>(DEFAULT_X_RANGE); | |
| const yRangeRef = useRef<[number, number]>(DEFAULT_Y_RANGE); | |
| function handleRangeChange(xRange: [number, number], yRange: [number, number]) { | |
| xRangeRef.current = xRange; | |
| yRangeRef.current = yRange; | |
| updateDecisionBoundary(); | |
| } | |
| const [modelSettings, setModelSettings] = useState<ModelSettings>({ | |
| type: "logistic_regression", | |
| params: {}, | |
| }); | |
| const [modelError, setModelError] = useState<string | null>(null); | |
| // From the last fit; cleared when the data or model changes without a refit | |
| const [modelResults, setModelResults] = useState<ModelResults | null>(null); | |
| // Refit whenever the data or model changes, instead of waiting for the button | |
| const [autoFit, setAutoFit] = useState(true); | |
| const autoFitTimer = useRef<ReturnType<typeof setTimeout> | null>(null); | |
| // Counts dataset changes, so only the newest one triggers a refit | |
| const datasetVersion = useRef(0); | |
| // After the data or model changes: refit if auto-fit is on and there are | |
| // two classes to separate, otherwise just redraw (an unfitted model draws nothing) | |
| async function refresh(labels: string[]) { | |
| if (autoFit && new Set(labels).size >= 2) { | |
| await handleGetDecisionBoundary(); | |
| return; | |
| } | |
| if (autoFit) { | |
| setModelError(null); | |
| } | |
| setModelResults(null); | |
| await updateDecisionBoundary(); | |
| } | |
| function handleAutoFitChange(on: boolean) { | |
| setAutoFit(on); | |
| if (on && new Set(dataPoints.labels).size >= 2) { | |
| handleGetDecisionBoundary(); | |
| } | |
| } | |
| async function handleModelSettingsChange(settings: ModelSettings) { | |
| // A setting changes with each keystroke; a new model doesn't | |
| const settingChanged = settings.type === modelSettings.type; | |
| setModelSettings(settings); | |
| // The old results no longer describe the model; with auto-fit they stay until the refit | |
| if (!autoFit) { | |
| setModelResults(null); | |
| } | |
| dataOf(await python.call("handle_set_model_config", settings)); | |
| if (!autoFit) { | |
| await updateDecisionBoundary(); | |
| return; | |
| } | |
| // Wait for a pause so half-typed values don't show errors | |
| if (autoFitTimer.current) { | |
| clearTimeout(autoFitTimer.current); | |
| } | |
| autoFitTimer.current = setTimeout(() => refresh(dataPoints.labels), settingChanged ? AUTO_FIT_DELAY_MS : 0); | |
| } | |
| const [decisionBoundaryResult, setDecisionBoundaryResult] = useState<DecisionBoundaryResult | null>(null); | |
| // this is needed for svg export | |
| const plotHandle = useRef<Plotly.PlotlyHTMLElement | null>(null); | |
| // The dataset from the picker: plot its projected points and send the full | |
| // inputs to Python. fitView zooms to the points; an empty plot resets the view. | |
| async function handleSetDataset(dataset: Dataset | null, fitView: boolean) { | |
| if (!autoFit) { | |
| setModelResults(null); | |
| } | |
| const labels = dataset ? dataset.targets.map(String) : []; | |
| const next = { xPoints: dataset?.points.x ?? [], yPoints: dataset?.points.y ?? [], labels }; | |
| if (labels.length === 0) { | |
| xRangeRef.current = DEFAULT_X_RANGE; | |
| yRangeRef.current = DEFAULT_Y_RANGE; | |
| } else if (fitView) { | |
| xRangeRef.current = paddedRange(next.xPoints); | |
| yRangeRef.current = paddedRange(next.yPoints); | |
| } | |
| setDataPoints(next); | |
| // A new table's labels don't match the old boundary, so don't show it meanwhile | |
| if (fitView) { | |
| setDecisionBoundaryResult(null); | |
| } | |
| const version = ++datasetVersion.current; | |
| dataOf(await python.call("handle_set_dataset", dataset | |
| ? { inputs: dataset.inputs, outputs: labels, input_names: dataset.inputNames, projection: dataset.projection } | |
| : { inputs: [], outputs: [] })); | |
| // A newer dataset is already on its way; let it do the refit | |
| if (version === datasetVersion.current) { | |
| await refresh(labels); | |
| } | |
| } | |
| async function updateDecisionBoundary() { | |
| const settings: DecisionBoundarySettings = { | |
| xmin: xRangeRef.current[0], | |
| xmax: xRangeRef.current[1], | |
| ymin: yRangeRef.current[0], | |
| ymax: yRangeRef.current[1], | |
| resolution: resolution, | |
| }; | |
| const result = dataOf(await python.call("handle_get_decision_boundary", settings), setModelError); | |
| if (result) { | |
| setDecisionBoundaryResult(result); | |
| } | |
| } | |
| async function handleGetDecisionBoundary() { | |
| const built = await python.call("handle_build_model"); | |
| setModelResults(dataOf(built, setModelError)); | |
| if (!built.ok) { | |
| return; | |
| } | |
| setModelError(null); | |
| await updateDecisionBoundary(); | |
| } | |
| function getPlotImage(format: "svg" | "png") { | |
| if (!plotHandle.current) { | |
| return; | |
| } | |
| (plotHandle.current as any)?.downloadImage(format) | |
| } | |
| console.log("rendering app"); | |
| // Python runs calls in the order they are made, so send these together | |
| // rather than awaiting each: user actions made while Python loads queue after them | |
| useEffect(() => { | |
| python.call("handle_set_model_config", modelSettings).then((result) => dataOf(result)); | |
| updateDecisionBoundary(); | |
| }, []); | |
| return { | |
| pythonStatus, | |
| dataPoints, | |
| labelColors, | |
| xRangeRef, | |
| yRangeRef, | |
| handleRangeChange, | |
| modelSettings, | |
| handleModelSettingsChange, | |
| modelError, | |
| modelResults, | |
| autoFit, | |
| handleAutoFitChange, | |
| decisionBoundaryResult, | |
| handleSetDataset, | |
| handleGetDecisionBoundary, | |
| plotHandle, | |
| getPlotImage, | |
| } | |
| } | |