Added simple showcase, related components

This commit is contained in:
Ozzie Gooen 2020-02-17 21:52:21 +00:00
parent 17e40a9098
commit 17071602e8
19 changed files with 830 additions and 10 deletions

View File

@ -3,10 +3,16 @@
"reason": {
"react-jsx": 3
},
"sources": {
"dir": "src",
"subdirs": true
},
"sources": [{
"dir": "src",
"subdirs": true
},
{
"dir": "showcase",
"type": "dev",
"subdirs": true
}
],
"bsc-flags": ["-bs-super-errors", "-bs-no-version-header"],
"package-specs": [{
"module": "commonjs",

View File

@ -9,6 +9,7 @@
"clean": "bsb -clean-world",
"parcel": "parcel ./src/index.html --public-url / --no-autoinstall -- watch",
"parcel-build": "parcel build ./src/index.html --no-source-maps --no-autoinstall",
"showcase": "PORT=12345 parcel showcase/index.html",
"server": "moduleserve ./ --port 8000",
"predeploy": "parcel build ./src/index.html --no-source-maps --no-autoinstall",
"deploy": "gh-pages -d dist",
@ -31,6 +32,7 @@
"bs-css": "^11.0.0",
"bs-moment": "0.4.4",
"bs-reform": "9.7.1",
"d3": "^5.15.0",
"lenses-ppx": "4.0.0",
"less": "^3.10.3",
"lodash": "^4.17.15",

1
showcase/Entries.re Normal file
View File

@ -0,0 +1 @@
let entries = EntryTypes.[Continuous.entry];

30
showcase/EntryTypes.re Normal file
View File

@ -0,0 +1,30 @@
type compEntry = {
mutable id: string,
title: string,
render: unit => React.element,
container: containerType,
}
and folderEntry = {
mutable id: string,
title: string,
children: list(navEntry),
}
and navEntry =
| CompEntry(compEntry)
| FolderEntry(folderEntry)
and containerType =
| FullWidth
| Sidebar;
let entry = (~title, ~render): navEntry => {
CompEntry({id: "", title, render, container: FullWidth});
};
// Maybe different api, this avoids breaking changes
let sidebar = (~title, ~render): navEntry => {
CompEntry({id: "", title, render, container: Sidebar});
};
let folder = (~title, ~children): navEntry => {
FolderEntry({id: "", title, children});
};

200
showcase/Lib.re Normal file
View File

@ -0,0 +1,200 @@
open EntryTypes;
module HS = Belt.HashMap.String;
let entriesByPath: HS.t(navEntry) = HS.make(~hintSize=100);
/* Creates unique id's per scope based on title */
let buildIds = entries => {
let genId = (title, path) => {
let noSpaces = Js.String.replaceByRe([%bs.re "/\\s+/g"], "-", title);
if (!HS.has(entriesByPath, path ++ "/" ++ noSpaces)) {
noSpaces;
} else {
let rec loop = num => {
let testId = noSpaces ++ "-" ++ string_of_int(num);
if (!HS.has(entriesByPath, path ++ "/" ++ testId)) {
testId;
} else {
loop(num + 1);
};
};
loop(2);
};
};
let rec processFolder = (f: folderEntry, curPath) => {
f.id = curPath ++ "/" ++ genId(f.title, curPath);
HS.set(entriesByPath, f.id, FolderEntry(f));
f.children
|> E.L.iter(e =>
switch (e) {
| CompEntry(c) => processEntry(c, f.id)
| FolderEntry(f) => processFolder(f, f.id)
}
);
}
and processEntry = (c: compEntry, curPath) => {
c.id = curPath ++ "/" ++ genId(c.title, curPath);
HS.set(entriesByPath, c.id, CompEntry(c));
};
entries
|> E.L.iter(e =>
switch (e) {
| CompEntry(c) => processEntry(c, "")
| FolderEntry(f) => processFolder(f, "")
}
);
};
let entries = Entries.entries;
buildIds(entries);
module Styles = {
open Css;
let pageContainer = style([display(`flex), height(`vh(100.))]);
let leftNav =
style([
padding(`em(2.)),
flexBasis(`px(200)),
flexShrink(0.),
backgroundColor(`hex("eaeff3")),
boxShadows([
Shadow.box(
~x=px(-1),
~blur=px(1),
~inset=true,
rgba(0, 0, 0, 0.1),
),
]),
]);
let folderNav =
style([
selector(
">h4",
[
cursor(`pointer),
margin2(~v=`em(0.3), ~h=`zero),
hover([color(`hex("7089ad"))]),
],
),
]);
let folderChildren = style([paddingLeft(`px(7))]);
let compNav =
style([
cursor(`pointer),
paddingBottom(`px(3)),
hover([color(`hex("7089ad"))]),
]);
let compContainer = style([padding(`em(2.)), flexGrow(1.)]);
// Approximate sidebar container for entry
let sidebarContainer = style([maxWidth(`px(430))]);
let folderChildContainer = style([marginBottom(`em(2.))]);
};
let baseUrl = "/showcase/index.html";
module Index = {
type state = {route: ReasonReactRouter.url};
type action =
| ItemClick(string)
| ChangeRoute(ReasonReactRouter.url);
let changeId = (id: string) => {
ReasonReactRouter.push(baseUrl ++ "#" ++ id);
();
};
let buildNav = setRoute => {
let rec buildFolder = (f: folderEntry) => {
<div key={f.id} className=Styles.folderNav>
<h4 onClick={_e => changeId(f.id)}> f.title->React.string </h4>
<div className=Styles.folderChildren>
{(
f.children
|> E.L.fmap(e =>
switch (e) {
| FolderEntry(folder) => buildFolder(folder)
| CompEntry(entry) => buildEntry(entry)
}
)
|> E.L.toArray
)
->React.array}
</div>
</div>;
}
and buildEntry = (e: compEntry) => {
<div key={e.id} className=Styles.compNav onClick={_e => changeId(e.id)}>
e.title->React.string
</div>;
};
(
entries
|> E.L.fmap(e =>
switch (e) {
| FolderEntry(folder) => buildFolder(folder)
| CompEntry(entry) => buildEntry(entry)
}
)
|> E.L.toArray
)
->React.array;
};
let renderEntry = e => {
switch (e.container) {
| FullWidth => e.render()
| Sidebar => <div className=Styles.sidebarContainer> {e.render()} </div>
};
};
[@react.component]
let make = () => {
let (route, setRoute) =
React.useState(() => {
let url: ReasonReactRouter.url = {path: [], hash: "", search: ""};
url;
});
React.useState(() => {
ReasonReactRouter.watchUrl(url => setRoute(_ => url));
();
})
|> ignore;
<div className=Styles.pageContainer>
<div className=Styles.leftNav> {buildNav(setRoute)} </div>
<div className=Styles.compContainer>
{if (route.hash == "") {
React.null;
} else {
switch (HS.get(entriesByPath, route.hash)) {
| Some(navEntry) =>
switch (navEntry) {
| CompEntry(c) => renderEntry(c)
| FolderEntry(f) =>
/* Rendering immediate children */
(
f.children
|> E.L.fmap(child =>
switch (child) {
| CompEntry(c) =>
<div className=Styles.folderChildContainer key={c.id}>
{renderEntry(c)}
</div>
| _ => React.null
}
)
|> E.L.toArray
)
->React.array
}
| None => <div> "Component not found"->React.string </div>
};
}}
</div>
</div>;
};
};

View File

@ -0,0 +1,2 @@
ReactDOMRe.renderToElementWithId(<div> <Lib.Index /> </div>, "main");
ReasonReactRouter.push("");

View File

@ -0,0 +1,19 @@
open ForetoldComponents.Base;
let data: DistributionTypes.xyShape = {
xs: [|0.2, 20., 80., 212., 330.|],
ys: [|0.0, 0.3, 0.5, 0.2, 0.1|],
};
let alerts = () =>
<div>
<div> <ChartWithNumber data color={`hex("333")} /> </div>
<div>
<ChartWithNumber
data={data |> Shape.XYShape.integral}
color={`hex("333")}
/>
</div>
</div>;
let entry = EntryTypes.(entry(~title="Pdf", ~render=alerts));

23
showcase/index.html Normal file
View File

@ -0,0 +1,23 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<link href="https://fonts.googleapis.com/css?family=Lato:300,400,700,900" rel="stylesheet">
<!--<link rel="stylesheet" href="styles.css">-->
<style>
body {
margin: 0;
}
</style>
<title>Showcase</title>
</head>
<body>
<div id="main"></div>
<script src=" ./ShowcaseIndex.bs.js "></script>
</body>
</html>

View File

@ -0,0 +1,43 @@
[@bs.module "./cdfChartReact.js"]
external cdfChart: ReasonReact.reactClass = "default";
type primaryDistribution = {
.
"xs": array(float),
"ys": array(float),
};
[@react.component]
let make =
(
~height=?,
~verticalLine=?,
~showVerticalLine=?,
~marginBottom=?,
~marginTop=?,
~showDistributionLines=?,
~maxX=?,
~minX=?,
~onHover=(f: float) => (),
~primaryDistribution=?,
~children=[||],
) =>
ReasonReact.wrapJsForReason(
~reactClass=cdfChart,
~props=
makeProps(
~height?,
~verticalLine?,
~marginBottom?,
~marginTop?,
~onHover,
~showVerticalLine?,
~showDistributionLines?,
~maxX?,
~minX?,
~primaryDistribution?,
(),
),
children,
)
|> ReasonReact.element;

View File

@ -0,0 +1,41 @@
module Styles = {
open Css;
let textOverlay = style([position(`absolute)]);
let mainText = style([fontSize(`em(1.1))]);
let secondaryText = style([fontSize(`em(0.9))]);
let graph = chartColor =>
style([
position(`relative),
selector(".axis", [fontSize(`px(9))]),
selector(".domain", [display(`none)]),
selector(".tick line", [display(`none)]),
selector(".tick text", [color(`hex("bfcad4"))]),
selector(".chart .area-path", [SVG.fill(chartColor)]),
]);
};
[@react.component]
let make =
(
~data,
~minX=?,
~maxX=?,
~height=200,
~color=`hex("111"),
~onHover: float => unit,
) => {
<div className={Styles.graph(color)}>
<CdfChart__Base
height
?minX
?maxX
marginBottom=50
marginTop=0
onHover
showVerticalLine=false
showDistributionLines=false
primaryDistribution={data |> Shape.XYShape.toJs}
/>
</div>;
};

View File

@ -0,0 +1,24 @@
module Styles = {
open Css;
let graph = chartColor =>
style([
selector(".axis", [fontSize(`px(9))]),
selector(".domain", [display(`none)]),
selector(".tick line", [display(`none)]),
selector(".tick text", [color(`hex("bfcad4"))]),
selector(".chart .area-path", [SVG.fill(chartColor)]),
]);
};
[@react.component]
let make = (~minX=None, ~maxX=None, ~height=50, ~color=`hex("7e9db7")) =>
<div className={Styles.graph(color)}>
<CdfChart__Base
height
?minX
?maxX
marginBottom=20
showVerticalLine=false
showDistributionLines=false
/>
</div>;

View File

@ -0,0 +1,23 @@
[@react.component]
let make = (~data, ~color=?) => {
let (x, setX) = React.useState(() => 0.);
let chart =
React.useMemo1(
() => <CdfChart__Plain data ?color onHover={r => setX(_ => r)} />,
[|data|],
);
<div>
chart
<div> {x |> E.Float.toString |> ReasonReact.string} </div>
<div>
{Shape.Continuous.findY(x, data)
|> E.Float.toString
|> ReasonReact.string}
</div>
<div>
{Shape.Continuous.findY(x, Shape.XYShape.integral(data))
|> E.Float.toString
|> ReasonReact.string}
</div>
</div>;
};

View File

@ -0,0 +1,66 @@
import React, { useEffect } from 'react';
import { useSize } from 'react-use';
import chart from './cdfChartd3';
/**
* @param min
* @param max
* @returns {number}
*/
function getRandomInt(min, max) {
min = Math.ceil(min);
max = Math.floor(max);
return Math.floor(Math.random() * (max - min + 1)) + min;
}
/**
* Example input:
* {
* xs: [50,100,300,400,500,600],
* ys: [0.1, 0.4, 0.6, 0.7,0.8, 0.9]}
* }
*/
function CdfChart(props) {
const id = "chart-" + getRandomInt(0, 100000);
const [sized, { width }] = useSize(() => {
return React.createElement("div", {
key: "resizable-div",
});
}, {
width: props.width,
});
useEffect(() => {
chart()
.svgWidth(width)
.svgHeight(props.height)
.maxX(props.maxX)
.minX(props.minX)
.onHover(props.onHover)
.marginBottom(props.marginBottom || 15)
.marginLeft(5)
.marginRight(5)
.marginTop(5)
.showDistributionLines(props.showDistributionLines)
.verticalLine(props.verticalLine)
.showVerticalLine(props.showVerticalLine)
.container("#" + id)
.data({ primary: props.primaryDistribution }).render();
});
const style = !!props.width ? { width: props.width + "px" } : {};
const key = id;
return React.createElement("div", {
style: {
paddingLeft: "10px",
paddingRight: "10px",
},
}, [
sized,
React.createElement("div", { id, style, key }),
]);
}
export default CdfChart;

View File

@ -0,0 +1,302 @@
import * as d3 from 'd3';
function chart() {
// Id for event handlings.
var attrs = {
id: 'ID' + Math.floor(Math.random() * 1000000),
svgWidth: 400,
svgHeight: 400,
marginTop: 5,
marginBottom: 5,
marginRight: 5,
marginLeft: 5,
container: 'body',
minX: false,
maxX: false,
scale: 'linear',
showDistributionLines: true,
areaColors: ['#E1E5EC', '#E1E5EC'],
logBase: 10,
verticalLine: 110,
showVerticalLine: true,
data: null,
onHover: (e) => {},
};
var main = function main() {
// Drawing containers.
var container = d3.select(attrs.container);
if (container.node() === null) {
return;
}
var containerRect = container.node().getBoundingClientRect();
if (containerRect.width > 0) {
attrs.svgWidth = containerRect.width;
}
// Calculated properties.
// id for event handlings.
var calc = {};
calc.id = 'ID' + Math.floor(Math.random() * 1000000);
calc.chartLeftMargin = attrs.marginLeft;
calc.chartTopMargin = attrs.marginTop;
calc.chartWidth = attrs.svgWidth - attrs.marginRight - attrs.marginLeft;
calc.chartHeight = attrs.svgHeight - attrs.marginBottom - attrs.marginTop;
var areaColor = d3.scaleOrdinal().range(attrs.areaColors);
var dataPoints = [getDatapoints('primary')];
// Scales.
var xScale;
var xMin = d3.min(attrs.data.primary.xs);
var xMax = d3.max(attrs.data.primary.xs);
if (attrs.scale === 'linear') {
xScale = d3.scaleLinear()
.domain([
attrs.minX || xMin,
attrs.maxX || xMax
])
.range([0, calc.chartWidth]);
} else {
xScale = d3.scaleLog()
.base(attrs.logBase)
.domain([
attrs.minX,
attrs.maxX,
])
.range([0, calc.chartWidth]);
}
var yMin = d3.min(attrs.data.primary.ys);
var yMax = d3.max(attrs.data.primary.ys);
var yScale = d3.scaleLinear()
.domain([
yMin,
yMax,
])
.range([calc.chartHeight, 0]);
// Axis generator.
var xAxis = d3.axisBottom(xScale)
.ticks(3)
.tickFormat(d => {
if (Math.abs(d) < 1) {
return d3.format(".2")(d);
} else if (xMin > 1000 && xMax < 3000) {
// Condition which identifies years; 2019, 2020, 2021.
return d3.format(".0")(d);
} else {
var prefix = d3.formatPrefix(".0", d);
var output = prefix(d);
return output.replace("G", "B");
}
});
// Line generator.
var line = d3.line()
.x(function (d, i) {
return xScale(d.x);
})
.y(function (d, i) {
return yScale(d.y);
});
var area = d3.area()
.x(function (d, i) {
return xScale(d.x);
})
.y1(function (d, i) {
return yScale(d.y);
})
.y0(calc.chartHeight);
// Add svg.
var svg = container
.patternify({ tag: 'svg', selector: 'svg-chart-container' })
.attr('width', "100%")
.attr('height', attrs.svgHeight)
.attr('pointer-events', 'none');
// Add container g element.
var chart = svg
.patternify({ tag: 'g', selector: 'chart' })
.attr(
'transform',
'translate(' + calc.chartLeftMargin + ',' + calc.chartTopMargin + ')',
);
// Add axis.
chart.patternify({ tag: 'g', selector: 'axis' })
.attr('transform', 'translate(' + 0 + ',' + calc.chartHeight + ')')
.call(xAxis);
// Draw area.
chart
.patternify({
tag: 'path',
selector: 'area-path',
data: dataPoints
})
.attr('d', area)
.attr('fill', (d, i) => areaColor(i))
.attr('opacity', (d, i) => i === 0 ? 0.7 : 1);
// Draw line.
if (attrs.showDistributionLines) {
chart
.patternify({
tag: 'path',
selector: 'line-path',
data: dataPoints
})
.attr('d', line)
.attr('id', (d, i) => 'line-' + (i + 1))
.attr('opacity', (d, i) => {
return i === 0 ? 0.7 : 1
})
.attr('fill', 'none');
}
if (attrs.showVerticalLine) {
chart.patternify({ tag: 'line', selector: 'v-line' })
.attr('x1', xScale(attrs.verticalLine))
.attr('x2', xScale(attrs.verticalLine))
.attr('y1', 0)
.attr('y2', calc.chartHeight)
.attr('stroke-width', 1.5)
.attr('stroke-dasharray', '6 6')
.attr('stroke', 'steelblue');
}
var hoverLine = chart.patternify({ tag: 'line', selector: 'hover-line' })
.attr('x1', 0)
.attr('x2', 0)
.attr('y1', 0)
.attr('y2', calc.chartHeight)
.attr('opacity', 0)
.attr('stroke-width', 1.5)
.attr('stroke-dasharray', '6 6')
.attr('stroke', '#22313F');
// Add drawing rectangle.
chart.patternify({ tag: 'rect', selector: 'mouse-rect' })
.attr('width', calc.chartWidth)
.attr('height', calc.chartHeight)
.attr('fill', 'transparent')
.attr('pointer-events', 'all')
.on('mouseover', mouseover)
.on('mousemove', mouseover)
.on('mouseout', mouseout);
function mouseover() {
var mouse = d3.mouse(this);
hoverLine.attr('opacity', 1)
.attr('x1', mouse[0])
.attr('x2', mouse[0]);
var range = [
xScale(dataPoints[dataPoints.length - 1][0].x),
xScale(
dataPoints
[dataPoints.length - 1]
[dataPoints[dataPoints.length - 1].length - 1].x,
),
];
var xValue = xScale.invert(mouse[0]).toFixed(2);
if (mouse[0] > range[0] && mouse[0] < range[1]) {
attrs.onHover(xValue);
} else {
attrs.onHover(0.0);
}
}
function mouseout() {
hoverLine.attr('opacity', 0)
}
/**
* @param key
* @returns {[]}
*/
function getDatapoints(key) {
var dt = [];
var data = attrs.data[key];
var len = data.xs.length;
for (let i = 0; i < len; i++) {
dt.push({
x: data.xs[i],
y: data.ys[i]
})
}
return dt;
}
};
d3.selection.prototype.patternify = function patternify(params) {
var container = this;
var selector = params.selector;
var elementTag = params.tag;
var data = params.data || [selector];
// Pattern in action.
var selection = container.selectAll('.' + selector).data(data, (d, i) => {
if (typeof d === 'object') {
if (d.id) {
return d.id;
}
}
return i;
});
selection.exit().remove();
selection = selection.enter().append(elementTag).merge(selection);
selection.attr('class', selector);
return selection;
};
// @todo: Do not do like that.
// Dynamic keys functions.
// Attach variables to main function.
Object.keys(attrs).forEach((key) => {
main[key] = function (_) {
if (!arguments.length) {
return attrs[key];
}
attrs[key] = _;
return main;
};
});
//Set attrs as property.
main.attrs = attrs;
//Exposed update functions.
main.data = function data(value) {
if (!arguments.length) return attrs.data;
attrs.data = value;
return main;
};
// Run visual.
main.render = function render() {
main();
return main;
};
return main;
}
export default chart;

View File

@ -8,8 +8,7 @@ type yPdfPoint = {
let getY = (t: t, x: float): yPdfPoint => {
continuous: Shape.Continuous.findY(x, t.continuous),
discrete: Shape.Discrete.findY(x, t.discrete),
} /* }*/;
} /* discrete: Shape.Discrete.findY(x, t.discrete)*/;
// let getIntegralY = (t: t, x: float): float => {
// continuous: Shape.Continuous.findY(x, t.continuous),
// discrete: Shape.Discrete.findY(x, t.discrete),

View File

@ -3,7 +3,7 @@ open DistributionTypes;
let _lastElement = (a: array('a)) =>
switch (Belt.Array.size(a)) {
| 0 => None
| n => Belt.Array.get(a, n)
| n => Belt.Array.get(a, n - 1)
};
module XYShape = {
@ -28,7 +28,8 @@ module XYShape = {
Belt.Array.zip(p.xs, p.ys)
->Belt.Array.reduce([||], (items, (x, y)) =>
switch (_lastElement(items)) {
| Some((_, yLast)) => [|(x, fn(y, yLast))|]
| Some((_, yLast)) =>
Belt.Array.concat(items, [|(x, fn(y, yLast))|])
| None => [|(x, y)|]
}
)

View File

@ -1,5 +1,6 @@
const {
Cdf,
Pdf,
ContinuousDistribution,
ContinuousDistributionCombination,
scoringFunctions,

View File

@ -38,7 +38,7 @@ module JS = {
module Distribution = {
let toPdf = dist => dist |> JS.doAsDist(JS.cdfToPdf);
let toCdf = dist => dist |> JS.doAsDist(JS.cdfToPdf);
let toCdf = dist => dist |> JS.doAsDist(JS.pdfToCdf);
let findX = (y, dist) => dist |> JS.distToJs |> JS.findX(y);
let findY = (x, dist) => dist |> JS.distToJs |> JS.findY(x);
let integral = dist => dist |> JS.distToJs |> JS.integral;

View File

@ -2967,6 +2967,43 @@ d3@5.9.2:
d3-voronoi "1"
d3-zoom "1"
d3@^5.15.0:
version "5.15.0"
resolved "https://registry.yarnpkg.com/d3/-/d3-5.15.0.tgz#ffd44958e6a3cb8a59a84429c45429b8bca5677a"
integrity sha512-C+E80SL2nLLtmykZ6klwYj5rPqB5nlfN5LdWEAVdWPppqTD8taoJi2PxLZjPeYT8FFRR2yucXq+kBlOnnvZeLg==
dependencies:
d3-array "1"
d3-axis "1"
d3-brush "1"
d3-chord "1"
d3-collection "1"
d3-color "1"
d3-contour "1"
d3-dispatch "1"
d3-drag "1"
d3-dsv "1"
d3-ease "1"
d3-fetch "1"
d3-force "1"
d3-format "1"
d3-geo "1"
d3-hierarchy "1"
d3-interpolate "1"
d3-path "1"
d3-polygon "1"
d3-quadtree "1"
d3-random "1"
d3-scale "2"
d3-scale-chromatic "1"
d3-selection "1"
d3-shape "1"
d3-time "1"
d3-time-format "2"
d3-timer "1"
d3-transition "1"
d3-voronoi "1"
d3-zoom "1"
dashdash@^1.12.0:
version "1.14.1"
resolved "https://registry.yarnpkg.com/dashdash/-/dashdash-1.14.1.tgz#853cfa0f7cbe2fed5de20326b8dd581035f6e2f0"