Simple implementation of function hover working
This commit is contained in:
parent
624e788094
commit
f63c775cb6
|
@ -13,6 +13,7 @@ import * as chartSpecification from "./spec-distributions.json";
|
||||||
import * as percentilesSpec from "./spec-percentiles.json";
|
import * as percentilesSpec from "./spec-percentiles.json";
|
||||||
import { NumberShower } from "./NumberShower";
|
import { NumberShower } from "./NumberShower";
|
||||||
import styled from "styled-components";
|
import styled from "styled-components";
|
||||||
|
import { CONTINUOUS_TO_DISCRETE_SCALES } from "vega-lite/build/src/scale";
|
||||||
|
|
||||||
let SquiggleVegaChart = createClassFromSpec({
|
let SquiggleVegaChart = createClassFromSpec({
|
||||||
spec: chartSpecification as Spec,
|
spec: chartSpecification as Spec,
|
||||||
|
@ -65,6 +66,195 @@ const ShowError: React.FC<{ heading: string; children: React.ReactNode }> = ({
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
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}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const _rangeByCount = (start, stop, count) => {
|
||||||
|
const step = (stop - start) / (count - 1);
|
||||||
|
const items = _.range(start, stop, step);
|
||||||
|
const result = items.concat([stop]);
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
|
type distPlusFn = (
|
||||||
|
a: number
|
||||||
|
) => { tag: "Ok"; value: DistPlus } | { tag: "Error"; value: string };
|
||||||
|
|
||||||
|
// This could really use a line in the location of the signal. I couldn't get it to work.
|
||||||
|
// https://vega.github.io/vega/docs/signals/#handlers
|
||||||
|
|
||||||
|
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 data = _rangeByCount(diagramStart, diagramStop, diagramCount)
|
||||||
|
.map((x) => {
|
||||||
|
let result = distPlusFn(x);
|
||||||
|
if (result.tag === "Ok") {
|
||||||
|
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],
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
console.log("Error", x, result);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.filter((x) => x !== null);
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<SquigglePercentilesChart
|
||||||
|
data={{ facet: data }}
|
||||||
|
actions={false}
|
||||||
|
signalListeners={signalListeners}
|
||||||
|
/>
|
||||||
|
{showChart}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export const SquiggleChart: React.FC<SquiggleChartProps> = ({
|
export const SquiggleChart: React.FC<SquiggleChartProps> = ({
|
||||||
squiggleString = "",
|
squiggleString = "",
|
||||||
sampleCount = 1000,
|
sampleCount = 1000,
|
||||||
|
@ -95,154 +285,20 @@ export const SquiggleChart: React.FC<SquiggleChartProps> = ({
|
||||||
if (chartResult["NAME"] === "Float") {
|
if (chartResult["NAME"] === "Float") {
|
||||||
return <NumberShower precision={3} number={chartResult["VAL"]} />;
|
return <NumberShower precision={3} number={chartResult["VAL"]} />;
|
||||||
} else if (chartResult["NAME"] === "DistPlus") {
|
} 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 (
|
return (
|
||||||
<SquigglePercentilesChart
|
<DistPlusChart
|
||||||
data={{ facet: data.filter((x) => x !== null) }}
|
distPlus={chartResult.VAL}
|
||||||
actions={false}
|
height={height}
|
||||||
|
width={width}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
} else if (chartResult.NAME === "Function") {
|
||||||
|
return (
|
||||||
|
<FunctionChart
|
||||||
|
distPlusFn={chartResult.VAL}
|
||||||
|
diagramStart={diagramStart}
|
||||||
|
diagramStop={diagramStop}
|
||||||
|
diagramCount={diagramCount}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -250,11 +306,7 @@ export const SquiggleChart: React.FC<SquiggleChartProps> = ({
|
||||||
return <>{chartResults}</>;
|
return <>{chartResults}</>;
|
||||||
} else if (result.tag === "Error") {
|
} else if (result.tag === "Error") {
|
||||||
// At this point, we came across an error. What was our error?
|
// At this point, we came across an error. What was our error?
|
||||||
return (
|
return <ShowError heading={"Parse Error"}>{result.value}</ShowError>;
|
||||||
<ShowError heading={"Parse Error"}>
|
|
||||||
{result.value}
|
|
||||||
</ShowError>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
return <p>{"Invalid Response"}</p>;
|
return <p>{"Invalid Response"}</p>;
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"$schema": "https://vega.github.io/schema/vega/v5.json",
|
"$schema": "https://vega.github.io/schema/vega/v5.json",
|
||||||
"width": 500,
|
"width": 500,
|
||||||
"height": 400,
|
"height": 200,
|
||||||
"padding": 5,
|
"padding": 5,
|
||||||
"data": [
|
"data": [
|
||||||
{
|
{
|
||||||
|
@ -93,6 +93,16 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"signals": [
|
||||||
|
{
|
||||||
|
"name": "mousemove",
|
||||||
|
"on": [{"events": "mousemove", "update": "invert('xscale', x())"}]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "mouseout",
|
||||||
|
"on": [{"events": "mouseout", "update": "invert('xscale', x())"}]
|
||||||
|
}
|
||||||
|
],
|
||||||
"axes": [
|
"axes": [
|
||||||
{
|
{
|
||||||
"orient": "bottom",
|
"orient": "bottom",
|
||||||
|
@ -118,6 +128,14 @@
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"marks": [
|
"marks": [
|
||||||
|
{
|
||||||
|
"type": "rule",
|
||||||
|
"encode": {
|
||||||
|
"update": {
|
||||||
|
"xscale": {"scale": "xscale", "signal": "mousemove"}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"type": "area",
|
"type": "area",
|
||||||
"from": {
|
"from": {
|
||||||
|
|
|
@ -83,7 +83,7 @@ The default is show 10 points between 0 and 10.
|
||||||
<Story
|
<Story
|
||||||
name="Function"
|
name="Function"
|
||||||
args={{
|
args={{
|
||||||
squiggleString: "f(x) = normal(x^2,x^1.8)\nf",
|
squiggleString: "f(x) = normal(x^2,(x+.1)^1.8)\nf",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{Template.bind({})}
|
{Template.bind({})}
|
||||||
|
|
Loading…
Reference in New Issue
Block a user