Merge pull request #40 from foretold-app/drawer

Drawer
This commit is contained in:
Ozzie Gooen 2020-05-26 17:13:11 +01:00 committed by GitHub
commit bd63a5f920
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 1022 additions and 5 deletions

View File

@ -19,7 +19,11 @@
"subdirs": true
}
],
"bsc-flags": ["-bs-super-errors", "-bs-no-version-header", "-bs-g"],
"bsc-flags": [
"-bs-super-errors",
"-bs-no-version-header",
"-bs-g"
],
"package-specs": [
{
"module": "commonjs",
@ -38,8 +42,12 @@
"bs-css",
"rationale",
"bs-moment",
"reschema"
"reschema",
"bs-webapi",
"bs-fetch"
],
"refmt": 3,
"ppx-flags": ["lenses-ppx/ppx"]
}
"ppx-flags": [
"lenses-ppx/ppx"
]
}

View File

@ -35,8 +35,10 @@
"binary-search-tree": "0.2.6",
"bs-ant-design-alt": "2.0.0-alpha.33",
"bs-css": "11.0.0",
"bs-fetch": "^0.5.2",
"bs-moment": "0.4.4",
"bs-reform": "9.7.1",
"bs-webapi": "^0.15.9",
"bsb-js": "1.1.7",
"d3": "5.15.0",
"gh-pages": "2.2.0",

View File

@ -3,6 +3,7 @@ type route =
| DistBuilder
| DistBuilder2
| DistBuilder3
| Drawer
| Home
| NotFound;
@ -12,6 +13,7 @@ let routeToPath = route =>
| DistBuilder => "/dist-builder"
| DistBuilder2 => "/dist-builder2"
| DistBuilder3 => "/dist-builder3"
| Drawer => "/drawer"
| Home => "/"
| _ => "/"
};
@ -79,6 +81,10 @@ module Menu = {
<Item href={routeToPath(DistBuilder3)} key="dist-builder-3">
{"Dist Builder 3" |> R.ste}
</Item>
<Item href={routeToPath(Drawer)} key="drawer">
{"Drawer" |> R.ste}
</Item>
</div>;
};
};
@ -93,6 +99,7 @@ let make = () => {
| ["dist-builder"] => DistBuilder
| ["dist-builder2"] => DistBuilder2
| ["dist-builder3"] => DistBuilder3
| ["drawer"] => Drawer
| [] => Home
| _ => NotFound
};
@ -108,6 +115,7 @@ let make = () => {
| DistBuilder => <DistBuilder />
| DistBuilder2 => <DistBuilder2 />
| DistBuilder3 => <DistBuilder3 />
| Drawer => <Drawer />
| Home => <Home />
| _ => <div> {"Page is not found" |> R.ste} </div>
}}

989
src/components/Drawer.re Normal file
View File

@ -0,0 +1,989 @@
module Types = {
type rectangle = {
// Ref: https://developer.mozilla.org/en-US/docs/Web/API/Element/getBoundingClientRect
left: int,
top: int,
right: int,
bottom: int,
x: int,
y: int,
width: int,
height: int,
};
type webapi = Webapi.Canvas.Canvas2d.t;
type xyShape = DistTypes.xyShape; /* {
xs: array(float),
ys: array(float),
};*/
type continuousShape = DistTypes.continuousShape; /*{
xyShape,
interpolation: [ | `Stepwise | `Linear],
};*/
type canvasPoint = {
w: float,
h: float,
};
type canvasShape = {
ws: array(float),
hs: array(float),
xValues: array(float),
};
type foretoldFormElements = {
measurableId: string,
token: string,
comment: string,
};
type distributionLimits = {
lower: float,
upper: float,
};
type canvasState = {
canvasShape: option(canvasShape),
lastMousePosition: option(canvasPoint),
isMouseDown: bool,
readyToRender: bool,
hasJustBeenSentToForetold: bool,
limitsHaveJustBeenUpdated: bool,
foretoldFormElements,
distributionLimits,
};
};
module CanvasContext = {
type t = Types.webapi;
/* Externals */
[@bs.send]
external getBoundingClientRect: Dom.element => Types.rectangle =
"getBoundingClientRect";
[@bs.send] external setLineDash: (t, array(int)) => unit = "setLineDash";
/* Webapi functions */
// Ref: https://github.com/reasonml-community/bs-webapi-incubator/blob/master/src/Webapi/Webapi__Canvas/Webapi__Canvas__Canvas2d.re
let getContext2d: Dom.element => t = Webapi.Canvas.CanvasElement.getContext2d;
module Canvas2d = Webapi.Canvas.Canvas2d;
let clearRect = Canvas2d.clearRect;
let setFillStyle = Canvas2d.setFillStyle;
let fillRect = Canvas2d.fillRect;
let beginPath = Canvas2d.beginPath;
let closePath = Canvas2d.closePath;
let setStrokeStyle = Canvas2d.setStrokeStyle;
let lineWidth = Canvas2d.lineWidth;
let moveTo = Canvas2d.moveTo;
let lineTo = Canvas2d.lineTo;
let stroke = Canvas2d.stroke;
let font = Canvas2d.font;
let textAlign = Canvas2d.textAlign;
let strokeText = Canvas2d.strokeText;
let fillText = Canvas2d.fillText;
/* Padding */
let paddingRatioX = 0.9;
let paddingRatioY = 0.9;
let paddingFactorX = (rectangleWidth: int) =>
(1. -. paddingRatioX) *. float_of_int(rectangleWidth) /. 2.0;
let paddingFactorY = (rectangleHeight: int) =>
(1. -. paddingRatioY) *. float_of_int(rectangleHeight) /. 2.0;
let translatePointToInside = (canvasElement: Dom.element) => {
let rectangle: Types.rectangle = getBoundingClientRect(canvasElement);
let translate = (p: Types.canvasPoint): Types.canvasPoint => {
let w = p.w -. float_of_int(rectangle.x);
let h = p.h -. float_of_int(rectangle.y);
{w, h};
};
translate;
};
};
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
*/
let xyShapeToCanvasShape =
(~xyShape: Types.xyShape, ~canvasElement: Dom.element) => {
let xs = xyShape.xs;
let ys = xyShape.ys;
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);
let ws =
E.A.fmap(
x =>
(x -. minX)
*. ratioXs
+. float_of_int(rectangle.left)
+. (1. -. CanvasContext.paddingRatioX)
*. float_of_int(rectangle.width)
/. 2.0,
xs,
);
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 hs =
E.A.fmap(
y =>
float_of_int(rectangle.bottom)
-. y
*. ratioYs
-. CanvasContext.paddingFactorY(rectangle.height),
ys,
);
let canvasShape: Types.canvasShape = {ws, hs, xValues: xs};
canvasShape;
};
let canvasShapeToContinuousShape =
(~canvasShape: Types.canvasShape, ~canvasElement: Dom.element)
: Types.continuousShape => {
let xs = canvasShape.xValues;
let hs = canvasShape.hs;
let rectangle: Types.rectangle =
CanvasContext.getBoundingClientRect(canvasElement);
let bottom = float_of_int(rectangle.bottom);
let paddingFactorY = CanvasContext.paddingFactorX(rectangle.height);
let windowScrollY: float = [%raw "window.scrollY"];
let y0Line = bottom+.windowScrollY-.paddingFactorY;
let ys = E.A.fmap( h => y0Line -. h, hs);
let xyShape: Types.xyShape = {xs, ys};
let continuousShape: Types.continuousShape = {
xyShape,
interpolation: `Linear,
};
let integral = XYShape.Analysis.integrateContinuousShape(continuousShape);
let ys = E.A.fmap(y => y /. integral, ys);
let continuousShape: Types.continuousShape = {
xyShape: {
xs,
ys,
},
interpolation: `Linear,
};
continuousShape;
};
/* Misc helper functions */
let log2 = x => log(x) /. log(2.0);
let findClosestInOrderedArrayDangerously = (x: float, xs: array(float)) => {
let l = Array.length(xs);
let a = ref(0);
let b = ref(l - 1);
let numSteps = int_of_float(log2(float_of_int(l))) + 1;
for (_ in 0 to numSteps) {
let c = (a^ + b^) / 2;
xs[c] > x ? b := c : a := c;
};
b^;
};
let getPoint = (canvasShape: Types.canvasShape, n: int): Types.canvasPoint => {
let point: Types.canvasPoint = {
w: canvasShape.ws[n],
h: canvasShape.hs[n],
};
point;
};
};
module Draw = {
let line =
(
canvasElement: Dom.element,
~point0: Types.canvasPoint,
~point1: Types.canvasPoint,
)
: unit => {
let translator = CanvasContext.translatePointToInside(canvasElement);
let point0 = translator(point0);
let point1 = translator(point1);
let context = CanvasContext.getContext2d(canvasElement);
CanvasContext.beginPath(context);
CanvasContext.moveTo(context, ~x=point0.w, ~y=point0.h);
CanvasContext.lineTo(context, ~x=point1.w, ~y=point1.h);
CanvasContext.stroke(context);
};
let canvasPlot =
(canvasElement: Dom.element, canvasShape: Types.canvasShape) => {
let context = CanvasContext.getContext2d(canvasElement);
let rectangle: Types.rectangle =
CanvasContext.getBoundingClientRect(canvasElement);
/* Some useful reference points */
let paddingFactorX = CanvasContext.paddingFactorX(rectangle.width);
let paddingFactorY = CanvasContext.paddingFactorX(rectangle.height);
let p00: Types.canvasPoint = {
w: float_of_int(rectangle.left) +. paddingFactorX,
h: float_of_int(rectangle.bottom) -. paddingFactorY,
};
let p01: Types.canvasPoint = {
w: float_of_int(rectangle.left) +. paddingFactorX,
h: float_of_int(rectangle.top) +. paddingFactorY,
};
let p10: Types.canvasPoint = {
w: float_of_int(rectangle.right) -. paddingFactorX,
h: float_of_int(rectangle.bottom) -. paddingFactorY,
};
let p11: Types.canvasPoint = {
w: float_of_int(rectangle.right) -. paddingFactorX,
h: float_of_int(rectangle.top) +. paddingFactorY,
};
/* Clear the canvas with new white sheet */
CanvasContext.clearRect(
context,
~x=0.,
~y=0.,
~w=float_of_int(rectangle.width),
~h=float_of_int(rectangle.height),
);
/* 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 */
// 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");
CanvasContext.lineWidth(context, 2.);
CanvasContext.font(context, "18px Roboto");
CanvasContext.textAlign(context, "center");
let numLines = 8;
let height =
float_of_int(rectangle.height)
*. CanvasContext.paddingRatioX
/. float_of_int(numLines);
for (i in 0 to numLines - 1) {
let pLeft = {...p00, h: p00.h -. height *. float_of_int(i)};
let pRight = {...p10, h: p10.h -. height *. float_of_int(i)};
line(canvasElement, ~point0=pLeft, ~point1=pRight);
CanvasContext.fillText(
string_of_int(i) ++ "x",
~x=pLeft.w -. float_of_int(rectangle.left) +. 15.0,
~y=pLeft.h -. float_of_int(rectangle.top) -. 5.0,
context,
);
};
/* Draw a frame around the drawable area */
CanvasContext.lineWidth(context, 2.0);
CanvasContext.setStrokeStyle(context, String, "#000");
line(canvasElement, ~point0=p00, ~point1=p01);
line(canvasElement, ~point0=p01, ~point1=p11);
line(canvasElement, ~point0=p11, ~point1=p10);
line(canvasElement, ~point0=p10, ~point1=p00);
/* draw units along the x axis */
CanvasContext.font(context, "16px Roboto");
CanvasContext.lineWidth(context, 2.0);
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(numIntervals - 1);
for (i in 0 to numIntervals - 1) {
let x =
float_of_int(rectangle.left)
+. width
*. float_of_int(i)
/. float_of_int(numIntervals);
let dashValue = xMin +. xSpan *. float_of_int(i);
CanvasContext.fillText(
Js.Float.toFixedWithPrecision(dashValue, ~digits=2),
~x,
~y=height,
context,
);
line(
canvasElement,
~point0={
w: x +. CanvasContext.paddingFactorX(rectangle.width),
h: p00.h,
},
~point1={
w: x +. CanvasContext.paddingFactorX(rectangle.width),
h: p00.h +. 10.0,
},
);
};
};
let initialDistribution = (canvasElement: Dom.element, setState) => {
let mean = 100.0;
let stdev = 15.0;
let numSamples = 3000;
let normal: SymbolicDist.dist = `Normal({mean, stdev});
let normalShape = SymbolicDist.GenericSimple.toShape(normal, numSamples);
let xyShape: Types.xyShape =
switch (normalShape) {
| Mixed(_) => {xs: [||], ys: [||]}
| 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 canvasShape = Convert.xyShapeToCanvasShape(~xyShape, ~canvasElement);
/* let continuousShapeBack =
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)}
});
canvasPlot(canvasElement, canvasShape);
};
};
module ForetoldAPI = {
let predict = (~measurableId, ~token, ~xs, ~ys, ~comment) => {
let payload = Js.Dict.empty();
let xsString = Js.Array.toString(xs);
let ysString = Js.Array.toString(ys);
let query = {j|mutation {
measurementCreate(input: {
value: {
floatCdf: {
xs: [$xsString]
ys: [$ysString]
}
}
valueText: "Drawn by hand."
description: "$comment"
competitorType: COMPETITIVE
measurableId: "$measurableId"
}) {
id
}
}|j};
Js.Dict.set(payload, "query", Js.Json.string(query));
Js.Promise.(
Fetch.fetchWithInit(
"https://api.foretold.io/graphql?token=" ++ token,
Fetch.RequestInit.make(
~method_=Post,
~body=
Fetch.BodyInit.make(
Js.Json.stringify(Js.Json.object_(payload)),
),
~headers=
Fetch.HeadersInit.make({
"Accept-Encoding": "gzip, deflate, br",
"Content-Type": "application/json",
"Accept": "application/json",
"Connection": "keep-alive",
"DNT": "1",
"Origin": "https://api.foretold.io",
}),
(),
),
)
|> then_(Fetch.Response.json)
);
};
};
module State = {
type t = Types.canvasState;
let initialState: t = {
canvasShape: None,
lastMousePosition: None,
isMouseDown: false,
readyToRender: false,
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 onMouseMovement =
(
~event: ReactEvent.Mouse.t,
~potentialCanvas: option(Dom.element),
~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) +. windowScrollY,
};
let pointIsInBetween =
(a: Types.canvasPoint, b: Types.canvasPoint, c: Types.canvasPoint)
: bool => {
let x0 = a.w;
let x1 = b.w;
let x2 = c.w;
x0 < x2 && x2 < x1 || x1 < x2 && x2 < x0;
};
/* If all conditions are met, update the distribution */
let updateDistWithMouseMovement =
(
~point0: Types.canvasPoint,
~point1: Types.canvasPoint,
~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)
*/
let slope = (point1.h -. point0.h) /. (point1.w -. point0.w);
let pos0 =
Convert.findClosestInOrderedArrayDangerously(
point0.w,
canvasShape.ws,
);
let pos1 =
Convert.findClosestInOrderedArrayDangerously(
point1.w,
canvasShape.ws,
);
// Mouse is moving to the right
for (i in pos0 to pos1) {
let pointN = Convert.getPoint(canvasShape, i);
switch (pointIsInBetween(point0, point1, pointN)) {
| false => ()
| true =>
canvasShape.hs[i] = point0.h +. slope *. (pointN.w -. point0.w)
};
};
// Mouse is moving to the left
for (i in pos0 downto pos1) {
let pointN = Convert.getPoint(canvasShape, i);
switch (pointIsInBetween(point0, point1, pointN)) {
| false => ()
| true =>
canvasShape.hs[i] = point0.h +. slope *. (pointN.w -. point0.w)
};
};
canvasShape;
};
/* Check that the mouse movement was within the paddding box. */
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
-. windowScrollY >= float_of_int(rectangle.top)
+. CanvasContext.paddingFactorY(rectangle.height),
point.h
-. windowScrollY <= float_of_int(rectangle.bottom)
-. CanvasContext.paddingFactorY(rectangle.height),
) {
| (true, true) => true
| _ => false
};
};
let decideWithCanvas = (~canvasElement, ~canvasShape, ~point0) => {
let rectangle = CanvasContext.getBoundingClientRect(canvasElement);
switch (
validateYCoordinates(~point=point0, ~rectangle),
validateYCoordinates(~point=point1, ~rectangle),
) {
| (true, true) =>
let newCanvasShape =
updateDistWithMouseMovement(~point0, ~point1, ~canvasShape);
setState((state: t) => {
{
...state,
lastMousePosition: Some(point1),
canvasShape: Some(newCanvasShape),
readyToRender: false,
}
});
state.readyToRender
? Draw.canvasPlot(canvasElement, newCanvasShape) : ();
| (false, true) => updateMousePosition(~point=point1, ~setState)
| (_, false) => ()
};
};
switch (
potentialCanvas,
state.canvasShape,
state.isMouseDown,
state.lastMousePosition,
) {
| (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) : ();
| _ => ()
};
};
let onMouseClick = (~setState, ~state) => {
setState((state: t) => {
{...state, isMouseDown: !state.isMouseDown, lastMousePosition: None}
});
};
let onSubmitForetoldForm =
(
~state: Types.canvasState,
~potentialCanvasElement: option(Dom.element),
~setState,
) => {
let potentialCanvasShape = state.canvasShape;
switch (potentialCanvasShape, potentialCanvasElement) {
| (None, _) => ()
| (_, None) => ()
| (Some(canvasShape), Some(canvasElement)) =>
let pdf =
Convert.canvasShapeToContinuousShape(~canvasShape, ~canvasElement);
/* create a cdf from a pdf */
let _pdf =
Distributions.Continuous.T.scaleToIntegralSum(
~cache=None,
~intendedSum=1.0,
pdf,
);
let cdf = Distributions.Continuous.T.integral(~cache=None, _pdf);
let xs = [||];
let ys = [||];
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.
*/
let j = i * 3;
Js.Array.push(cdf.xyShape.xs[j], xs);
Js.Array.push(cdf.xyShape.ys[j], ys);
();
};
ForetoldAPI.predict(
~measurableId=state.foretoldFormElements.measurableId,
~token=state.foretoldFormElements.token,
~comment=state.foretoldFormElements.comment,
~xs,
~ys,
);
setState((state: t) => {...state, hasJustBeenSentToForetold: true});
Js.Global.setTimeout(
() => {
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,
);
();
};
();
};
};
module Styles = {
open Css;
let dist = style([padding(em(1.))]);
let spacer = style([marginTop(em(1.))]);
};
[@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);
/* Draw the initial distribution */
React.useEffect0(() => {
let potentialCanvas = React.Ref.current(canvasRef);
(
switch (potentialCanvas) {
| Some(canvasElement) =>
Draw.initialDistribution(canvasElement, setState)
| None => ()
}
)
|> ignore;
None;
});
/* Render the current distribution every 30ms, while the mouse is moving and changing it */
React.useEffect0(() => {
let runningInterval =
Js.Global.setInterval(
() => {
setState((state: Types.canvasState) => {
{...state, readyToRender: true}
})
},
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>
<canvas
width="1000"
height="700"
ref={ReactDOMRe.Ref.callbackDomRef(elem =>
React.Ref.setCurrent(canvasRef, Js.Nullable.toOption(elem))
)}
onMouseMove={event =>
State.onMouseMovement(
~event,
~potentialCanvas=React.Ref.current(canvasRef),
~state,
~setState,
)
}
onClick={_e => State.onMouseClick(~setState, ~state)}
/>
<div className=Styles.spacer />
<div className=Styles.spacer />
<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.onSubmitForetoldForm(
~state,
~potentialCanvasElement=React.Ref.current(canvasRef),
~setState,
);
();
}}>
<div>
<label> {"MeasurableId: " |> R.ste} </label>
<input
type_="text"
id="measurableId"
name="measurableId"
value={state.foretoldFormElements.measurableId}
placeholder="The last bit in the url, after the m"
required=true
onChange={event => {
let value = ReactEvent.Form.target(event)##value;
setState((state: Types.canvasState) => {
{
...state,
foretoldFormElements: {
...state.foretoldFormElements,
measurableId: value,
},
}
});
}}
/>
</div>
<br />
<div>
<label> {"Foretold bot token: " |> R.ste} </label>
<input
type_="text"
id="foretoldToken"
name="foretoldToken"
value={state.foretoldFormElements.token}
placeholder="Profile -> Bots -> (New Bot) -> Token"
required=true
onChange={event => {
let value = ReactEvent.Form.target(event)##value;
setState((state: Types.canvasState) => {
{
...state,
foretoldFormElements: {
...state.foretoldFormElements,
token: value,
},
}
});
}}
/>
</div>
<br />
<textarea
id="comment"
name="comment"
rows=20
cols=70
placeholder="Explain a little bit what this distribution is about"
onChange={event => {
let value = ReactEvent.Form.target(event)##value;
setState((state: Types.canvasState) => {
{
...state,
foretoldFormElements: {
...state.foretoldFormElements,
comment: value,
},
}
});
}}
/>
<br />
<button type_="submit" id="sendToForetoldButton">
{"Send to foretold" |> R.ste}
</button>
<br />
<p hidden={!state.hasJustBeenSentToForetold}> {"Sent!" |> R.ste} </p>
</form>
</Antd.Card>
</Antd.Card>;
};

View File

@ -2476,6 +2476,11 @@ bs-css@11.0.0:
dependencies:
emotion "^10.0.7"
bs-fetch@^0.5.2:
version "0.5.2"
resolved "https://registry.yarnpkg.com/bs-fetch/-/bs-fetch-0.5.2.tgz#a9f4582ddb1414d3467b900305c738813481988c"
integrity sha512-CYweTJcgLeLOqzR3vNsB+NmyEFhWThnPNdZmmF28nkFmo0CQEf+20eSHZJDO6EuAhhoRqJ0qp2VgxtH6zBB5xA==
bs-moment@0.4.4:
version "0.4.4"
resolved "https://registry.yarnpkg.com/bs-moment/-/bs-moment-0.4.4.tgz#3d59767e8cd0107393c4f371e15b95bf862ceaac"
@ -2486,7 +2491,7 @@ bs-moment@0.4.5, bs-moment@^0.4.4:
resolved "https://registry.yarnpkg.com/bs-moment/-/bs-moment-0.4.5.tgz#3f84fed55c2a70d25b0b6025e4e8d821fcdd4dc8"
integrity sha512-anPYkFSof+X8EeomnP0fbQBvWFJeganwPqqARVB+fcdKYX2Uog/n3CCiFGEA+66yHbwnWZD5YFhtHCuyLMcQfQ==
bs-platform@7.2.2:
bs-platform@7.2.2, bs-platform@^7.2.2:
version "7.2.2"
resolved "https://registry.yarnpkg.com/bs-platform/-/bs-platform-7.2.2.tgz#76fdc63e4889458ae3d257a0132107a792f2309c"
integrity sha512-PWcFfN+jCTtT/rMaHDhKh+W9RUTpaRunmSF9vbLYcrJbpgCNW6aFKAY33u0P3mLxwuhshN3b4FxqGUBPj6exZQ==
@ -2500,6 +2505,11 @@ bs-reform@9.7.1:
reason-react-update "^1.0.0"
reschema "^1.3.0"
bs-webapi@^0.15.9:
version "0.15.9"
resolved "https://registry.yarnpkg.com/bs-webapi/-/bs-webapi-0.15.9.tgz#d1dcfbd40499a3b0914daacc4ceef47a0f3c906f"
integrity sha512-bmzO6na2HmK01a34qB7afMwfVSrPxkP3EGzUslWObuxbHIlLC0PAMDu4lA8ZL5NasBUctlwnA1QZxox+1aNWWw==
bsb-js@1.1.7:
version "1.1.7"
resolved "https://registry.yarnpkg.com/bsb-js/-/bsb-js-1.1.7.tgz#12cc91e974f5896b3a2aa8358419d24e56f552c3"