Merge pull request #1 from foretold-app/improvements/1080

Improvements/1080
This commit is contained in:
Ozzie Gooen 2020-02-19 12:20:36 +00:00 committed by GitHub
commit 00301ec7d3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 594 additions and 1594 deletions

1
.gitignore vendored
View File

@ -13,3 +13,4 @@ yarn-error.log
*.bs.js *.bs.js
# Local Netlify folder # Local Netlify folder
.netlify .netlify
.idea

View File

@ -1,5 +1,5 @@
{ {
"name": "probtest", "name": "estiband",
"version": "0.1.0", "version": "0.1.0",
"homepage": "https://foretold-app.github.io/estiband/", "homepage": "https://foretold-app.github.io/estiband/",
"scripts": { "scripts": {
@ -26,38 +26,35 @@
"author": "", "author": "",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@foretold/cdf": "^1.0.15", "@foretold/cdf": "1.0.15",
"@foretold/components": "^0.0.3", "@foretold/components": "0.0.3",
"@foretold/guesstimator": "^1.0.10", "@foretold/guesstimator": "1.0.10",
"@glennsl/bs-jest": "^0.4.9",
"antd": "3.17.0", "antd": "3.17.0",
"autoprefixer": "^9.7.4", "autoprefixer": "9.7.4",
"babel-jest": "^25.1.0", "babel-jest": "25.1.0",
"bs-ant-design-alt": "2.0.0-alpha.31", "bs-ant-design-alt": "2.0.0-alpha.31",
"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", "d3": "5.15.0",
"lenses-ppx": "5.1.0",
"jest": "^25.1.0", "jest": "^25.1.0",
"lenses-ppx": "4.0.0", "less": "3.10.3",
"less": "^3.10.3", "lodash": "4.17.15",
"lodash": "^4.17.15", "moment": "2.24.0",
"moment": "^2.24.0", "parcel-bundler": "1.12.4",
"parcel-bundler": "^1.12.4", "parcel-plugin-less-js-enabled": "1.0.2",
"parcel-plugin-less-js-enabled": "^1.0.2", "postcss-cli": "7.1.0",
"postcss-cli": "^7.1.0", "rationale": "0.2.0",
"rationale": "^0.2.0", "react": "16.12.0",
"react": "^16.8.1", "react-dom": "16.12.0",
"react-dom": "^16.8.1",
"reason-react": ">=0.7.0", "reason-react": ">=0.7.0",
"reschema": "^1.3.0" "reschema": "1.3.0",
}, "bs-platform": "7.0.1",
"devDependencies": { "bsb-js": "1.1.7",
"bs-platform": "5.2.1", "gh-pages": "2.2.0",
"bsb-js": "^1.1.7", "moduleserve": "0.9.1",
"gh-pages": "^2.2.0", "tailwindcss": "1.2.0"
"moduleserve": "^0.9.0",
"tailwindcss": "^1.2.0"
}, },
"alias": { "alias": {
"react": "./node_modules/react", "react": "./node_modules/react",

View File

@ -1,5 +1,3 @@
open ForetoldComponents.Base;
let data: DistributionTypes.xyShape = { let data: DistributionTypes.xyShape = {
xs: [|1., 10., 10., 200., 250., 292., 330.|], xs: [|1., 10., 10., 200., 250., 292., 330.|],
ys: [|0.0, 0.0, 0.1, 0.3, 0.5, 0.2, 0.1|], ys: [|0.0, 0.0, 0.1, 0.3, 0.5, 0.2, 0.1|],
@ -13,7 +11,7 @@ let mixedDist =
), ),
~probabilityType=Pdf, ~probabilityType=Pdf,
~domain=Complete, ~domain=Complete,
~unit=Unspecified, ~unit=UnspecifiedDistribution,
(), (),
) )
|> GenericDistribution.renderIfNeeded(~sampleCount=1000); |> GenericDistribution.renderIfNeeded(~sampleCount=1000);
@ -23,7 +21,7 @@ let timeDist =
~generationSource=GuesstimatorString("mm(3, normal(5,1), [.5,.5])"), ~generationSource=GuesstimatorString("mm(3, normal(5,1), [.5,.5])"),
~probabilityType=Pdf, ~probabilityType=Pdf,
~domain=Complete, ~domain=Complete,
~unit=Time({zero: MomentRe.momentNow(), unit: `years}), ~unit=TimeDistribution({zero: MomentRe.momentNow(), unit: `years}),
(), (),
) )
|> GenericDistribution.renderIfNeeded(~sampleCount=1000); |> GenericDistribution.renderIfNeeded(~sampleCount=1000);
@ -33,7 +31,7 @@ let domainLimitedDist =
~generationSource=GuesstimatorString("mm(3, normal(5,1), [.5,.5])"), ~generationSource=GuesstimatorString("mm(3, normal(5,1), [.5,.5])"),
~probabilityType=Pdf, ~probabilityType=Pdf,
~domain=RightLimited({xPoint: 6.0, excludingProbabilityMass: 0.3}), ~domain=RightLimited({xPoint: 6.0, excludingProbabilityMass: 0.3}),
~unit=Unspecified, ~unit=UnspecifiedDistribution,
(), (),
) )
|> GenericDistribution.renderIfNeeded(~sampleCount=1000); |> GenericDistribution.renderIfNeeded(~sampleCount=1000);

View File

@ -1,33 +0,0 @@
// type t = {
// distribution: Types.ContinuousDistribution.t,
// domainMaxX: float,
// };
// let make = (~distribution, ~domainMaxX): t => {distribution, domainMaxX};
// let fromCdf =
// (
// cdf: Types.ContinuousDistribution.t,
// domainMaxX: float,
// probabilityAtMaxX: float,
// ) => {
// let distribution: Types.ContinuousDistribution.t = {
// xs: cdf.xs,
// ys: cdf.ys |> E.A.fmap(r => r *. probabilityAtMaxX),
// };
// {distribution, domainMaxX};
// };
// let _lastElement = (a: array('a)) =>
// switch (Belt.Array.size(a)) {
// | 0 => None
// | n => Belt.Array.get(a, n)
// };
// let probabilityBeforeDomainMax = (t: t) => _lastElement(t.distribution.ys);
// let domainMaxX = (t: t) => t.domainMaxX /* CdfLibrary.Distribution.findX(yPoint, t.distribution)*/;
// let probabilityDistribution = (t: t) =>
// t.distribution |> CdfLibrary.Distribution.toPdf;
// let probability = (t: t, xPoint: float) =>
// CdfLibrary.Distribution.findY(xPoint, probabilityDistribution(t));
// let probabilityInverse = (t: t, yPoint: float) =>
// CdfLibrary.Distribution.findX(yPoint, probabilityDistribution(t));
// let cumulativeProbability = (t: t, xPoint: float) =>
// CdfLibrary.Distribution.findY(xPoint, t.distribution);
/* let cumulativeProbabilityInverse = (t: t, yPoint: float) =*/

View File

@ -1,50 +0,0 @@
// open EAFunds_Data;
// let handleChange = (handleChange, event) =>
// handleChange(ReactEvent.Form.target(event)##value);
// [@react.component]
// let make = () => {
// let (year, setYear) = React.useState(() => 2021.);
// <>
// <h1> {"EA Funds Forecasting Model 0.1" |> ReasonReact.string} </h1>
// <input
// type_="number"
// value={year |> Js.Float.toString}
// onChange={handleChange(r =>
// switch (Js.Float.fromString(r)) {
// | r when r >= 2020.0 && r <= 2050.0 => setYear(_ => r)
// | _ => ()
// }
// )}
// />
// <table className="table-auto">
// <thead>
// <tr>
// <th className="px-4 py-2"> {"Fund Name" |> ReasonReact.string} </th>
// <th className="px-4 py-2"> {"Donations" |> ReasonReact.string} </th>
// <th className="px-4 py-2"> {"Payouts" |> ReasonReact.string} </th>
// </tr>
// </thead>
// <tbody>
// {funds
// |> Belt.Array.map(_, r =>
// <tr>
// <th className="px-4 py-2 border ">
// {r.name |> ReasonReact.string}
// </th>
// <th className="px-4 py-2 border font-normal">
// {EAFunds_Model.go(r.group, year, DONATIONS)
// |> Model.InputTypes.to_string
// |> ReasonReact.string}
// </th>
// <th className="px-4 py-2 border font-normal">
// {EAFunds_Model.go(r.group, year, PAYOUTS)
// |> Model.InputTypes.to_string
// |> ReasonReact.string}
// </th>
// </tr>
// )
// |> ReasonReact.array}
// </tbody>
// </table>
// </>;
/* }*/

View File

@ -1,56 +0,0 @@
type senate =
| DEMOCRAT_VICTORY;
type house =
| DEMOCRAT_VICTORY;
type statement =
| Senate(senate)
| House(house);
type outcome =
| Bool(bool)
| Option(string);
type fullStatement = {
statement,
outcome,
};
type jointStatement = {
statements: array(fullStatement),
probability: option(float),
};
let foo = (joint: jointStatement) => {
[|
{
statements: [|
{statement: Senate(DEMOCRAT_VICTORY), outcome: Bool(true)},
{statement: House(DEMOCRAT_VICTORY), outcome: Bool(true)},
|],
probability: Some(0.2),
},
{
statements: [|
{statement: Senate(DEMOCRAT_VICTORY), outcome: Bool(true)},
{statement: House(DEMOCRAT_VICTORY), outcome: Bool(false)},
|],
probability: Some(0.2),
},
{
statements: [|
{statement: Senate(DEMOCRAT_VICTORY), outcome: Bool(false)},
{statement: House(DEMOCRAT_VICTORY), outcome: Bool(true)},
|],
probability: Some(0.5),
},
{
statements: [|
{statement: Senate(DEMOCRAT_VICTORY), outcome: Bool(false)},
{statement: House(DEMOCRAT_VICTORY), outcome: Bool(false)},
|],
probability: Some(0.1),
},
|];
};

View File

@ -1,60 +0,0 @@
module Math = {
let normal = (mean: float, std: float) =>
Js.Float.(
{
let nMean = toString(mean);
let nStd = toString(std);
{j|normal($(nMean), $(nStd))|j};
}
);
let divide = (str1: string, str2: string) => {j|$(str1)/$(str2)|j};
};
type param =
| SHARE_PRICE
| SHARES_OUTSTANDING
| MARKET_CAP;
type company = {
name: string,
currentPrice: option(float),
marketCap: option(float),
};
type otherSettings = {currentYear: int};
let sharesOutstanding = (price, marketCap) =>
switch (price, marketCap) {
| (Some(price), Some(marketCap)) =>
Some(GuesstimatorDist.divide(marketCap, price))
| _ => None
};
let rec run =
(
company: company,
year: int,
param: param,
otherSettings: otherSettings,
) => {
switch (param, year, company.currentPrice, company.marketCap) {
| (SHARE_PRICE, year, Some(price), _) when year > 2019 && year < 2030 =>
let diffYears = year - otherSettings.currentYear;
let diffPerYear = 0.1;
Some(
GuesstimatorDist.normal(price, float_of_int(diffYears) *. diffPerYear),
);
| (MARKET_CAP, year, _, Some(price)) when year > 2019 && year < 2030 =>
let diffYears = year - otherSettings.currentYear;
let diffPerYear = 0.1;
Some(
GuesstimatorDist.normal(price, float_of_int(diffYears) *. diffPerYear),
);
| (SHARES_OUTSTANDING, year, _, _) when year > 2019 && year < 2030 =>
let price = run(company, year, SHARE_PRICE, otherSettings);
let marketCap = run(company, year, MARKET_CAP, otherSettings);
sharesOutstanding(price, marketCap);
| _ => None
};
};

View File

@ -1,38 +0,0 @@
// open TimeTypes;
// type t = {
// timeVector,
// limitedDomainCdf: LimitedDomainCdf.t,
// };
// let make =
// (
// ~timeVector: timeVector,
// ~distribution: Types.ContinuousDistribution.t,
// ~probabilityAtMaxX: float,
// ~maxX: [ | `time(MomentRe.Moment.t) | `x(float)],
// )
// : t => {
// let domainMaxX =
// switch (maxX) {
// | `time(m) => TimePoint.fromMoment(timeVector, m)
// | `x(r) => r
// };
// let limitedDomainCdf =
// LimitedDomainCdf.fromCdf(distribution, domainMaxX, probabilityAtMaxX);
// {timeVector, limitedDomainCdf};
// };
// let probabilityBeforeDomainMax = (t: t) =>
// LimitedDomainCdf.probabilityBeforeDomainMax(t.limitedDomainCdf);
// let domainMaxX = (t: t) =>
// LimitedDomainCdf.probabilityBeforeDomainMax(t.limitedDomainCdf) /* |> (r => RelativeTimePoint.toTime(t.timeVector, XValue(r)))*/;
// let probability = (t: t, m: MomentRe.Moment.t) => {
// RelativeTimePoint.toXValue(t.timeVector, Time(m))
// |> LimitedDomainCdf.probability(t.limitedDomainCdf);
// };
// let probabilityInverse = (t: t, y: float) =>
// LimitedDomainCdf.probabilityInverse(t.limitedDomainCdf, y)
// |> (r => RelativeTimePoint.toTime(t.timeVector, XValue(r)));
// let cumulativeProbability = (t: t, m: MomentRe.Moment.t) =>
// RelativeTimePoint.toXValue(t.timeVector, Time(m))
// |> LimitedDomainCdf.cumulativeProbability(t.limitedDomainCdf);
// let cumulativeProbabilityInverse = (t: t, y: float) =>
/* LimitedDomainCdf.cumulativeProbabilityInverse(t.limitedDomainCdf, y*/

View File

@ -11,15 +11,17 @@ type primaryDistribution = {
let make = let make =
( (
~height=?, ~height=?,
~verticalLine=?,
~showVerticalLine=?,
~marginBottom=?, ~marginBottom=?,
~marginTop=?, ~marginTop=?,
~showDistributionLines=?,
~maxX=?, ~maxX=?,
~minX=?, ~minX=?,
~onHover=(f: float) => (), ~onHover=(f: float) => (),
~primaryDistribution=?, ~primaryDistribution=?,
~scale=?,
~showDistributionLines=?,
~showVerticalLine=?,
~timeScale=?,
~verticalLine=?,
~children=[||], ~children=[||],
) => ) =>
ReasonReact.wrapJsForReason( ReasonReact.wrapJsForReason(
@ -27,15 +29,17 @@ let make =
~props= ~props=
makeProps( makeProps(
~height?, ~height?,
~verticalLine?,
~marginBottom?, ~marginBottom?,
~marginTop?, ~marginTop?,
~onHover,
~showVerticalLine?,
~showDistributionLines?,
~maxX?, ~maxX?,
~minX?, ~minX?,
~onHover,
~primaryDistribution?, ~primaryDistribution?,
~scale?,
~showDistributionLines?,
~showVerticalLine?,
~timeScale?,
~verticalLine?,
(), (),
), ),
children, children,

View File

@ -18,24 +18,28 @@ module Styles = {
[@react.component] [@react.component]
let make = let make =
( (
~data,
~minX=?,
~maxX=?,
~height=200,
~color=`hex("111"), ~color=`hex("111"),
~data,
~height=200,
~maxX=?,
~minX=?,
~onHover: float => unit, ~onHover: float => unit,
~scale=?,
~timeScale=?,
) => { ) => {
<div className={Styles.graph(color)}> <div className={Styles.graph(color)}>
<CdfChart__Base <CdfChart__Base
height
?minX
?maxX ?maxX
?minX
?scale
?timeScale
height
marginBottom=50 marginBottom=50
marginTop=0 marginTop=0
onHover onHover
showVerticalLine=false
showDistributionLines=false
primaryDistribution={data |> Shape.XYShape.toJs} primaryDistribution={data |> Shape.XYShape.toJs}
showDistributionLines=false
showVerticalLine=false
/> />
</div>; </div>;
}; };

View File

@ -1,17 +1,22 @@
module Continuous = { module Continuous = {
[@react.component] [@react.component]
let make = (~data) => { let make = (~data, ~
) => {
let (x, setX) = React.useState(() => 0.); let (x, setX) = React.useState(() => 0.);
let timeScale = unit |> DistributionTypes.DistributionUnit.toJson;
let chart = let chart =
React.useMemo1( React.useMemo1(
() => () =>
<CdfChart__Plain <CdfChart__Plain
data data
timeScale
color={`hex("333")} color={`hex("333")}
onHover={r => setX(_ => r)} onHover={r => setX(_ => r)}
/>, />,
[|data|], [|data|],
); );
<div> <div>
chart chart
<table className="table-auto"> <table className="table-auto">
@ -100,6 +105,7 @@ module Mixed = {
let make = (~dist) => { let make = (~dist) => {
switch ((dist: option(DistributionTypes.genericDistribution))) { switch ((dist: option(DistributionTypes.genericDistribution))) {
| Some({ | Some({
unit,
generationSource: generationSource:
Shape( Shape(
Mixed({ Mixed({
@ -110,8 +116,9 @@ let make = (~dist) => {
), ),
}) => }) =>
<div> <div>
<Continuous data=n /> <Continuous data={n |> Shape.Continuous.toPdf} unit />
<Continuous <Continuous
unit
data={ data={
n n
|> Shape.XYShape.Range.integrateWithTriangles |> Shape.XYShape.Range.integrateWithTriangles
@ -133,6 +140,7 @@ let make = (~dist) => {
} }
/> />
<Continuous <Continuous
unit
data={ data={
n n
|> Shape.XYShape.Range.integrateWithTriangles |> Shape.XYShape.Range.integrateWithTriangles

View File

@ -0,0 +1,407 @@
const _ = require('lodash');
const d3 = require('d3');
const moment = require('moment');
export class CdfChartD3 {
constructor() {
this.attrs = {
svgWidth: 400,
svgHeight: 400,
marginTop: 5,
marginBottom: 5,
marginRight: 50,
marginLeft: 5,
container: null,
minX: false,
maxX: false,
scale: 'linear',
timeScale: null,
showDistributionLines: true,
areaColors: ['#E1E5EC', '#E1E5EC'],
logBase: 10,
verticalLine: 110,
showVerticalLine: true,
data: null,
onHover: (e) => {
},
};
this.hoverLine = null;
this.xScale = null;
this.dataPoints = null;
this.mouseover = this.mouseover.bind(this);
this.mouseout = this.mouseout.bind(this);
this.formatDates = this.formatDates.bind(this);
}
svgWidth(svgWidth) {
this.attrs.svgWidth = svgWidth;
return this;
}
svgHeight(height) {
this.attrs.svgHeight = height;
return this;
}
maxX(maxX) {
this.attrs.maxX = maxX;
return this;
}
minX(minX) {
this.attrs.minX = minX;
return this;
}
scale(scale) {
this.attrs.scale = scale;
return this;
}
timeScale(timeScale) {
this.attrs.timeScale = timeScale;
return this;
}
onHover(onHover) {
this.attrs.onHover = onHover;
return this;
}
marginBottom(marginBottom) {
this.attrs.marginBottom = marginBottom;
return this;
}
marginLeft(marginLeft) {
this.attrs.marginLeft = marginLeft;
return this;
}
marginRight(marginRight) {
this.attrs.marginRight = marginRight;
return this;
}
marginTop(marginTop) {
this.attrs.marginTop = marginTop;
return this;
}
showDistributionLines(showDistributionLines) {
this.attrs.showDistributionLines = showDistributionLines;
return this;
}
verticalLine(verticalLine) {
this.attrs.verticalLine = verticalLine;
return this;
}
showVerticalLine(showVerticalLine) {
this.attrs.showVerticalLine = showVerticalLine;
return this;
}
container(container) {
this.attrs.container = container;
return this;
}
data(data) {
this.attrs.data = data;
return this;
}
/**
* @param key
* @returns {[]}
*/
getDataPoints(key) {
const dt = [];
const data = this.attrs.data[key];
const len = data.xs.length;
for (let i = 0; i < len; i++) {
dt.push({ x: data.xs[i], y: data.ys[i] });
}
return dt;
}
render() {
const attrs = this.attrs;
const container = d3.select(attrs.container);
if (container.node() === null) {
console.error('Container for D3 is not defined.');
return;
}
// Sets the width from the DOM element.
const containerRect = container.node().getBoundingClientRect();
if (containerRect.width > 0) {
attrs.svgWidth = containerRect.width;
}
// Calculated properties.
const calc = {};
calc.chartLeftMargin = attrs.marginLeft;
calc.chartTopMargin = attrs.marginTop;
calc.chartWidth = attrs.svgWidth - attrs.marginRight - attrs.marginLeft;
calc.chartHeight = attrs.svgHeight - attrs.marginBottom - attrs.marginTop;
const areaColorRange = d3.scaleOrdinal().range(attrs.areaColors);
this.dataPoints = [this.getDataPoints('primary')];
// Scales.
const xMin = d3.min(attrs.data.primary.xs);
const xMax = d3.max(attrs.data.primary.xs);
if (attrs.scale === 'linear') {
this.xScale = d3.scaleLinear()
.domain([attrs.minX || xMin, attrs.maxX || xMax])
.range([0, calc.chartWidth]);
} else {
this.xScale = d3.scaleLog()
.base(attrs.logBase)
.domain([attrs.minX, attrs.maxX])
.range([0, calc.chartWidth]);
}
const yMin = d3.min(attrs.data.primary.ys);
const yMax = d3.max(attrs.data.primary.ys);
this.yScale = d3.scaleLinear()
.domain([yMin, yMax])
.range([calc.chartHeight, 0]);
// Axis generator.
if (!!this.attrs.timeScale) {
const zero = _.get(this.attrs.timeScale, 'zero', moment());
const unit = _.get(this.attrs.timeScale, 'unit', 'years');
const diff = Math.abs(xMax - xMin);
const left = zero.clone().add(xMin, unit);
const right = left.clone().add(diff, unit);
const xScaleTime = d3.scaleTime()
.domain([left.toDate(), right.toDate()])
.nice()
.range([0, calc.chartWidth]);
this.xAxis = d3.axisBottom()
.scale(xScaleTime)
.ticks(this.getTimeTicksByStr(unit))
.tickFormat(this.formatDates);
} else {
this.xAxis = d3.axisBottom(this.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 {
const prefix = d3.formatPrefix(".0", d);
return prefix(d).replace("G", "B");
}
});
}
// Objects.
const line = d3.line()
.x(d => this.xScale(d.x))
.y(d => this.yScale(d.y));
const area = d3.area()
.x(d => this.xScale(d.x))
.y1(d => this.yScale(d.y))
.y0(calc.chartHeight);
// Add svg.
const svg = container
.createObject({ tag: 'svg', selector: 'svg-chart-container' })
.attr('width', "100%")
.attr('height', attrs.svgHeight)
.attr('pointer-events', 'none');
// Add container g element.
this.chart = svg
.createObject({ tag: 'g', selector: 'chart' })
.attr(
'transform',
'translate(' + calc.chartLeftMargin + ',' + calc.chartTopMargin + ')',
);
// Add axis.
this.chart.createObject({ tag: 'g', selector: 'axis' })
.attr('transform', 'translate(' + 0 + ',' + calc.chartHeight + ')')
.call(this.xAxis);
// Draw area.
this.chart
.createObjectsWithData({
tag: 'path',
selector: 'area-path',
data: this.dataPoints,
})
.attr('d', area)
.attr('fill', (d, i) => areaColorRange(i))
.attr('opacity', (d, i) => i === 0 ? 0.7 : 0.5);
// Draw line.
if (attrs.showDistributionLines) {
this.chart
.createObjectsWithData({
tag: 'path',
selector: 'line-path',
data: this.dataPoints,
})
.attr('d', line)
.attr('id', (d, i) => 'line-' + (i + 1))
.attr('opacity', (d, i) => i === 0 ? 0.7 : 1)
.attr('fill', 'none');
}
if (attrs.showVerticalLine) {
this.chart
.createObject({ tag: 'line', selector: 'v-line' })
.attr('x1', this.xScale(attrs.verticalLine))
.attr('x2', this.xScale(attrs.verticalLine))
.attr('y1', 0)
.attr('y2', calc.chartHeight)
.attr('stroke-width', 1.5)
.attr('stroke-dasharray', '6 6')
.attr('stroke', 'steelblue');
}
this.hoverLine = this.chart
.createObject({ 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.
const thi$ = this;
this.chart
.createObject({ tag: 'rect', selector: 'mouse-rect' })
.attr('width', calc.chartWidth)
.attr('height', calc.chartHeight)
.attr('fill', 'transparent')
.attr('pointer-events', 'all')
.on('mouseover', function () {
thi$.mouseover(this);
})
.on('mousemove', function () {
thi$.mouseover(this);
})
.on('mouseout', this.mouseout);
return this;
}
mouseover(constructor) {
const mouse = d3.mouse(constructor);
this.hoverLine.attr('opacity', 1)
.attr('x1', mouse[0])
.attr('x2', mouse[0]);
const xValue = this.xScale.invert(mouse[0]).toFixed(2);
const range = [
this.xScale(this.dataPoints[this.dataPoints.length - 1][0].x),
this.xScale(
this.dataPoints
[this.dataPoints.length - 1]
[this.dataPoints[this.dataPoints.length - 1].length - 1].x,
),
];
if (mouse[0] > range[0] && mouse[0] < range[1]) {
this.attrs.onHover(xValue);
} else {
this.attrs.onHover(0.0);
}
}
mouseout() {
this.hoverLine.attr('opacity', 0)
}
formatDates(ts) {
return moment(ts).format("MMMM Do YYYY");
}
/**
* @param {string} unit
* @returns {*}
*/
getTimeTicksByStr(unit) {
switch (unit) {
case "months":
return d3.timeMonth.every(1);
case "quarters":
return d3.timeMonth.every(3);
case "hours":
return d3.timeHour.every(1);
case "days":
return d3.timeDay.every(1);
case "seconds":
return d3.timeSecond.every(1);
case "years":
return d3.timeYear.every(1);
case "minutes":
return d3.timeMinute.every(1);
case "weeks":
return d3.timeWeek.every(1);
case "milliseconds":
return d3.timeMillisecond.every(1);
default:
return d3.timeYear.every(1);
}
}
}
/**
* @docs: https://github.com/d3/d3-selection
* @param params
* @returns {*}
*/
d3.selection.prototype.createObject = function createObject(params) {
const selector = params.selector;
const tag = params.tag;
return this.insert(tag).attr('class', selector);
};
/**
* @example:
* This call example
* createObjectsByData({
* tag: 'path',
* selector: 'line-path',
* data: this.dataPoints,
* })
* will create a new tag "<path class="line-path">1,2,3</path>".
* @docs: https://github.com/d3/d3-selection
* @param params
* @returns {*}
*/
d3.selection.prototype.createObjectsWithData = function createObjectsWithData(params) {
const selector = params.selector;
const tag = params.tag;
const data = params.data;
return this.selectAll('.' + selector)
.data(data)
.enter()
.insert(tag)
.attr('class', selector);
};

View File

@ -1,7 +1,7 @@
import React, { useEffect } from 'react'; import React, { useEffect } from 'react';
import { useSize } from 'react-use'; import { useSize } from 'react-use';
import chart from './cdfChartd3'; import { CdfChartD3 } from './cdfChartD3';
/** /**
* @param min * @param min
@ -15,14 +15,16 @@ function getRandomInt(min, max) {
} }
/** /**
* Example input: * @param props
* { * @returns {*}
* xs: [50,100,300,400,500,600], * @constructor
* ys: [0.1, 0.4, 0.6, 0.7,0.8, 0.9]}
* }
*/ */
function CdfChart(props) { function CdfChartReact(props) {
const id = "chart-" + getRandomInt(0, 100000); const containerRef = React.createRef();
const key = "cdf-chart-react-" + getRandomInt(0, 1000);
const scale = props.scale || 'linear';
const style = !!props.width ? { width: props.width + "px" } : {};
const [sized, { width }] = useSize(() => { const [sized, { width }] = useSize(() => {
return React.createElement("div", { return React.createElement("div", {
key: "resizable-div", key: "resizable-div",
@ -32,7 +34,7 @@ function CdfChart(props) {
}); });
useEffect(() => { useEffect(() => {
chart() new CdfChartD3()
.svgWidth(width) .svgWidth(width)
.svgHeight(props.height) .svgHeight(props.height)
.maxX(props.maxX) .maxX(props.maxX)
@ -45,13 +47,13 @@ function CdfChart(props) {
.showDistributionLines(props.showDistributionLines) .showDistributionLines(props.showDistributionLines)
.verticalLine(props.verticalLine) .verticalLine(props.verticalLine)
.showVerticalLine(props.showVerticalLine) .showVerticalLine(props.showVerticalLine)
.container("#" + id) .container(containerRef.current)
.data({ primary: props.primaryDistribution }).render(); .data({ primary: props.primaryDistribution })
.scale(scale)
.timeScale(props.timeScale)
.render();
}); });
const style = !!props.width ? { width: props.width + "px" } : {};
const key = id;
return React.createElement("div", { return React.createElement("div", {
style: { style: {
paddingLeft: "10px", paddingLeft: "10px",
@ -59,8 +61,12 @@ function CdfChart(props) {
}, },
}, [ }, [
sized, sized,
React.createElement("div", { id, style, key }), React.createElement("div", {
key,
style,
ref: containerRef,
}),
]); ]);
} }
export default CdfChart; export default CdfChartReact;

View File

@ -1,302 +0,0 @@
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: 50,
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

@ -33,8 +33,8 @@ type generationSource =
| Shape(pointsType); | Shape(pointsType);
type distributionUnit = type distributionUnit =
| Unspecified | UnspecifiedDistribution
| Time(TimeTypes.timeVector); | TimeDistribution(TimeTypes.timeVector);
type probabilityType = type probabilityType =
| Cdf | Cdf
@ -47,3 +47,14 @@ type genericDistribution = {
domain, domain,
unit: distributionUnit, unit: distributionUnit,
}; };
module DistributionUnit = {
let toJson = (distributionUnit: distributionUnit) =>
switch (distributionUnit) {
| TimeDistribution({zero, unit}) =>
Js.Null.fromOption(
Some({"zero": zero, "unit": unit |> TimeTypes.TimeUnit.toString}),
)
| _ => Js.Null.fromOption(None)
};
};

View File

@ -1,10 +1,11 @@
open DistributionTypes; open DistributionTypes;
let make = let make =
( (
~generationSource, ~generationSource,
~probabilityType=Pdf, ~probabilityType=Pdf,
~domain=Complete, ~domain=Complete,
~unit=Unspecified, ~unit=UnspecifiedDistribution,
(), (),
) )
: genericDistribution => { : genericDistribution => {

View File

@ -1,11 +1,13 @@
type assumption = type assumption =
| ADDS_TO_1 | ADDS_TO_1
| ADDS_TO_CORRECT_PROBABILITY; | ADDS_TO_CORRECT_PROBABILITY;
type assumptions = { type assumptions = {
continuous: assumption, continuous: assumption,
discrete: assumption, discrete: assumption,
discreteProbabilityMass: option(float), discreteProbabilityMass: option(float),
}; };
let build = (~continuous, ~discrete, ~assumptions) => let build = (~continuous, ~discrete, ~assumptions) =>
switch (assumptions) { switch (assumptions) {
| { | {
@ -21,6 +23,7 @@ let build = (~continuous, ~discrete, ~assumptions) =>
~discreteProbabilityMassFraction=r, ~discreteProbabilityMassFraction=r,
), ),
) )
| { | {
continuous: ADDS_TO_1, continuous: ADDS_TO_1,
discrete: ADDS_TO_1, discrete: ADDS_TO_1,
@ -33,18 +36,21 @@ let build = (~continuous, ~discrete, ~assumptions) =>
~discreteProbabilityMassFraction=r, ~discreteProbabilityMassFraction=r,
), ),
) )
| { | {
continuous: ADDS_TO_1, continuous: ADDS_TO_1,
discrete: ADDS_TO_1, discrete: ADDS_TO_1,
discreteProbabilityMass: None, discreteProbabilityMass: None,
} => } =>
None None
| { | {
continuous: ADDS_TO_CORRECT_PROBABILITY, continuous: ADDS_TO_CORRECT_PROBABILITY,
discrete: ADDS_TO_1, discrete: ADDS_TO_1,
discreteProbabilityMass: None, discreteProbabilityMass: None,
} => } =>
None None
| { | {
continuous: ADDS_TO_1, continuous: ADDS_TO_1,
discrete: ADDS_TO_CORRECT_PROBABILITY, discrete: ADDS_TO_CORRECT_PROBABILITY,

View File

@ -20,6 +20,21 @@ type timePoint = {
value: float, value: float,
}; };
module TimeUnit = {
let toString = (timeUnit: timeUnit) =>
switch (timeUnit) {
| `days => "days"
| `hours => "hours"
| `milliseconds => "milliseconds"
| `minutes => "minutes"
| `months => "months"
| `quarters => "quarters"
| `seconds => "seconds"
| `weeks => "weeks"
| `years => "years"
};
};
module TimePoint = { module TimePoint = {
let fromTimeVector = (timeVector, value): timePoint => {timeVector, value}; let fromTimeVector = (timeVector, value): timePoint => {timeVector, value};

View File

@ -139,10 +139,11 @@ module Model = {
~generationSource=GuesstimatorString(str), ~generationSource=GuesstimatorString(str),
~probabilityType=Cdf, ~probabilityType=Cdf,
~domain=Complete, ~domain=Complete,
~unit=Unspecified, ~unit=UnspecifiedDistribution,
(), (),
); );
Prop.Value.GenericDistribution(genericDistribution); Prop.Value.GenericDistribution(genericDistribution);
| CHANCE_OF_EXISTENCE => | CHANCE_OF_EXISTENCE =>
Prop.Value.GenericDistribution( Prop.Value.GenericDistribution(
GenericDistribution.make( GenericDistribution.make(
@ -155,7 +156,7 @@ module Model = {
), ),
~probabilityType=Cdf, ~probabilityType=Cdf,
~domain=RightLimited({xPoint: 100., excludingProbabilityMass: 0.3}), ~domain=RightLimited({xPoint: 100., excludingProbabilityMass: 0.3}),
~unit=Time({zero: currentDateTime, unit: `years}), ~unit=TimeDistribution({zero: currentDateTime, unit: `years}),
(), (),
), ),
) )

View File

@ -7,7 +7,7 @@ module Model = {
~generationSource=GuesstimatorString(guesstimatorString), ~generationSource=GuesstimatorString(guesstimatorString),
~probabilityType=Cdf, ~probabilityType=Cdf,
~domain=RightLimited({xPoint: 200., excludingProbabilityMass: 0.3}), ~domain=RightLimited({xPoint: 200., excludingProbabilityMass: 0.3}),
~unit=Time({zero: currentDateTime, unit: `years}), ~unit=TimeDistribution({zero: currentDateTime, unit: `years}),
(), (),
); );
Prop.Value.GenericDistribution(genericDistribution); Prop.Value.GenericDistribution(genericDistribution);

1046
yarn.lock

File diff suppressed because it is too large Load Diff