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)]), ]); };