Merge pull request #197 from QURIresearch/Components-improvement-april-2
Components improvement april 2
This commit is contained in:
commit
e1d7a3ff7d
|
@ -1,346 +0,0 @@
|
|||
import * as React from "react";
|
||||
import _ from "lodash";
|
||||
import type { Spec } from "vega";
|
||||
import { run } from "@quri/squiggle-lang";
|
||||
import type {
|
||||
DistPlus,
|
||||
SamplingInputs,
|
||||
exportEnv,
|
||||
exportDistribution,
|
||||
} from "@quri/squiggle-lang";
|
||||
import { createClassFromSpec } from "react-vega";
|
||||
import * as chartSpecification from "./spec-distributions.json";
|
||||
import * as percentilesSpec from "./spec-percentiles.json";
|
||||
import { NumberShower } from "./NumberShower";
|
||||
import styled from "styled-components";
|
||||
|
||||
let SquiggleVegaChart = createClassFromSpec({
|
||||
spec: chartSpecification as Spec,
|
||||
});
|
||||
|
||||
let SquigglePercentilesChart = createClassFromSpec({
|
||||
spec: percentilesSpec as Spec,
|
||||
});
|
||||
|
||||
export interface SquiggleChartProps {
|
||||
/** The input string for squiggle */
|
||||
squiggleString?: string;
|
||||
|
||||
/** If the output requires monte carlo sampling, the amount of samples */
|
||||
sampleCount?: number;
|
||||
/** The amount of points returned to draw the distribution */
|
||||
outputXYPoints?: number;
|
||||
kernelWidth?: number;
|
||||
pointDistLength?: number;
|
||||
/** If the result is a function, where the function starts */
|
||||
diagramStart?: number;
|
||||
/** If the result is a function, where the function ends */
|
||||
diagramStop?: number;
|
||||
/** If the result is a function, how many points along the function it samples */
|
||||
diagramCount?: number;
|
||||
/** variables declared before this expression */
|
||||
environment?: exportEnv;
|
||||
/** When the environment changes */
|
||||
onEnvChange?(env: exportEnv): void;
|
||||
/** CSS width of the element */
|
||||
width?: number;
|
||||
height?: number;
|
||||
}
|
||||
|
||||
const Error = styled.div`
|
||||
border: 1px solid #792e2e;
|
||||
background: #eee2e2;
|
||||
padding: 0.4em 0.8em;
|
||||
`;
|
||||
|
||||
const ShowError: React.FC<{ heading: string; children: React.ReactNode }> = ({
|
||||
heading = "Error",
|
||||
children,
|
||||
}) => {
|
||||
return (
|
||||
<Error>
|
||||
<h3>{heading}</h3>
|
||||
{children}
|
||||
</Error>
|
||||
);
|
||||
};
|
||||
|
||||
export const SquiggleChart: React.FC<SquiggleChartProps> = ({
|
||||
squiggleString = "",
|
||||
sampleCount = 1000,
|
||||
outputXYPoints = 1000,
|
||||
kernelWidth,
|
||||
pointDistLength = 1000,
|
||||
diagramStart = 0,
|
||||
diagramStop = 10,
|
||||
diagramCount = 20,
|
||||
environment = [],
|
||||
onEnvChange = () => {},
|
||||
width = 500,
|
||||
height = 60,
|
||||
}: SquiggleChartProps) => {
|
||||
let samplingInputs: SamplingInputs = {
|
||||
sampleCount: sampleCount,
|
||||
outputXYPoints: outputXYPoints,
|
||||
kernelWidth: kernelWidth,
|
||||
pointDistLength: pointDistLength,
|
||||
};
|
||||
|
||||
let result = run(squiggleString, samplingInputs, environment);
|
||||
if (result.tag === "Ok") {
|
||||
let environment = result.value.environment;
|
||||
let exports = result.value.exports;
|
||||
onEnvChange(environment);
|
||||
let chartResults = exports.map((chartResult: exportDistribution) => {
|
||||
if (chartResult["NAME"] === "Float") {
|
||||
return <NumberShower precision={3} number={chartResult["VAL"]} />;
|
||||
} else if (chartResult["NAME"] === "DistPlus") {
|
||||
let shape = chartResult.VAL.pointSetDist;
|
||||
if (shape.tag === "Continuous") {
|
||||
let xyShape = shape.value.xyShape;
|
||||
let totalY = xyShape.ys.reduce((a, b) => a + b);
|
||||
let total = 0;
|
||||
let cdf = xyShape.ys.map((y) => {
|
||||
total += y;
|
||||
return total / totalY;
|
||||
});
|
||||
let values = _.zip(cdf, xyShape.xs, xyShape.ys).map(([c, x, y]) => ({
|
||||
cdf: (c * 100).toFixed(2) + "%",
|
||||
x: x,
|
||||
y: y,
|
||||
}));
|
||||
|
||||
return (
|
||||
<SquiggleVegaChart
|
||||
width={width}
|
||||
height={height}
|
||||
data={{ con: values }}
|
||||
actions={false}
|
||||
/>
|
||||
);
|
||||
} else if (shape.tag === "Discrete") {
|
||||
let xyShape = shape.value.xyShape;
|
||||
let totalY = xyShape.ys.reduce((a, b) => a + b);
|
||||
let total = 0;
|
||||
let cdf = xyShape.ys.map((y) => {
|
||||
total += y;
|
||||
return total / totalY;
|
||||
});
|
||||
let values = _.zip(cdf, xyShape.xs, xyShape.ys).map(([c, x, y]) => ({
|
||||
cdf: (c * 100).toFixed(2) + "%",
|
||||
x: x,
|
||||
y: y,
|
||||
}));
|
||||
|
||||
return <SquiggleVegaChart data={{ dis: values }} actions={false} />;
|
||||
} else if (shape.tag === "Mixed") {
|
||||
let discreteShape = shape.value.discrete.xyShape;
|
||||
let totalDiscrete = discreteShape.ys.reduce((a, b) => a + b);
|
||||
|
||||
let discretePoints = _.zip(discreteShape.xs, discreteShape.ys);
|
||||
let continuousShape = shape.value.continuous.xyShape;
|
||||
let continuousPoints = _.zip(continuousShape.xs, continuousShape.ys);
|
||||
|
||||
interface labeledPoint {
|
||||
x: number;
|
||||
y: number;
|
||||
type: "discrete" | "continuous";
|
||||
}
|
||||
|
||||
let markedDisPoints: labeledPoint[] = discretePoints.map(
|
||||
([x, y]) => ({ x: x, y: y, type: "discrete" })
|
||||
);
|
||||
let markedConPoints: labeledPoint[] = continuousPoints.map(
|
||||
([x, y]) => ({ x: x, y: y, type: "continuous" })
|
||||
);
|
||||
|
||||
let sortedPoints = _.sortBy(
|
||||
markedDisPoints.concat(markedConPoints),
|
||||
"x"
|
||||
);
|
||||
|
||||
let totalContinuous = 1 - totalDiscrete;
|
||||
let totalY = continuousShape.ys.reduce(
|
||||
(a: number, b: number) => a + b
|
||||
);
|
||||
|
||||
let total = 0;
|
||||
let cdf = sortedPoints.map((point: labeledPoint) => {
|
||||
if (point.type === "discrete") {
|
||||
total += point.y;
|
||||
return total;
|
||||
} else if (point.type === "continuous") {
|
||||
total += (point.y / totalY) * totalContinuous;
|
||||
return total;
|
||||
}
|
||||
});
|
||||
|
||||
interface cdfLabeledPoint {
|
||||
cdf: string;
|
||||
x: number;
|
||||
y: number;
|
||||
type: "discrete" | "continuous";
|
||||
}
|
||||
let cdfLabeledPoint: cdfLabeledPoint[] = _.zipWith(
|
||||
cdf,
|
||||
sortedPoints,
|
||||
(c: number, point: labeledPoint) => ({
|
||||
...point,
|
||||
cdf: (c * 100).toFixed(2) + "%",
|
||||
})
|
||||
);
|
||||
let continuousValues = cdfLabeledPoint.filter(
|
||||
(x) => x.type === "continuous"
|
||||
);
|
||||
let discreteValues = cdfLabeledPoint.filter(
|
||||
(x) => x.type === "discrete"
|
||||
);
|
||||
|
||||
return (
|
||||
<SquiggleVegaChart
|
||||
data={{ con: continuousValues, dis: discreteValues }}
|
||||
actions={false}
|
||||
/>
|
||||
);
|
||||
}
|
||||
} else if (chartResult.NAME === "Function") {
|
||||
// We are looking at a function. In this case, we draw a Percentiles chart
|
||||
let start = diagramStart;
|
||||
let stop = diagramStop;
|
||||
let count = diagramCount;
|
||||
let step = (stop - start) / count;
|
||||
let data = _.range(start, stop, step).map((x) => {
|
||||
if (chartResult.NAME === "Function") {
|
||||
let result = chartResult.VAL(x);
|
||||
if (result.tag === "Ok") {
|
||||
let percentileArray = [
|
||||
0.01, 0.05, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 0.95,
|
||||
0.99,
|
||||
];
|
||||
|
||||
let percentiles = getPercentiles(percentileArray, result.value);
|
||||
return {
|
||||
x: x,
|
||||
p1: percentiles[0],
|
||||
p5: percentiles[1],
|
||||
p10: percentiles[2],
|
||||
p20: percentiles[3],
|
||||
p30: percentiles[4],
|
||||
p40: percentiles[5],
|
||||
p50: percentiles[6],
|
||||
p60: percentiles[7],
|
||||
p70: percentiles[8],
|
||||
p80: percentiles[9],
|
||||
p90: percentiles[10],
|
||||
p95: percentiles[11],
|
||||
p99: percentiles[12],
|
||||
};
|
||||
}
|
||||
return null;
|
||||
}
|
||||
});
|
||||
return (
|
||||
<SquigglePercentilesChart
|
||||
data={{ facet: data.filter((x) => x !== null) }}
|
||||
actions={false}
|
||||
/>
|
||||
);
|
||||
}
|
||||
});
|
||||
return <>{chartResults}</>;
|
||||
} else if (result.tag === "Error") {
|
||||
// At this point, we came across an error. What was our error?
|
||||
return (
|
||||
<ShowError heading={"Parse Error"}>
|
||||
{result.value}
|
||||
</ShowError>
|
||||
);
|
||||
}
|
||||
return <p>{"Invalid Response"}</p>;
|
||||
};
|
||||
|
||||
function getPercentiles(percentiles: number[], t: DistPlus) {
|
||||
if (t.pointSetDist.tag === "Discrete") {
|
||||
let total = 0;
|
||||
let maxX = _.max(t.pointSetDist.value.xyShape.xs);
|
||||
let bounds = percentiles.map((_) => maxX);
|
||||
_.zipWith(
|
||||
t.pointSetDist.value.xyShape.xs,
|
||||
t.pointSetDist.value.xyShape.ys,
|
||||
(x, y) => {
|
||||
total += y;
|
||||
percentiles.forEach((v, i) => {
|
||||
if (total > v && bounds[i] === maxX) {
|
||||
bounds[i] = x;
|
||||
}
|
||||
});
|
||||
}
|
||||
);
|
||||
return bounds;
|
||||
} else if (t.pointSetDist.tag === "Continuous") {
|
||||
let total = 0;
|
||||
let maxX = _.max(t.pointSetDist.value.xyShape.xs);
|
||||
let totalY = _.sum(t.pointSetDist.value.xyShape.ys);
|
||||
let bounds = percentiles.map((_) => maxX);
|
||||
_.zipWith(
|
||||
t.pointSetDist.value.xyShape.xs,
|
||||
t.pointSetDist.value.xyShape.ys,
|
||||
(x, y) => {
|
||||
total += y / totalY;
|
||||
percentiles.forEach((v, i) => {
|
||||
if (total > v && bounds[i] === maxX) {
|
||||
bounds[i] = x;
|
||||
}
|
||||
});
|
||||
}
|
||||
);
|
||||
return bounds;
|
||||
} else if (t.pointSetDist.tag === "Mixed") {
|
||||
let discreteShape = t.pointSetDist.value.discrete.xyShape;
|
||||
let totalDiscrete = discreteShape.ys.reduce((a, b) => a + b);
|
||||
|
||||
let discretePoints = _.zip(discreteShape.xs, discreteShape.ys);
|
||||
let continuousShape = t.pointSetDist.value.continuous.xyShape;
|
||||
let continuousPoints = _.zip(continuousShape.xs, continuousShape.ys);
|
||||
|
||||
interface labeledPoint {
|
||||
x: number;
|
||||
y: number;
|
||||
type: "discrete" | "continuous";
|
||||
}
|
||||
|
||||
let markedDisPoints: labeledPoint[] = discretePoints.map(([x, y]) => ({
|
||||
x: x,
|
||||
y: y,
|
||||
type: "discrete",
|
||||
}));
|
||||
let markedConPoints: labeledPoint[] = continuousPoints.map(([x, y]) => ({
|
||||
x: x,
|
||||
y: y,
|
||||
type: "continuous",
|
||||
}));
|
||||
|
||||
let sortedPoints = _.sortBy(markedDisPoints.concat(markedConPoints), "x");
|
||||
|
||||
let totalContinuous = 1 - totalDiscrete;
|
||||
let totalY = continuousShape.ys.reduce((a: number, b: number) => a + b);
|
||||
|
||||
let total = 0;
|
||||
let maxX = _.max(sortedPoints.map((x) => x.x));
|
||||
let bounds = percentiles.map((_) => maxX);
|
||||
sortedPoints.map((point: labeledPoint) => {
|
||||
if (point.type === "discrete") {
|
||||
total += point.y;
|
||||
} else if (point.type === "continuous") {
|
||||
total += (point.y / totalY) * totalContinuous;
|
||||
}
|
||||
percentiles.forEach((v, i) => {
|
||||
if (total > v && bounds[i] === maxX) {
|
||||
bounds[i] = total;
|
||||
}
|
||||
});
|
||||
return total;
|
||||
});
|
||||
return bounds;
|
||||
}
|
||||
}
|
124
packages/components/src/components/DistPlusChart.tsx
Normal file
124
packages/components/src/components/DistPlusChart.tsx
Normal file
|
@ -0,0 +1,124 @@
|
|||
import * as React from "react";
|
||||
import _ from "lodash";
|
||||
import type { Spec } from "vega";
|
||||
import type {
|
||||
DistPlus,
|
||||
} from "@quri/squiggle-lang";
|
||||
import { createClassFromSpec } from "react-vega";
|
||||
import * as chartSpecification from "../vega-specs/spec-distributions.json";
|
||||
|
||||
let SquiggleVegaChart = createClassFromSpec({
|
||||
spec: chartSpecification as Spec,
|
||||
});
|
||||
|
||||
export const DistPlusChart: React.FC<{
|
||||
distPlus: DistPlus;
|
||||
width: number;
|
||||
height: number;
|
||||
}> = ({ distPlus, width, height }) => {
|
||||
let shape = distPlus.pointSetDist;
|
||||
if (shape.tag === "Continuous") {
|
||||
let xyShape = shape.value.xyShape;
|
||||
let totalY = xyShape.ys.reduce((a, b) => a + b);
|
||||
let total = 0;
|
||||
let cdf = xyShape.ys.map((y) => {
|
||||
total += y;
|
||||
return total / totalY;
|
||||
});
|
||||
let values = _.zip(cdf, xyShape.xs, xyShape.ys).map(([c, x, y]) => ({
|
||||
cdf: (c * 100).toFixed(2) + "%",
|
||||
x: x,
|
||||
y: y,
|
||||
}));
|
||||
|
||||
return (
|
||||
<SquiggleVegaChart
|
||||
width={width}
|
||||
height={height}
|
||||
data={{ con: values }}
|
||||
actions={false}
|
||||
/>
|
||||
);
|
||||
} else if (shape.tag === "Discrete") {
|
||||
let xyShape = shape.value.xyShape;
|
||||
let totalY = xyShape.ys.reduce((a, b) => a + b);
|
||||
let total = 0;
|
||||
let cdf = xyShape.ys.map((y) => {
|
||||
total += y;
|
||||
return total / totalY;
|
||||
});
|
||||
let values = _.zip(cdf, xyShape.xs, xyShape.ys).map(([c, x, y]) => ({
|
||||
cdf: (c * 100).toFixed(2) + "%",
|
||||
x: x,
|
||||
y: y,
|
||||
}));
|
||||
|
||||
return <SquiggleVegaChart data={{ dis: values }} actions={false} />;
|
||||
} else if (shape.tag === "Mixed") {
|
||||
let discreteShape = shape.value.discrete.xyShape;
|
||||
let totalDiscrete = discreteShape.ys.reduce((a, b) => a + b);
|
||||
|
||||
let discretePoints = _.zip(discreteShape.xs, discreteShape.ys);
|
||||
let continuousShape = shape.value.continuous.xyShape;
|
||||
let continuousPoints = _.zip(continuousShape.xs, continuousShape.ys);
|
||||
|
||||
interface labeledPoint {
|
||||
x: number;
|
||||
y: number;
|
||||
type: "discrete" | "continuous";
|
||||
}
|
||||
|
||||
let markedDisPoints: labeledPoint[] = discretePoints.map(([x, y]) => ({
|
||||
x: x,
|
||||
y: y,
|
||||
type: "discrete",
|
||||
}));
|
||||
let markedConPoints: labeledPoint[] = continuousPoints.map(([x, y]) => ({
|
||||
x: x,
|
||||
y: y,
|
||||
type: "continuous",
|
||||
}));
|
||||
|
||||
let sortedPoints = _.sortBy(markedDisPoints.concat(markedConPoints), "x");
|
||||
|
||||
let totalContinuous = 1 - totalDiscrete;
|
||||
let totalY = continuousShape.ys.reduce((a: number, b: number) => a + b);
|
||||
|
||||
let total = 0;
|
||||
let cdf = sortedPoints.map((point: labeledPoint) => {
|
||||
if (point.type === "discrete") {
|
||||
total += point.y;
|
||||
return total;
|
||||
} else if (point.type === "continuous") {
|
||||
total += (point.y / totalY) * totalContinuous;
|
||||
return total;
|
||||
}
|
||||
});
|
||||
|
||||
interface cdfLabeledPoint {
|
||||
cdf: string;
|
||||
x: number;
|
||||
y: number;
|
||||
type: "discrete" | "continuous";
|
||||
}
|
||||
let cdfLabeledPoint: cdfLabeledPoint[] = _.zipWith(
|
||||
cdf,
|
||||
sortedPoints,
|
||||
(c: number, point: labeledPoint) => ({
|
||||
...point,
|
||||
cdf: (c * 100).toFixed(2) + "%",
|
||||
})
|
||||
);
|
||||
let continuousValues = cdfLabeledPoint.filter(
|
||||
(x) => x.type === "continuous"
|
||||
);
|
||||
let discreteValues = cdfLabeledPoint.filter((x) => x.type === "discrete");
|
||||
|
||||
return (
|
||||
<SquiggleVegaChart
|
||||
data={{ con: continuousValues, dis: discreteValues }}
|
||||
actions={false}
|
||||
/>
|
||||
);
|
||||
}
|
||||
};
|
20
packages/components/src/components/Error.tsx
Normal file
20
packages/components/src/components/Error.tsx
Normal file
|
@ -0,0 +1,20 @@
|
|||
import * as React from "react";
|
||||
import styled from "styled-components";
|
||||
|
||||
const ShowError = styled.div`
|
||||
border: 1px solid #792e2e;
|
||||
background: #eee2e2;
|
||||
padding: 0.4em 0.8em;
|
||||
`;
|
||||
|
||||
export const Error: React.FC<{ heading: string; children: React.ReactNode }> = ({
|
||||
heading = "Error",
|
||||
children,
|
||||
}) => {
|
||||
return (
|
||||
<ShowError>
|
||||
<h3>{heading}</h3>
|
||||
{children}
|
||||
</ShowError>
|
||||
);
|
||||
};
|
188
packages/components/src/components/FunctionChart.tsx
Normal file
188
packages/components/src/components/FunctionChart.tsx
Normal file
|
@ -0,0 +1,188 @@
|
|||
import * as React from "react";
|
||||
import _ from "lodash";
|
||||
import type { Spec } from "vega";
|
||||
import type { DistPlus } from "@quri/squiggle-lang";
|
||||
import { createClassFromSpec } from "react-vega";
|
||||
import * as percentilesSpec from "../vega-specs/spec-percentiles.json";
|
||||
import { DistPlusChart } from "./DistPlusChart";
|
||||
import { Error } from "./Error";
|
||||
|
||||
let SquigglePercentilesChart = createClassFromSpec({
|
||||
spec: percentilesSpec as Spec,
|
||||
});
|
||||
|
||||
type distPlusFn = (
|
||||
a: number
|
||||
) => { tag: "Ok"; value: DistPlus } | { tag: "Error"; value: string };
|
||||
|
||||
const _rangeByCount = (start, stop, count) => {
|
||||
const step = (stop - start) / (count - 1);
|
||||
const items = _.range(start, stop, step);
|
||||
const result = items.concat([stop]);
|
||||
return result;
|
||||
};
|
||||
|
||||
export const FunctionChart: React.FC<{
|
||||
distPlusFn: distPlusFn;
|
||||
diagramStart: number;
|
||||
diagramStop: number;
|
||||
diagramCount: number;
|
||||
}> = ({ distPlusFn, diagramStart, diagramStop, diagramCount }) => {
|
||||
let [mouseOverlay, setMouseOverlay] = React.useState(NaN);
|
||||
function handleHover(...args) {
|
||||
setMouseOverlay(args[1]);
|
||||
}
|
||||
function handleOut(...args) {
|
||||
setMouseOverlay(NaN);
|
||||
}
|
||||
const signalListeners = { mousemove: handleHover, mouseout: handleOut };
|
||||
let percentileArray = [
|
||||
0.01, 0.05, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 0.95, 0.99,
|
||||
];
|
||||
let mouseItem = distPlusFn(mouseOverlay);
|
||||
let showChart =
|
||||
mouseItem.tag === "Ok" ? (
|
||||
<DistPlusChart distPlus={mouseItem.value} width={400} height={140} />
|
||||
) : (
|
||||
<></>
|
||||
);
|
||||
let data1 = _rangeByCount(diagramStart, diagramStop, diagramCount);
|
||||
let valueData = data1
|
||||
.map((x) => {
|
||||
let result = distPlusFn(x);
|
||||
if (result.tag === "Ok") {
|
||||
return { x: x, value: result.value };
|
||||
} else return null;
|
||||
})
|
||||
.filter((x) => x !== null)
|
||||
.map(({ x, value }) => {
|
||||
let percentiles = getPercentiles(percentileArray, value);
|
||||
return {
|
||||
x: x,
|
||||
p1: percentiles[0],
|
||||
p5: percentiles[1],
|
||||
p10: percentiles[2],
|
||||
p20: percentiles[3],
|
||||
p30: percentiles[4],
|
||||
p40: percentiles[5],
|
||||
p50: percentiles[6],
|
||||
p60: percentiles[7],
|
||||
p70: percentiles[8],
|
||||
p80: percentiles[9],
|
||||
p90: percentiles[10],
|
||||
p95: percentiles[11],
|
||||
p99: percentiles[12],
|
||||
};
|
||||
});
|
||||
|
||||
let errorData = data1
|
||||
.map((x) => {
|
||||
let result = distPlusFn(x);
|
||||
if (result.tag === "Error") {
|
||||
return { x: x, error: result.value };
|
||||
} else return null;
|
||||
})
|
||||
.filter((x) => x !== null);
|
||||
let error2 = _.groupBy(errorData, (x) => x.error);
|
||||
return (
|
||||
<>
|
||||
<SquigglePercentilesChart
|
||||
data={{ facet: valueData }}
|
||||
actions={false}
|
||||
signalListeners={signalListeners}
|
||||
/>
|
||||
{showChart}
|
||||
{_.keysIn(error2).map((k) => (
|
||||
<Error heading={k}>
|
||||
{`Values: [${error2[k].map((r) => r.x.toFixed(2)).join(",")}]`}
|
||||
</Error>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
function getPercentiles(percentiles: number[], t: DistPlus) {
|
||||
if (t.pointSetDist.tag === "Discrete") {
|
||||
let total = 0;
|
||||
let maxX = _.max(t.pointSetDist.value.xyShape.xs);
|
||||
let bounds = percentiles.map((_) => maxX);
|
||||
_.zipWith(
|
||||
t.pointSetDist.value.xyShape.xs,
|
||||
t.pointSetDist.value.xyShape.ys,
|
||||
(x, y) => {
|
||||
total += y;
|
||||
percentiles.forEach((v, i) => {
|
||||
if (total > v && bounds[i] === maxX) {
|
||||
bounds[i] = x;
|
||||
}
|
||||
});
|
||||
}
|
||||
);
|
||||
return bounds;
|
||||
} else if (t.pointSetDist.tag === "Continuous") {
|
||||
let total = 0;
|
||||
let maxX = _.max(t.pointSetDist.value.xyShape.xs);
|
||||
let totalY = _.sum(t.pointSetDist.value.xyShape.ys);
|
||||
let bounds = percentiles.map((_) => maxX);
|
||||
_.zipWith(
|
||||
t.pointSetDist.value.xyShape.xs,
|
||||
t.pointSetDist.value.xyShape.ys,
|
||||
(x, y) => {
|
||||
total += y / totalY;
|
||||
percentiles.forEach((v, i) => {
|
||||
if (total > v && bounds[i] === maxX) {
|
||||
bounds[i] = x;
|
||||
}
|
||||
});
|
||||
}
|
||||
);
|
||||
return bounds;
|
||||
} else if (t.pointSetDist.tag === "Mixed") {
|
||||
let discreteShape = t.pointSetDist.value.discrete.xyShape;
|
||||
let totalDiscrete = discreteShape.ys.reduce((a, b) => a + b);
|
||||
|
||||
let discretePoints = _.zip(discreteShape.xs, discreteShape.ys);
|
||||
let continuousShape = t.pointSetDist.value.continuous.xyShape;
|
||||
let continuousPoints = _.zip(continuousShape.xs, continuousShape.ys);
|
||||
|
||||
interface labeledPoint {
|
||||
x: number;
|
||||
y: number;
|
||||
type: "discrete" | "continuous";
|
||||
}
|
||||
|
||||
let markedDisPoints: labeledPoint[] = discretePoints.map(([x, y]) => ({
|
||||
x: x,
|
||||
y: y,
|
||||
type: "discrete",
|
||||
}));
|
||||
let markedConPoints: labeledPoint[] = continuousPoints.map(([x, y]) => ({
|
||||
x: x,
|
||||
y: y,
|
||||
type: "continuous",
|
||||
}));
|
||||
|
||||
let sortedPoints = _.sortBy(markedDisPoints.concat(markedConPoints), "x");
|
||||
|
||||
let totalContinuous = 1 - totalDiscrete;
|
||||
let totalY = continuousShape.ys.reduce((a: number, b: number) => a + b);
|
||||
|
||||
let total = 0;
|
||||
let maxX = _.max(sortedPoints.map((x) => x.x));
|
||||
let bounds = percentiles.map((_) => maxX);
|
||||
sortedPoints.map((point: labeledPoint) => {
|
||||
if (point.type === "discrete") {
|
||||
total += point.y;
|
||||
} else if (point.type === "continuous") {
|
||||
total += (point.y / totalY) * totalContinuous;
|
||||
}
|
||||
percentiles.forEach((v, i) => {
|
||||
if (total > v && bounds[i] === maxX) {
|
||||
bounds[i] = total;
|
||||
}
|
||||
});
|
||||
return total;
|
||||
});
|
||||
return bounds;
|
||||
}
|
||||
}
|
91
packages/components/src/components/SquiggleChart.tsx
Normal file
91
packages/components/src/components/SquiggleChart.tsx
Normal file
|
@ -0,0 +1,91 @@
|
|||
import * as React from "react";
|
||||
import _ from "lodash";
|
||||
import { run } from "@quri/squiggle-lang";
|
||||
import type {
|
||||
SamplingInputs,
|
||||
exportEnv,
|
||||
exportDistribution,
|
||||
} from "@quri/squiggle-lang";
|
||||
import { NumberShower } from "./NumberShower";
|
||||
import { DistPlusChart } from "./DistPlusChart";
|
||||
import { FunctionChart } from "./FunctionChart";
|
||||
import { Error } from "./Error";
|
||||
|
||||
export interface SquiggleChartProps {
|
||||
/** The input string for squiggle */
|
||||
squiggleString?: string;
|
||||
/** If the output requires monte carlo sampling, the amount of samples */
|
||||
sampleCount?: number;
|
||||
/** The amount of points returned to draw the distribution */
|
||||
outputXYPoints?: number;
|
||||
kernelWidth?: number;
|
||||
pointDistLength?: number;
|
||||
/** If the result is a function, where the function starts */
|
||||
diagramStart?: number;
|
||||
/** If the result is a function, where the function ends */
|
||||
diagramStop?: number;
|
||||
/** If the result is a function, how many points along the function it samples */
|
||||
diagramCount?: number;
|
||||
/** variables declared before this expression */
|
||||
environment?: exportEnv;
|
||||
/** When the environment changes */
|
||||
onEnvChange?(env: exportEnv): void;
|
||||
/** CSS width of the element */
|
||||
width?: number;
|
||||
height?: number;
|
||||
}
|
||||
|
||||
export const SquiggleChart: React.FC<SquiggleChartProps> = ({
|
||||
squiggleString = "",
|
||||
sampleCount = 1000,
|
||||
outputXYPoints = 1000,
|
||||
kernelWidth,
|
||||
pointDistLength = 1000,
|
||||
diagramStart = 0,
|
||||
diagramStop = 10,
|
||||
diagramCount = 20,
|
||||
environment = [],
|
||||
onEnvChange = () => {},
|
||||
width = 500,
|
||||
height = 60,
|
||||
}: SquiggleChartProps) => {
|
||||
let samplingInputs: SamplingInputs = {
|
||||
sampleCount: sampleCount,
|
||||
outputXYPoints: outputXYPoints,
|
||||
kernelWidth: kernelWidth,
|
||||
pointDistLength: pointDistLength,
|
||||
};
|
||||
|
||||
let result = run(squiggleString, samplingInputs, environment);
|
||||
if (result.tag === "Ok") {
|
||||
let environment = result.value.environment;
|
||||
let exports = result.value.exports;
|
||||
onEnvChange(environment);
|
||||
let chartResults = exports.map((chartResult: exportDistribution) => {
|
||||
if (chartResult["NAME"] === "Float") {
|
||||
return <NumberShower precision={3} number={chartResult["VAL"]} />;
|
||||
} else if (chartResult["NAME"] === "DistPlus") {
|
||||
return (
|
||||
<DistPlusChart
|
||||
distPlus={chartResult.VAL}
|
||||
height={height}
|
||||
width={width}
|
||||
/>
|
||||
);
|
||||
} else if (chartResult.NAME === "Function") {
|
||||
return (
|
||||
<FunctionChart
|
||||
distPlusFn={chartResult.VAL}
|
||||
diagramStart={diagramStart}
|
||||
diagramStop={diagramStop}
|
||||
diagramCount={diagramCount}
|
||||
/>
|
||||
);
|
||||
}
|
||||
});
|
||||
return <>{chartResults}</>;
|
||||
} else if (result.tag === "Error") {
|
||||
// At this point, we came across an error. What was our error?
|
||||
return <Error heading={"Parse Error"}>{result.value}</Error>;
|
||||
}
|
||||
};
|
|
@ -1,6 +1,6 @@
|
|||
export { SquiggleChart } from "./SquiggleChart";
|
||||
export { SquiggleEditor, renderSquiggleEditorToDom } from "./SquiggleEditor";
|
||||
export { SquiggleChart } from "./components/SquiggleChart";
|
||||
export { SquiggleEditor, renderSquiggleEditorToDom } from "./components/SquiggleEditor";
|
||||
import SquigglePlayground, {
|
||||
renderSquigglePlaygroundToDom,
|
||||
} from "./SquigglePlayground";
|
||||
} from "./components/SquigglePlayground";
|
||||
export { SquigglePlayground, renderSquigglePlaygroundToDom };
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { NumberShower } from "../NumberShower";
|
||||
import { NumberShower } from "../components/NumberShower";
|
||||
import { Canvas, Meta, Story, Props } from "@storybook/addon-docs";
|
||||
|
||||
<Meta title="Squiggle/NumberShower" component={NumberShower} />
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { SquiggleChart } from "../SquiggleChart";
|
||||
import { SquiggleChart } from "../components/SquiggleChart";
|
||||
import { Canvas, Meta, Story, Props } from "@storybook/addon-docs";
|
||||
|
||||
<Meta title="Squiggle/SquiggleChart" component={SquiggleChart} />
|
||||
|
@ -83,7 +83,7 @@ The default is show 10 points between 0 and 10.
|
|||
<Story
|
||||
name="Function"
|
||||
args={{
|
||||
squiggleString: "f(x) = normal(x^2,x^1.8)\nf",
|
||||
squiggleString: "f(x) = normal(x^2,(x+.1)^1.8)\nf",
|
||||
}}
|
||||
>
|
||||
{Template.bind({})}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { SquiggleEditor } from "../SquiggleEditor";
|
||||
import { SquiggleEditor } from "../components/SquiggleEditor";
|
||||
import { Canvas, Meta, Story, Props } from "@storybook/addon-docs";
|
||||
|
||||
<Meta title="Squiggle/SquiggleEditor" component={SquiggleEditor} />
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import SquigglePlayground from "../SquigglePlayground";
|
||||
import SquigglePlayground from "../components/SquigglePlayground";
|
||||
import { Canvas, Meta, Story, Props } from "@storybook/addon-docs";
|
||||
|
||||
<Meta title="Squiggle/SquigglePlayground" component={SquigglePlayground} />
|
||||
|
|
|
@ -82,10 +82,13 @@
|
|||
{
|
||||
"orient": "bottom",
|
||||
"scale": "xscale",
|
||||
"labelColor": "#666",
|
||||
"tickColor": "#ddd",
|
||||
"labelColor": "#727d93",
|
||||
"tickColor": "#fff",
|
||||
"tickOpacity": 0.0,
|
||||
"domainColor": "#fff",
|
||||
"domainOpacity": 0.0,
|
||||
"format": "~s",
|
||||
"tickCount": 20
|
||||
"tickCount": 10
|
||||
}
|
||||
],
|
||||
"marks": [
|
||||
|
@ -157,9 +160,7 @@
|
|||
"shape": {
|
||||
"value": "circle"
|
||||
},
|
||||
"width": {
|
||||
"value": 5
|
||||
},
|
||||
"size": [{"value": 30}],
|
||||
"tooltip": {
|
||||
"signal": "datum.y"
|
||||
}
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"$schema": "https://vega.github.io/schema/vega/v5.json",
|
||||
"width": 500,
|
||||
"height": 400,
|
||||
"height": 200,
|
||||
"padding": 5,
|
||||
"data": [
|
||||
{
|
||||
|
@ -93,54 +93,49 @@
|
|||
}
|
||||
}
|
||||
],
|
||||
"signals": [
|
||||
{
|
||||
"name": "mousemove",
|
||||
"on": [{"events": "mousemove", "update": "invert('xscale', x())"}]
|
||||
},
|
||||
{
|
||||
"name": "mouseout",
|
||||
"on": [{"events": "mouseout", "update": "invert('xscale', x())"}]
|
||||
}
|
||||
],
|
||||
"axes": [
|
||||
{
|
||||
"orient": "bottom",
|
||||
"scale": "xscale",
|
||||
"grid": false,
|
||||
"tickSize": 2,
|
||||
"encode": {
|
||||
"grid": {
|
||||
"enter": {
|
||||
"stroke": {
|
||||
"value": "#ccc"
|
||||
}
|
||||
}
|
||||
},
|
||||
"ticks": {
|
||||
"enter": {
|
||||
"stroke": {
|
||||
"value": "#ccc"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
"labelColor": "#727d93",
|
||||
"tickColor": "#fff",
|
||||
"tickOpacity": 0.0,
|
||||
"domainColor": "#727d93",
|
||||
"domainOpacity": 0.1,
|
||||
"tickCount": 5
|
||||
},
|
||||
{
|
||||
"orient": "left",
|
||||
"scale": "yscale",
|
||||
"grid": false,
|
||||
"domain": false,
|
||||
"tickSize": 2,
|
||||
"encode": {
|
||||
"grid": {
|
||||
"enter": {
|
||||
"stroke": {
|
||||
"value": "#ccc"
|
||||
}
|
||||
}
|
||||
},
|
||||
"ticks": {
|
||||
"enter": {
|
||||
"stroke": {
|
||||
"value": "#ccc"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
"labelColor": "#727d93",
|
||||
"tickColor": "#fff",
|
||||
"tickOpacity": 0.0,
|
||||
"domainColor": "#727d93",
|
||||
"domainOpacity": 0.1,
|
||||
"tickCount": 5
|
||||
}
|
||||
],
|
||||
"marks": [
|
||||
{
|
||||
"type": "rule",
|
||||
"encode": {
|
||||
"update": {
|
||||
"xscale": {"scale": "xscale", "signal": "mousemove"}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "area",
|
||||
"from": {
|
|
@ -16,7 +16,7 @@
|
|||
"declaration": true,
|
||||
"sourceMap": true
|
||||
},
|
||||
"files": ["src/spec-distributions.json", "src/spec-percentiles.json"],
|
||||
"files": ["src/vega-specs/spec-distributions.json", "src/vega-specs/spec-percentiles.json"],
|
||||
"target": "ES6",
|
||||
"include": ["src/**/*", "src/*"],
|
||||
"exclude": ["node_modules", "**/*.spec.ts", "webpack.config.js"],
|
||||
|
|
Loading…
Reference in New Issue
Block a user