Simple line chart for Functions

This commit is contained in:
Ozzie Gooen 2022-05-26 14:41:58 -04:00
parent c9a54d4c14
commit f393cfda9f
6 changed files with 504 additions and 193 deletions

View File

@ -7,34 +7,11 @@ import {
lambdaValue, lambdaValue,
environment, environment,
runForeign, runForeign,
squiggleExpression,
errorValue,
errorValueToString,
} from "@quri/squiggle-lang"; } from "@quri/squiggle-lang";
import { createClassFromSpec } from "react-vega"; import { FunctionChart1Dist } from "./FunctionChart1Dist";
import * as percentilesSpec from "../vega-specs/spec-percentiles.json"; import { FunctionChart1Number } from "./FunctionChart1Number";
import { DistributionChart } from "./DistributionChart";
import { NumberShower } from "./NumberShower";
import { ErrorBox } from "./ErrorBox"; import { ErrorBox } from "./ErrorBox";
let SquigglePercentilesChart = createClassFromSpec({
spec: percentilesSpec as Spec,
});
const _rangeByCount = (start: number, stop: number, count: number) => {
const step = (stop - start) / (count - 1);
const items = _.range(start, stop, step);
const result = items.concat([stop]);
return result;
};
function unwrap<a, b>(x: result<a, b>): a {
if (x.tag === "Ok") {
return x.value;
} else {
throw Error("FAILURE TO UNWRAP");
}
}
export type FunctionChartSettings = { export type FunctionChartSettings = {
start: number; start: number;
stop: number; stop: number;
@ -48,167 +25,47 @@ interface FunctionChartProps {
height: number; height: number;
} }
type percentiles = {
x: number;
p1: number;
p5: number;
p10: number;
p20: number;
p30: number;
p40: number;
p50: number;
p60: number;
p70: number;
p80: number;
p90: number;
p95: number;
p99: number;
}[];
type errors = _.Dictionary<
{
x: number;
value: string;
}[]
>;
type point = { x: number; value: result<Distribution, string> };
let getPercentiles = ({ chartSettings, fn, environment }) => {
let chartPointsToRender = _rangeByCount(
chartSettings.start,
chartSettings.stop,
chartSettings.count
);
let chartPointsData: point[] = chartPointsToRender.map((x) => {
let result = runForeign(fn, [x], environment);
if (result.tag === "Ok") {
if (result.value.tag == "distribution") {
return { x, value: { tag: "Ok", value: result.value.value } };
} else {
return {
x,
value: {
tag: "Error",
value:
"Cannot currently render functions that don't return distributions",
},
};
}
} else {
return {
x,
value: { tag: "Error", value: errorValueToString(result.value) },
};
}
});
let initialPartition: [
{ x: number; value: Distribution }[],
{ x: number; value: string }[]
] = [[], []];
let [functionImage, errors] = chartPointsData.reduce((acc, current) => {
if (current.value.tag === "Ok") {
acc[0].push({ x: current.x, value: current.value.value });
} else {
acc[1].push({ x: current.x, value: current.value.value });
}
return acc;
}, initialPartition);
let groupedErrors: errors = _.groupBy(errors, (x) => x.value);
let percentiles: percentiles = functionImage.map(({ x, value }) => {
// We convert it to to a pointSet distribution first, so that in case its a sample set
// distribution, it doesn't internally convert it to a pointSet distribution for every
// single inv() call.
let toPointSet: Distribution = unwrap(value.toPointSet());
return {
x: x,
p1: unwrap(toPointSet.inv(0.01)),
p5: unwrap(toPointSet.inv(0.05)),
p10: unwrap(toPointSet.inv(0.1)),
p20: unwrap(toPointSet.inv(0.2)),
p30: unwrap(toPointSet.inv(0.3)),
p40: unwrap(toPointSet.inv(0.4)),
p50: unwrap(toPointSet.inv(0.5)),
p60: unwrap(toPointSet.inv(0.6)),
p70: unwrap(toPointSet.inv(0.7)),
p80: unwrap(toPointSet.inv(0.8)),
p90: unwrap(toPointSet.inv(0.9)),
p95: unwrap(toPointSet.inv(0.95)),
p99: unwrap(toPointSet.inv(0.99)),
};
});
return { percentiles, errors: groupedErrors };
};
export const FunctionChart: React.FC<FunctionChartProps> = ({ export const FunctionChart: React.FC<FunctionChartProps> = ({
fn, fn,
chartSettings, chartSettings,
environment, environment,
height height,
}: FunctionChartProps) => { }: FunctionChartProps) => {
let [mouseOverlay, setMouseOverlay] = React.useState(0); let result = runForeign(fn, [chartSettings.start], environment);
function handleHover(_name: string, value: unknown) { let resultType = result.tag === "Ok" ? result.value.tag : "Error";
setMouseOverlay(value as number);
}
function handleOut() {
setMouseOverlay(NaN);
}
const signalListeners = { mousemove: handleHover, mouseout: handleOut };
let mouseItem: result<squiggleExpression, errorValue> = !!mouseOverlay
? runForeign(fn, [mouseOverlay], environment)
: {
tag: "Error",
value: {
tag: "REExpectedType",
value: "Hover x-coordinate returned NaN. Expected a number.",
},
};
let showChart =
mouseItem.tag === "Ok" && mouseItem.value.tag == "distribution" ? (
<DistributionChart
distribution={mouseItem.value.value}
width={400}
height={50}
showSummary={false}
/>
) : (
<></>
);
let getPercentilesMemoized = React.useMemo( let comp = () => {
() => getPercentiles({ chartSettings, fn, environment }), switch (resultType) {
[environment, fn] case "distribution":
); return (
<FunctionChart1Dist
return ( fn={fn}
<> chartSettings={chartSettings}
<SquigglePercentilesChart environment={environment}
data={{ facet: getPercentilesMemoized.percentiles }} height={height}
height={height} />
actions={false} );
signalListeners={signalListeners} case "number":
/> return (
{showChart} <FunctionChart1Number
{_.entries(getPercentilesMemoized.errors).map( fn={fn}
([errorName, errorPoints]) => ( chartSettings={chartSettings}
<ErrorBox key={errorName} heading={errorName}> environment={environment}
Values:{" "} height={height}
{errorPoints />
.map((r, i) => <NumberShower key={i} number={r.x} />) );
.reduce((a, b) => ( case "Error":
<> return (
{a}, {b} <ErrorBox heading="Error">The function failed to be run</ErrorBox>
</> );
))} default:
return (
<ErrorBox heading="No Viewer">
There is no function visualization for this type of function
</ErrorBox> </ErrorBox>
) );
)} }
</> };
);
return comp();
}; };

View File

@ -0,0 +1,214 @@
import * as React from "react";
import _ from "lodash";
import type { Spec } from "vega";
import {
Distribution,
result,
lambdaValue,
environment,
runForeign,
squiggleExpression,
errorValue,
errorValueToString,
} from "@quri/squiggle-lang";
import { createClassFromSpec } from "react-vega";
import * as percentilesSpec from "../vega-specs/spec-percentiles.json";
import { DistributionChart } from "./DistributionChart";
import { NumberShower } from "./NumberShower";
import { ErrorBox } from "./ErrorBox";
let SquigglePercentilesChart = createClassFromSpec({
spec: percentilesSpec as Spec,
});
const _rangeByCount = (start: number, stop: number, count: number) => {
const step = (stop - start) / (count - 1);
const items = _.range(start, stop, step);
const result = items.concat([stop]);
return result;
};
function unwrap<a, b>(x: result<a, b>): a {
if (x.tag === "Ok") {
return x.value;
} else {
throw Error("FAILURE TO UNWRAP");
}
}
export type FunctionChartSettings = {
start: number;
stop: number;
count: number;
};
interface FunctionChartProps {
fn: lambdaValue;
chartSettings: FunctionChartSettings;
environment: environment;
height: number;
}
type percentiles = {
x: number;
p1: number;
p5: number;
p10: number;
p20: number;
p30: number;
p40: number;
p50: number;
p60: number;
p70: number;
p80: number;
p90: number;
p95: number;
p99: number;
}[];
type errors = _.Dictionary<
{
x: number;
value: string;
}[]
>;
type point = { x: number; value: result<Distribution, string> };
let getPercentiles = ({ chartSettings, fn, environment }) => {
let chartPointsToRender = _rangeByCount(
chartSettings.start,
chartSettings.stop,
chartSettings.count
);
let chartPointsData: point[] = chartPointsToRender.map((x) => {
let result = runForeign(fn, [x], environment);
if (result.tag === "Ok") {
if (result.value.tag == "distribution") {
return { x, value: { tag: "Ok", value: result.value.value } };
} else {
return {
x,
value: {
tag: "Error",
value:
"Cannot currently render functions that don't return distributions",
},
};
}
} else {
return {
x,
value: { tag: "Error", value: errorValueToString(result.value) },
};
}
});
let initialPartition: [
{ x: number; value: Distribution }[],
{ x: number; value: string }[]
] = [[], []];
let [functionImage, errors] = chartPointsData.reduce((acc, current) => {
if (current.value.tag === "Ok") {
acc[0].push({ x: current.x, value: current.value.value });
} else {
acc[1].push({ x: current.x, value: current.value.value });
}
return acc;
}, initialPartition);
let groupedErrors: errors = _.groupBy(errors, (x) => x.value);
let percentiles: percentiles = functionImage.map(({ x, value }) => {
// We convert it to to a pointSet distribution first, so that in case its a sample set
// distribution, it doesn't internally convert it to a pointSet distribution for every
// single inv() call.
let toPointSet: Distribution = unwrap(value.toPointSet());
return {
x: x,
p1: unwrap(toPointSet.inv(0.01)),
p5: unwrap(toPointSet.inv(0.05)),
p10: unwrap(toPointSet.inv(0.1)),
p20: unwrap(toPointSet.inv(0.2)),
p30: unwrap(toPointSet.inv(0.3)),
p40: unwrap(toPointSet.inv(0.4)),
p50: unwrap(toPointSet.inv(0.5)),
p60: unwrap(toPointSet.inv(0.6)),
p70: unwrap(toPointSet.inv(0.7)),
p80: unwrap(toPointSet.inv(0.8)),
p90: unwrap(toPointSet.inv(0.9)),
p95: unwrap(toPointSet.inv(0.95)),
p99: unwrap(toPointSet.inv(0.99)),
};
});
return { percentiles, errors: groupedErrors };
};
export const FunctionChart1Dist: React.FC<FunctionChartProps> = ({
fn,
chartSettings,
environment,
height
}: FunctionChartProps) => {
let [mouseOverlay, setMouseOverlay] = React.useState(0);
function handleHover(_name: string, value: unknown) {
setMouseOverlay(value as number);
}
function handleOut() {
setMouseOverlay(NaN);
}
const signalListeners = { mousemove: handleHover, mouseout: handleOut };
let mouseItem: result<squiggleExpression, errorValue> = !!mouseOverlay
? runForeign(fn, [mouseOverlay], environment)
: {
tag: "Error",
value: {
tag: "REExpectedType",
value: "Hover x-coordinate returned NaN. Expected a number.",
},
};
let showChart =
mouseItem.tag === "Ok" && mouseItem.value.tag == "distribution" ? (
<DistributionChart
distribution={mouseItem.value.value}
width={400}
height={50}
showSummary={false}
/>
) : (
<></>
);
let getPercentilesMemoized = React.useMemo(
() => getPercentiles({ chartSettings, fn, environment }),
[environment, fn]
);
return (
<>
<SquigglePercentilesChart
data={{ facet: getPercentilesMemoized.percentiles }}
height={height}
actions={false}
signalListeners={signalListeners}
/>
{showChart}
{_.entries(getPercentilesMemoized.errors).map(
([errorName, errorPoints]) => (
<ErrorBox key={errorName} heading={errorName}>
Values:{" "}
{errorPoints
.map((r, i) => <NumberShower key={i} number={r.x} />)
.reduce((a, b) => (
<>
{a}, {b}
</>
))}
</ErrorBox>
)
)}
</>
);
};

View File

@ -0,0 +1,145 @@
import * as React from "react";
import _ from "lodash";
import type { Spec } from "vega";
import {
Distribution,
result,
lambdaValue,
environment,
runForeign,
squiggleExpression,
errorValue,
errorValueToString,
} from "@quri/squiggle-lang";
import { createClassFromSpec } from "react-vega";
import * as lineChartSpec from "../vega-specs/spec-line-chart.json";
import { DistributionChart } from "./DistributionChart";
import { NumberShower } from "./NumberShower";
import { ErrorBox } from "./ErrorBox";
let SquiggleLineChart = createClassFromSpec({
spec: lineChartSpec as Spec,
});
const _rangeByCount = (start: number, stop: number, count: number) => {
const step = (stop - start) / (count - 1);
const items = _.range(start, stop, step);
const result = items.concat([stop]);
return result;
};
function unwrap<a, b>(x: result<a, b>): a {
if (x.tag === "Ok") {
return x.value;
} else {
throw Error("FAILURE TO UNWRAP");
}
}
export type FunctionChartSettings = {
start: number;
stop: number;
count: number;
};
interface FunctionChartProps {
fn: lambdaValue;
chartSettings: FunctionChartSettings;
environment: environment;
height: number;
}
type point = { x: number; value: result<number, string> };
let getFunctionImage = ({ chartSettings, fn, environment }) => {
let chartPointsToRender = _rangeByCount(
chartSettings.start,
chartSettings.stop,
chartSettings.count
);
let chartPointsData: point[] = chartPointsToRender.map((x) => {
let result = runForeign(fn, [x], environment);
if (result.tag === "Ok") {
if (result.value.tag == "number") {
return { x, value: { tag: "Ok", value: result.value.value } };
} else {
return {
x,
value: {
tag: "Error",
value:
"Cannot currently render functions that don't return distributions",
},
};
}
} else {
return {
x,
value: { tag: "Error", value: errorValueToString(result.value) },
};
}
});
let initialPartition: [
{ x: number; value: number }[],
{ x: number; value: string }[]
] = [[], []];
let [functionImage, errors] = chartPointsData.reduce((acc, current) => {
if (current.value.tag === "Ok") {
acc[0].push({ x: current.x, value: current.value.value });
} else {
acc[1].push({ x: current.x, value: current.value.value });
}
return acc;
}, initialPartition);
return { errors, functionImage };
};
export const FunctionChart1Number: React.FC<FunctionChartProps> = ({
fn,
chartSettings,
environment,
height,
}: FunctionChartProps) => {
let [mouseOverlay, setMouseOverlay] = React.useState(0);
function handleHover(_name: string, value: unknown) {
setMouseOverlay(value as number);
}
function handleOut() {
setMouseOverlay(NaN);
}
const signalListeners = { mousemove: handleHover, mouseout: handleOut };
let mouseItem: result<squiggleExpression, errorValue> = !!mouseOverlay
? runForeign(fn, [mouseOverlay], environment)
: {
tag: "Error",
value: {
tag: "REExpectedType",
value: "Hover x-coordinate returned NaN. Expected a number.",
},
};
let getFunctionImageMemoized = React.useMemo(
() => getFunctionImage({ chartSettings, fn, environment }),
[environment, fn]
);
let data = getFunctionImageMemoized.functionImage.map(({x, value}) => ({x, y:value}))
return (
<>
<SquiggleLineChart
data={{ facet: data }}
height={height}
actions={false}
signalListeners={signalListeners}
/>
{getFunctionImageMemoized.errors.map(({ x, value }) => (
<ErrorBox key={x} heading={value}>
Error at point ${x}
</ErrorBox>
))}
</>
);
};

View File

@ -72,9 +72,13 @@ const Display = styled.div<TitleProps>`
max-height: ${(props) => props.maxHeight}px; max-height: ${(props) => props.maxHeight}px;
`; `;
const Row = styled.div` interface RowProps {
readonly leftPercentage: number;
}
const Row = styled.div<RowProps>`
display: grid; display: grid;
grid-template-columns: 50% 50%; grid-template-columns: ${(p) => p.leftPercentage}% ${(p) => 100 - p.leftPercentage}%;
`; `;
const Col = styled.div``; const Col = styled.div``;
@ -111,6 +115,7 @@ const schema = yup
.min(10) .min(10)
.max(10000), .max(10000),
chartHeight: yup.number().required().positive().integer().default(350), chartHeight: yup.number().required().positive().integer().default(350),
leftSize: yup.number().required().positive().integer().min(10).max(100).default(50),
showTypes: yup.boolean(), showTypes: yup.boolean(),
showControls: yup.boolean(), showControls: yup.boolean(),
showSummary: yup.boolean(), showSummary: yup.boolean(),
@ -152,14 +157,15 @@ let SquigglePlayground: FC<PlaygroundProps> = ({
showTypes: showTypes, showTypes: showTypes,
showControls: showControls, showControls: showControls,
showSummary: showSummary, showSummary: showSummary,
leftSize: 50,
}, },
}); });
const foo = useWatch({ const vars = useWatch({
control, control,
}); });
let env: environment = { let env: environment = {
sampleCount: Number(foo.sampleCount), sampleCount: Number(vars.sampleCount),
xyPointLength: Number(foo.xyPointLength), xyPointLength: Number(vars.xyPointLength),
}; };
let getChangeJson = (r: string) => { let getChangeJson = (r: string) => {
setImportString(r); setImportString(r);
@ -169,17 +175,17 @@ let SquigglePlayground: FC<PlaygroundProps> = ({
} catch (e) { } catch (e) {
setImportsAreValid(false); setImportsAreValid(false);
} }
("");
}; };
return ( return (
<ShowBox height={height}> <ShowBox height={height}>
<input type="number" {...register("sampleCount")} /> <input type="number" {...register("sampleCount")} />
<input type="number" {...register("xyPointLength")} /> <input type="number" {...register("xyPointLength")} />
<input type="number" {...register("chartHeight")} /> <input type="number" {...register("chartHeight")} />
<input type="number" {...register("leftSize")} />
<input type="checkbox" {...register("showTypes")} /> <input type="checkbox" {...register("showTypes")} />
<input type="checkbox" {...register("showControls")} /> <input type="checkbox" {...register("showControls")} />
<input type="checkbox" {...register("showSummary")} /> <input type="checkbox" {...register("showSummary")} />
<Row> <Row leftPercentage={vars.leftSize || 50}>
<Col> <Col>
<CodeEditor <CodeEditor
value={squiggleString} value={squiggleString}
@ -203,12 +209,12 @@ let SquigglePlayground: FC<PlaygroundProps> = ({
squiggleString={squiggleString} squiggleString={squiggleString}
environment={env} environment={env}
chartSettings={chartSettings} chartSettings={chartSettings}
height={foo.chartHeight} height={vars.chartHeight}
showTypes={foo.showTypes} showTypes={vars.showTypes}
showControls={foo.showControls} showControls={vars.showControls}
bindings={defaultBindings} bindings={defaultBindings}
jsImports={imports} jsImports={imports}
showSummary={foo.showSummary} showSummary={vars.showSummary}
/> />
</Display> </Display>
</Col> </Col>

View File

@ -0,0 +1,88 @@
{
"$schema": "https://vega.github.io/schema/vega/v5.json",
"width": 500,
"height": 200,
"padding": 5,
"data": [
{
"name": "facet",
"values": [],
"format": {
"type": "json",
"parse": {
"timestamp": "date"
}
}
}
],
"scales": [
{
"name": "x",
"type": "linear",
"nice": true,
"domain": {
"data": "facet",
"field": "x"
},
"range": "width"
},
{
"name": "y",
"type": "linear",
"range": "height",
"nice": true,
"zero": true,
"domain": {
"data": "facet",
"field": "y"
}
}
],
"signals": [
{
"name": "mousemove",
"on": [{ "events": "mousemove", "update": "invert('x', x())" }]
},
{
"name": "mouseout",
"on": [{ "events": "mouseout", "update": "invert('x', x())" }]
}
],
"axes": [
{
"orient": "bottom",
"scale": "x",
"grid": false,
"labelColor": "#727d93",
"tickColor": "#fff",
"tickOpacity": 0.0,
"domainColor": "#727d93",
"domainOpacity": 0.1,
"tickCount": 5
},
{
"orient": "left",
"scale": "y",
"grid": false,
"labelColor": "#727d93",
"tickColor": "#fff",
"tickOpacity": 0.0,
"domainColor": "#727d93",
"domainOpacity": 0.1,
"tickCount": 5
}
],
"marks": [
{
"type": "line",
"from": { "data": "facet" },
"encode": {
"enter": {
"x": { "scale": "x", "field": "x" },
"y": { "scale": "y", "field": "y" },
"strokeWidth": { "value": 2 }
}
}
}
]
}

View File

@ -20,7 +20,8 @@
}, },
"files": [ "files": [
"src/vega-specs/spec-distributions.json", "src/vega-specs/spec-distributions.json",
"src/vega-specs/spec-percentiles.json" "src/vega-specs/spec-percentiles.json",
"src/vega-specs/spec-line-chart.json"
], ],
"target": "ES6", "target": "ES6",
"include": ["src/**/*", "src/*"], "include": ["src/**/*", "src/*"],