Various components cleanups

- coding style (let -> const, etc.)
- avoid double typing of component props, it's not necessary
- consistent non-default exports everywhere (except for
  SquigglePlayground since I'm not 100% sure it's not used somewhere
  else)
- extract common components in SquigglePlayground to avoid copy-paste
- other minor improvements
This commit is contained in:
Vyacheslav Matyukhin 2022-06-04 23:38:16 +03:00
parent 9b0def16ef
commit 958c187e82
No known key found for this signature in database
GPG Key ID: 3D2A774C5489F96C
12 changed files with 429 additions and 445 deletions

View File

@ -10,23 +10,32 @@ export const Alert: React.FC<{
backgroundColor: string; backgroundColor: string;
headingColor: string; headingColor: string;
bodyColor: string; bodyColor: string;
icon: React.ReactNode; icon: (props: React.ComponentProps<"svg">) => JSX.Element;
children: React.ReactNode; iconColor: string;
children?: React.ReactNode;
}> = ({ }> = ({
heading = "Error", heading = "Error",
backgroundColor, backgroundColor,
headingColor, headingColor,
bodyColor, bodyColor,
icon, icon: Icon,
iconColor,
children, children,
}) => { }) => {
return ( return (
<div className={`rounded-md p-4 ${backgroundColor}`}> <div className={`rounded-md p-4 ${backgroundColor}`}>
<div className="flex"> <div className="flex">
<div className="flex-shrink-0">{icon}</div> <Icon
className={`h-5 w-5 flex-shrink-0 ${iconColor}`}
aria-hidden="true"
/>
<div className="ml-3"> <div className="ml-3">
<h3 className={`text-sm font-medium ${headingColor}`}>{heading}</h3> <header className={`text-sm font-medium ${headingColor}`}>
{heading}
</header>
{children && React.Children.count(children) ? (
<div className={`mt-2 text-sm ${bodyColor}`}>{children}</div> <div className={`mt-2 text-sm ${bodyColor}`}>{children}</div>
) : null}
</div> </div>
</div> </div>
</div> </div>
@ -35,49 +44,42 @@ export const Alert: React.FC<{
export const ErrorAlert: React.FC<{ export const ErrorAlert: React.FC<{
heading: string; heading: string;
children: React.ReactNode; children?: React.ReactNode;
}> = ({ heading = "Error", children }) => ( }> = (props) => (
<Alert <Alert
heading={heading} {...props}
children={children}
backgroundColor="bg-red-100" backgroundColor="bg-red-100"
headingColor="text-red-800" headingColor="text-red-800"
bodyColor="text-red-700" bodyColor="text-red-700"
icon={<XCircleIcon className="h-5 w-5 text-red-400" aria-hidden="true" />} icon={XCircleIcon}
iconColor="text-red-400"
/> />
); );
export const MessageAlert: React.FC<{ export const MessageAlert: React.FC<{
heading: string; heading: string;
children: React.ReactNode; children?: React.ReactNode;
}> = ({ heading = "Error", children }) => ( }> = (props) => (
<Alert <Alert
heading={heading} {...props}
children={children}
backgroundColor="bg-slate-100" backgroundColor="bg-slate-100"
headingColor="text-slate-700" headingColor="text-slate-700"
bodyColor="text-slate-700" bodyColor="text-slate-700"
icon={ icon={InformationCircleIcon}
<InformationCircleIcon iconColor="text-slate-400"
className="h-5 w-5 text-slate-400"
aria-hidden="true"
/>
}
/> />
); );
export const SuccessAlert: React.FC<{ export const SuccessAlert: React.FC<{
heading: string; heading: string;
children: React.ReactNode; children?: React.ReactNode;
}> = ({ heading = "Error", children }) => ( }> = (props) => (
<Alert <Alert
heading={heading} {...props}
children={children}
backgroundColor="bg-green-50" backgroundColor="bg-green-50"
headingColor="text-green-800" headingColor="text-green-800"
bodyColor="text-green-700" bodyColor="text-green-700"
icon={ icon={CheckCircleIcon}
<CheckCircleIcon className="h-5 w-5 text-green-400" aria-hidden="true" /> iconColor="text-green-400"
}
/> />
); );

View File

@ -14,13 +14,13 @@ interface CodeEditorProps {
showGutter?: boolean; showGutter?: boolean;
} }
export let CodeEditor: FC<CodeEditorProps> = ({ export const CodeEditor: FC<CodeEditorProps> = ({
value, value,
onChange, onChange,
oneLine = false, oneLine = false,
showGutter = false, showGutter = false,
height, height,
}: CodeEditorProps) => { }) => {
let lineCount = value.split("\n").length; let lineCount = value.split("\n").length;
let id = _.uniqueId(); let id = _.uniqueId();
return ( return (
@ -48,4 +48,3 @@ export let CodeEditor: FC<CodeEditorProps> = ({
/> />
); );
}; };
export default CodeEditor;

View File

@ -1,5 +1,4 @@
import * as React from "react"; import * as React from "react";
import _ from "lodash";
import { import {
Distribution, Distribution,
result, result,
@ -34,16 +33,24 @@ export const DistributionChart: React.FC<DistributionChartProps> = ({
showSummary, showSummary,
width, width,
showControls = false, showControls = false,
}: DistributionChartProps) => { }) => {
let [isLogX, setLogX] = React.useState(false); const [isLogX, setLogX] = React.useState(false);
let [isExpY, setExpY] = React.useState(false); const [isExpY, setExpY] = React.useState(false);
let shape = distribution.pointSet(); const shape = distribution.pointSet();
const [sized, _] = useSize((size) => { const [sized] = useSize((size) => {
if (shape.tag === "Ok") { if (shape.tag === "Error") {
let massBelow0 = return (
<ErrorAlert heading="Distribution Error">
{distributionErrorToString(shape.value)}
</ErrorAlert>
);
}
const massBelow0 =
shape.value.continuous.some((x) => x.x <= 0) || shape.value.continuous.some((x) => x.x <= 0) ||
shape.value.discrete.some((x) => x.x <= 0); shape.value.discrete.some((x) => x.x <= 0);
let spec = buildVegaSpec(isLogX, isExpY); const spec = buildVegaSpec(isLogX, isExpY);
let widthProp = width ? width : size.width; let widthProp = width ? width : size.width;
if (widthProp < 20) { if (widthProp < 20) {
console.warn( console.warn(
@ -52,26 +59,8 @@ export const DistributionChart: React.FC<DistributionChartProps> = ({
widthProp = 20; widthProp = 20;
} }
// Check whether we should disable the checkbox return (
var logCheckbox = ( <div style={{ width: widthProp }}>
<CheckBox label="Log X scale" value={isLogX} onChange={setLogX} />
);
if (massBelow0) {
logCheckbox = (
<CheckBox
label="Log X scale"
value={isLogX}
onChange={setLogX}
disabled={true}
tooltip={
"Your distribution has mass lower than or equal to 0. Log only works on strictly positive values."
}
/>
);
}
var result = (
<div style={{ width: widthProp + "px" }}>
<Vega <Vega
spec={spec} spec={spec}
data={{ con: shape.value.continuous, dis: shape.value.discrete }} data={{ con: shape.value.continuous, dis: shape.value.discrete }}
@ -84,21 +73,24 @@ export const DistributionChart: React.FC<DistributionChartProps> = ({
</div> </div>
{showControls && ( {showControls && (
<div> <div>
{logCheckbox} <CheckBox
label="Log X scale"
value={isLogX}
onChange={setLogX}
// Check whether we should disable the checkbox
{...(massBelow0
? {
disabled: true,
tooltip:
"Your distribution has mass lower than or equal to 0. Log only works on strictly positive values.",
}
: {})}
/>
<CheckBox label="Exp Y scale" value={isExpY} onChange={setExpY} /> <CheckBox label="Exp Y scale" value={isExpY} onChange={setExpY} />
</div> </div>
)} )}
</div> </div>
); );
} else {
var result = (
<ErrorAlert heading="Distribution Error">
{distributionErrorToString(shape.value)}
</ErrorAlert>
);
}
return result;
}); });
return sized; return sized;
}; };
@ -121,13 +113,13 @@ interface CheckBoxProps {
tooltip?: string; tooltip?: string;
} }
export const CheckBox = ({ export const CheckBox: React.FC<CheckBoxProps> = ({
label, label,
onChange, onChange,
value, value,
disabled = false, disabled = false,
tooltip, tooltip,
}: CheckBoxProps) => { }) => {
return ( return (
<span title={tooltip}> <span title={tooltip}>
<input <input
@ -141,22 +133,35 @@ export const CheckBox = ({
); );
}; };
const TableHeadCell: React.FC<{ children: React.ReactNode }> = ({
children,
}) => (
<th className="border border-slate-200 bg-slate-50 py-1 px-2 text-slate-500 font-semibold">
{children}
</th>
);
const Cell: React.FC<{ children: React.ReactNode }> = ({ children }) => (
<td className="border border-slate-200 py-1 px-2 text-slate-900">
{children}
</td>
);
type SummaryTableProps = { type SummaryTableProps = {
distribution: Distribution; distribution: Distribution;
}; };
const SummaryTable: React.FC<SummaryTableProps> = ({ const SummaryTable: React.FC<SummaryTableProps> = ({ distribution }) => {
distribution, const mean = distribution.mean();
}: SummaryTableProps) => { const p5 = distribution.inv(0.05);
let mean = distribution.mean(); const p10 = distribution.inv(0.1);
let p5 = distribution.inv(0.05); const p25 = distribution.inv(0.25);
let p10 = distribution.inv(0.1); const p50 = distribution.inv(0.5);
let p25 = distribution.inv(0.25); const p75 = distribution.inv(0.75);
let p50 = distribution.inv(0.5); const p90 = distribution.inv(0.9);
let p75 = distribution.inv(0.75); const p95 = distribution.inv(0.95);
let p90 = distribution.inv(0.9);
let p95 = distribution.inv(0.95); const unwrapResult = (
let unwrapResult = (
x: result<number, distributionError> x: result<number, distributionError>
): React.ReactNode => { ): React.ReactNode => {
if (x.tag === "Ok") { if (x.tag === "Ok") {
@ -170,19 +175,6 @@ const SummaryTable: React.FC<SummaryTableProps> = ({
} }
}; };
let TableHeadCell: React.FC<{ children: React.ReactNode }> = ({
children,
}) => (
<th className="border border-slate-200 bg-slate-50 py-1 px-2 text-slate-500 font-semibold">
{children}
</th>
);
let Cell: React.FC<{ children: React.ReactNode }> = ({ children }) => (
<td className="border border-slate-200 py-1 px-2 text-slate-900 ">
{children}
</td>
);
return ( return (
<table className="border border-collapse border-slate-400"> <table className="border border-collapse border-slate-400">
<thead className="bg-slate-50"> <thead className="bg-slate-50">

View File

@ -22,7 +22,7 @@ export const FunctionChart: React.FC<FunctionChartProps> = ({
chartSettings, chartSettings,
environment, environment,
height, height,
}: FunctionChartProps) => { }) => {
if (fn.parameters.length > 1) { if (fn.parameters.length > 1) {
return ( return (
<MessageAlert heading="Function Display Not Supported"> <MessageAlert heading="Function Display Not Supported">

View File

@ -151,7 +151,7 @@ export const FunctionChart1Dist: React.FC<FunctionChart1DistProps> = ({
chartSettings, chartSettings,
environment, environment,
height, height,
}: FunctionChart1DistProps) => { }) => {
let [mouseOverlay, setMouseOverlay] = React.useState(0); let [mouseOverlay, setMouseOverlay] = React.useState(0);
function handleHover(_name: string, value: unknown) { function handleHover(_name: string, value: unknown) {
setMouseOverlay(value as number); setMouseOverlay(value as number);
@ -170,16 +170,14 @@ export const FunctionChart1Dist: React.FC<FunctionChart1DistProps> = ({
}, },
}; };
let showChart = let showChart =
mouseItem.tag === "Ok" && mouseItem.value.tag == "distribution" ? ( mouseItem.tag === "Ok" && mouseItem.value.tag === "distribution" ? (
<DistributionChart <DistributionChart
distribution={mouseItem.value.value} distribution={mouseItem.value.value}
width={400} width={400}
height={50} height={50}
showSummary={false} showSummary={false}
/> />
) : ( ) : null;
<></>
);
let getPercentilesMemoized = React.useMemo( let getPercentilesMemoized = React.useMemo(
() => getPercentiles({ chartSettings, fn, environment }), () => getPercentiles({ chartSettings, fn, environment }),

View File

@ -14,15 +14,15 @@ interface CodeEditorProps {
showGutter?: boolean; showGutter?: boolean;
} }
export let JsonEditor: FC<CodeEditorProps> = ({ export const JsonEditor: FC<CodeEditorProps> = ({
value, value,
onChange, onChange,
oneLine = false, oneLine = false,
showGutter = false, showGutter = false,
height, height,
}: CodeEditorProps) => { }) => {
let lineCount = value.split("\n").length; const lineCount = value.split("\n").length;
let id = _.uniqueId(); const id = _.uniqueId();
return ( return (
<AceEditor <AceEditor
value={value} value={value}
@ -47,5 +47,3 @@ export let JsonEditor: FC<CodeEditorProps> = ({
/> />
); );
}; };
export default JsonEditor;

View File

@ -1,5 +1,4 @@
import * as React from "react"; import * as React from "react";
import _ from "lodash";
const orderOfMagnitudeNum = (n: number) => { const orderOfMagnitudeNum = (n: number) => {
return Math.pow(10, n); return Math.pow(10, n);
@ -74,25 +73,23 @@ export interface NumberShowerProps {
precision?: number; precision?: number;
} }
export let NumberShower: React.FC<NumberShowerProps> = ({ export const NumberShower: React.FC<NumberShowerProps> = ({
number, number,
precision = 2, precision = 2,
}: NumberShowerProps) => { }) => {
let numberWithPresentation = numberShow(number, precision); const numberWithPresentation = numberShow(number, precision);
return ( return (
<span> <span>
{numberWithPresentation.value} {numberWithPresentation.value}
{numberWithPresentation.symbol} {numberWithPresentation.symbol}
{numberWithPresentation.power ? ( {numberWithPresentation.power ? (
<span> <span>
{"\u00b710"} {"\u00b7" /* dot symbol */}10
<span style={{ fontSize: "0.6em", verticalAlign: "super" }}> <span style={{ fontSize: "0.6em", verticalAlign: "super" }}>
{numberWithPresentation.power} {numberWithPresentation.power}
</span> </span>
</span> </span>
) : ( ) : null}
<></>
)}
</span> </span>
); );
}; };

View File

@ -1,5 +1,4 @@
import * as React from "react"; import * as React from "react";
import _ from "lodash";
import { import {
run, run,
errorValueToString, errorValueToString,
@ -28,6 +27,7 @@ function getRange<a>(x: declaration<a>) {
} }
} }
} }
function getChartSettings<a>(x: declaration<a>): FunctionChartSettings { function getChartSettings<a>(x: declaration<a>): FunctionChartSettings {
let range = getRange(x); let range = getRange(x);
let min = range.floats ? range.floats.min : 0; let min = range.floats ? range.floats.min : 0;
@ -49,12 +49,12 @@ export const VariableBox: React.FC<VariableBoxProps> = ({
heading = "Error", heading = "Error",
children, children,
showTypes = false, showTypes = false,
}: VariableBoxProps) => { }) => {
if (showTypes) { if (showTypes) {
return ( return (
<div className="bg-white border border-grey-200 m-2"> <div className="bg-white border border-grey-200 m-2">
<div className="border-b border-grey-200 p-3"> <div className="border-b border-grey-200 p-3">
<h3 className="font-mono">{heading}</h3> <header className="font-mono">{heading}</header>
</div> </div>
<div className="p-3">{children}</div> <div className="p-3">{children}</div>
</div> </div>
@ -90,7 +90,7 @@ const SquiggleItem: React.FC<SquiggleItemProps> = ({
showControls = false, showControls = false,
chartSettings, chartSettings,
environment, environment,
}: SquiggleItemProps) => { }) => {
switch (expression.tag) { switch (expression.tag) {
case "number": case "number":
return ( return (
@ -108,12 +108,8 @@ const SquiggleItem: React.FC<SquiggleItemProps> = ({
showTypes={showTypes} showTypes={showTypes}
> >
{distType === "Symbolic" && showTypes ? ( {distType === "Symbolic" && showTypes ? (
<>
<div>{expression.value.toString()}</div> <div>{expression.value.toString()}</div>
</> ) : null}
) : (
<></>
)}
<DistributionChart <DistributionChart
distribution={expression.value} distribution={expression.value}
height={height} height={height}
@ -157,9 +153,9 @@ const SquiggleItem: React.FC<SquiggleItemProps> = ({
return ( return (
<VariableBox heading="Array" showTypes={showTypes}> <VariableBox heading="Array" showTypes={showTypes}>
{expression.value.map((r, i) => ( {expression.value.map((r, i) => (
<div key={i} className="flex flex-row pt-1"> <div key={i} className="flex pt-1">
<div className="flex-none bg-slate-100 rounded-sm px-1"> <div className="flex-none bg-slate-100 rounded-sm px-1">
<h3 className="text-slate-400 font-mono">{i}</h3> <header className="text-slate-400 font-mono">{i}</header>
</div> </div>
<div className="px-2 mb-2 grow"> <div className="px-2 mb-2 grow">
<SquiggleItem <SquiggleItem
@ -181,12 +177,13 @@ const SquiggleItem: React.FC<SquiggleItemProps> = ({
case "record": case "record":
return ( return (
<VariableBox heading="Record" showTypes={showTypes}> <VariableBox heading="Record" showTypes={showTypes}>
<div className="space-y-3">
{Object.entries(expression.value).map(([key, r]) => ( {Object.entries(expression.value).map(([key, r]) => (
<div key={key} className="flex flex-row pt-1"> <div key={key} className="flex space-x-2">
<div className="flex-none pr-2"> <div className="flex-none">
<h3 className="text-slate-500 font-mono">{key}:</h3> <header className="text-slate-500 font-mono">{key}:</header>
</div> </div>
<div className="pl-2 pr-2 mb-2 grow bg-gray-50 border border-gray-100 rounded-sm"> <div className="px-2 grow bg-gray-50 border border-gray-100 rounded-sm">
<SquiggleItem <SquiggleItem
expression={r} expression={r}
width={width !== undefined ? width - 20 : width} width={width !== undefined ? width - 20 : width}
@ -200,6 +197,7 @@ const SquiggleItem: React.FC<SquiggleItemProps> = ({
</div> </div>
</div> </div>
))} ))}
</div>
</VariableBox> </VariableBox>
); );
case "arraystring": case "arraystring":
@ -285,7 +283,7 @@ export interface SquiggleChartProps {
showControls?: boolean; showControls?: boolean;
} }
let defaultChartSettings = { start: 0, stop: 10, count: 20 }; const defaultChartSettings = { start: 0, stop: 10, count: 20 };
export const SquiggleChart: React.FC<SquiggleChartProps> = ({ export const SquiggleChart: React.FC<SquiggleChartProps> = ({
squiggleString = "", squiggleString = "",
@ -299,14 +297,20 @@ export const SquiggleChart: React.FC<SquiggleChartProps> = ({
showTypes = false, showTypes = false,
showControls = false, showControls = false,
chartSettings = defaultChartSettings, chartSettings = defaultChartSettings,
}: SquiggleChartProps) => { }) => {
let expressionResult = run(squiggleString, bindings, environment, jsImports); let expressionResult = run(squiggleString, bindings, environment, jsImports);
let e = environment ? environment : defaultEnvironment; if (expressionResult.tag !== "Ok") {
let internal: JSX.Element; return (
if (expressionResult.tag === "Ok") { <ErrorAlert heading={"Parse Error"}>
{errorValueToString(expressionResult.value)}
</ErrorAlert>
);
}
let e = environment ?? defaultEnvironment;
let expression = expressionResult.value; let expression = expressionResult.value;
onChange(expression); onChange(expression);
internal = ( return (
<SquiggleItem <SquiggleItem
expression={expression} expression={expression}
width={width} width={width}
@ -318,12 +322,4 @@ export const SquiggleChart: React.FC<SquiggleChartProps> = ({
environment={e} environment={e}
/> />
); );
} else {
internal = (
<ErrorAlert heading={"Parse Error"}>
{errorValueToString(expressionResult.value)}
</ErrorAlert>
);
}
return internal;
}; };

View File

@ -57,8 +57,8 @@ export let SquiggleEditor: React.FC<SquiggleEditorProps> = ({
showControls = false, showControls = false,
showSummary = false, showSummary = false,
}: SquiggleEditorProps) => { }: SquiggleEditorProps) => {
let [expression, setExpression] = React.useState(initialSquiggleString); const [expression, setExpression] = React.useState(initialSquiggleString);
let chartSettings = { const chartSettings = {
start: diagramStart, start: diagramStart,
stop: diagramStop, stop: diagramStop,
count: diagramCount, count: diagramCount,
@ -150,17 +150,17 @@ export let SquigglePartial: React.FC<SquigglePartialProps> = ({
environment, environment,
jsImports = defaultImports, jsImports = defaultImports,
}: SquigglePartialProps) => { }: SquigglePartialProps) => {
let [expression, setExpression] = React.useState(initialSquiggleString); const [expression, setExpression] = React.useState(initialSquiggleString);
let [error, setError] = React.useState<string | null>(null); const [error, setError] = React.useState<string | null>(null);
let runSquiggleAndUpdateBindings = () => { const runSquiggleAndUpdateBindings = () => {
let squiggleResult = runPartial( const squiggleResult = runPartial(
expression, expression,
bindings, bindings,
environment, environment,
jsImports jsImports
); );
if (squiggleResult.tag == "Ok") { if (squiggleResult.tag === "Ok") {
if (onChange) onChange(squiggleResult.value); if (onChange) onChange(squiggleResult.value);
setError(null); setError(null);
} else { } else {
@ -181,11 +181,7 @@ export let SquigglePartial: React.FC<SquigglePartialProps> = ({
height={20} height={20}
/> />
</div> </div>
{error !== null ? ( {error !== null ? <ErrorAlert heading="Error">{error}</ErrorAlert> : null}
<ErrorAlert heading="Error">{error}</ErrorAlert>
) : (
<></>
)}
</div> </div>
); );
}; };

View File

@ -1,19 +1,21 @@
import _ from "lodash"; import React, { FC, Fragment, useState } from "react";
import React, { FC, ReactElement, useState } from "react";
import ReactDOM from "react-dom"; import ReactDOM from "react-dom";
import { SquiggleChart } from "./SquiggleChart"; import { Path, useForm, UseFormRegister, useWatch } from "react-hook-form";
import CodeEditor from "./CodeEditor";
import JsonEditor from "./JsonEditor";
import { useForm, useWatch } from "react-hook-form";
import * as yup from "yup"; import * as yup from "yup";
import { yupResolver } from "@hookform/resolvers/yup"; import { yupResolver } from "@hookform/resolvers/yup";
import { defaultBindings, environment } from "@quri/squiggle-lang";
import { Tab } from "@headlessui/react"; import { Tab } from "@headlessui/react";
import { CodeIcon } from "@heroicons/react/solid"; import {
import { CogIcon } from "@heroicons/react/solid"; ChartSquareBarIcon,
import { ChartSquareBarIcon } from "@heroicons/react/solid"; CodeIcon,
import { CurrencyDollarIcon } from "@heroicons/react/solid"; CogIcon,
import { Fragment } from "react"; CurrencyDollarIcon,
} from "@heroicons/react/solid";
import { defaultBindings, environment } from "@quri/squiggle-lang";
import { SquiggleChart } from "./SquiggleChart";
import { CodeEditor } from "./CodeEditor";
import { JsonEditor } from "./JsonEditor";
import { ErrorAlert, SuccessAlert } from "./Alert"; import { ErrorAlert, SuccessAlert } from "./Alert";
interface PlaygroundProps { interface PlaygroundProps {
@ -85,49 +87,20 @@ const schema = yup
}) })
.required(); .required();
type InputProps = {
label: string;
children: ReactElement;
};
const InputItem: React.FC<InputProps> = ({ label, children }) => (
<div className="col-span-4">
<label className="block text-sm font-medium text-gray-600">{label}</label>
<div className="mt-1">{children}</div>
</div>
);
let numberStyle =
"max-w-lg block w-full shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:max-w-xs sm:text-sm border-gray-300 rounded-md";
function classNames(...classes: string[]) { function classNames(...classes: string[]) {
return classes.filter(Boolean).join(" "); return classes.filter(Boolean).join(" ");
} }
type StyledTabProps = { type StyledTabProps = {
name: string; name: string;
iconName: string; icon: (props: React.ComponentProps<"svg">) => JSX.Element;
}; };
const StyledTab: React.FC<StyledTabProps> = ({ name, iconName }) => { const StyledTab: React.FC<StyledTabProps> = ({ name, icon: Icon }) => {
let iconStyle = (isSelected: boolean) =>
classNames(
"-ml-0.5 mr-2 h-4 w-4 ",
isSelected ? "text-slate-500" : "text-gray-400 group-hover:text-gray-900"
);
let icon = (selected: boolean) =>
({
code: <CodeIcon className={iconStyle(selected)} />,
cog: <CogIcon className={iconStyle(selected)} />,
squareBar: <ChartSquareBarIcon className={iconStyle(selected)} />,
dollar: <CurrencyDollarIcon className={iconStyle(selected)} />,
}[iconName]);
return ( return (
<Tab key={name} as={Fragment}> <Tab key={name} as={Fragment}>
{({ selected }) => ( {({ selected }) => (
<button className="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
className={classNames( className={classNames(
"p-1 pl-2.5 pr-3.5 rounded-md flex items-center text-sm font-medium", "p-1 pl-2.5 pr-3.5 rounded-md flex items-center text-sm font-medium",
@ -136,7 +109,14 @@ const StyledTab: React.FC<StyledTabProps> = ({ name, iconName }) => {
: "" : ""
)} )}
> >
{icon(selected)} <Icon
className={classNames(
"-ml-0.5 mr-2 h-4 w-4",
selected
? "text-slate-500"
: "text-gray-400 group-hover:text-gray-900"
)}
/>
<span <span
className={ className={
selected selected
@ -153,17 +133,77 @@ const StyledTab: React.FC<StyledTabProps> = ({ name, iconName }) => {
); );
}; };
let SquigglePlayground: FC<PlaygroundProps> = ({ const HeadedSection: FC<{ title: string; children: React.ReactNode }> = ({
title,
children,
}) => (
<div>
<header className="text-lg leading-6 font-medium text-gray-900">
{title}
</header>
<div className="mt-4">{children}</div>
</div>
);
const Text: FC<{ children: React.ReactNode }> = ({ children }) => (
<p className="text-sm text-gray-500">{children}</p>
);
function InputItem<T>({
name,
label,
type,
register,
}: {
name: Path<T>;
label: string;
type: "number";
register: UseFormRegister<T>;
}) {
const numberStyle =
"max-w-lg block w-full shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:max-w-xs sm:text-sm border-gray-300 rounded-md";
return (
<label className="block">
<div className="text-sm font-medium text-gray-600 mb-1">{label}</div>
<input type={type} {...register(name)} className={numberStyle} />
</label>
);
}
function Checkbox<T>({
name,
label,
register,
}: {
name: Path<T>;
label: string;
register: UseFormRegister<T>;
}) {
return (
<label className="flex items-center">
<input
type="checkbox"
{...register(name)}
className="focus:ring-indigo-500 h-4 w-4 text-indigo-600 border-gray-300 rounded"
/>
{/* Clicking on the div makes the checkbox lose focus while mouse button is pressed, leading to annoying blinking; I couldn't figure out how to fix this. */}
<div className="ml-3 text-sm font-medium text-gray-700">{label}</div>
</label>
);
}
const SquigglePlayground: FC<PlaygroundProps> = ({
initialSquiggleString = "", initialSquiggleString = "",
height = 500, height = 500,
showTypes = false, showTypes = false,
showControls = false, showControls = false,
showSummary = false, showSummary = false,
}: PlaygroundProps) => { }) => {
let [squiggleString, setSquiggleString] = useState(initialSquiggleString); const [squiggleString, setSquiggleString] = useState(initialSquiggleString);
let [importString, setImportString] = useState("{}"); const [importString, setImportString] = useState("{}");
let [imports, setImports] = useState({}); const [imports, setImports] = useState({});
let [importsAreValid, setImportsAreValid] = useState(true); const [importsAreValid, setImportsAreValid] = useState(true);
const { register, control } = useForm({ const { register, control } = useForm({
resolver: yupResolver(schema), resolver: yupResolver(schema),
defaultValues: { defaultValues: {
@ -183,16 +223,16 @@ let SquigglePlayground: FC<PlaygroundProps> = ({
const vars = useWatch({ const vars = useWatch({
control, control,
}); });
let chartSettings = { const chartSettings = {
start: Number(vars.diagramStart), start: Number(vars.diagramStart),
stop: Number(vars.diagramStop), stop: Number(vars.diagramStop),
count: Number(vars.diagramCount), count: Number(vars.diagramCount),
}; };
let env: environment = { const env: environment = {
sampleCount: Number(vars.sampleCount), sampleCount: Number(vars.sampleCount),
xyPointLength: Number(vars.xyPointLength), xyPointLength: Number(vars.xyPointLength),
}; };
let getChangeJson = (r: string) => { const getChangeJson = (r: string) => {
setImportString(r); setImportString(r);
try { try {
setImports(JSON.parse(r)); setImports(JSON.parse(r));
@ -202,149 +242,118 @@ let SquigglePlayground: FC<PlaygroundProps> = ({
} }
}; };
let samplingSettings = ( const samplingSettings = (
<div className="space-y-6 p-3 max-w-xl"> <div className="space-y-6 p-3 max-w-xl">
<InputItem label="Sample Count"> <div>
<> <InputItem
<input name="sampleCount"
type="number" type="number"
{...register("sampleCount")} label="Sample Count"
className={numberStyle} register={register}
/> />
<p className="mt-2 text-sm text-gray-500"> <div className="mt-2">
<Text>
How many samples to use for Monte Carlo simulations. This can How many samples to use for Monte Carlo simulations. This can
occasionally be overridden by specific Squiggle programs. occasionally be overridden by specific Squiggle programs.
</p> </Text>
</> </div>
</InputItem> </div>
<InputItem label="Coordinate Count (For PointSet Shapes)"> <div>
<> <InputItem
<input name="xyPointLength"
type="number" type="number"
{...register("xyPointLength")} register={register}
className={numberStyle} label="Coordinate Count (For PointSet Shapes)"
/> />
<p className="mt-2 text-sm text-gray-500"> <div className="mt-2">
<Text>
When distributions are converted into PointSet shapes, we need to When distributions are converted into PointSet shapes, we need to
know how many coordinates to use. know how many coordinates to use.
</p> </Text>
</> </div>
</InputItem> </div>
</div> </div>
); );
let viewSettings = ( const viewSettings = (
<div className="space-y-6 p-3 divide-y divide-gray-200 max-w-xl"> <div className="space-y-6 p-3 divide-y divide-gray-200 max-w-xl">
<HeadedSection title="General Display Settings">
<div className="space-y-4">
<InputItem
name="chartHeight"
type="number"
register={register}
label="Chart Height (in pixels)"
/>
<Checkbox
name="showTypes"
register={register}
label="Show information about displayed types"
/>
</div>
</HeadedSection>
<div className="pt-8">
<HeadedSection title="Distribution Display Settings">
<div className="space-y-2"> <div className="space-y-2">
<h3 className="text-lg leading-6 font-medium text-gray-900 pb-2"> <Checkbox
General Display Settings register={register}
</h3> name="showControls"
<InputItem label="Chart Height (in pixels)"> label="Show toggles to adjust scale of x and y axes"
<input
type="number"
{...register("chartHeight")}
className={numberStyle}
/> />
</InputItem> <Checkbox
<div className="relative flex items-start pt-3"> register={register}
<div className="flex items-center h-5"> name="showSummary"
<input label="Show summary statistics"
type="checkbox"
{...register("showTypes")}
className="focus:ring-indigo-500 h-4 w-4 text-indigo-600 border-gray-300 rounded"
/> />
</div> </div>
<div className="ml-3 text-sm"> </HeadedSection>
<label className="font-medium text-gray-700">
Show information about displayed types.
</label>
</div>
</div>
</div> </div>
<div className="space-y-2 pt-8"> <div className="pt-8">
<h3 className="text-lg leading-6 font-medium text-gray-900 pb-2"> <HeadedSection title="Function Display Settings">
Distribution Display Settings <div className="space-y-6">
</h3> <Text>
When displaying functions of single variables that return numbers
<div className="relative flex items-start"> or distributions, we need to use defaults for the x-axis. We need
<div className="flex items-center h-5"> to select a minimum and maximum value of x to sample, and a number
<input n of the number of points to sample.
type="checkbox" </Text>
{...register("showControls")} <div className="space-y-4">
className="focus:ring-indigo-500 h-4 w-4 text-indigo-600 border-gray-300 rounded" <InputItem
/>
</div>
<div className="ml-3 text-sm">
<label className="font-medium text-gray-700">
Show toggles to adjust scale of x and y axes
</label>
</div>
</div>
<div className="relative flex items-start">
<div className="flex items-center h-5">
<input
type="checkbox"
{...register("showSummary")}
className="focus:ring-indigo-500 h-4 w-4 text-indigo-600 border-gray-300 rounded"
/>
</div>
<div className="ml-3 text-sm">
<label className="font-medium text-gray-700">
Show summary statistics
</label>
</div>
</div>
</div>
<div className="space-y-2 pt-8">
<h3 className="text-lg leading-6 font-medium text-gray-900 pb-2">
Function Display Settings
</h3>
<p className="mt-2 text-sm text-gray-500">
When displaying functions of single variables that return numbers or
distributions, we need to use defaults for the x-axis. We need to
select a minimum and maximum value of x to sample, and a number n of
the number of points to sample.
</p>
<div className="pt-4 grid grid-cols-1 gap-y-4 gap-x-4">
<InputItem label="Min X Value">
<input
type="number" type="number"
{...register("diagramStart")} name="diagramStart"
className={numberStyle} register={register}
label="Min X Value"
/> />
</InputItem> <InputItem
<InputItem label="Max X Value">
<input
type="number" type="number"
{...register("diagramStop")} name="diagramStop"
className={numberStyle} register={register}
label="Max X Value"
/> />
</InputItem> <InputItem
<InputItem label="Points between X min and X max to sample">
<input
type="number" type="number"
{...register("diagramCount")} name="diagramCount"
className={numberStyle} register={register}
label="Points between X min and X max to sample"
/> />
</InputItem>
</div> </div>
</div> </div>
</HeadedSection>
</div>
</div> </div>
); );
let inputVariableSettings = ( const inputVariableSettings = (
<div className="space-y-6 p-3 max-w-3xl"> <div className="p-3 max-w-3xl">
<h3 className="text-lg leading-6 font-medium text-gray-900"> <HeadedSection title="Import Variables from JSON">
Import Variables from JSON <div className="space-y-6">
</h3> <Text>
<p className="mt-2 text-sm text-gray-500"> You can import variables from JSON into your Squiggle code.
You can import variables from JSON into your Squiggle code. Variables Variables are accessed with dollar signs. For example, "timeNow"
are accessed with dollar signs. For example, "timeNow" would be accessed would be accessed as "$timeNow".
as "$timeNow". </Text>
</p>
<div className="border border-slate-200 mt-6 mb-2"> <div className="border border-slate-200 mt-6 mb-2">
<JsonEditor <JsonEditor
value={importString} value={importString}
@ -356,30 +365,29 @@ let SquigglePlayground: FC<PlaygroundProps> = ({
</div> </div>
<div className="p-1 pt-2"> <div className="p-1 pt-2">
{importsAreValid ? ( {importsAreValid ? (
<SuccessAlert heading="Valid Json"> <SuccessAlert heading="Valid JSON" />
<></>
</SuccessAlert>
) : ( ) : (
<ErrorAlert heading="Invalid JSON"> <ErrorAlert heading="Invalid JSON">
You must use valid json in this editor. You must use valid JSON in this editor.
</ErrorAlert> </ErrorAlert>
)} )}
</div> </div>
</div> </div>
</HeadedSection>
</div>
); );
return ( return (
<Tab.Group> <Tab.Group>
<div className=" flex-col flex">
<div className="pb-4"> <div className="pb-4">
<Tab.List className="p-0.5 rounded-md bg-slate-100 hover:bg-slate-200 inline-flex"> <Tab.List className="flex w-fit p-0.5 rounded-md bg-slate-100 hover:bg-slate-200">
<StyledTab name="Code" iconName="code" /> <StyledTab name="Code" icon={CodeIcon} />
<StyledTab name="Sampling Settings" iconName="cog" /> <StyledTab name="Sampling Settings" icon={CogIcon} />
<StyledTab name="View Settings" iconName="squareBar" /> <StyledTab name="View Settings" icon={ChartSquareBarIcon} />
<StyledTab name="Input Variables" iconName="dollar" /> <StyledTab name="Input Variables" icon={CurrencyDollarIcon} />
</Tab.List> </Tab.List>
</div> </div>
<div className="flex" style={{ height: height + "px" }}> <div className="flex" style={{ height }}>
<div className="w-1/2"> <div className="w-1/2">
<Tab.Panels> <Tab.Panels>
<Tab.Panel> <Tab.Panel>
@ -400,7 +408,7 @@ let SquigglePlayground: FC<PlaygroundProps> = ({
</div> </div>
<div className="w-1/2 p-2 pl-4"> <div className="w-1/2 p-2 pl-4">
<div style={{ maxHeight: height + "px" }}> <div style={{ maxHeight: height }}>
<SquiggleChart <SquiggleChart
squiggleString={squiggleString} squiggleString={squiggleString}
environment={env} environment={env}
@ -415,13 +423,12 @@ let SquigglePlayground: FC<PlaygroundProps> = ({
</div> </div>
</div> </div>
</div> </div>
</div>
</Tab.Group> </Tab.Group>
); );
}; };
export default SquigglePlayground; export default SquigglePlayground;
export function renderSquigglePlaygroundToDom(props: PlaygroundProps) { export function renderSquigglePlaygroundToDom(props: PlaygroundProps) {
let parent = document.createElement("div"); const parent = document.createElement("div");
ReactDOM.render(<SquigglePlayground {...props} />, parent); ReactDOM.render(<SquigglePlayground {...props} />, parent);
return parent; return parent;
} }

View File

@ -5,9 +5,9 @@ export {
renderSquiggleEditorToDom, renderSquiggleEditorToDom,
renderSquigglePartialToDom, renderSquigglePartialToDom,
} from "./components/SquiggleEditor"; } from "./components/SquiggleEditor";
import SquigglePlayground, { export {
default as SquigglePlayground,
renderSquigglePlaygroundToDom, renderSquigglePlaygroundToDom,
} from "./components/SquigglePlayground"; } from "./components/SquigglePlayground";
export { SquigglePlayground, renderSquigglePlaygroundToDom };
export { mergeBindings } from "@quri/squiggle-lang"; export { mergeBindings } from "@quri/squiggle-lang";

View File

@ -1 +0,0 @@
{"version":3,"file":"SquiggleChart.stories.js","sourceRoot":"","sources":["SquiggleChart.stories.tsx"],"names":[],"mappings":";;;AAAA,6BAA8B;AAC9B,iDAA+C;AAG/C,qBAAe;IACb,KAAK,EAAE,uBAAuB;IAC9B,SAAS,EAAE,6BAAa;CACzB,CAAA;AAED,IAAM,QAAQ,GAAG,UAAC,EAAgB;QAAf,cAAc,oBAAA;IAAM,OAAA,oBAAC,6BAAa,IAAC,cAAc,EAAE,cAAc,GAAI;AAAjD,CAAiD,CAAA;AAE3E,QAAA,OAAO,GAAG,QAAQ,CAAC,IAAI,CAAC,EAAE,CAAC,CAAA;AACxC,eAAO,CAAC,IAAI,GAAG;IACb,cAAc,EAAE,cAAc;CAC/B,CAAC"}