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:
parent
0d4c71190d
commit
ff5b26d865
|
@ -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"
|
||||
]
|
||||
}
|
|
@ -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",
|
||||
|
|
|
@ -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
814
src/components/Drawer.re
Normal 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>;
|
||||
};
|
12
yarn.lock
12
yarn.lock
|
@ -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"
|
||||
|
|
Loading…
Reference in New Issue
Block a user