+ Added the ability to change the upper and lower boundaries.

+ Made the drawings relative to the canvas, not to the screen.
- Removed the mean line, as it didn't play nice with the ability to change upper and lower boundaries.
This commit is contained in:
Nuno Sempere 2020-05-06 00:15:51 +02:00
parent ff5b26d865
commit 23952af460

View File

@ -1,5 +1,4 @@
module Types = {
type rectangle = {
// Ref: https://developer.mozilla.org/en-US/docs/Web/API/Element/getBoundingClientRect
left: int,
@ -35,24 +34,31 @@ module Types = {
xValues: array(float),
};
type formElements = {
type foretoldFormElements = {
measurableId: string,
token: string,
comment: string,
};
type distributionLimits = {
lower: float,
upper: float,
};
type canvasState = {
isMouseDown: bool,
lastMousePosition: option(canvasPoint),
canvasShape: option(canvasShape),
lastMousePosition: option(canvasPoint),
isMouseDown: bool,
readyToRender: bool,
formElements,
hasJustBeenSent: bool,
hasJustBeenSentToForetold: bool,
limitsHaveJustBeenUpdated: bool,
foretoldFormElements,
distributionLimits,
};
};
module CanvasContext = {
type t = Types.webapi;
type t = Types.webapi;
/* Externals */
[@bs.send]
@ -101,22 +107,25 @@ module CanvasContext = {
module Convert = {
/*
- In this module, the fundamental unit for the canvas shape is the distance vector from the (0,0) point at the upper leftmost corner of the screen.
- For some drawing functions, this is instead from the (0,0) point at the upper leftmost corner of the canvas element. This is irrelevant in this module.
- The fundamental unit for a probability distribution is an x coordinate and its corresponding y probability density
*/
- In this module, the fundamental unit for the canvas shape is the distance vector from the (0,0) point at the upper leftmost corner of the screen.
- For some drawing functions, this is instead from the (0,0) point at the upper leftmost corner of the canvas element. This is irrelevant in this module.
- The fundamental unit for a probability distribution is an x coordinate and its corresponding y probability density
*/
let xyShapeToCanvasShape =
(~xyShape: Types.xyShape, ~canvasElement: Dom.element) => {
let xs = xyShape.xs;
let ys = xyShape.ys;
let rectangle: Types.rectangle = CanvasContext.getBoundingClientRect(canvasElement);
let rectangle: Types.rectangle =
CanvasContext.getBoundingClientRect(canvasElement);
let lengthX = E.A.length(xs);
let minX = xs[0];
let maxX = xs[lengthX - 1];
let ratioXs =
float_of_int(rectangle.width) *. CanvasContext.paddingRatioX /. (maxX -. minX);
float_of_int(rectangle.width)
*. CanvasContext.paddingRatioX
/. (maxX -. minX);
let ws =
E.A.fmap(
x =>
@ -129,9 +138,12 @@ module Convert = {
xs,
);
let minY = 0.;
let minY = 0.;
let maxY = E.A.reduce(ys, 0., (x, y) => x > y ? x : y);
let ratioYs = float_of_int(rectangle.height) *. CanvasContext.paddingRatioY /. (maxY -. minY);
let ratioYs =
float_of_int(rectangle.height)
*. CanvasContext.paddingRatioY
/. (maxY -. minY);
let hs =
E.A.fmap(
y =>
@ -151,11 +163,15 @@ module Convert = {
: Types.continuousShape => {
let xs = canvasShape.xValues;
let hs = canvasShape.hs;
let rectangle: Types.rectangle = CanvasContext.getBoundingClientRect(canvasElement);
let rectangle: Types.rectangle =
CanvasContext.getBoundingClientRect(canvasElement);
let bottom = float_of_int(rectangle.bottom);
let ysRelative =
E.A.fmap(h => bottom -. h +. CanvasContext.paddingFactorY(rectangle.height), hs);
E.A.fmap(
h => bottom -. h +. CanvasContext.paddingFactorY(rectangle.height),
hs,
);
let xyShape: Types.xyShape = {xs, ys: ysRelative};
let continuousShape: Types.continuousShape = {
xyShape,
@ -196,7 +212,6 @@ module Convert = {
};
module Draw = {
let line =
(
canvasElement: Dom.element,
@ -218,7 +233,8 @@ module Draw = {
let canvasPlot =
(canvasElement: Dom.element, canvasShape: Types.canvasShape) => {
let context = CanvasContext.getContext2d(canvasElement);
let rectangle: Types.rectangle = CanvasContext.getBoundingClientRect(canvasElement);
let rectangle: Types.rectangle =
CanvasContext.getBoundingClientRect(canvasElement);
/* Some useful reference points */
let paddingFactorX = CanvasContext.paddingFactorX(rectangle.width);
@ -252,36 +268,45 @@ module Draw = {
/* Draw a line between every two adjacent points */
let length = Array.length(canvasShape.ws);
let windowScrollY: float = [%raw "window.scrollY"];
CanvasContext.setStrokeStyle(context, String, "#5680cc");
CanvasContext.lineWidth(context, 4.);
for (i in 1 to length - 1) {
let point0 = Convert.getPoint(canvasShape, i - 1);
let point1 = Convert.getPoint(canvasShape, i);
let point0 = {...point0, h: point0.h -. windowScrollY};
let point1 = {...point1, h: point1.h -. windowScrollY};
line(canvasElement, ~point0, ~point1);
};
/* Draws the expected value line */
let continuousShape =
Convert.canvasShapeToContinuousShape(~canvasShape, ~canvasElement);
let mean = Distributions.Continuous.T.mean(continuousShape);
let variance = Distributions.Continuous.T.variance(continuousShape);
let meanLocation =
Convert.findClosestInOrderedArrayDangerously(mean, canvasShape.xValues);
let meanLocationCanvasX = canvasShape.ws[meanLocation];
let meanLocationCanvasY = canvasShape.hs[meanLocation];
CanvasContext.beginPath(context);
CanvasContext.setStrokeStyle(context, String, "#5680cc");
CanvasContext.setLineDash(context, [|5, 10|]);
line(
canvasElement,
~point0={w: meanLocationCanvasX, h: p00.h},
~point1={w: meanLocationCanvasX, h: meanLocationCanvasY},
);
CanvasContext.stroke(context);
CanvasContext.setLineDash(context, [||]);
// Removed on the grounds that it didn't play nice with changes in limits.
/*
let continuousShape =
Convert.canvasShapeToContinuousShape(~canvasShape, ~canvasElement);
let mean = Distributions.Continuous.T.mean(continuousShape);
let variance = Distributions.Continuous.T.variance(continuousShape);
let meanLocation =
Convert.findClosestInOrderedArrayDangerously(mean, canvasShape.xValues);
let meanLocationCanvasX = canvasShape.ws[meanLocation];
let meanLocationCanvasY = canvasShape.hs[meanLocation];
CanvasContext.beginPath(context);
CanvasContext.setStrokeStyle(context, String, "#5680cc");
CanvasContext.setLineDash(context, [|5, 10|]);
line(
canvasElement,
~point0={w: meanLocationCanvasX, h: p00.h},
~point1={
w: meanLocationCanvasX,
h: meanLocationCanvasY -. windowScrollY,
},
);
CanvasContext.stroke(context);
CanvasContext.setLineDash(context, [||]);
*/
/* draws lines parallel to x axis + factors to help w/ precise drawing. */
CanvasContext.beginPath(context);
CanvasContext.setStrokeStyle(context, String, "#CCC");
@ -318,19 +343,19 @@ module Draw = {
/* draw units along the x axis */
CanvasContext.font(context, "16px Roboto");
CanvasContext.lineWidth(context, 2.0);
let numUnits = 10;
let numIntervals = 10;
let width = float_of_int(rectangle.width);
let height = float_of_int(rectangle.height);
let xMin = canvasShape.xValues[0];
let xMax = canvasShape.xValues[length - 1];
let xSpan = (xMax -. xMin) /. float_of_int(numUnits);
let xSpan = (xMax -. xMin) /. float_of_int(numIntervals - 1);
for (i in 0 to numUnits - 1) {
for (i in 0 to numIntervals - 1) {
let x =
float_of_int(rectangle.left)
+. width
*. float_of_int(i)
/. float_of_int(numUnits);
/. float_of_int(numIntervals);
let dashValue = xMin +. xSpan *. float_of_int(i);
CanvasContext.fillText(
Js.Float.toFixedWithPrecision(dashValue, ~digits=2),
@ -340,7 +365,10 @@ module Draw = {
);
line(
canvasElement,
~point0={w: x +. CanvasContext.paddingFactorX(rectangle.width), h: p00.h},
~point0={
w: x +. CanvasContext.paddingFactorX(rectangle.width),
h: p00.h,
},
~point1={
w: x +. CanvasContext.paddingFactorX(rectangle.width),
h: p00.h +. 10.0,
@ -350,9 +378,8 @@ module Draw = {
};
let initialDistribution = (canvasElement: Dom.element, setState) => {
let mean = 10.0;
let stdev = 4.0;
let mean = 50.0;
let stdev = 20.0;
let numSamples = 3000;
let normal: SymbolicDist.dist = `Normal({mean, stdev});
@ -363,24 +390,29 @@ module Draw = {
| Discrete(_) => {xs: [||], ys: [||]}
| Continuous(m) => Distributions.Continuous.getShape(m)
};
/* // To use a lognormal instead:
let lognormal = SymbolicDist.Lognormal.fromMeanAndStdev(mean, stdev);
let lognormalShape =
SymbolicDist.GenericSimple.toShape(lognormal, numSamples);
let lognormalXYShape: Types.xyShape =
switch (lognormalShape) {
| Mixed(_) => {xs: [||], ys: [||]}
| Discrete(_) => {xs: [||], ys: [||]}
| Continuous(m) => Distributions.Continuous.getShape(m)
};
*/
let lognormal = SymbolicDist.Lognormal.fromMeanAndStdev(mean, stdev);
let lognormalShape =
SymbolicDist.GenericSimple.toShape(lognormal, numSamples);
let lognormalXYShape: Types.xyShape =
switch (lognormalShape) {
| Mixed(_) => {xs: [||], ys: [||]}
| Discrete(_) => {xs: [||], ys: [||]}
| Continuous(m) => Distributions.Continuous.getShape(m)
};
*/
let canvasShape = Convert.xyShapeToCanvasShape(~xyShape, ~canvasElement);
/* let continuousShapeBack =
Convert.canvasShapeToContinuousShape(~canvasShape, ~canvasElement);
*/
Convert.canvasShapeToContinuousShape(~canvasShape, ~canvasElement);
*/
let windowScrollY: float = [%raw "window.scrollY"];
let canvasShape = {
...canvasShape,
hs: E.A.fmap(h => h +. windowScrollY, canvasShape.hs),
};
setState((state: Types.canvasState) => {
{...state, canvasShape: Some(canvasShape)}
});
@ -443,20 +475,25 @@ module State = {
type t = Types.canvasState;
let initialState: t = {
isMouseDown: false,
lastMousePosition: None,
canvasShape: None,
lastMousePosition: None,
isMouseDown: false,
readyToRender: false,
hasJustBeenSent: false,
formElements: {
hasJustBeenSentToForetold: false,
limitsHaveJustBeenUpdated: false,
foretoldFormElements: {
measurableId: "",
token: "",
comment: "",
},
distributionLimits: {
lower: 0.0,
upper: 1000.0,
},
};
let updateMousePosition = (~point: Types.canvasPoint, ~setState) =>{
setState((state: t) => ({...state, lastMousePosition: Some(point)}));
let updateMousePosition = (~point: Types.canvasPoint, ~setState) => {
setState((state: t) => {...state, lastMousePosition: Some(point)});
};
let onMouseMovement =
@ -466,14 +503,15 @@ module State = {
~state: t,
~setState,
) => {
/* Helper functions and objects*/
let x = ReactEvent.Mouse.clientX(event);
let y = ReactEvent.Mouse.clientY(event);
let windowScrollY: float = [%raw "window.scrollY"];
let point1: Types.canvasPoint = {
w: float_of_int(x),
h: float_of_int(y),
h: float_of_int(y) +. windowScrollY,
};
let pointIsInBetween =
@ -484,7 +522,6 @@ module State = {
let x2 = c.w;
x0 < x2 && x2 < x1 || x1 < x2 && x2 < x0;
};
/* If all conditions are met, update the distribution */
let updateDistWithMouseMovement =
@ -494,12 +531,12 @@ module State = {
~canvasShape: Types.canvasShape,
) => {
/*
The mouse moves across the screen, and we get a series of (x,y) positions.
We know where the mouse last was
we update everything between the last (x,y) position and the new (x,y), using linear interpolation
Note that we only want to update & iterate over the parts of the canvas which are changed by the mouse movement
(otherwise, things might be too slow)
*/
The mouse moves across the screen, and we get a series of (x,y) positions.
We know where the mouse last was
we update everything between the last (x,y) position and the new (x,y), using linear interpolation
Note that we only want to update & iterate over the parts of the canvas which are changed by the mouse movement
(otherwise, things might be too slow)
*/
let slope = (point1.h -. point0.h) /. (point1.w -. point0.w);
let pos0 =
@ -539,20 +576,22 @@ module State = {
let validateYCoordinates =
(~point: Types.canvasPoint, ~rectangle: Types.rectangle) => {
switch (
/*
- If we also validate the xs, this produces a jaded user experience around the edges.
- Instead, we will also update the first and last points in the updateDistWithMouseMovement, with the findClosestInOrderedArrayDangerously function, even when the x is outside the padding zone
- When we send the distribution to foretold, we'll get rid of the first and last points.
*/
/*
point.w >= float_of_int(rectangle.left)
+. CanvasContext.paddingFactorX(rectangle.width),
point.w <= float_of_int(rectangle.right)
-. CanvasContext.paddingFactorX(rectangle.width),
*/
point.h >= float_of_int(rectangle.top)
/*
- If we also validate the xs, this produces a jaded user experience around the edges.
- Instead, we will also update the first and last points in the updateDistWithMouseMovement, with the findClosestInOrderedArrayDangerously function, even when the x is outside the padding zone
- When we send the distribution to foretold, we'll get rid of the first and last points.
*/
/*
point.w >= float_of_int(rectangle.left)
+. CanvasContext.paddingFactorX(rectangle.width),
point.w <= float_of_int(rectangle.right)
-. CanvasContext.paddingFactorX(rectangle.width),
*/
point.h
-. windowScrollY >= float_of_int(rectangle.top)
+. CanvasContext.paddingFactorY(rectangle.height),
point.h <= float_of_int(rectangle.bottom)
point.h
-. windowScrollY <= float_of_int(rectangle.bottom)
-. CanvasContext.paddingFactorY(rectangle.height),
) {
| (true, true) => true
@ -567,8 +606,8 @@ module State = {
validateYCoordinates(~point=point1, ~rectangle),
) {
| (true, true) =>
let newCanvasShape = updateDistWithMouseMovement(~point0, ~point1, ~canvasShape);
state.readyToRender ? Draw.canvasPlot(canvasElement, newCanvasShape) : ();
let newCanvasShape =
updateDistWithMouseMovement(~point0, ~point1, ~canvasShape);
setState((state: t) => {
{
...state,
@ -577,10 +616,13 @@ module State = {
readyToRender: false,
}
});
state.readyToRender
? Draw.canvasPlot(canvasElement, newCanvasShape) : ();
| (false, true) => updateMousePosition(~point=point1, ~setState)
| (_, false) => ()
};
}
};
switch (
potentialCanvas,
@ -588,11 +630,12 @@ module State = {
state.isMouseDown,
state.lastMousePosition,
) {
| (Some(canvasElement), Some(canvasShape), true, Some(point0)) =>
decideWithCanvas(~canvasElement, ~canvasShape, ~point0);
| (Some(canvasElement), Some(canvasShape), true, Some(point0)) =>
decideWithCanvas(~canvasElement, ~canvasShape, ~point0)
| (Some(canvasElement), _, true, None) =>
let rectangle = CanvasContext.getBoundingClientRect(canvasElement);
validateYCoordinates(~point=point1, ~rectangle) ? updateMousePosition(~point=point1, ~setState) : ();
validateYCoordinates(~point=point1, ~rectangle)
? updateMousePosition(~point=point1, ~setState) : ();
| _ => ()
};
};
@ -603,7 +646,7 @@ module State = {
});
};
let onSubmitForm =
let onSubmitForetoldForm =
(
~state: Types.canvasState,
~potentialCanvasElement: option(Dom.element),
@ -615,8 +658,7 @@ module State = {
| (None, _) => ()
| (_, None) => ()
| (Some(canvasShape), Some(canvasElement)) =>
let pdf =
let pdf =
Convert.canvasShapeToContinuousShape(~canvasShape, ~canvasElement);
/* create a cdf from a pdf */
@ -626,32 +668,86 @@ module State = {
~intendedSum=1.0,
pdf,
);
let cdf = Distributions.Continuous.T.integral(~cache=None, _pdf);
let cdf = Distributions.Continuous.T.integral(~cache=None, _pdf);
let xs = [||];
let ys = [||];
for (i in 1 to 999) {
for (i in 1 to 999) {
/*
- see comment in validateYCoordinates as to why this starts at 1.
- foretold accepts distributions with up to 1000 points.
*/
- see comment in validateYCoordinates as to why this starts at 1.
- foretold accepts distributions with up to 1000 points.
*/
let j = i * 3;
Js.Array.push(cdf.xyShape.xs[j], xs);
Js.Array.push(cdf.xyShape.ys[j], ys);
();
};
ForetoldAPI.predict(
~measurableId=state.formElements.measurableId,
~token=state.formElements.token,
~comment=state.formElements.comment,
~measurableId=state.foretoldFormElements.measurableId,
~token=state.foretoldFormElements.token,
~comment=state.foretoldFormElements.comment,
~xs,
~ys,
);
setState((state: t) => {...state, hasJustBeenSent: true});
setState((state: t) => {...state, hasJustBeenSentToForetold: true});
Js.Global.setTimeout(
() => {
setState((state: t) => {...state, hasJustBeenSent: false});
setState((state: t) =>
{...state, hasJustBeenSentToForetold: false}
)
},
5000,
);
();
};
();
};
let onSubmitLimitsForm =
(
~state: Types.canvasState,
~potentialCanvasElement: option(Dom.element),
~setState,
) => {
let potentialCanvasShape = state.canvasShape;
switch (potentialCanvasShape, potentialCanvasElement) {
| (None, _) => ()
| (_, None) => ()
| (Some(canvasShape), Some(canvasElement)) =>
let xValues = canvasShape.xValues;
let length = Array.length(xValues);
let xMin = xValues[0];
let xMax = xValues[length - 1];
let lower = state.distributionLimits.lower;
let upper = state.distributionLimits.upper;
let slope = (upper -. lower) /. (xMax -. xMin);
let delta = lower -. slope *. xMin;
let xValues = E.A.fmap(x => delta +. x *. slope, xValues);
let newCanvasShape = {...canvasShape, xValues};
setState((state: t) =>
{
...state,
canvasShape: Some(newCanvasShape),
limitsHaveJustBeenUpdated: true,
}
);
Draw.canvasPlot(canvasElement, newCanvasShape);
Js.Global.setTimeout(
() => {
setState((state: t) =>
{...state, limitsHaveJustBeenUpdated: false}
)
},
5000,
);
();
};
();
@ -666,7 +762,6 @@ module Styles = {
[@react.component]
let make = () => {
let canvasRef: React.Ref.t(option(Dom.element)) = React.useRef(None); // should morally live inside the state, but this is tricky.
let (state, setState) = React.useState(() => State.initialState);
@ -684,7 +779,7 @@ let make = () => {
None;
});
/* Render the current distribution every 40ms, while the mouse is moving and changing it */
/* Render the current distribution every 30ms, while the mouse is moving and changing it */
React.useEffect0(() => {
let runningInterval =
Js.Global.setInterval(
@ -693,14 +788,14 @@ let make = () => {
{...state, readyToRender: true}
})
},
40,
30,
);
Some(() => Js.Global.clearInterval(runningInterval));
});
<Antd.Card title={"Distribution Drawer" |> R.ste}>
<div className=Styles.spacer />
<p>{"Click to begin drawing, click to stop drawing" |> R.ste}</p>
<p> {"Click to begin drawing, click to stop drawing" |> R.ste} </p>
<canvas
width="1000"
height="700"
@ -722,13 +817,88 @@ let make = () => {
<br />
<br />
<br />
<Antd.Card title={"Update upper and lower limits" |> R.ste}>
<form
id="update-limits"
onSubmit={(e: ReactEvent.Form.t): unit => {
ReactEvent.Form.preventDefault(e);
/* code to run on submit */
State.onSubmitLimitsForm(
~state,
~potentialCanvasElement=React.Ref.current(canvasRef),
~setState,
);
();
}}>
<div>
<label> {"Lower: " |> R.ste} </label>
<input
type_="number"
id="lowerlimit"
name="lowerlimit"
value={Js.Float.toString(state.distributionLimits.lower)}
placeholder="a number. f.ex., 0"
required=true
step=0.001
onChange={event => {
let value = ReactEvent.Form.target(event)##value;
setState((state: Types.canvasState) => {
{
...state,
distributionLimits: {
...state.distributionLimits,
lower: value,
},
}
});
}}
/>
</div>
<br />
<div>
<label> {"Upper: " |> R.ste} </label>
<input
type_="number"
id="upperlimit"
name="upperlimit"
value={Js.Float.toString(state.distributionLimits.upper)}
placeholder="a number. f.ex., 100"
required=true
step=0.001
onChange={event => {
let value = ReactEvent.Form.target(event)##value;
setState((state: Types.canvasState) => {
{
...state,
distributionLimits: {
...state.distributionLimits,
upper: value,
},
}
});
}}
/>
</div>
<br />
<button type_="submit" id="updatelimits">
{"Update limits" |> R.ste}
</button>
<br />
<p hidden={!state.limitsHaveJustBeenUpdated}>
{"Updated!" |> R.ste}
</p>
</form>
</Antd.Card>
<br />
<br />
<br />
<Antd.Card title={"Send to foretold" |> R.ste}>
<form
id="send-to-foretold"
onSubmit={(e: ReactEvent.Form.t): unit => {
ReactEvent.Form.preventDefault(e);
/* code to run on submit */
State.onSubmitForm(
State.onSubmitForetoldForm(
~state,
~potentialCanvasElement=React.Ref.current(canvasRef),
~setState,
@ -741,7 +911,7 @@ let make = () => {
type_="text"
id="measurableId"
name="measurableId"
value={state.formElements.measurableId}
value={state.foretoldFormElements.measurableId}
placeholder="The last bit in the url, after the m"
required=true
onChange={event => {
@ -749,8 +919,8 @@ let make = () => {
setState((state: Types.canvasState) => {
{
...state,
formElements: {
...state.formElements,
foretoldFormElements: {
...state.foretoldFormElements,
measurableId: value,
},
}
@ -765,7 +935,7 @@ let make = () => {
type_="text"
id="foretoldToken"
name="foretoldToken"
value={state.formElements.token}
value={state.foretoldFormElements.token}
placeholder="Profile -> Bots -> (New Bot) -> Token"
required=true
onChange={event => {
@ -773,8 +943,8 @@ let make = () => {
setState((state: Types.canvasState) => {
{
...state,
formElements: {
...state.formElements,
foretoldFormElements: {
...state.foretoldFormElements,
token: value,
},
}
@ -794,8 +964,8 @@ let make = () => {
setState((state: Types.canvasState) => {
{
...state,
formElements: {
...state.formElements,
foretoldFormElements: {
...state.foretoldFormElements,
comment: value,
},
}
@ -807,7 +977,7 @@ let make = () => {
{"Send to foretold" |> R.ste}
</button>
<br />
<p hidden={!state.hasJustBeenSent}> {"Sent!" |> R.ste} </p>
<p hidden={!state.hasJustBeenSentToForetold}> {"Sent!" |> R.ste} </p>
</form>
</Antd.Card>
</Antd.Card>;