diff --git a/src/components/charts/CdfChart__Base.re b/src/components/charts/CdfChart__Base.re
index ffcaf2f0..fdd2d8e4 100644
--- a/src/components/charts/CdfChart__Base.re
+++ b/src/components/charts/CdfChart__Base.re
@@ -1,11 +1,19 @@
[@bs.module "./cdfChartReact.js"]
external cdfChart: ReasonReact.reactClass = "default";
-type primaryDistribution = {
- .
- "xs": array(float),
- "ys": array(float),
-};
+type primaryDistribution =
+ option({
+ .
+ "xs": array(float),
+ "ys": array(float),
+ });
+
+type discrete =
+ option({
+ .
+ "xs": array(float),
+ "ys": array(float),
+ });
[@react.component]
let make =
@@ -17,8 +25,10 @@ let make =
~minX=?,
~onHover=(f: float) => (),
~primaryDistribution=?,
+ ~discrete=?,
~scale=?,
~showDistributionLines=?,
+ ~showDistributionYAxis=?,
~showVerticalLine=?,
~timeScale=?,
~verticalLine=?,
@@ -35,8 +45,10 @@ let make =
~minX?,
~onHover,
~primaryDistribution?,
+ ~discrete?,
~scale?,
~showDistributionLines?,
+ ~showDistributionYAxis?,
~showVerticalLine?,
~timeScale?,
~verticalLine?,
diff --git a/src/components/charts/CdfChart__Plain.re b/src/components/charts/CdfChart__Plain.re
index 16d3b37a..f283b3db 100644
--- a/src/components/charts/CdfChart__Plain.re
+++ b/src/components/charts/CdfChart__Plain.re
@@ -7,11 +7,19 @@ module Styles = {
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(".x-axis", [fontSize(`px(9))]),
+ selector(".x-axis .domain", [display(`none)]),
+ selector(".x-axis .tick line", [display(`none)]),
+ selector(".x-axis .tick text", [color(`hex("bfcad4"))]),
selector(".chart .area-path", [SVG.fill(chartColor)]),
+ selector(".lollipops-line", [SVG.stroke(`hex("bfcad4"))]),
+ selector(
+ ".lollipops-circle",
+ [SVG.stroke(`hex("bfcad4")), SVG.fill(`hex("bfcad4"))],
+ ),
+ selector(".lollipops-x-axis .domain", [display(`none)]),
+ selector(".lollipops-x-axis .tick line", [display(`none)]),
+ selector(".lollipops-x-axis .tick text", [display(`none)]),
]);
};
@@ -19,12 +27,16 @@ module Styles = {
let make =
(
~color=`hex("111"),
- ~data,
+ ~discrete=?,
~height=200,
~maxX=?,
~minX=?,
~onHover: float => unit,
+ ~primaryDistribution=?,
~scale=?,
+ ~showDistributionLines=false,
+ ~showDistributionYAxis=false,
+ ~showVerticalLine=false,
~timeScale=?,
) => {
@@ -33,13 +45,17 @@ let make =
?minX
?scale
?timeScale
+ discrete={discrete |> E.O.fmap(d => d |> Shape.Discrete.toJs)}
height
marginBottom=50
marginTop=0
onHover
- primaryDistribution={data |> Shape.XYShape.toJs}
- showDistributionLines=false
- showVerticalLine=false
+ primaryDistribution={
+ primaryDistribution |> E.O.fmap(pd => pd |> Shape.XYShape.toJs)
+ }
+ showDistributionLines
+ showDistributionYAxis
+ showVerticalLine
/>
;
};
\ No newline at end of file
diff --git a/src/components/charts/GenericDistributionChart.re b/src/components/charts/GenericDistributionChart.re
index 1545c280..fad95085 100644
--- a/src/components/charts/GenericDistributionChart.re
+++ b/src/components/charts/GenericDistributionChart.re
@@ -7,10 +7,12 @@ module Mixed = {
React.useMemo1(
() =>
setX(_ => r)}
+ showDistributionYAxis=true
/>,
[|data|],
);
@@ -68,7 +70,7 @@ module Cont = {
React.useMemo1(
() =>
{
},
};
- this.hoverLine = null;
- this.xScale = null;
- this.dataPoints = null;
- this.mouseover = this.mouseover.bind(this);
- this.mouseout = this.mouseout.bind(this);
+
+ this.calc = {
+ chartLeftMargin: null,
+ chartTopMargin: null,
+ chartWidth: null,
+ chartHeight: null,
+ };
+
+ this.chart = null;
+ this.svg = null;
+ this._container = null;
+
this.formatDates = this.formatDates.bind(this);
}
@@ -96,6 +107,11 @@ export class CdfChartD3 {
return this;
}
+ showDistributionYAxis(showDistributionYAxis) {
+ this.attrs.showDistributionYAxis = showDistributionYAxis;
+ return this;
+ }
+
verticalLine(verticalLine) {
this.attrs.verticalLine = verticalLine;
return this;
@@ -116,69 +132,77 @@ export class CdfChartD3 {
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) {
+ this._container = d3.select(this.attrs.container);
+ if (this._container.node() === null) {
console.error('Container for D3 is not defined.');
return;
}
-
// Sets the width from the DOM element.
- const containerRect = container.node().getBoundingClientRect();
+ const containerRect = this._container.node().getBoundingClientRect();
if (containerRect.width > 0) {
- attrs.svgWidth = containerRect.width;
+ this.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;
+ this.calc.chartLeftMargin = this.attrs.marginLeft;
+ this.calc.chartTopMargin = this.attrs.marginTop;
+ this.calc.chartWidth = this.attrs.svgWidth - this.attrs.marginRight - this.attrs.marginLeft;
+ this.calc.chartHeight = this.attrs.svgHeight - this.attrs.marginBottom - this.attrs.marginTop;
- const areaColorRange = d3.scaleOrdinal().range(attrs.areaColors);
- this.dataPoints = [this.getDataPoints('primary')];
+ // Add svg.
+ this.svg = this._container
+ .createObject({ tag: 'svg', selector: 'svg-chart-container' })
+ .attr('width', "100%")
+ .attr('height', this.attrs.svgHeight)
+ .attr('pointer-events', 'none');
+
+ // Add container g element.
+ this.chart = this.svg
+ .createObject({ tag: 'g', selector: 'chart' })
+ .attr(
+ 'transform',
+ 'translate(' + this.calc.chartLeftMargin + ',' + this.calc.chartTopMargin + ')',
+ );
+
+ if(this.hasDate('primary')){
+ const distributionChart = this.addDistributionChart();
+ if(this.hasDate('discrete')) {
+ this.addLollipopsChart(distributionChart);
+ }
+ }
+ return this;
+ }
+
+ addDistributionChart() {
+ const areaColorRange = d3.scaleOrdinal().range(this.attrs.areaColors);
+ const dataPoints = [this.getDataPoints('primary')];
+
+ // Boundaries.
+ const xMin = this.attrs.minX || d3.min(this.attrs.data.primary.xs);
+ const xMax = this.attrs.maxX || d3.max(this.attrs.data.primary.xs);
+ const yMin = d3.min(this.attrs.data.primary.ys);
+ const yMax = d3.max(this.attrs.data.primary.ys);
// 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]);
+ let xScale = null;
+ if (this.attrs.scale === 'linear') {
+ xScale = d3.scaleLinear()
+ .domain([xMin, xMax])
+ .range([0, this.calc.chartWidth]);
} else {
- this.xScale = d3.scaleLog()
- .base(attrs.logBase)
- .domain([attrs.minX, attrs.maxX])
- .range([0, calc.chartWidth]);
+ xScale = d3.scaleLog()
+ .base(this.attrs.logBase)
+ .domain([xMin, xMax])
+ .range([0, this.calc.chartWidth]);
}
- const yMin = d3.min(attrs.data.primary.ys);
- const yMax = d3.max(attrs.data.primary.ys);
-
- this.yScale = d3.scaleLinear()
+ const yScale = d3.scaleLinear()
.domain([yMin, yMax])
- .range([calc.chartHeight, 0]);
+ .range([this.calc.chartHeight, 0]);
// Axis generator.
+ let xAxis = null;
if (!!this.attrs.timeScale) {
const zero = _.get(this.attrs.timeScale, 'zero', moment());
const unit = _.get(this.attrs.timeScale, 'unit', 'years');
@@ -189,14 +213,14 @@ export class CdfChartD3 {
const xScaleTime = d3.scaleTime()
.domain([left.toDate(), right.toDate()])
.nice()
- .range([0, calc.chartWidth]);
+ .range([0, this.calc.chartWidth]);
- this.xAxis = d3.axisBottom()
+ xAxis = d3.axisBottom()
.scale(xScaleTime)
.ticks(this.getTimeTicksByStr(unit))
.tickFormat(this.formatDates);
} else {
- this.xAxis = d3.axisBottom(this.xScale)
+ xAxis = d3.axisBottom(xScale)
.ticks(3)
.tickFormat(d => {
if (Math.abs(d) < 1) {
@@ -211,54 +235,46 @@ export class CdfChartD3 {
});
}
+ const yAxis = d3.axisRight(yScale);
+
// Objects.
const line = d3.line()
- .x(d => this.xScale(d.x))
- .y(d => this.yScale(d.y));
+ .x(d => xScale(d.x))
+ .y(d => 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 + ')',
- );
+ .x(d => xScale(d.x))
+ .y1(d => yScale(d.y))
+ .y0(this.calc.chartHeight);
// Add axis.
- this.chart.createObject({ tag: 'g', selector: 'axis' })
- .attr('transform', 'translate(' + 0 + ',' + calc.chartHeight + ')')
- .call(this.xAxis);
+ this.chart.createObject({ tag: 'g', selector: 'x-axis' })
+ .attr('transform', 'translate(0,' + this.calc.chartHeight + ')')
+ .call(xAxis);
+
+ if (this.attrs.showDistributionYAxis) {
+ this.chart.createObject({ tag: 'g', selector: 'y-axis' })
+ .call(yAxis);
+ }
// Draw area.
this.chart
.createObjectsWithData({
tag: 'path',
selector: 'area-path',
- data: this.dataPoints,
+ data: 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) {
+ if (this.attrs.showDistributionLines) {
this.chart
.createObjectsWithData({
tag: 'path',
selector: 'line-path',
- data: this.dataPoints,
+ data: dataPoints,
})
.attr('d', line)
.attr('id', (d, i) => 'line-' + (i + 1))
@@ -266,74 +282,109 @@ export class CdfChartD3 {
.attr('fill', 'none');
}
- if (attrs.showVerticalLine) {
+ if (this.attrs.showVerticalLine) {
this.chart
.createObject({ tag: 'line', selector: 'v-line' })
- .attr('x1', this.xScale(attrs.verticalLine))
- .attr('x2', this.xScale(attrs.verticalLine))
+ .attr('x1', xScale(this.attrs.verticalLine))
+ .attr('x2', xScale(this.attrs.verticalLine))
.attr('y1', 0)
- .attr('y2', calc.chartHeight)
+ .attr('y2', this.calc.chartHeight)
.attr('stroke-width', 1.5)
.attr('stroke-dasharray', '6 6')
.attr('stroke', 'steelblue');
}
- this.hoverLine = this.chart
+ const hoverLine = this.chart
.createObject({ tag: 'line', selector: 'hover-line' })
.attr('x1', 0)
.attr('x2', 0)
.attr('y1', 0)
- .attr('y2', calc.chartHeight)
+ .attr('y2', this.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);
+ {
+ const context = this;
+ const range = [
+ xScale(dataPoints[dataPoints.length - 1][0].x),
+ xScale(
+ dataPoints
+ [dataPoints.length - 1]
+ [dataPoints[dataPoints.length - 1].length - 1].x,
+ ),
+ ];
- return this;
- }
+ function mouseover() {
+ const mouse = d3.mouse(this);
+ hoverLine.attr('opacity', 1).attr('x1', mouse[0]).attr('x2', mouse[0]);
+ const xValue = mouse[0] > range[0] && mouse[0] < range[1]
+ ? xScale.invert(mouse[0]).toFixed(2)
+ : 0;
+ context.attrs.onHover(xValue);
+ }
- mouseover(constructor) {
- const mouse = d3.mouse(constructor);
- this.hoverLine.attr('opacity', 1)
- .attr('x1', mouse[0])
- .attr('x2', mouse[0]);
+ function mouseout() {
+ hoverLine.attr('opacity', 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);
+ this.chart
+ .createObject({ tag: 'rect', selector: 'mouse-rect' })
+ .attr('width', this.calc.chartWidth)
+ .attr('height', this.calc.chartHeight)
+ .attr('fill', 'transparent')
+ .attr('pointer-events', 'all')
+ .on('mouseover', mouseover)
+ .on('mousemove', mouseover)
+ .on('mouseout', mouseout);
}
+
+ return { xScale, yScale };
}
- mouseout() {
- this.hoverLine.attr('opacity', 0)
+ addLollipopsChart(distributionChart) {
+ const data = this.getDataPoints('discrete');
+ const ys = data.map(item => item.y);
+ const yMax = d3.max(ys);
+
+ // X axis
+ this.chart.append("g")
+ .attr("class", 'lollipops-x-axis')
+ .attr("transform", "translate(0," + this.calc.chartHeight + ")")
+ .call(d3.axisBottom(distributionChart.xScale));
+
+ // Y axis
+ const yScale = d3.scaleLinear()
+ .domain([0, yMax])
+ .range([this.calc.chartHeight, 0]);
+
+ this.chart.append("g")
+ .attr("class", 'lollipops-y-axis')
+ .attr("transform", "translate(" + this.calc.chartWidth + ",0)")
+ .call(d3.axisLeft(yScale));
+
+ // Lines
+ this.chart.selectAll("lollipops-line")
+ .data(data)
+ .enter()
+ .append("line")
+ .attr("class", 'lollipops-line')
+ .attr("x1", d => distributionChart.xScale(d.x))
+ .attr("x2", d => distributionChart.xScale(d.x))
+ .attr("y1", d => yScale(d.y))
+ .attr("y2", yScale(0));
+
+ // Circles
+ this.chart.selectAll("lollipops-circle")
+ .data(data)
+ .enter()
+ .append("circle")
+ .attr("class", 'lollipops-circle')
+ .attr("cx", d => distributionChart.xScale(d.x))
+ .attr("cy", d => yScale(d.y))
+ .attr("r", "4");
}
formatDates(ts) {
@@ -368,11 +419,39 @@ export class CdfChartD3 {
return d3.timeYear.every(10);
}
}
+
+ /**
+ * @param {name} key
+ * @returns {{x: number[], y: number[]}}
+ */
+ getDataPoints(key) {
+ const dt = [];
+ const emptyShape = { xs: [], ys: [] };
+ const data = _.get(this.attrs.data, key, emptyShape);
+ const len = data.xs.length;
+
+ for (let i = 0; i < len; i++) {
+ dt.push({ x: data.xs[i], y: data.ys[i] });
+ }
+
+ return dt;
+ }
+
+ /**
+ * @param {string} key
+ * @returns {boolean}
+ */
+ hasDate(key) {
+ const data = _.get(this.attrs.data, key);
+ return !!data;
+ }
}
/**
* @docs: https://github.com/d3/d3-selection
- * @param params
+ * @param {object} params
+ * @param {string} params.selector
+ * @param {string} params.tag
* @returns {*}
*/
d3.selection.prototype.createObject = function createObject(params) {
@@ -382,16 +461,11 @@ d3.selection.prototype.createObject = function createObject(params) {
};
/**
- * @example:
- * This call example
- * createObjectsByData({
- * tag: 'path',
- * selector: 'line-path',
- * data: this.dataPoints,
- * })
- * will create a new tag "1,2,3".
* @docs: https://github.com/d3/d3-selection
- * @param params
+ * @param {object} params
+ * @param {string} params.selector
+ * @param {string} params.tag
+ * @param {*[]} params.data
* @returns {*}
*/
d3.selection.prototype.createObjectsWithData = function createObjectsWithData(params) {
diff --git a/src/components/charts/cdfChartReact.js b/src/components/charts/cdfChartReact.js
index e2c48952..0425e400 100644
--- a/src/components/charts/cdfChartReact.js
+++ b/src/components/charts/cdfChartReact.js
@@ -45,10 +45,14 @@ function CdfChartReact(props) {
.marginRight(5)
.marginTop(5)
.showDistributionLines(props.showDistributionLines)
+ .showDistributionYAxis(props.showDistributionYAxis)
.verticalLine(props.verticalLine)
.showVerticalLine(props.showVerticalLine)
.container(containerRef.current)
- .data({ primary: props.primaryDistribution })
+ .data({
+ primary: props.primaryDistribution,
+ discrete: props.discrete,
+ })
.scale(scale)
.timeScale(props.timeScale)
.render();
diff --git a/src/lib/Chart.re b/src/lib/Chart.re
index 9002108b..1935f039 100644
--- a/src/lib/Chart.re
+++ b/src/lib/Chart.re
@@ -2,11 +2,19 @@ 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(".x-axis", [fontSize(`px(9))]),
+ selector(".x-axis .domain", [display(`none)]),
+ selector(".x-axis .tick line", [display(`none)]),
+ selector(".x-axis .tick text", [color(`hex("bfcad4"))]),
selector(".chart .area-path", [SVG.fill(chartColor)]),
+ selector(".lollipops-line", [SVG.stroke(`hex("bfcad4"))]),
+ selector(
+ ".lollipops-circle",
+ [SVG.stroke(`hex("bfcad4")), SVG.fill(`hex("bfcad4"))],
+ ),
+ selector(".lollipops-x-axis .domain", [display(`none)]),
+ selector(".lollipops-x-axis .tick line", [display(`none)]),
+ selector(".lollipops-x-axis .tick text", [display(`none)]),
]);
};