Added simple showcase, related components
This commit is contained in:
parent
17e40a9098
commit
17071602e8
|
@ -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",
|
||||
|
|
|
@ -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
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 => {
|
||||
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),
|
||||
// continuous: Shape.Continuous.findY(x, t.continuous),
|
|
@ -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)|]
|
||||
}
|
||||
)
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
const {
|
||||
Cdf,
|
||||
Pdf,
|
||||
ContinuousDistribution,
|
||||
ContinuousDistributionCombination,
|
||||
scoringFunctions,
|
||||
|
|
|
@ -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;
|
||||
|
|
37
yarn.lock
37
yarn.lock
|
@ -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"
|
||||
|
|
Loading…
Reference in New Issue
Block a user