From ff5b26d865757f6b1075de50003b248113087f64 Mon Sep 17 00:00:00 2001 From: Nuno Sempere Date: Tue, 5 May 2020 12:52:50 +0200 Subject: [PATCH] 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? --- bsconfig.json | 16 +- package.json | 4 +- src/App.re | 8 + src/components/Drawer.re | 814 +++++++++++++++++++++++++++++++++++++++ yarn.lock | 12 +- 5 files changed, 848 insertions(+), 6 deletions(-) create mode 100644 src/components/Drawer.re diff --git a/bsconfig.json b/bsconfig.json index 8de81a25..7bef0766 100644 --- a/bsconfig.json +++ b/bsconfig.json @@ -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" + ] +} \ No newline at end of file diff --git a/package.json b/package.json index 8bc2c8fe..c8634944 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/App.re b/src/App.re index 509c7a4a..5fc8ba85 100644 --- a/src/App.re +++ b/src/App.re @@ -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 = { {"Dist Builder 3" |> R.ste} + + {"Drawer" |> R.ste} + + ; }; }; @@ -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 => | DistBuilder2 => | DistBuilder3 => + | Drawer => | Home => | _ =>
{"Page is not found" |> R.ste}
}} diff --git a/src/components/Drawer.re b/src/components/Drawer.re new file mode 100644 index 00000000..d0dcfaca --- /dev/null +++ b/src/components/Drawer.re @@ -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)); + }); + + R.ste}> +
+

{"Click to begin drawing, click to stop drawing" |> R.ste}

+ + 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)} + /> +
+
+
+
+
+ R.ste}> +
{ + ReactEvent.Form.preventDefault(e); + /* code to run on submit */ + State.onSubmitForm( + ~state, + ~potentialCanvasElement=React.Ref.current(canvasRef), + ~setState, + ); + (); + }}> +
+ + { + let value = ReactEvent.Form.target(event)##value; + setState((state: Types.canvasState) => { + { + ...state, + formElements: { + ...state.formElements, + measurableId: value, + }, + } + }); + }} + /> +
+
+
+ + { + let value = ReactEvent.Form.target(event)##value; + setState((state: Types.canvasState) => { + { + ...state, + formElements: { + ...state.formElements, + token: value, + }, + } + }); + }} + /> +
+
+