Added simple showcase, related components
This commit is contained in:
parent
17e40a9098
commit
17071602e8
|
@ -3,10 +3,16 @@
|
||||||
"reason": {
|
"reason": {
|
||||||
"react-jsx": 3
|
"react-jsx": 3
|
||||||
},
|
},
|
||||||
"sources": {
|
"sources": [{
|
||||||
"dir": "src",
|
"dir": "src",
|
||||||
"subdirs": true
|
"subdirs": true
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"dir": "showcase",
|
||||||
|
"type": "dev",
|
||||||
|
"subdirs": true
|
||||||
|
}
|
||||||
|
],
|
||||||
"bsc-flags": ["-bs-super-errors", "-bs-no-version-header"],
|
"bsc-flags": ["-bs-super-errors", "-bs-no-version-header"],
|
||||||
"package-specs": [{
|
"package-specs": [{
|
||||||
"module": "commonjs",
|
"module": "commonjs",
|
||||||
|
|
|
@ -9,6 +9,7 @@
|
||||||
"clean": "bsb -clean-world",
|
"clean": "bsb -clean-world",
|
||||||
"parcel": "parcel ./src/index.html --public-url / --no-autoinstall -- watch",
|
"parcel": "parcel ./src/index.html --public-url / --no-autoinstall -- watch",
|
||||||
"parcel-build": "parcel build ./src/index.html --no-source-maps --no-autoinstall",
|
"parcel-build": "parcel build ./src/index.html --no-source-maps --no-autoinstall",
|
||||||
|
"showcase": "PORT=12345 parcel showcase/index.html",
|
||||||
"server": "moduleserve ./ --port 8000",
|
"server": "moduleserve ./ --port 8000",
|
||||||
"predeploy": "parcel build ./src/index.html --no-source-maps --no-autoinstall",
|
"predeploy": "parcel build ./src/index.html --no-source-maps --no-autoinstall",
|
||||||
"deploy": "gh-pages -d dist",
|
"deploy": "gh-pages -d dist",
|
||||||
|
@ -31,6 +32,7 @@
|
||||||
"bs-css": "^11.0.0",
|
"bs-css": "^11.0.0",
|
||||||
"bs-moment": "0.4.4",
|
"bs-moment": "0.4.4",
|
||||||
"bs-reform": "9.7.1",
|
"bs-reform": "9.7.1",
|
||||||
|
"d3": "^5.15.0",
|
||||||
"lenses-ppx": "4.0.0",
|
"lenses-ppx": "4.0.0",
|
||||||
"less": "^3.10.3",
|
"less": "^3.10.3",
|
||||||
"lodash": "^4.17.15",
|
"lodash": "^4.17.15",
|
||||||
|
|
1
showcase/Entries.re
Normal file
1
showcase/Entries.re
Normal file
|
@ -0,0 +1 @@
|
||||||
|
let entries = EntryTypes.[Continuous.entry];
|
30
showcase/EntryTypes.re
Normal file
30
showcase/EntryTypes.re
Normal 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
200
showcase/Lib.re
Normal 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>;
|
||||||
|
};
|
||||||
|
};
|
2
showcase/ShowcaseIndex.re
Normal file
2
showcase/ShowcaseIndex.re
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
ReactDOMRe.renderToElementWithId(<div> <Lib.Index /> </div>, "main");
|
||||||
|
ReasonReactRouter.push("");
|
19
showcase/entries/Continuous.re
Normal file
19
showcase/entries/Continuous.re
Normal 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
23
showcase/index.html
Normal 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>
|
43
src/components/charts/CdfChart__Base.re
Normal file
43
src/components/charts/CdfChart__Base.re
Normal 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;
|
41
src/components/charts/CdfChart__Plain.re
Normal file
41
src/components/charts/CdfChart__Plain.re
Normal 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>;
|
||||||
|
};
|
24
src/components/charts/ChartSimple.re
Normal file
24
src/components/charts/ChartSimple.re
Normal 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>;
|
23
src/components/charts/ChartWithNumber.re
Normal file
23
src/components/charts/ChartWithNumber.re
Normal 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>;
|
||||||
|
};
|
66
src/components/charts/cdfChartReact.js
Normal file
66
src/components/charts/cdfChartReact.js
Normal 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;
|
302
src/components/charts/cdfChartd3.js
Normal file
302
src/components/charts/cdfChartd3.js
Normal 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;
|
|
@ -8,8 +8,7 @@ type yPdfPoint = {
|
||||||
let getY = (t: t, x: float): yPdfPoint => {
|
let getY = (t: t, x: float): yPdfPoint => {
|
||||||
continuous: Shape.Continuous.findY(x, t.continuous),
|
continuous: Shape.Continuous.findY(x, t.continuous),
|
||||||
discrete: Shape.Discrete.findY(x, t.discrete),
|
discrete: Shape.Discrete.findY(x, t.discrete),
|
||||||
} /* }*/;
|
} /* discrete: Shape.Discrete.findY(x, t.discrete)*/;
|
||||||
|
|
||||||
// let getIntegralY = (t: t, x: float): float => {
|
// let getIntegralY = (t: t, x: float): float => {
|
||||||
// continuous: Shape.Continuous.findY(x, t.continuous),
|
// continuous: Shape.Continuous.findY(x, t.continuous),
|
||||||
// discrete: Shape.Discrete.findY(x, t.discrete),
|
|
|
@ -3,7 +3,7 @@ open DistributionTypes;
|
||||||
let _lastElement = (a: array('a)) =>
|
let _lastElement = (a: array('a)) =>
|
||||||
switch (Belt.Array.size(a)) {
|
switch (Belt.Array.size(a)) {
|
||||||
| 0 => None
|
| 0 => None
|
||||||
| n => Belt.Array.get(a, n)
|
| n => Belt.Array.get(a, n - 1)
|
||||||
};
|
};
|
||||||
|
|
||||||
module XYShape = {
|
module XYShape = {
|
||||||
|
@ -28,7 +28,8 @@ module XYShape = {
|
||||||
Belt.Array.zip(p.xs, p.ys)
|
Belt.Array.zip(p.xs, p.ys)
|
||||||
->Belt.Array.reduce([||], (items, (x, y)) =>
|
->Belt.Array.reduce([||], (items, (x, y)) =>
|
||||||
switch (_lastElement(items)) {
|
switch (_lastElement(items)) {
|
||||||
| Some((_, yLast)) => [|(x, fn(y, yLast))|]
|
| Some((_, yLast)) =>
|
||||||
|
Belt.Array.concat(items, [|(x, fn(y, yLast))|])
|
||||||
| None => [|(x, y)|]
|
| None => [|(x, y)|]
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
const {
|
const {
|
||||||
Cdf,
|
Cdf,
|
||||||
|
Pdf,
|
||||||
ContinuousDistribution,
|
ContinuousDistribution,
|
||||||
ContinuousDistributionCombination,
|
ContinuousDistributionCombination,
|
||||||
scoringFunctions,
|
scoringFunctions,
|
||||||
|
|
|
@ -38,7 +38,7 @@ module JS = {
|
||||||
|
|
||||||
module Distribution = {
|
module Distribution = {
|
||||||
let toPdf = dist => dist |> JS.doAsDist(JS.cdfToPdf);
|
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 findX = (y, dist) => dist |> JS.distToJs |> JS.findX(y);
|
||||||
let findY = (x, dist) => dist |> JS.distToJs |> JS.findY(x);
|
let findY = (x, dist) => dist |> JS.distToJs |> JS.findY(x);
|
||||||
let integral = dist => dist |> JS.distToJs |> JS.integral;
|
let integral = dist => dist |> JS.distToJs |> JS.integral;
|
||||||
|
|
37
yarn.lock
37
yarn.lock
|
@ -2967,6 +2967,43 @@ d3@5.9.2:
|
||||||
d3-voronoi "1"
|
d3-voronoi "1"
|
||||||
d3-zoom "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:
|
dashdash@^1.12.0:
|
||||||
version "1.14.1"
|
version "1.14.1"
|
||||||
resolved "https://registry.yarnpkg.com/dashdash/-/dashdash-1.14.1.tgz#853cfa0f7cbe2fed5de20326b8dd581035f6e2f0"
|
resolved "https://registry.yarnpkg.com/dashdash/-/dashdash-1.14.1.tgz#853cfa0f7cbe2fed5de20326b8dd581035f6e2f0"
|
||||||
|
|
Loading…
Reference in New Issue
Block a user