Merge branch 'develop' into Umur-reducer-dev

This commit is contained in:
Umur Ozkul 2022-06-08 16:05:02 +02:00
commit 2dc204e222
38 changed files with 1010 additions and 933 deletions

View File

@ -154,5 +154,7 @@ jobs:
run: cd ../../ && yarn run: cd ../../ && yarn
- name: Build rescript in squiggle-lang - name: Build rescript in squiggle-lang
run: cd ../squiggle-lang && yarn build run: cd ../squiggle-lang && yarn build
- name: Build components
run: cd ../components && yarn build
- name: Build website assets - name: Build website assets
run: yarn build run: yarn build

View File

@ -5,7 +5,7 @@
"dependencies": { "dependencies": {
"@headlessui/react": "^1.6.4", "@headlessui/react": "^1.6.4",
"@heroicons/react": "^1.0.6", "@heroicons/react": "^1.0.6",
"@hookform/resolvers": "^2.8.10", "@hookform/resolvers": "^2.9.0",
"@quri/squiggle-lang": "^0.2.8", "@quri/squiggle-lang": "^0.2.8",
"@react-hook/size": "^2.1.2", "@react-hook/size": "^2.1.2",
"lodash": "^4.17.21", "lodash": "^4.17.21",
@ -22,40 +22,42 @@
}, },
"devDependencies": { "devDependencies": {
"@babel/plugin-proposal-private-property-in-object": "^7.17.12", "@babel/plugin-proposal-private-property-in-object": "^7.17.12",
"@storybook/addon-actions": "^6.5.6", "@storybook/addon-actions": "^6.5.7",
"@storybook/addon-essentials": "^6.5.6", "@storybook/addon-essentials": "^6.5.7",
"@storybook/addon-links": "^6.5.6", "@storybook/addon-links": "^6.5.7",
"@storybook/builder-webpack5": "^6.5.6", "@storybook/builder-webpack5": "^6.5.7",
"@storybook/manager-webpack5": "^6.5.6", "@storybook/manager-webpack5": "^6.5.7",
"@storybook/node-logger": "^6.5.6", "@storybook/node-logger": "^6.5.6",
"@storybook/preset-create-react-app": "^4.1.1", "@storybook/preset-create-react-app": "^4.1.2",
"@storybook/react": "^6.5.6", "@storybook/react": "^6.5.7",
"@tailwindcss/forms": "^0.5.2", "@tailwindcss/forms": "^0.5.2",
"@testing-library/jest-dom": "^5.16.4", "@testing-library/jest-dom": "^5.16.4",
"@testing-library/react": "^13.3.0", "@testing-library/react": "^13.3.0",
"@testing-library/user-event": "^14.2.0", "@testing-library/user-event": "^14.2.0",
"@types/jest": "^27.5.0", "@types/jest": "^27.5.0",
"@types/lodash": "^4.14.182", "@types/lodash": "^4.14.182",
"@types/node": "^17.0.36", "@types/node": "^17.0.40",
"@types/react": "^18.0.9", "@types/react": "^18.0.9",
"@types/react-dom": "^18.0.5", "@types/react-dom": "^18.0.5",
"@types/styled-components": "^5.1.24", "@types/styled-components": "^5.1.24",
"@types/webpack": "^5.28.0", "@types/webpack": "^5.28.0",
"cross-env": "^7.0.3", "cross-env": "^7.0.3",
"mini-css-extract-plugin": "^2.6.0",
"postcss-loader": "^7.0.0",
"react-scripts": "^5.0.1", "react-scripts": "^5.0.1",
"style-loader": "^3.3.1", "style-loader": "^3.3.1",
"tailwindcss": "^3.0.24", "tailwindcss": "^3.0.24",
"ts-loader": "^9.3.0", "ts-loader": "^9.3.0",
"tsconfig-paths-webpack-plugin": "^3.5.2", "tsconfig-paths-webpack-plugin": "^3.5.2",
"typescript": "^4.7.2", "typescript": "^4.7.3",
"web-vitals": "^2.1.4", "web-vitals": "^2.1.4",
"webpack": "^5.72.1", "webpack": "^5.73.0",
"webpack-cli": "^4.9.2", "webpack-cli": "^4.9.2",
"webpack-dev-server": "^4.9.0" "webpack-dev-server": "^4.9.0"
}, },
"scripts": { "scripts": {
"start": "cross-env REACT_APP_FAST_REFRESH=false && start-storybook -p 6006 -s public", "start": "cross-env REACT_APP_FAST_REFRESH=false && start-storybook -p 6006 -s public",
"build": "tsc -b && build-storybook -s public", "build": "tsc -b && tailwindcss -i ./src/tailwind.css -o ./dist/main.css && build-storybook -s public",
"bundle": "webpack", "bundle": "webpack",
"all": "yarn bundle && yarn build", "all": "yarn bundle && yarn build",
"lint": "prettier --check .", "lint": "prettier --check .",

View File

@ -0,0 +1,7 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
cssnano: {},
},
};

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}`}>
<div className={`mt-2 text-sm ${bodyColor}`}>{children}</div> {heading}
</header>
{children && React.Children.count(children) ? (
<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,71 +33,64 @@ 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 (
shape.value.continuous.some((x) => x.x <= 0) ||
shape.value.discrete.some((x) => x.x <= 0);
let spec = buildVegaSpec(isLogX, isExpY);
let widthProp = width ? width : size.width;
if (widthProp < 20) {
console.warn(
`Width of Distribution is set to ${widthProp}, which is too small`
);
widthProp = 20;
}
// Check whether we should disable the checkbox
var logCheckbox = (
<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
spec={spec}
data={{ con: shape.value.continuous, dis: shape.value.discrete }}
width={widthProp - 10}
height={height}
actions={false}
/>
<div className="flex justify-center">
{showSummary && <SummaryTable distribution={distribution} />}
</div>
{showControls && (
<div>
{logCheckbox}
<CheckBox label="Exp Y scale" value={isExpY} onChange={setExpY} />
</div>
)}
</div>
);
} else {
var result = (
<ErrorAlert heading="Distribution Error"> <ErrorAlert heading="Distribution Error">
{distributionErrorToString(shape.value)} {distributionErrorToString(shape.value)}
</ErrorAlert> </ErrorAlert>
); );
} }
return result; const massBelow0 =
shape.value.continuous.some((x) => x.x <= 0) ||
shape.value.discrete.some((x) => x.x <= 0);
const spec = buildVegaSpec(isLogX, isExpY);
let widthProp = width ? width : size.width;
if (widthProp < 20) {
console.warn(
`Width of Distribution is set to ${widthProp}, which is too small`
);
widthProp = 20;
}
return (
<div style={{ width: widthProp }}>
<Vega
spec={spec}
data={{ con: shape.value.continuous, dis: shape.value.discrete }}
width={widthProp - 10}
height={height}
actions={false}
/>
<div className="flex justify-center">
{showSummary && <SummaryTable distribution={distribution} />}
</div>
{showControls && (
<div>
<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} />
</div>
)}
</div>
);
}); });
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,39 @@ 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 stdev = distribution.stdev();
let mean = distribution.mean(); const p5 = distribution.inv(0.05);
let p5 = distribution.inv(0.05); const p10 = distribution.inv(0.1);
let p10 = distribution.inv(0.1); const p25 = distribution.inv(0.25);
let p25 = distribution.inv(0.25); const p50 = distribution.inv(0.5);
let p50 = distribution.inv(0.5); const p75 = distribution.inv(0.75);
let p75 = distribution.inv(0.75); const p90 = distribution.inv(0.9);
let p90 = distribution.inv(0.9); const p95 = distribution.inv(0.95);
let p95 = distribution.inv(0.95);
let unwrapResult = ( const hasResult = (x: result<number, distributionError>): boolean =>
x.tag === "Ok";
const unwrapResult = (
x: result<number, distributionError> x: result<number, distributionError>
): React.ReactNode => { ): React.ReactNode => {
if (x.tag === "Ok") { if (x.tag === "Ok") {
@ -170,24 +179,12 @@ 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">
<tr> <tr>
<TableHeadCell>{"Mean"}</TableHeadCell> <TableHeadCell>{"Mean"}</TableHeadCell>
{hasResult(stdev) && <TableHeadCell>{"Stdev"}</TableHeadCell>}
<TableHeadCell>{"5%"}</TableHeadCell> <TableHeadCell>{"5%"}</TableHeadCell>
<TableHeadCell>{"10%"}</TableHeadCell> <TableHeadCell>{"10%"}</TableHeadCell>
<TableHeadCell>{"25%"}</TableHeadCell> <TableHeadCell>{"25%"}</TableHeadCell>
@ -200,6 +197,7 @@ const SummaryTable: React.FC<SummaryTableProps> = ({
<tbody> <tbody>
<tr> <tr>
<Cell>{unwrapResult(mean)}</Cell> <Cell>{unwrapResult(mean)}</Cell>
{hasResult(stdev) && <Cell>{unwrapResult(stdev)}</Cell>}
<Cell>{unwrapResult(p5)}</Cell> <Cell>{unwrapResult(p5)}</Cell>
<Cell>{unwrapResult(p10)}</Cell> <Cell>{unwrapResult(p10)}</Cell>
<Cell>{unwrapResult(p25)}</Cell> <Cell>{unwrapResult(p25)}</Cell>

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,11 +153,11 @@ 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
key={i} key={i}
expression={r} expression={r}
@ -181,25 +177,27 @@ const SquiggleItem: React.FC<SquiggleItemProps> = ({
case "record": case "record":
return ( return (
<VariableBox heading="Record" showTypes={showTypes}> <VariableBox heading="Record" showTypes={showTypes}>
{Object.entries(expression.value).map(([key, r]) => ( <div className="space-y-3">
<div key={key} className="flex flex-row pt-1"> {Object.entries(expression.value).map(([key, r]) => (
<div className="flex-none pr-2"> <div key={key} className="flex space-x-2">
<h3 className="text-slate-500 font-mono">{key}:</h3> <div className="flex-none">
<header className="text-slate-500 font-mono">{key}:</header>
</div>
<div className="px-2 grow bg-gray-50 border border-gray-100 rounded-sm">
<SquiggleItem
expression={r}
width={width !== undefined ? width - 20 : width}
height={height / 3}
showTypes={showTypes}
showSummary={showSummary}
showControls={showControls}
chartSettings={chartSettings}
environment={environment}
/>
</div>
</div> </div>
<div className="pl-2 pr-2 mb-2 grow bg-gray-50 border border-gray-100 rounded-sm"> ))}
<SquiggleItem </div>
expression={r}
width={width !== undefined ? width - 20 : width}
height={height / 3}
showTypes={showTypes}
showSummary={showSummary}
showControls={showControls}
chartSettings={chartSettings}
environment={environment}
/>
</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,31 +297,29 @@ 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") {
let expression = expressionResult.value;
onChange(expression);
internal = (
<SquiggleItem
expression={expression}
width={width}
height={height}
showSummary={showSummary}
showTypes={showTypes}
showControls={showControls}
chartSettings={chartSettings}
environment={e}
/>
);
} else {
internal = (
<ErrorAlert heading={"Parse Error"}> <ErrorAlert heading={"Parse Error"}>
{errorValueToString(expressionResult.value)} {errorValueToString(expressionResult.value)}
</ErrorAlert> </ErrorAlert>
); );
} }
return internal;
let e = environment ?? defaultEnvironment;
let expression = expressionResult.value;
onChange(expression);
return (
<SquiggleItem
expression={expression}
width={width}
height={height}
showSummary={showSummary}
showTypes={showTypes}
showControls={showControls}
chartSettings={chartSettings}
environment={e}
/>
);
}; };

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,217 +242,184 @@ 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">
<div className="space-y-2"> <HeadedSection title="General Display Settings">
<h3 className="text-lg leading-6 font-medium text-gray-900 pb-2"> <div className="space-y-4">
General Display Settings <InputItem
</h3> name="chartHeight"
<InputItem label="Chart Height (in pixels)">
<input
type="number" type="number"
{...register("chartHeight")} register={register}
className={numberStyle} label="Chart Height (in pixels)"
/> />
</InputItem> <Checkbox
<div className="relative flex items-start pt-3"> name="showTypes"
<div className="flex items-center h-5"> register={register}
<input label="Show information about displayed types"
type="checkbox" />
{...register("showTypes")} </div>
className="focus:ring-indigo-500 h-4 w-4 text-indigo-600 border-gray-300 rounded" </HeadedSection>
<div className="pt-8">
<HeadedSection title="Distribution Display Settings">
<div className="space-y-2">
<Checkbox
register={register}
name="showControls"
label="Show toggles to adjust scale of x and y axes"
/>
<Checkbox
register={register}
name="showSummary"
label="Show summary statistics"
/> />
</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
/> type="number"
name="diagramStart"
register={register}
label="Min X Value"
/>
<InputItem
type="number"
name="diagramStop"
register={register}
label="Max X Value"
/>
<InputItem
type="number"
name="diagramCount"
register={register}
label="Points between X min and X max to sample"
/>
</div>
</div> </div>
<div className="ml-3 text-sm"> </HeadedSection>
<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"
{...register("diagramStart")}
className={numberStyle}
/>
</InputItem>
<InputItem label="Max X Value">
<input
type="number"
{...register("diagramStop")}
className={numberStyle}
/>
</InputItem>
<InputItem label="Points between X min and X max to sample">
<input
type="number"
{...register("diagramCount")}
className={numberStyle}
/>
</InputItem>
</div>
</div> </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} onChange={getChangeJson}
onChange={getChangeJson} oneLine={false}
oneLine={false} showGutter={true}
showGutter={true} height={150}
height={150} />
/> </div>
</div> <div className="p-1 pt-2">
<div className="p-1 pt-2"> {importsAreValid ? (
{importsAreValid ? ( <SuccessAlert heading="Valid JSON" />
<SuccessAlert heading="Valid Json"> ) : (
<></> <ErrorAlert heading="Invalid JSON">
</SuccessAlert> You must use valid JSON in this editor.
) : ( </ErrorAlert>
<ErrorAlert heading="Invalid JSON"> )}
You must use valid json in this editor. </div>
</ErrorAlert> </div>
)} </HeadedSection>
</div>
</div> </div>
); );
return ( return (
<Tab.Group> <Tab.Group>
<div className=" flex-col flex"> <div className="pb-4">
<div className="pb-4"> <Tab.List className="flex w-fit p-0.5 rounded-md bg-slate-100 hover:bg-slate-200">
<Tab.List className="p-0.5 rounded-md bg-slate-100 hover:bg-slate-200 inline-flex"> <StyledTab name="Code" icon={CodeIcon} />
<StyledTab name="Code" iconName="code" /> <StyledTab name="Sampling Settings" icon={CogIcon} />
<StyledTab name="Sampling Settings" iconName="cog" /> <StyledTab name="View Settings" icon={ChartSquareBarIcon} />
<StyledTab name="View Settings" iconName="squareBar" /> <StyledTab name="Input Variables" icon={CurrencyDollarIcon} />
<StyledTab name="Input Variables" iconName="dollar" /> </Tab.List>
</Tab.List> </div>
<div className="flex" style={{ height }}>
<div className="w-1/2">
<Tab.Panels>
<Tab.Panel>
<div className="border border-slate-200">
<CodeEditor
value={squiggleString}
onChange={setSquiggleString}
oneLine={false}
showGutter={true}
height={height - 1}
/>
</div>
</Tab.Panel>
<Tab.Panel>{samplingSettings}</Tab.Panel>
<Tab.Panel>{viewSettings}</Tab.Panel>
<Tab.Panel>{inputVariableSettings}</Tab.Panel>
</Tab.Panels>
</div> </div>
<div className="flex" style={{ height: height + "px" }}>
<div className="w-1/2">
<Tab.Panels>
<Tab.Panel>
<div className="border border-slate-200">
<CodeEditor
value={squiggleString}
onChange={setSquiggleString}
oneLine={false}
showGutter={true}
height={height - 1}
/>
</div>
</Tab.Panel>
<Tab.Panel>{samplingSettings}</Tab.Panel>
<Tab.Panel>{viewSettings}</Tab.Panel>
<Tab.Panel>{inputVariableSettings}</Tab.Panel>
</Tab.Panels>
</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}
chartSettings={chartSettings} chartSettings={chartSettings}
height={vars.chartHeight} height={vars.chartHeight}
showTypes={vars.showTypes} showTypes={vars.showTypes}
showControls={vars.showControls} showControls={vars.showControls}
bindings={defaultBindings} bindings={defaultBindings}
jsImports={imports} jsImports={imports}
showSummary={vars.showSummary} showSummary={vars.showSummary}
/> />
</div>
</div> </div>
</div> </div>
</div> </div>
@ -421,7 +428,7 @@ let SquigglePlayground: FC<PlaygroundProps> = ({
}; };
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"}

View File

@ -1,24 +1,26 @@
const path = require("path"); const path = require("path");
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
module.exports = { module.exports = {
mode: "production", mode: "production",
devtool: "source-map", devtool: "source-map",
profile: true, profile: true,
entry: "./src/index.ts", entry: ["./src/index.ts", "./src/tailwind.css"],
module: { module: {
rules: [ rules: [
{ {
test: /\.tsx?$/, test: /\.tsx?$/,
loader: "ts-loader", loader: "ts-loader",
options: { projectReferences: true, transpileOnly: true }, options: { projectReferences: true },
exclude: /node_modules/, exclude: /node_modules/,
}, },
{ {
test: /\.css$/i, test: /\.css$/i,
use: ["style-loader", "css-loader"], use: [MiniCssExtractPlugin.loader, "css-loader", "postcss-loader"],
}, },
], ],
}, },
plugins: [new MiniCssExtractPlugin()],
resolve: { resolve: {
extensions: [".js", ".tsx", ".ts"], extensions: [".js", ".tsx", ".ts"],
alias: { alias: {

View File

@ -232,6 +232,7 @@ describe("Peggy parse", () => {
}) })
describe("unit", () => { describe("unit", () => {
testParse("1m", "{(::fromUnit_m 1)}") testParse("1m", "{(::fromUnit_m 1)}")
testParse("1M", "{(::fromUnit_M 1)}")
testParse("1m+2cm", "{(::add (::fromUnit_m 1) (::fromUnit_cm 2))}") testParse("1m+2cm", "{(::add (::fromUnit_m 1) (::fromUnit_cm 2))}")
}) })
}) })

View File

@ -56,14 +56,14 @@
"lodash": "^4.17.21", "lodash": "^4.17.21",
"moduleserve": "^0.9.1", "moduleserve": "^0.9.1",
"nyc": "^15.1.0", "nyc": "^15.1.0",
"peggy": "^2.0.0", "peggy": "^2.0.1",
"reanalyze": "^2.22.0", "reanalyze": "^2.22.0",
"rescript-fast-check": "^1.1.1", "rescript-fast-check": "^1.1.1",
"ts-jest": "^27.1.4", "ts-jest": "^27.1.4",
"ts-loader": "^9.3.0", "ts-loader": "^9.3.0",
"ts-node": "^10.8.0", "ts-node": "^10.8.1",
"typescript": "^4.7.2", "typescript": "^4.7.3",
"webpack": "^5.72.1", "webpack": "^5.73.0",
"webpack-cli": "^4.9.2" "webpack-cli": "^4.9.2"
}, },
"source": "./src/js/index.ts", "source": "./src/js/index.ts",

View File

@ -11,6 +11,7 @@ import {
import { result, resultMap, Ok } from "./types"; import { result, resultMap, Ok } from "./types";
import { import {
Constructors_mean, Constructors_mean,
Constructors_stdev,
Constructors_sample, Constructors_sample,
Constructors_pdf, Constructors_pdf,
Constructors_cdf, Constructors_cdf,
@ -69,6 +70,10 @@ export class Distribution {
return Constructors_mean({ env: this.env }, this.t); return Constructors_mean({ env: this.env }, this.t);
} }
stdev(): result<number, distributionError> {
return Constructors_stdev({ env: this.env }, this.t);
}
sample(): result<number, distributionError> { sample(): result<number, distributionError> {
return Constructors_sample({ env: this.env }, this.t); return Constructors_sample({ env: this.env }, this.t);
} }

View File

@ -265,6 +265,8 @@ module Constructors = {
module C = DistributionTypes.Constructors.UsingDists module C = DistributionTypes.Constructors.UsingDists
open OutputLocal open OutputLocal
let mean = (~env, dist) => C.mean(dist)->run(~env)->toFloatR let mean = (~env, dist) => C.mean(dist)->run(~env)->toFloatR
let stdev = (~env, dist) => C.stdev(dist)->run(~env)->toFloatR
let variance = (~env, dist) => C.variance(dist)->run(~env)->toFloatR
let sample = (~env, dist) => C.sample(dist)->run(~env)->toFloatR let sample = (~env, dist) => C.sample(dist)->run(~env)->toFloatR
let cdf = (~env, dist, f) => C.cdf(dist, f)->run(~env)->toFloatR let cdf = (~env, dist, f) => C.cdf(dist, f)->run(~env)->toFloatR
let inv = (~env, dist, f) => C.inv(dist, f)->run(~env)->toFloatR let inv = (~env, dist, f) => C.inv(dist, f)->run(~env)->toFloatR

View File

@ -49,6 +49,10 @@ module Constructors: {
@genType @genType
let mean: (~env: env, genericDist) => result<float, error> let mean: (~env: env, genericDist) => result<float, error>
@genType @genType
let stdev: (~env: env, genericDist) => result<float, error>
@genType
let variance: (~env: env, genericDist) => result<float, error>
@genType
let sample: (~env: env, genericDist) => result<float, error> let sample: (~env: env, genericDist) => result<float, error>
@genType @genType
let cdf: (~env: env, genericDist, float) => result<float, error> let cdf: (~env: env, genericDist, float) => result<float, error>

View File

@ -30,9 +30,9 @@ module Error = {
@genType @genType
let toString = (err: error): string => let toString = (err: error): string =>
switch err { switch err {
| NotYetImplemented => "Function Not Yet Implemented" | NotYetImplemented => "Function not yet implemented"
| Unreachable => "Unreachable" | Unreachable => "Unreachable"
| DistributionVerticalShiftIsInvalid => "Distribution Vertical Shift is Invalid" | DistributionVerticalShiftIsInvalid => "Distribution vertical shift is invalid"
| ArgumentError(s) => `Argument Error ${s}` | ArgumentError(s) => `Argument Error ${s}`
| LogarithmOfDistributionError(s) => `Logarithm of input error: ${s}` | LogarithmOfDistributionError(s) => `Logarithm of input error: ${s}`
| SampleSetError(TooFewSamples) => "Too Few Samples" | SampleSetError(TooFewSamples) => "Too Few Samples"
@ -68,6 +68,11 @@ module DistributionOperation = {
| #Mean | #Mean
| #Sample | #Sample
| #IntegralSum | #IntegralSum
| #Mode
| #Stdev
| #Min
| #Max
| #Variance
] ]
type toScaleFn = [ type toScaleFn = [
@ -117,6 +122,11 @@ module DistributionOperation = {
| ToFloat(#Cdf(r)) => `cdf(${E.Float.toFixed(r)})` | ToFloat(#Cdf(r)) => `cdf(${E.Float.toFixed(r)})`
| ToFloat(#Inv(r)) => `inv(${E.Float.toFixed(r)})` | ToFloat(#Inv(r)) => `inv(${E.Float.toFixed(r)})`
| ToFloat(#Mean) => `mean` | ToFloat(#Mean) => `mean`
| ToFloat(#Min) => `min`
| ToFloat(#Max) => `max`
| ToFloat(#Stdev) => `stdev`
| ToFloat(#Variance) => `variance`
| ToFloat(#Mode) => `mode`
| ToFloat(#Pdf(r)) => `pdf(${E.Float.toFixed(r)})` | ToFloat(#Pdf(r)) => `pdf(${E.Float.toFixed(r)})`
| ToFloat(#Sample) => `sample` | ToFloat(#Sample) => `sample`
| ToFloat(#IntegralSum) => `integralSum` | ToFloat(#IntegralSum) => `integralSum`
@ -151,6 +161,8 @@ module Constructors = {
module UsingDists = { module UsingDists = {
@genType @genType
let mean = (dist): t => FromDist(ToFloat(#Mean), dist) let mean = (dist): t => FromDist(ToFloat(#Mean), dist)
let stdev = (dist): t => FromDist(ToFloat(#Stdev), dist)
let variance = (dist): t => FromDist(ToFloat(#Variance), dist)
let sample = (dist): t => FromDist(ToFloat(#Sample), dist) let sample = (dist): t => FromDist(ToFloat(#Sample), dist)
let cdf = (dist, x): t => FromDist(ToFloat(#Cdf(x)), dist) let cdf = (dist, x): t => FromDist(ToFloat(#Cdf(x)), dist)
let inv = (dist, x): t => FromDist(ToFloat(#Inv(x)), dist) let inv = (dist, x): t => FromDist(ToFloat(#Inv(x)), dist)

View File

@ -108,7 +108,7 @@ let toFloatOperation = (
) => { ) => {
switch distToFloatOperation { switch distToFloatOperation {
| #IntegralSum => Ok(integralEndY(t)) | #IntegralSum => Ok(integralEndY(t))
| (#Pdf(_) | #Cdf(_) | #Inv(_) | #Mean | #Sample) as op => { | (#Pdf(_) | #Cdf(_) | #Inv(_) | #Mean | #Sample | #Min | #Max) as op => {
let trySymbolicSolution = switch (t: t) { let trySymbolicSolution = switch (t: t) {
| Symbolic(r) => SymbolicDist.T.operate(op, r)->E.R.toOption | Symbolic(r) => SymbolicDist.T.operate(op, r)->E.R.toOption
| _ => None | _ => None
@ -118,6 +118,8 @@ let toFloatOperation = (
| (SampleSet(sampleSet), #Mean) => SampleSetDist.mean(sampleSet)->Some | (SampleSet(sampleSet), #Mean) => SampleSetDist.mean(sampleSet)->Some
| (SampleSet(sampleSet), #Sample) => SampleSetDist.sample(sampleSet)->Some | (SampleSet(sampleSet), #Sample) => SampleSetDist.sample(sampleSet)->Some
| (SampleSet(sampleSet), #Inv(r)) => SampleSetDist.percentile(sampleSet, r)->Some | (SampleSet(sampleSet), #Inv(r)) => SampleSetDist.percentile(sampleSet, r)->Some
| (SampleSet(sampleSet), #Min) => SampleSetDist.min(sampleSet)->Some
| (SampleSet(sampleSet), #Max) => SampleSetDist.max(sampleSet)->Some
| _ => None | _ => None
} }
@ -130,6 +132,16 @@ let toFloatOperation = (
} }
} }
} }
| (#Stdev | #Variance | #Mode) as op =>
switch t {
| SampleSet(s) =>
switch op {
| #Stdev => SampleSetDist.stdev(s)->Ok
| #Variance => SampleSetDist.variance(s)->Ok
| #Mode => SampleSetDist.mode(s)->Ok
}
| _ => Error(DistributionTypes.NotYetImplemented)
}
} }
} }

View File

@ -254,6 +254,8 @@ let operate = (distToFloatOp: Operation.distToFloatOperation, s): float =>
| #Inv(f) => inv(f, s) | #Inv(f) => inv(f, s)
| #Sample => sample(s) | #Sample => sample(s)
| #Mean => T.mean(s) | #Mean => T.mean(s)
| #Min => T.minX(s)
| #Max => T.maxX(s)
} }
let toSparkline = (t: t, bucketCount): result<string, PointSetTypes.sparklineError> => let toSparkline = (t: t, bucketCount): result<string, PointSetTypes.sparklineError> =>

View File

@ -449,6 +449,8 @@ module T = {
| #Cdf(f) => Ok(cdf(f, s)) | #Cdf(f) => Ok(cdf(f, s))
| #Pdf(f) => Ok(pdf(f, s)) | #Pdf(f) => Ok(pdf(f, s))
| #Inv(f) => Ok(inv(f, s)) | #Inv(f) => Ok(inv(f, s))
| #Min => Ok(min(s))
| #Max => Ok(max(s))
| #Sample => Ok(sample(s)) | #Sample => Ok(sample(s))
| #Mean => mean(s) | #Mean => mean(s)
} }

View File

@ -264,6 +264,9 @@ basicLiteral
identifier 'identifier' identifier 'identifier'
= ([_a-z]+[_a-z0-9]i*) {return nodeIdentifier(text())} = ([_a-z]+[_a-z0-9]i*) {return nodeIdentifier(text())}
unitIdentifier 'identifier'
= ([_a-zA-Z]+[_a-z0-9]i*) {return nodeIdentifier(text())}
dollarIdentifier '$identifier' dollarIdentifier '$identifier'
= ([\$_a-z]+[\$_a-z0-9]i*) {return nodeIdentifier(text())} = ([\$_a-z]+[\$_a-z0-9]i*) {return nodeIdentifier(text())}
@ -271,7 +274,7 @@ string 'string'
= characters:("'" @([^'])* "'") {return nodeString(characters.join(''))} = characters:("'" @([^'])* "'") {return nodeString(characters.join(''))}
/ characters:('"' @([^"])* '"') {return nodeString(characters.join(''))} / characters:('"' @([^"])* '"') {return nodeString(characters.join(''))}
number = number:(float / integer) unit:identifier? number = number:(float / integer) unit:unitIdentifier?
{ {
if (unit === null) if (unit === null)
{ return number } { return number }

View File

@ -0,0 +1,27 @@
module EV = ReducerInterface_ExpressionValue
type expressionValue = EV.expressionValue
let dispatch = (call: EV.functionCall, _: DistributionOperation.env): option<
result<expressionValue, QuriSquiggleLang.Reducer_ErrorValue.errorValue>,
> => {
switch call {
| ("toString", [EvDate(t)]) => EV.EvString(DateTime.Date.toString(t))->Ok->Some
| ("makeDateFromYear", [EvNumber(year)]) =>
switch DateTime.Date.makeFromYear(year) {
| Ok(t) => EV.EvDate(t)->Ok->Some
| Error(e) => Reducer_ErrorValue.RETodo(e)->Error->Some
}
| ("dateFromNumber", [EvNumber(f)]) => EV.EvDate(DateTime.Date.fromFloat(f))->Ok->Some
| ("toNumber", [EvDate(f)]) => EV.EvNumber(DateTime.Date.toFloat(f))->Ok->Some
| ("subtract", [EvDate(d1), EvDate(d2)]) =>
switch DateTime.Date.subtract(d1, d2) {
| Ok(d) => EV.EvTimeDuration(d)->Ok
| Error(e) => Error(RETodo(e))
}->Some
| ("subtract", [EvDate(d1), EvTimeDuration(d2)]) =>
EV.EvDate(DateTime.Date.subtractDuration(d1, d2))->Ok->Some
| ("add", [EvDate(d1), EvTimeDuration(d2)]) =>
EV.EvDate(DateTime.Date.addDuration(d1, d2))->Ok->Some
| _ => None
}
}

View File

@ -1,32 +1,7 @@
module EV = ReducerInterface_ExpressionValue module EV = ReducerInterface_ExpressionValue
type expressionValue = EV.expressionValue type expressionValue = EV.expressionValue
let dateDispatch = (call: EV.functionCall, _: DistributionOperation.env): option< let dispatch = (call: EV.functionCall, _: DistributionOperation.env): option<
result<expressionValue, QuriSquiggleLang.Reducer_ErrorValue.errorValue>,
> => {
switch call {
| ("toString", [EvDate(t)]) => EV.EvString(DateTime.Date.toString(t))->Ok->Some
| ("makeDateFromYear", [EvNumber(year)]) =>
switch DateTime.Date.makeFromYear(year) {
| Ok(t) => EV.EvDate(t)->Ok->Some
| Error(e) => Reducer_ErrorValue.RETodo(e)->Error->Some
}
| ("dateFromNumber", [EvNumber(f)]) => EV.EvDate(DateTime.Date.fromFloat(f))->Ok->Some
| ("toNumber", [EvDate(f)]) => EV.EvNumber(DateTime.Date.toFloat(f))->Ok->Some
| ("subtract", [EvDate(d1), EvDate(d2)]) =>
switch DateTime.Date.subtract(d1, d2) {
| Ok(d) => EV.EvTimeDuration(d)->Ok
| Error(e) => Error(RETodo(e))
}->Some
| ("subtract", [EvDate(d1), EvTimeDuration(d2)]) =>
EV.EvDate(DateTime.Date.subtractDuration(d1, d2))->Ok->Some
| ("add", [EvDate(d1), EvTimeDuration(d2)]) =>
EV.EvDate(DateTime.Date.addDuration(d1, d2))->Ok->Some
| _ => None
}
}
let durationDispatch = (call: EV.functionCall, _: DistributionOperation.env): option<
result<expressionValue, QuriSquiggleLang.Reducer_ErrorValue.errorValue>, result<expressionValue, QuriSquiggleLang.Reducer_ErrorValue.errorValue>,
> => { > => {
switch call { switch call {
@ -55,16 +30,3 @@ let durationDispatch = (call: EV.functionCall, _: DistributionOperation.env): op
| _ => None | _ => None
} }
} }
let dispatch = (call: EV.functionCall, env: DistributionOperation.env): option<
result<expressionValue, QuriSquiggleLang.Reducer_ErrorValue.errorValue>,
> => {
switch dateDispatch(call, env) {
| Some(r) => Some(r)
| None =>
switch durationDispatch(call, env) {
| Some(r) => Some(r)
| None => None
}
}
}

View File

@ -14,15 +14,27 @@ type expressionValue = ExpressionValue.expressionValue
Map external calls of Reducer Map external calls of Reducer
*/ */
// I expect that it's important to build this first, so it doesn't get recalculated for each tryRegistry() call.
let registry = FunctionRegistry_Library.registry
let tryRegistry = ((fnName, args): ExpressionValue.functionCall, env) => {
FunctionRegistry_Core.Registry.matchAndRun(~registry, ~fnName, ~args, ~env)->E.O2.fmap(
E.R2.errMap(_, s => Reducer_ErrorValue.RETodo(s)),
)
}
let dispatch = (call: ExpressionValue.functionCall, environment, chain): result< let dispatch = (call: ExpressionValue.functionCall, environment, chain): result<
expressionValue, expressionValue,
'e, 'e,
> => > => {
switch ReducerInterface_GenericDistribution.dispatch(call, environment) { E.A.O.firstSomeFn([
| Some(r) => r () => ReducerInterface_GenericDistribution.dispatch(call, environment),
| None => () => ReducerInterface_Date.dispatch(call, environment),
ReducerInterface_DateTime.dispatch(call, environment) |> E.O.default(chain(call, environment)) () => ReducerInterface_Duration.dispatch(call, environment),
} () => ReducerInterface_Number.dispatch(call, environment),
() => tryRegistry(call, environment),
])->E.O2.default(chain(call, environment))
}
/* /*
If your dispatch is too big you can divide it into smaller dispatches and pass the call so that it gets called finally. If your dispatch is too big you can divide it into smaller dispatches and pass the call so that it gets called finally.

View File

@ -209,7 +209,18 @@ let dispatchToGenericOutput = (
| ("sample", [EvDistribution(dist)]) => Helpers.toFloatFn(#Sample, dist, ~env) | ("sample", [EvDistribution(dist)]) => Helpers.toFloatFn(#Sample, dist, ~env)
| ("sampleN", [EvDistribution(dist), EvNumber(n)]) => | ("sampleN", [EvDistribution(dist), EvNumber(n)]) =>
Some(FloatArray(GenericDist.sampleN(dist, Belt.Int.fromFloat(n)))) Some(FloatArray(GenericDist.sampleN(dist, Belt.Int.fromFloat(n))))
| ("mean", [EvDistribution(dist)]) => Helpers.toFloatFn(#Mean, dist, ~env) | (("mean" | "stdev" | "variance" | "min" | "max" | "mode") as op, [EvDistribution(dist)]) => {
let fn = switch op {
| "mean" => #Mean
| "stdev" => #Stdev
| "variance" => #Variance
| "min" => #Min
| "max" => #Max
| "mode" => #Mode
| _ => #Mean
}
Helpers.toFloatFn(fn, dist, ~env)
}
| ("integralSum", [EvDistribution(dist)]) => Helpers.toFloatFn(#IntegralSum, dist, ~env) | ("integralSum", [EvDistribution(dist)]) => Helpers.toFloatFn(#IntegralSum, dist, ~env)
| ("toString", [EvDistribution(dist)]) => Helpers.toStringFn(ToString, dist, ~env) | ("toString", [EvDistribution(dist)]) => Helpers.toStringFn(ToString, dist, ~env)
| ("toSparkline", [EvDistribution(dist)]) => | ("toSparkline", [EvDistribution(dist)]) =>
@ -350,20 +361,5 @@ let genericOutputToReducerValue = (o: DistributionOperation.outputType): result<
| GenDistError(err) => Error(REDistributionError(err)) | GenDistError(err) => Error(REDistributionError(err))
} }
// I expect that it's important to build this first, so it doesn't get recalculated for each tryRegistry() call. let dispatch = (call: ExpressionValue.functionCall, environment) =>
let registry = FunctionRegistry_Library.registry dispatchToGenericOutput(call, environment)->E.O2.fmap(genericOutputToReducerValue)
let tryRegistry = ((fnName, args): ExpressionValue.functionCall, env) => {
FunctionRegistry_Core.Registry.matchAndRun(~registry, ~fnName, ~args, ~env)->E.O2.fmap(
E.R2.errMap(_, s => Reducer_ErrorValue.RETodo(s)),
)
}
let dispatch = (call: ExpressionValue.functionCall, environment) => {
let regularDispatch =
dispatchToGenericOutput(call, environment)->E.O2.fmap(genericOutputToReducerValue)
switch regularDispatch {
| Some(x) => Some(x)
| None => tryRegistry(call, environment)
}
}

View File

@ -0,0 +1,45 @@
module EV = ReducerInterface_ExpressionValue
type expressionValue = EV.expressionValue
module ScientificUnit = {
let nameToMultiplier = str =>
switch str {
| "n" => Some(1E-9)
| "m" => Some(1E-3)
| "k" => Some(1E3)
| "M" => Some(1E6)
| "B" => Some(1E9)
| "G" => Some(1E9)
| "T" => Some(1E12)
| "P" => Some(1E15)
| _ => None
}
let getMultiplier = (r: string) => {
let match = Js.String2.match_(r, %re(`/fromUnit_([_a-zA-Z]*)/`))
switch match {
| Some([_, unit]) => nameToMultiplier(unit)
| _ => None
}
}
}
let dispatch = (call: EV.functionCall, _: DistributionOperation.env): option<
result<expressionValue, QuriSquiggleLang.Reducer_ErrorValue.errorValue>,
> => {
switch call {
| (
("fromUnit_n"
| "fromUnit_m"
| "fromUnit_k"
| "fromUnit_M"
| "fromUnit_B"
| "fromUnit_G"
| "fromUnit_T"
| "fromUnit_P") as op,
[EvNumber(f)],
) =>
op->ScientificUnit.getMultiplier->E.O2.fmap(multiplier => EV.EvNumber(f *. multiplier)->Ok)
| _ => None
}
}

View File

@ -26,6 +26,8 @@ type distToFloatOperation = [
| #Inv(float) | #Inv(float)
| #Mean | #Mean
| #Sample | #Sample
| #Min
| #Max
] ]
module Convolution = { module Convolution = {

View File

@ -19,27 +19,7 @@ const config = {
organizationName: "quantified-uncertainty", // Usually your GitHub org/user name. organizationName: "quantified-uncertainty", // Usually your GitHub org/user name.
projectName: "squiggle", // Usually your repo name. projectName: "squiggle", // Usually your repo name.
plugins: [ plugins: [],
"docusaurus-tailwindcss",
() => ({
configureWebpack(config, isServer, utils, content) {
return {
resolve: {
alias: {
"@quri/squiggle-components": path.resolve(
__dirname,
"../components/src"
),
"@quri/squiggle-lang": path.resolve(
__dirname,
"../squiggle-lang/src/js"
),
},
},
};
},
}),
],
presets: [ presets: [
[ [
@ -61,7 +41,10 @@ const config = {
"https://github.com/quantified-uncertainty/squiggle/tree/develop/packages/website/", "https://github.com/quantified-uncertainty/squiggle/tree/develop/packages/website/",
}, },
theme: { theme: {
customCss: require.resolve("./src/css/custom.css"), customCss: [
require.resolve("./src/css/custom.css"),
require.resolve("@quri/squiggle-components/dist/main.css"),
],
}, },
}), }),
], ],

View File

@ -1,6 +1,6 @@
[build] [build]
base = "packages/website/" base = "packages/website/"
command = "cd ../squiggle-lang && yarn build && cd ../website && yarn build" command = "cd ../squiggle-lang && yarn build && cd ../components && yarn build && cd ../website && yarn build"
publish = "build/" publish = "build/"
ignore = "node -e 'process.exitCode = process.env.BRANCH.includes(\"dependabot\") ? 0 : 1' && git diff --quiet $CACHED_COMMIT_REF $COMMIT_REF ../" ignore = "node -e 'process.exitCode = process.env.BRANCH.includes(\"dependabot\") ? 0 : 1' && git diff --quiet $CACHED_COMMIT_REF $COMMIT_REF ../"

View File

@ -4,10 +4,6 @@
* work well for content-centric websites. * work well for content-centric websites.
*/ */
@tailwind base;
@tailwind components;
@tailwind utilities;
/* You can override the default Infima variables here. */ /* You can override the default Infima variables here. */
:root { :root {
--ifm-color-primary: #2488df; --ifm-color-primary: #2488df;

View File

@ -0,0 +1,7 @@
module.exports = {
content: ["./src/**/*.{html,tsx,ts,js,jsx}"],
theme: {
extend: {},
},
plugins: [require("@tailwindcss/forms")],
};

View File

@ -0,0 +1 @@
module.exports = {};

748
yarn.lock

File diff suppressed because it is too large Load Diff