playground: manual play mode

This commit is contained in:
Vyacheslav Matyukhin 2022-06-26 21:15:09 +03:00
parent 2c2f299e46
commit 9208330038
No known key found for this signature in database
GPG Key ID: 3D2A774C5489F96C
3 changed files with 180 additions and 67 deletions

View File

@ -48,57 +48,59 @@ export interface SquiggleChartProps {
const defaultOnChange = () => {}; const defaultOnChange = () => {};
export const SquiggleChart: React.FC<SquiggleChartProps> = ({ export const SquiggleChart: React.FC<SquiggleChartProps> = React.memo(
code = "", ({
environment, code = "",
onChange = defaultOnChange, // defaultOnChange must be constant, don't move its definition here
height = 200,
bindings = defaultBindings,
jsImports = defaultImports,
showSummary = false,
width,
showTypes = false,
showControls = false,
logX = false,
expY = false,
diagramStart = 0,
diagramStop = 10,
diagramCount = 100,
}) => {
const result = useSquiggle({
code,
bindings,
environment, environment,
jsImports, onChange = defaultOnChange, // defaultOnChange must be constant, don't move its definition here
onChange, height = 200,
}); bindings = defaultBindings,
jsImports = defaultImports,
showSummary = false,
width,
showTypes = false,
showControls = false,
logX = false,
expY = false,
diagramStart = 0,
diagramStop = 10,
diagramCount = 100,
}) => {
const result = useSquiggle({
code,
bindings,
environment,
jsImports,
onChange,
});
if (result.tag !== "Ok") { if (result.tag !== "Ok") {
return <SquiggleErrorAlert error={result.value} />; return <SquiggleErrorAlert error={result.value} />;
}
let distributionPlotSettings = {
showControls,
showSummary,
logX,
expY,
};
let chartSettings = {
start: diagramStart,
stop: diagramStop,
count: diagramCount,
};
return (
<SquiggleItem
expression={result.value}
width={width}
height={height}
distributionPlotSettings={distributionPlotSettings}
showTypes={showTypes}
chartSettings={chartSettings}
environment={environment ?? defaultEnvironment}
/>
);
} }
);
let distributionPlotSettings = {
showControls,
showSummary,
logX,
expY,
};
let chartSettings = {
start: diagramStart,
stop: diagramStop,
count: diagramCount,
};
return (
<SquiggleItem
expression={result.value}
width={width}
height={height}
distributionPlotSettings={distributionPlotSettings}
showTypes={showTypes}
chartSettings={chartSettings}
environment={environment ?? defaultEnvironment}
/>
);
};

View File

@ -1,4 +1,4 @@
import React, { FC, Fragment, useState, useEffect } from "react"; import React, { FC, Fragment, useState, useEffect, useMemo } from "react";
import { Path, useForm, UseFormRegister, useWatch } from "react-hook-form"; import { Path, useForm, UseFormRegister, useWatch } from "react-hook-form";
import * as yup from "yup"; import * as yup from "yup";
import { useMaybeControlledValue } from "../lib/hooks"; import { useMaybeControlledValue } from "../lib/hooks";
@ -6,10 +6,14 @@ import { yupResolver } from "@hookform/resolvers/yup";
import { Tab } from "@headlessui/react"; import { Tab } from "@headlessui/react";
import { import {
ChartSquareBarIcon, ChartSquareBarIcon,
CheckCircleIcon,
CodeIcon, CodeIcon,
CogIcon, CogIcon,
CurrencyDollarIcon, CurrencyDollarIcon,
EyeIcon, EyeIcon,
PauseIcon,
PlayIcon,
RefreshIcon,
} from "@heroicons/react/solid"; } from "@heroicons/react/solid";
import clsx from "clsx"; import clsx from "clsx";
@ -20,6 +24,7 @@ import { CodeEditor } from "./CodeEditor";
import { JsonEditor } from "./JsonEditor"; import { JsonEditor } from "./JsonEditor";
import { ErrorAlert, SuccessAlert } from "./Alert"; import { ErrorAlert, SuccessAlert } from "./Alert";
import { SquiggleContainer } from "./SquiggleContainer"; import { SquiggleContainer } from "./SquiggleContainer";
import { Toggle } from "./ui/Toggle";
interface PlaygroundProps { interface PlaygroundProps {
/** The initial squiggle string to put in the playground */ /** The initial squiggle string to put in the playground */
@ -110,7 +115,7 @@ type StyledTabProps = {
const StyledTab: React.FC<StyledTabProps> = ({ name, icon: Icon }) => { const StyledTab: React.FC<StyledTabProps> = ({ name, icon: Icon }) => {
return ( return (
<Tab key={name} as={Fragment}> <Tab as={Fragment}>
{({ selected }) => ( {({ selected }) => (
<button className="group flex rounded-md focus:outline-none focus-visible:ring-offset-gray-100"> <button className="group flex rounded-md focus:outline-none focus-visible:ring-offset-gray-100">
<span <span
@ -204,6 +209,53 @@ function Checkbox<T>({
); );
} }
const PlayControls: React.FC<{
autoplay: boolean;
isStale: boolean;
onAutoplayChange: (value: boolean) => void;
onPlay: () => void;
}> = ({ autoplay, isStale, onAutoplayChange, onPlay }) => {
const [playing, setPlaying] = useState(false);
const play = () => {
setPlaying(true);
};
// this is tricky; we need to render PlayControls first to make sure that the icon is spinning,
// and only then call onPlay (which freezes the UI)
useEffect(() => {
if (!autoplay && playing) {
onPlay();
setPlaying(false);
}
// don't add onPlay to the deps below
}, [autoplay, playing]); // eslint-disable-line react-hooks/exhaustive-deps
const CurrentPlayIcon = playing ? RefreshIcon : PlayIcon;
return (
<div className="flex space-x-1 items-center">
{autoplay ? null : (
<button onClick={play}>
<CurrentPlayIcon
className={clsx(
"w-8 h-8",
playing && "animate-spin",
isStale ? "text-indigo-500" : "text-gray-400"
)}
/>
</button>
)}
<Toggle
texts={["Autoplay", "Paused"]}
icons={[CheckCircleIcon, PauseIcon]}
status={autoplay}
onChange={onAutoplayChange}
/>
</div>
);
};
export const SquigglePlayground: FC<PlaygroundProps> = ({ export const SquigglePlayground: FC<PlaygroundProps> = ({
defaultCode = "", defaultCode = "",
height = 500, height = 500,
@ -225,7 +277,14 @@ export const SquigglePlayground: FC<PlaygroundProps> = ({
const [importString, setImportString] = useState("{}"); const [importString, setImportString] = useState("{}");
const [imports, setImports] = useState({}); const [imports, setImports] = useState({});
const [importsAreValid, setImportsAreValid] = useState(true); const [importsAreValid, setImportsAreValid] = useState(true);
const { register, control } = useForm({
const [renderedCode, setRenderedCode] = useState(""); // used only if autoplay is false
const {
register,
control,
setValue: setFormValue,
} = useForm({
resolver: yupResolver(schema), resolver: yupResolver(schema),
defaultValues: { defaultValues: {
sampleCount: 1000, sampleCount: 1000,
@ -242,6 +301,7 @@ export const SquigglePlayground: FC<PlaygroundProps> = ({
diagramStart: 0, diagramStart: 0,
diagramStop: 10, diagramStop: 10,
diagramCount: 20, diagramCount: 20,
autoplay: true,
}, },
}); });
const vars = useWatch({ const vars = useWatch({
@ -252,10 +312,14 @@ export const SquigglePlayground: FC<PlaygroundProps> = ({
onSettingsChange?.(vars); onSettingsChange?.(vars);
}, [vars, onSettingsChange]); }, [vars, onSettingsChange]);
const env: environment = { const env: environment = useMemo(
sampleCount: Number(vars.sampleCount), () => ({
xyPointLength: Number(vars.xyPointLength), sampleCount: Number(vars.sampleCount),
}; xyPointLength: Number(vars.xyPointLength),
}),
[vars.sampleCount, vars.xyPointLength]
);
const getChangeJson = (r: string) => { const getChangeJson = (r: string) => {
setImportString(r); setImportString(r);
try { try {
@ -418,7 +482,7 @@ export const SquigglePlayground: FC<PlaygroundProps> = ({
const squiggleChart = ( const squiggleChart = (
<SquiggleChart <SquiggleChart
code={code} code={vars.autoplay ? code : renderedCode}
environment={env} environment={env}
diagramStart={Number(vars.diagramStart)} diagramStart={Number(vars.diagramStart)}
diagramStop={Number(vars.diagramStop)} diagramStop={Number(vars.diagramStop)}
@ -470,15 +534,28 @@ export const SquigglePlayground: FC<PlaygroundProps> = ({
<SquiggleContainer> <SquiggleContainer>
<Tab.Group> <Tab.Group>
<div className="pb-4"> <div className="pb-4">
<Tab.List className="flex w-fit p-0.5 mt-2 rounded-md bg-slate-100 hover:bg-slate-200"> <div className="flex justify-between items-center mt-2">
<StyledTab <Tab.List className="flex w-fit p-0.5 rounded-md bg-slate-100 hover:bg-slate-200">
name={vars.showEditor ? "Code" : "Display"} <StyledTab
icon={vars.showEditor ? CodeIcon : EyeIcon} name={vars.showEditor ? "Code" : "Display"}
icon={vars.showEditor ? CodeIcon : EyeIcon}
/>
<StyledTab name="Sampling Settings" icon={CogIcon} />
<StyledTab name="View Settings" icon={ChartSquareBarIcon} />
<StyledTab name="Input Variables" icon={CurrencyDollarIcon} />
</Tab.List>
<PlayControls
autoplay={vars.autoplay || false}
isStale={!vars.autoplay && renderedCode !== code}
onPlay={() => {
setRenderedCode(code);
}}
onAutoplayChange={(newValue) => {
if (!newValue) setRenderedCode(code);
setFormValue("autoplay", newValue);
}}
/> />
<StyledTab name="Sampling Settings" icon={CogIcon} /> </div>
<StyledTab name="View Settings" icon={ChartSquareBarIcon} />
<StyledTab name="Input Variables" icon={CurrencyDollarIcon} />
</Tab.List>
{vars.showEditor ? withEditor : withoutEditor} {vars.showEditor ? withEditor : withoutEditor}
</div> </div>
</Tab.Group> </Tab.Group>

View File

@ -0,0 +1,34 @@
import clsx from "clsx";
import React from "react";
type IconType = (props: React.ComponentProps<"svg">) => JSX.Element;
type Props = {
status: boolean;
onChange: (status: boolean) => void;
texts: [string, string];
icons: [IconType, IconType];
};
export const Toggle: React.FC<Props> = ({
texts: [onText, offText],
icons: [OnIcon, OffIcon],
status,
onChange,
}) => {
const CurrentIcon = status ? OnIcon : OffIcon;
return (
<button
className={clsx(
"rounded-full py-1 bg-indigo-500 text-white text-xs font-semibold flex items-center space-x-1",
status ? "bg-indigo-500" : "bg-gray-400",
status ? "pl-1 pr-3" : "pl-3 pr-1",
!status && "flex-row-reverse space-x-reverse"
)}
onClick={() => onChange(!status)}
>
<CurrentIcon className="w-6 h-6" />
<span>{status ? onText : offText}</span>
</button>
);
};