+ 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 = { module Types = {
type rectangle = { type rectangle = {
// Ref: https://developer.mozilla.org/en-US/docs/Web/API/Element/getBoundingClientRect // Ref: https://developer.mozilla.org/en-US/docs/Web/API/Element/getBoundingClientRect
left: int, left: int,
@ -35,19 +34,26 @@ module Types = {
xValues: array(float), xValues: array(float),
}; };
type formElements = { type foretoldFormElements = {
measurableId: string, measurableId: string,
token: string, token: string,
comment: string, comment: string,
}; };
type distributionLimits = {
lower: float,
upper: float,
};
type canvasState = { type canvasState = {
isMouseDown: bool,
lastMousePosition: option(canvasPoint),
canvasShape: option(canvasShape), canvasShape: option(canvasShape),
lastMousePosition: option(canvasPoint),
isMouseDown: bool,
readyToRender: bool, readyToRender: bool,
formElements, hasJustBeenSentToForetold: bool,
hasJustBeenSent: bool, limitsHaveJustBeenUpdated: bool,
foretoldFormElements,
distributionLimits,
}; };
}; };
@ -110,13 +116,16 @@ module Convert = {
(~xyShape: Types.xyShape, ~canvasElement: Dom.element) => { (~xyShape: Types.xyShape, ~canvasElement: Dom.element) => {
let xs = xyShape.xs; let xs = xyShape.xs;
let ys = xyShape.ys; 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 lengthX = E.A.length(xs);
let minX = xs[0]; let minX = xs[0];
let maxX = xs[lengthX - 1]; let maxX = xs[lengthX - 1];
let ratioXs = let ratioXs =
float_of_int(rectangle.width) *. CanvasContext.paddingRatioX /. (maxX -. minX); float_of_int(rectangle.width)
*. CanvasContext.paddingRatioX
/. (maxX -. minX);
let ws = let ws =
E.A.fmap( E.A.fmap(
x => x =>
@ -131,7 +140,10 @@ module Convert = {
let minY = 0.; let minY = 0.;
let maxY = E.A.reduce(ys, 0., (x, y) => x > y ? x : y); 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 = let hs =
E.A.fmap( E.A.fmap(
y => y =>
@ -151,11 +163,15 @@ module Convert = {
: Types.continuousShape => { : Types.continuousShape => {
let xs = canvasShape.xValues; let xs = canvasShape.xValues;
let hs = canvasShape.hs; 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 bottom = float_of_int(rectangle.bottom);
let ysRelative = 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 xyShape: Types.xyShape = {xs, ys: ysRelative};
let continuousShape: Types.continuousShape = { let continuousShape: Types.continuousShape = {
xyShape, xyShape,
@ -196,7 +212,6 @@ module Convert = {
}; };
module Draw = { module Draw = {
let line = let line =
( (
canvasElement: Dom.element, canvasElement: Dom.element,
@ -218,7 +233,8 @@ module Draw = {
let canvasPlot = let canvasPlot =
(canvasElement: Dom.element, canvasShape: Types.canvasShape) => { (canvasElement: Dom.element, canvasShape: Types.canvasShape) => {
let context = CanvasContext.getContext2d(canvasElement); let context = CanvasContext.getContext2d(canvasElement);
let rectangle: Types.rectangle = CanvasContext.getBoundingClientRect(canvasElement); let rectangle: Types.rectangle =
CanvasContext.getBoundingClientRect(canvasElement);
/* Some useful reference points */ /* Some useful reference points */
let paddingFactorX = CanvasContext.paddingFactorX(rectangle.width); let paddingFactorX = CanvasContext.paddingFactorX(rectangle.width);
@ -252,16 +268,22 @@ module Draw = {
/* Draw a line between every two adjacent points */ /* Draw a line between every two adjacent points */
let length = Array.length(canvasShape.ws); let length = Array.length(canvasShape.ws);
let windowScrollY: float = [%raw "window.scrollY"];
CanvasContext.setStrokeStyle(context, String, "#5680cc"); CanvasContext.setStrokeStyle(context, String, "#5680cc");
CanvasContext.lineWidth(context, 4.); CanvasContext.lineWidth(context, 4.);
for (i in 1 to length - 1) { for (i in 1 to length - 1) {
let point0 = Convert.getPoint(canvasShape, i - 1); let point0 = Convert.getPoint(canvasShape, i - 1);
let point1 = Convert.getPoint(canvasShape, i); let point1 = Convert.getPoint(canvasShape, i);
let point0 = {...point0, h: point0.h -. windowScrollY};
let point1 = {...point1, h: point1.h -. windowScrollY};
line(canvasElement, ~point0, ~point1); line(canvasElement, ~point0, ~point1);
}; };
/* Draws the expected value line */ /* Draws the expected value line */
// Removed on the grounds that it didn't play nice with changes in limits.
/*
let continuousShape = let continuousShape =
Convert.canvasShapeToContinuousShape(~canvasShape, ~canvasElement); Convert.canvasShapeToContinuousShape(~canvasShape, ~canvasElement);
let mean = Distributions.Continuous.T.mean(continuousShape); let mean = Distributions.Continuous.T.mean(continuousShape);
@ -277,11 +299,14 @@ module Draw = {
line( line(
canvasElement, canvasElement,
~point0={w: meanLocationCanvasX, h: p00.h}, ~point0={w: meanLocationCanvasX, h: p00.h},
~point1={w: meanLocationCanvasX, h: meanLocationCanvasY}, ~point1={
w: meanLocationCanvasX,
h: meanLocationCanvasY -. windowScrollY,
},
); );
CanvasContext.stroke(context); CanvasContext.stroke(context);
CanvasContext.setLineDash(context, [||]); CanvasContext.setLineDash(context, [||]);
*/
/* draws lines parallel to x axis + factors to help w/ precise drawing. */ /* draws lines parallel to x axis + factors to help w/ precise drawing. */
CanvasContext.beginPath(context); CanvasContext.beginPath(context);
CanvasContext.setStrokeStyle(context, String, "#CCC"); CanvasContext.setStrokeStyle(context, String, "#CCC");
@ -318,19 +343,19 @@ module Draw = {
/* draw units along the x axis */ /* draw units along the x axis */
CanvasContext.font(context, "16px Roboto"); CanvasContext.font(context, "16px Roboto");
CanvasContext.lineWidth(context, 2.0); CanvasContext.lineWidth(context, 2.0);
let numUnits = 10; let numIntervals = 10;
let width = float_of_int(rectangle.width); let width = float_of_int(rectangle.width);
let height = float_of_int(rectangle.height); let height = float_of_int(rectangle.height);
let xMin = canvasShape.xValues[0]; let xMin = canvasShape.xValues[0];
let xMax = canvasShape.xValues[length - 1]; 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 = let x =
float_of_int(rectangle.left) float_of_int(rectangle.left)
+. width +. width
*. float_of_int(i) *. float_of_int(i)
/. float_of_int(numUnits); /. float_of_int(numIntervals);
let dashValue = xMin +. xSpan *. float_of_int(i); let dashValue = xMin +. xSpan *. float_of_int(i);
CanvasContext.fillText( CanvasContext.fillText(
Js.Float.toFixedWithPrecision(dashValue, ~digits=2), Js.Float.toFixedWithPrecision(dashValue, ~digits=2),
@ -340,7 +365,10 @@ module Draw = {
); );
line( line(
canvasElement, canvasElement,
~point0={w: x +. CanvasContext.paddingFactorX(rectangle.width), h: p00.h}, ~point0={
w: x +. CanvasContext.paddingFactorX(rectangle.width),
h: p00.h,
},
~point1={ ~point1={
w: x +. CanvasContext.paddingFactorX(rectangle.width), w: x +. CanvasContext.paddingFactorX(rectangle.width),
h: p00.h +. 10.0, h: p00.h +. 10.0,
@ -350,9 +378,8 @@ module Draw = {
}; };
let initialDistribution = (canvasElement: Dom.element, setState) => { let initialDistribution = (canvasElement: Dom.element, setState) => {
let mean = 50.0;
let mean = 10.0; let stdev = 20.0;
let stdev = 4.0;
let numSamples = 3000; let numSamples = 3000;
let normal: SymbolicDist.dist = `Normal({mean, stdev}); let normal: SymbolicDist.dist = `Normal({mean, stdev});
@ -381,6 +408,11 @@ module Draw = {
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) => { setState((state: Types.canvasState) => {
{...state, canvasShape: Some(canvasShape)} {...state, canvasShape: Some(canvasShape)}
}); });
@ -443,20 +475,25 @@ module State = {
type t = Types.canvasState; type t = Types.canvasState;
let initialState: t = { let initialState: t = {
isMouseDown: false,
lastMousePosition: None,
canvasShape: None, canvasShape: None,
lastMousePosition: None,
isMouseDown: false,
readyToRender: false, readyToRender: false,
hasJustBeenSent: false, hasJustBeenSentToForetold: false,
formElements: { limitsHaveJustBeenUpdated: false,
foretoldFormElements: {
measurableId: "", measurableId: "",
token: "", token: "",
comment: "", comment: "",
}, },
distributionLimits: {
lower: 0.0,
upper: 1000.0,
},
}; };
let updateMousePosition = (~point: Types.canvasPoint, ~setState) =>{ let updateMousePosition = (~point: Types.canvasPoint, ~setState) => {
setState((state: t) => ({...state, lastMousePosition: Some(point)})); setState((state: t) => {...state, lastMousePosition: Some(point)});
}; };
let onMouseMovement = let onMouseMovement =
@ -466,14 +503,15 @@ module State = {
~state: t, ~state: t,
~setState, ~setState,
) => { ) => {
/* Helper functions and objects*/ /* Helper functions and objects*/
let x = ReactEvent.Mouse.clientX(event); let x = ReactEvent.Mouse.clientX(event);
let y = ReactEvent.Mouse.clientY(event); let y = ReactEvent.Mouse.clientY(event);
let windowScrollY: float = [%raw "window.scrollY"];
let point1: Types.canvasPoint = { let point1: Types.canvasPoint = {
w: float_of_int(x), w: float_of_int(x),
h: float_of_int(y), h: float_of_int(y) +. windowScrollY,
}; };
let pointIsInBetween = let pointIsInBetween =
@ -485,7 +523,6 @@ module State = {
x0 < x2 && x2 < x1 || x1 < x2 && x2 < x0; x0 < x2 && x2 < x1 || x1 < x2 && x2 < x0;
}; };
/* If all conditions are met, update the distribution */ /* If all conditions are met, update the distribution */
let updateDistWithMouseMovement = let updateDistWithMouseMovement =
( (
@ -550,9 +587,11 @@ module State = {
point.w <= float_of_int(rectangle.right) point.w <= float_of_int(rectangle.right)
-. CanvasContext.paddingFactorX(rectangle.width), -. CanvasContext.paddingFactorX(rectangle.width),
*/ */
point.h >= float_of_int(rectangle.top) point.h
-. windowScrollY >= float_of_int(rectangle.top)
+. CanvasContext.paddingFactorY(rectangle.height), +. CanvasContext.paddingFactorY(rectangle.height),
point.h <= float_of_int(rectangle.bottom) point.h
-. windowScrollY <= float_of_int(rectangle.bottom)
-. CanvasContext.paddingFactorY(rectangle.height), -. CanvasContext.paddingFactorY(rectangle.height),
) { ) {
| (true, true) => true | (true, true) => true
@ -567,8 +606,8 @@ module State = {
validateYCoordinates(~point=point1, ~rectangle), validateYCoordinates(~point=point1, ~rectangle),
) { ) {
| (true, true) => | (true, true) =>
let newCanvasShape = updateDistWithMouseMovement(~point0, ~point1, ~canvasShape); let newCanvasShape =
state.readyToRender ? Draw.canvasPlot(canvasElement, newCanvasShape) : (); updateDistWithMouseMovement(~point0, ~point1, ~canvasShape);
setState((state: t) => { setState((state: t) => {
{ {
...state, ...state,
@ -577,10 +616,13 @@ module State = {
readyToRender: false, readyToRender: false,
} }
}); });
state.readyToRender
? Draw.canvasPlot(canvasElement, newCanvasShape) : ();
| (false, true) => updateMousePosition(~point=point1, ~setState) | (false, true) => updateMousePosition(~point=point1, ~setState)
| (_, false) => () | (_, false) => ()
}; };
} };
switch ( switch (
potentialCanvas, potentialCanvas,
@ -589,10 +631,11 @@ module State = {
state.lastMousePosition, state.lastMousePosition,
) { ) {
| (Some(canvasElement), Some(canvasShape), true, Some(point0)) => | (Some(canvasElement), Some(canvasShape), true, Some(point0)) =>
decideWithCanvas(~canvasElement, ~canvasShape, ~point0); decideWithCanvas(~canvasElement, ~canvasShape, ~point0)
| (Some(canvasElement), _, true, None) => | (Some(canvasElement), _, true, None) =>
let rectangle = CanvasContext.getBoundingClientRect(canvasElement); 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, ~state: Types.canvasState,
~potentialCanvasElement: option(Dom.element), ~potentialCanvasElement: option(Dom.element),
@ -615,7 +658,6 @@ module State = {
| (None, _) => () | (None, _) => ()
| (_, None) => () | (_, None) => ()
| (Some(canvasShape), Some(canvasElement)) => | (Some(canvasShape), Some(canvasElement)) =>
let pdf = let pdf =
Convert.canvasShapeToContinuousShape(~canvasShape, ~canvasElement); Convert.canvasShapeToContinuousShape(~canvasShape, ~canvasElement);
@ -637,21 +679,75 @@ module State = {
let j = i * 3; let j = i * 3;
Js.Array.push(cdf.xyShape.xs[j], xs); Js.Array.push(cdf.xyShape.xs[j], xs);
Js.Array.push(cdf.xyShape.ys[j], ys); Js.Array.push(cdf.xyShape.ys[j], ys);
();
}; };
ForetoldAPI.predict( ForetoldAPI.predict(
~measurableId=state.formElements.measurableId, ~measurableId=state.foretoldFormElements.measurableId,
~token=state.formElements.token, ~token=state.foretoldFormElements.token,
~comment=state.formElements.comment, ~comment=state.foretoldFormElements.comment,
~xs, ~xs,
~ys, ~ys,
); );
setState((state: t) => {...state, hasJustBeenSent: true});
setState((state: t) => {...state, hasJustBeenSentToForetold: true});
Js.Global.setTimeout( Js.Global.setTimeout(
() => { () => {
setState((state: t) => {...state, hasJustBeenSent: false}); setState((state: t) =>
{...state, hasJustBeenSentToForetold: false}
)
}, },
5000, 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] [@react.component]
let make = () => { let make = () => {
let canvasRef: React.Ref.t(option(Dom.element)) = React.useRef(None); // should morally live inside the state, but this is tricky. 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); let (state, setState) = React.useState(() => State.initialState);
@ -684,7 +779,7 @@ let make = () => {
None; 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(() => { React.useEffect0(() => {
let runningInterval = let runningInterval =
Js.Global.setInterval( Js.Global.setInterval(
@ -693,14 +788,14 @@ let make = () => {
{...state, readyToRender: true} {...state, readyToRender: true}
}) })
}, },
40, 30,
); );
Some(() => Js.Global.clearInterval(runningInterval)); Some(() => Js.Global.clearInterval(runningInterval));
}); });
<Antd.Card title={"Distribution Drawer" |> R.ste}> <Antd.Card title={"Distribution Drawer" |> R.ste}>
<div className=Styles.spacer /> <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 <canvas
width="1000" width="1000"
height="700" height="700"
@ -722,13 +817,88 @@ let make = () => {
<br /> <br />
<br /> <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}> <Antd.Card title={"Send to foretold" |> R.ste}>
<form <form
id="send-to-foretold" id="send-to-foretold"
onSubmit={(e: ReactEvent.Form.t): unit => { onSubmit={(e: ReactEvent.Form.t): unit => {
ReactEvent.Form.preventDefault(e); ReactEvent.Form.preventDefault(e);
/* code to run on submit */ /* code to run on submit */
State.onSubmitForm( State.onSubmitForetoldForm(
~state, ~state,
~potentialCanvasElement=React.Ref.current(canvasRef), ~potentialCanvasElement=React.Ref.current(canvasRef),
~setState, ~setState,
@ -741,7 +911,7 @@ let make = () => {
type_="text" type_="text"
id="measurableId" id="measurableId"
name="measurableId" name="measurableId"
value={state.formElements.measurableId} value={state.foretoldFormElements.measurableId}
placeholder="The last bit in the url, after the m" placeholder="The last bit in the url, after the m"
required=true required=true
onChange={event => { onChange={event => {
@ -749,8 +919,8 @@ let make = () => {
setState((state: Types.canvasState) => { setState((state: Types.canvasState) => {
{ {
...state, ...state,
formElements: { foretoldFormElements: {
...state.formElements, ...state.foretoldFormElements,
measurableId: value, measurableId: value,
}, },
} }
@ -765,7 +935,7 @@ let make = () => {
type_="text" type_="text"
id="foretoldToken" id="foretoldToken"
name="foretoldToken" name="foretoldToken"
value={state.formElements.token} value={state.foretoldFormElements.token}
placeholder="Profile -> Bots -> (New Bot) -> Token" placeholder="Profile -> Bots -> (New Bot) -> Token"
required=true required=true
onChange={event => { onChange={event => {
@ -773,8 +943,8 @@ let make = () => {
setState((state: Types.canvasState) => { setState((state: Types.canvasState) => {
{ {
...state, ...state,
formElements: { foretoldFormElements: {
...state.formElements, ...state.foretoldFormElements,
token: value, token: value,
}, },
} }
@ -794,8 +964,8 @@ let make = () => {
setState((state: Types.canvasState) => { setState((state: Types.canvasState) => {
{ {
...state, ...state,
formElements: { foretoldFormElements: {
...state.formElements, ...state.foretoldFormElements,
comment: value, comment: value,
}, },
} }
@ -807,7 +977,7 @@ let make = () => {
{"Send to foretold" |> R.ste} {"Send to foretold" |> R.ste}
</button> </button>
<br /> <br />
<p hidden={!state.hasJustBeenSent}> {"Sent!" |> R.ste} </p> <p hidden={!state.hasJustBeenSentToForetold}> {"Sent!" |> R.ste} </p>
</form> </form>
</Antd.Card> </Antd.Card>
</Antd.Card>; </Antd.Card>;