Adds a distribution drawer to widedomain.

Things to note:
- The code has comments. I feel protective of these comments, and feel that they help structure the code and will help me out when I come I come back to this code a couple of weeks or months from now.
- Originally based on code by Evan Ward (probability.dev). See also: observablehq.com/@nunosempere/distribution-drawer

To do, in order of importance:
- Add the ability to change the upper and lower boundaries.
- Make the drawings relative to the canvas, not to the screen.
- Add other features from probability.dev

Cool things yet to be done:
- Make it so that one can input a guesstimate function, and then draw on it. To do this, use the Convert.xyShapeToCanvasShape and modify the Draw.initial distribution function slightly.
- Maybe reach out to Metaculus to see if they want to use this somewhere?
This commit is contained in:
Nuno Sempere 2020-05-05 12:52:50 +02:00
parent 0d4c71190d
commit ff5b26d865
5 changed files with 848 additions and 6 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

@ -36,9 +36,11 @@
"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-platform": "7.2.2",
"bs-platform": "^7.2.2",
"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>
}}

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

@ -0,0 +1,814 @@
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 formElements = {
measurableId: string,
token: string,
comment: string,
};
type canvasState = {
isMouseDown: bool,
lastMousePosition: option(canvasPoint),
canvasShape: option(canvasShape),
readyToRender: bool,
formElements,
hasJustBeenSent: bool,
};
};
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 ysRelative =
E.A.fmap(h => bottom -. h +. CanvasContext.paddingFactorY(rectangle.height), hs);
let xyShape: Types.xyShape = {xs, ys: ysRelative};
let continuousShape: Types.continuousShape = {
xyShape,
interpolation: `Linear,
};
let integral = XYShape.Analysis.integrateContinuousShape(continuousShape);
let ys = E.A.fmap(y => y /. integral, ysRelative);
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);
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);
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, [||]);
/* 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 numUnits = 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);
for (i in 0 to numUnits - 1) {
let x =
float_of_int(rectangle.left)
+. width
*. float_of_int(i)
/. float_of_int(numUnits);
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 = 10.0;
let stdev = 4.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);
*/
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 = {
isMouseDown: false,
lastMousePosition: None,
canvasShape: None,
readyToRender: false,
hasJustBeenSent: false,
formElements: {
measurableId: "",
token: "",
comment: "",
},
};
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 point1: Types.canvasPoint = {
w: float_of_int(x),
h: float_of_int(y),
};
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 >= float_of_int(rectangle.top)
+. CanvasContext.paddingFactorY(rectangle.height),
point.h <= 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);
state.readyToRender ? Draw.canvasPlot(canvasElement, newCanvasShape) : ();
setState((state: t) => {
{
...state,
lastMousePosition: Some(point1),
canvasShape: Some(newCanvasShape),
readyToRender: false,
}
});
| (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 onSubmitForm =
(
~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.formElements.measurableId,
~token=state.formElements.token,
~comment=state.formElements.comment,
~xs,
~ys,
);
setState((state: t) => {...state, hasJustBeenSent: true});
Js.Global.setTimeout(
() => {
setState((state: t) => {...state, hasJustBeenSent: 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 40ms, while the mouse is moving and changing it */
React.useEffect0(() => {
let runningInterval =
Js.Global.setInterval(
() => {
setState((state: Types.canvasState) => {
{...state, readyToRender: true}
})
},
40,
);
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={"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,
~potentialCanvasElement=React.Ref.current(canvasRef),
~setState,
);
();
}}>
<div>
<label> {"MeasurableId: " |> R.ste} </label>
<input
type_="text"
id="measurableId"
name="measurableId"
value={state.formElements.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,
formElements: {
...state.formElements,
measurableId: value,
},
}
});
}}
/>
</div>
<br />
<div>
<label> {"Foretold bot token: " |> R.ste} </label>
<input
type_="text"
id="foretoldToken"
name="foretoldToken"
value={state.formElements.token}
placeholder="Profile -> Bots -> (New Bot) -> Token"
required=true
onChange={event => {
let value = ReactEvent.Form.target(event)##value;
setState((state: Types.canvasState) => {
{
...state,
formElements: {
...state.formElements,
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,
formElements: {
...state.formElements,
comment: value,
},
}
});
}}
/>
<br />
<button type_="submit" id="sendToForetoldButton">
{"Send to foretold" |> R.ste}
</button>
<br />
<p hidden={!state.hasJustBeenSent}> {"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"