UNPKG

@ohdsi/atlascharts

Version:

Visualizations is a collection of JavaScript modules to support D3 visualizations in web-based applications

1,623 lines (1,335 loc) 124 kB
/* Copyright 2017 Observational Health Data Sciences and Informatics Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. Author: Alexander Saltykov */ define('atlascharts/chart',["d3", "lodash", "d3-tip"], function (d3, lodash, d3tip) { "use strict"; class Chart { static get chartTypes() { return { AREA: 'AREA', BOXPLOT: 'BOXPLOT', DONUT: 'DONUT', HISTOGRAM: 'HISTOGRAM', LINE: 'LINE', TRELLISLINE: 'TRELLISLINE', }; } render(data, target, w, h, chartOptions) { if (typeof target == "string") { target = document.querySelector(target); } this.cachedData = data; if (!this.doResize) { this.doResize = lodash.debounce(() => { this.render(this.cachedData, target, target.clientWidth, target.clientHeight, chartOptions); }, 250); window.addEventListener("resize", this.doResize); } } getOptions(chartSpecificDefaults, customOptions) { const options = Object.assign({}, { margins: { top: 10, right: 10, bottom: 10, left: 10, }, xFormat: d3.format(',.0f'), yFormat: d3.format('s'), colors: d3.scaleOrdinal(d3.schemeCategory20.concat(d3.schemeCategory20c)), }, // clone objects Object.assign({}, chartSpecificDefaults), Object.assign({}, customOptions) ); return options; } createSvg(target, width, height) { this.destroyTipIfExists(); const container = d3.select(target); container.select('svg').remove(); const chart = container.append('svg') .attr('preserveAspectRatio', 'xMinYMin meet') .attr('viewBox', ` 0 0 ${width} ${height}`) .append('g') .attr('class', 'chart'); this.chart = chart; return chart; } useTip(tooltipConfigurer = () => { }, options) { this.destroyTipIfExists(); this.tip = d3tip() .attr('class', 'd3-tip'); tooltipConfigurer(this.tip, options); if (this.chart) { this.chart.call(this.tip); } return this.tip; } destroyTipIfExists() { if (this.tip) { this.tip.destroy(); } } static normalizeDataframe(dataframe) { // rjson serializes dataframes with 1 row as single element properties. // This function ensures fields are always arrays. const keys = d3.keys(dataframe); const frame = Object.assign({}, dataframe); keys.forEach((key) => { if (!(dataframe[key] instanceof Array)) { frame[key] = [dataframe[key]]; } }); return frame; } static dataframeToArray(dataframe) { // dataframes from R serialize into an obect where each column is an array of values. const keys = d3.keys(dataframe); let result; if (dataframe[keys[0]] instanceof Array) { result = dataframe[keys[0]].map((d, i) => { const item = {}; keys.forEach(p => { item[p] = dataframe[p][i]; }); return item; }); } else { result = [dataframe]; } return result; } get formatters() { return { formatSI: (p) => { p = p || 0; const prefix = d3.format(`,.${p}s`); return (d) => { if (d < 1) { return d.toFixed(p).replace(/(\.0*|(?<=(\.[0-9]*))0*)$/, ''); } return prefix(d).replace(/(\.0*|(?<=(\.[0-9]*))0*)$/, '');; } }, } } truncate(text, width) { text.each(function () { const t = d3.select(this); const originalText = t.text(); let textLength = t.node().getComputedTextLength(); let text = t.text(); while (textLength > width && text.length > 0) { text = text.slice(0, -1); t.text(`${text}...`); textLength = t.node().getComputedTextLength(); } t.append('title').text(originalText); }); } wrap(text, width, truncateAtLine) { text.each(function () { const text = d3.select(this); const fullText = text.text(); const words = text.text().split(/\s+/).reverse(); let line = []; let word; let lineNumber = 0; let lineCount = 0; const lineHeight = 1.1; // ems const y = text.attr('y'); const dy = parseFloat(text.attr('dy')); let tspan = text .text(null) .append('tspan') .attr('x', 0) .attr('y', y) .attr('dy', `${dy}em`); while (word = words.pop()) { line.push(word); tspan.text(line.join(' ')); if (tspan.node().getComputedTextLength() > width) { if (line.length > 1) { line.pop(); // remove word from line words.push(word); // put the word back on the stack const text = !!truncateAtLine && ++lineCount === truncateAtLine ? `${line.splice(0, line.length - 1).join(' ')}...` : line.join(' '); tspan.text(text); } line = []; tspan = text .append('tspan') .attr('x', 0) .attr('y', y) .attr('dy', `${++lineNumber * lineHeight + dy}em`); if (!!truncateAtLine && truncateAtLine === lineCount) { tspan.remove(); break; } } } text.append('title').text(fullText); }); } // Tooltips tooltipFactory(tooltips) { return (d) => { let tipText = ''; if (tooltips !== undefined) { for (let i = 0; i < tooltips.length; i = i + 1) { let value = tooltips[i].accessor(d); if (tooltips[i].format !== undefined) { value = tooltips[i].format(value); } tipText += `${tooltips[i].label}: ${value}</br>`; } } return tipText; }; } lineDefaultTooltip( xLabel, xFormat, xAccessor, yLabel, yFormat, yAccessor, seriesAccessor ) { return (d) => { let tipText = ''; if (seriesAccessor(d)) tipText = `Series: ${seriesAccessor(d)}</br>`; tipText += `${xLabel}: ${xFormat(xAccessor(d))}</br>`; tipText += `${yLabel}: ${yFormat(yAccessor(d))}`; return tipText; } } donutDefaultTooltip(labelAccessor, valueAccessor, percentageAccessor) { return (d) => `${labelAccessor(d)}: ${valueAccessor(d)} (${percentageAccessor(d)})` } static mapMonthYearDataToSeries(data, customOptions) { const defaults = { dateField: 'x', yValue: 'y', yPercent: 'p' }; const options = Object.assign({}, defaults, customOptions ); const series = {}; series.name = 'All Time'; series.values = []; data[options.dateField].map((datum, i) => { series.values.push({ xValue: new Date(Math.floor(data[options.dateField][i] / 100), (data[options.dateField][i] % 100) - 1, 1), yValue: data[options.yValue][i], yPercent: data[options.yPercent][i] }); }); series.values.sort((a, b) => a.xValue - b.xValue); return [series]; // return series wrapped in an array } static prepareData(rawData, chartType) { switch (chartType) { case this.chartTypes.BOXPLOT: if (!rawData.CATEGORY.length) { return null; } const data = rawData.CATEGORY.map((d, i) => ({ Category: rawData.CATEGORY[i], min: rawData.MIN_VALUE[i], max: rawData.MAX_VALUE[i], median: rawData.MEDIAN_VALUE[i], LIF: rawData.P10_VALUE[i], q1: rawData.P25_VALUE[i], q3: rawData.P75_VALUE[i], UIF: rawData.P90_VALUE[i], }), rawData); const values = Object.values(data); const flattenData = values.reduce((accumulator, currentValue) => accumulator.concat(currentValue), [] ); if (!flattenData.length) { return null; } return data; } } dispose() { this.destroyTipIfExists(); if (this.doResize) { window.removeEventListener("resize", this.doResize); } } } return Chart; }); /* Copyright 2017 Observational Health Data Sciences and Informatics Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. Authors: Christopher Knoll */ define('atlascharts/aster',["d3", "./chart"], function (d3, Chart) { "use strict"; class Aster extends Chart { render(data, target, w, h, chartOptions) { super.render(data, target, w, h, chartOptions); // options const options = this.getOptions({ maxPercent: 100.0 }, chartOptions); // container const svg = this.createSvg(target, w, h); const chart = svg.append("g") .attr('transform', `translate(${w / 2}, ${h / 2})`); // arc dimensions const radius = Math.min(w - 10, h - 10) / 2, innerRadius = 0.3 * radius; // linear scale const r = d3.scaleLinear() .domain([0, options.maxPercent]) .range([innerRadius, radius]); const arc = d3.arc() .innerRadius(innerRadius) .outerRadius(d => r(d.data.percent)); const outlineArc = d3.arc() .innerRadius(innerRadius) .outerRadius(radius); const arcRange = [0, 2 * Math.PI]; const pie = d3.pie() .sort(null) .startAngle(arcRange[0]) .endAngle(arcRange[1]) .value(function (d) { return d.weight; }); if (data.length > 1) { pie.padAngle(.01) } const path = chart.selectAll(".solidArc") .data(pie(data)) .enter().append("path") .attr("fill", d => options.colors(d.data.id)) .attr("class", "solidArc") //.attr("stroke", "gray") .attr("d", arc); const outerPath = chart.selectAll(".outlineArc") .data(pie(data)) .enter().append("path") .attr("fill", "none") .attr("stroke", d => options.colors(d.data.id)) .attr("class", "outlineArc") .attr("d", outlineArc); if (options.asterLabel) { svg.append("svg:text") .attr("class", "aster-label") .attr("dy", ".35em") .attr("text-anchor", "middle") // text-align: right .text(asterLabel()); } //Wrapper for the grid & axes var axisGrid = chart.append("g").attr("class", "axisWrapper"); const levels = Math.ceil(options.maxPercent / 25.0); for (var level = 1; level < levels; level++) { axisGrid.append("circle") .attr("class", "gridCircle") .attr("r", r(level * 25)) .style("fill", "none") .style("stroke", "#c5c5c5") .style("stroke-width", 0.6); if (level % 2 == 1) { axisGrid.append("rect") .attr("x", -8) .attr("y", -r(level * 25) - 5) .attr("width", 20) .style("height", 10) .attr("fill", "#fff"); axisGrid.append("text") .attr("class", "axisLabel") .attr("x", -6) .attr("y", -r(level * 25)) .attr("dy", ".4em") .style("font-size", "8px") .attr("fill", "#737373") .text(function (d, i) { return `${25 * level}%` }); } } /* //Text indicating at what % each level is axisGrid.selectAll(".axisLabel") .data(d3.range(1,(cfg.levels+1)).reverse()) .enter().append("text") .attr("class", "axisLabel") .attr("x", 4) .attr("y", function(d){return -d*radius/cfg.levels;}) .attr("dy", "0.4em") .style("font-size", "10px") .attr("fill", "#737373") .text(function(d,i) { return Format(maxValue * d/cfg.levels); }); */ } } return Aster; }); /* Copyright 2017 Observational Health Data Sciences and Informatics Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. Authors: Christopher Knoll */ define('atlascharts/areachart',["d3", "./chart"], function(d3, Chart) { "use strict"; class AreaChart extends Chart { render (data, target, w, h, options) { super.render(data, target, w, h, options); var defaults = { margin: { top: 20, right: 30, bottom: 20, left: 40 }, yTicks: 4, xFormat: d3.format(',.0f'), yFormat: d3.format('s'), }; options = Object.assign({}, defaults, options); var width = w - options.margin.left - options.margin.right, height = h - options.margin.top - options.margin.bottom; var x = d3.scaleLinear() .domain(d3.extent(data, function (d) { return d.x; })) .range([0, width]); var y = d3.scaleLinear() .domain([0, d3.max(data, function (d) { return d.y; })]) .range([height, 0]); var xAxis = d3.axisBottom() .scale(x) .tickFormat(options.xFormat) .ticks(10); var yAxis = d3.axisLeft() .scale(y) .tickFormat(options.yFormat) .ticks(options.yTicks); var area = d3.area() .x(function (d) { return x(d.x); }) .y0(height) .y1(function (d) { return y(d.y); }); const chart = this.createSvg(target, w, h); var vis = chart.append("g") .attr("transform", "translate(" + options.margin.left + "," + options.margin.top + ")"); vis.append("path") .data([data]) .attr("class", "area") .attr("d", area); vis.append("g") .attr("class", "x axis") .attr("transform", "translate(0," + height + ")") .call(xAxis); vis.append("g") .attr("class", "y axis") .call(yAxis); }; }; return AreaChart; }); /* Copyright 2017 Observational Health Data Sciences and Informatics Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. Authors: Christopher Knoll */ define('atlascharts/barchart',["d3", "./chart"], function(d3, Chart) { "use strict"; class BarChart extends Chart { get formatters() { return { commaseparated: d3.format(','), formatpercent: d3.format('.1%'), }; } render(data, target, w, h, chartOptions) { super.render(data, target, w, h, chartOptions); // options const defaults = { label: 'label', value: 'value', rotate: 0, textAnchor: 'middle', showLabels: false, }; const options = this.getOptions(defaults, chartOptions); // conatainer let svg = this.createSvg(target, w, h); this.useTip((tip) => { tip.attr('class', 'd3-tip') .offset([-10, 0]) .html(d => d.value); }); const label = options.label; const value = options.value; let total = 0; data.forEach((d) => { total = total + d[value]; }); let width = w - options.margins.left - options.margins.right; let height = h - options.margins.top - options.margins.bottom; // axes const x = d3.scaleBand() .range([0, width]) .padding(.1) .round(1.0 / data.length); const y = d3.scaleLinear() .range([height, 0]); const xAxis = d3.axisBottom() .scale(x) .tickSize(2, 0); const yAxis = d3.axisLeft() .scale(y) .tickFormat(options.yFormat) .ticks(5); x.domain(data.map(d => d[label])); y.domain([0, options.yMax || d3.max(data, d => d[value])]); // create temporary x axis const tempXAxis = svg.append('g').attr('class', 'axis'); tempXAxis.call(xAxis); // update width & height based on temp xaxis dimension and remove const xAxisHeight = Math.round(tempXAxis.node().getBBox().height); const xAxisWidth = Math.round(tempXAxis.node().getBBox().width); height -= xAxisHeight; width -= Math.max(0, (xAxisWidth - width)); // trim width if // xAxisWidth bleeds over the allocated width. tempXAxis.remove(); // create temporary y axis const tempYAxis = svg.append('g').attr('class', 'axis'); tempYAxis.call(yAxis); // update height based on temp xaxis dimension and remove const yAxisWidth = Math.round(tempYAxis.node().getBBox().width); width -= yAxisWidth; tempYAxis.remove(); // reset axis ranges x.range([0, width]); y.range([height, 0]); svg = svg.append('g') .attr('transform', `translate( ${options.margins.left + yAxisWidth}, ${options.margins.top} )`); svg.append('g') .attr('class', 'x axis') .attr('transform', `translate(0, ${height})`) .call(xAxis) .selectAll('.tick text') .style('text-anchor', options.textAnchor) .attr('transform', d => `rotate(${options.rotate})`); if (options.wrap) { svg.selectAll('.tick text') .call(this.wrap, x.bandwidth()); } svg.append('g') .attr('class', 'y axis') .attr('transform', 'translate(0, 0)') .call(yAxis); svg.selectAll('.bar') .data(data) .enter() .append('rect') .attr('class', 'bar') .attr('x', d => x(d[label])) .attr('width', x.bandwidth()) .attr('y', d => y(d[value])) .attr('height', d => height - y(d[value])) .attr('title', (d) => { let temp_title = `${d[label]}: ${this.formatters.commaseparated(d[value], ',')}`; if (total > 0) { temp_title += ` (${this.formatters.formatpercent(d[value] / total)})`; } else { temp_title += ` (${this.formatters.formatpercent(0)})`; } return temp_title; }) .style('fill', d => options.colors(d[label])) .on('mouseover', d => this.tip.show(d, event.target)) .on('mouseout', d => this.tip.hide(d, event.target)) .exit() .remove(); if (options.showLabels) { svg.selectAll('.barlabel') .data(data) .enter() .append('text') .attr('class', 'barlabel') .text(d => this.formatters.formatpercent(d[value] / total)) .attr('x', d => x(d[label]) + x.bandwidth() / 2) .attr('y', d => y(d[value]) - 3) .attr('text-anchor', 'middle'); } } } return BarChart; }); /* Copyright 2017 Observational Health Data Sciences and Informatics Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. Authors: Christopher Knoll, Mark Valez, Sigfried Gold, Alexander Saltykov */ define('atlascharts/boxplot',["d3", "./chart"], function(d3, Chart) { "use strict"; class Boxplot extends Chart { defaultTip(tip, options) { tip.attr('class', 'd3-tip') .offset(d => d.tipOffset || [-10,0]) .direction(d => d.tipDirection || "n") .html(d => `<table class='boxplotValues'> <tr> <td>Max:</td> <td>${options.valueFormatter(d.max)}</td> </tr> <tr> <td>P90:</td> <td>${options.valueFormatter(d.UIF)}</td> </tr> <tr> <td>P75:</td> <td>${options.valueFormatter(d.q3)}</td> </tr> <tr> <td>Median:</td> <td>${options.valueFormatter(d.median)}</td> </tr> <tr> <td>P25:</td> <td>${options.valueFormatter(d.q1)}</td> </tr> <tr> <td>P10:</td> <td>${options.valueFormatter(d.LIF)}</td> </tr> <tr> <td>Min:</td> <td>${options.valueFormatter(d.min)}</td> </tr> </table>` ); } render(data, target, w, h, chartOptions) { super.render(data, target, w, h, chartOptions); // options const defaults = {valueFormatter: this.formatters.formatSI(3)}; const options = this.getOptions(defaults, chartOptions); // container const svg = this.createSvg(target, w, h); const valueFormatter = options.valueFormatter; this.useTip(this.defaultTip, options); // apply labels (if specified) and offset margins accordingly let xAxisLabelHeight = 0; let yAxisLabelWidth = 0; if (options.xLabel) { const xAxisLabel = svg.append('g') .attr('transform', `translate(${w / 2}, ${h - options.margins.bottom})`) xAxisLabel.append('text') .attr('class', 'axislabel') .style('text-anchor', 'middle') .text(options.xLabel); const bbox = xAxisLabel.node().getBBox(); xAxisLabelHeight = bbox.height; } if (options.yLabel) { const yAxisLabel = svg.append('g') .attr( 'transform', `translate( ${options.margins.left}, ${((h - options.margins.bottom - options.margins.top) / 2) + options.margins.top} )` ); yAxisLabel.append('text') .attr('class', 'axislabel') .attr('transform', 'rotate(-90)') .attr('y', 0) .attr('x', 0) .attr('dy', '1em') .style('text-anchor', 'middle') .text(options.yLabel); const bbox = yAxisLabel.node().getBBox(); yAxisLabelWidth = bbox.width; } let width = w - options.margins.left - yAxisLabelWidth - options.margins.right; let height = h - options.margins.top - xAxisLabelHeight - options.margins.bottom; // define the intial scale (range will be updated after we determine the final dimensions) const x = d3.scaleBand() .range([0, width]) .round(1.0 / data.length) .domain(data.map(d => d.Category)); const y = d3.scaleLinear() .range([height, 0]) .domain([options.yMin || 0, options.yMax || d3.max(data, d => d.max)]); const xAxis = d3.axisBottom() .scale(x); const yAxis = d3.axisLeft() .scale(y) .tickFormat(options.yFormat) .ticks(5); // create temporary x axis const tempXAxis = svg.append('g').attr('class', 'axis'); tempXAxis.call(xAxis); // update width & height based on temp xaxis dimension and remove const xAxisHeight = Math.round(tempXAxis.node().getBBox().height); const xAxisWidth = Math.round(tempXAxis.node().getBBox().width); height -= xAxisHeight; width -= Math.max(0, (xAxisWidth - width)); // trim width if // xAxisWidth bleeds over the allocated width. tempXAxis.remove(); // create temporary y axis const tempYAxis = svg.append('g').attr('class', 'axis'); tempYAxis.call(yAxis); // update height based on temp xaxis dimension and remove const yAxisWidth = Math.round(tempYAxis.node().getBBox().width); width -= yAxisWidth; tempYAxis.remove(); // reset axis ranges x.range([0, width]); y.range([height, 0]); const boxWidth = 10; let boxOffset = (x.bandwidth() / 2) - (boxWidth / 2); let whiskerWidth = boxWidth / 2; let whiskerOffset = (x.bandwidth() / 2) - (whiskerWidth / 2); const chart = svg.append('g') .attr('transform', `translate( ${options.margins.left + yAxisLabelWidth + yAxisWidth}, ${options.margins.top} )`); // draw main box and whisker plots const boxplots = chart.selectAll('.boxplot') .data(data) .enter().append('g') .attr('class', 'boxplot') .attr('transform', d => `translate(${x(d.Category)}, 0)`); const self = this; // for each g element (containing the boxplot render surface), draw the whiskers, bars and rects boxplots.each(function (d) { const boxplot = d3.select(this); if (d.LIF != d.q1) { // draw whisker boxplot.append('line') .attr('class', 'bar') .attr('x1', whiskerOffset) .attr('y1', y(d.LIF)) .attr('x2', whiskerOffset + whiskerWidth) .attr('y2', y(d.LIF)); boxplot.append('line') .attr('class', 'whisker') .attr('x1', x.bandwidth() / 2) .attr('y1', y(d.LIF)) .attr('x2', x.bandwidth() / 2) .attr('y2', y(d.q1)); } boxplot.append('rect') .attr('class', 'box') .attr('x', boxOffset) .attr('y', y(d.q3)) .attr('width', boxWidth) .attr('height', Math.max(1, y(d.q1) - y(d.q3))) .on('mouseover', d => self.tip.show(d, event.target)) .on('mouseout', d => self.tip.hide(d, event.target)); boxplot.append('line') .attr('class', 'median') .attr('x1', boxOffset) .attr('y1', y(d.median)) .attr('x2', boxOffset + boxWidth) .attr('y2', y(d.median)); if (d.UIF != d.q3) { // draw whisker boxplot.append('line') .attr('class', 'bar') .attr('x1', whiskerOffset) .attr('y1', y(d.UIF)) .attr('x2', x.bandwidth() - whiskerOffset) .attr('y2', y(d.UIF)); boxplot.append('line') .attr('class', 'whisker') .attr('x1', x.bandwidth() / 2) .attr('y1', y(d.UIF)) .attr('x2', x.bandwidth() / 2) .attr('y2', y(d.q3)); } // to do: add max/min indicators }); // draw x and y axis chart.append('g') .attr('class', 'x axis') .attr('transform', `translate(0, ${height})`) .call(xAxis); chart.selectAll('.tick text') .call(this.wrap, x.bandwidth() || x.range()); chart.append('g') .attr('class', 'y axis') .attr('transform', `translate(0, 0)`) .call(yAxis); } } return Boxplot; }); /* Copyright 2017 Observational Health Data Sciences and Informatics Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. Authors: Christopher Knoll, Mark Valez, Sigfried Gold, Alexander Saltykov */ define('atlascharts/donut',["d3", "numeral", "./chart"], function(d3, numeral, Chart) { "use strict"; class Donut extends Chart { get formatters() { return { formatpercent: d3.format('.1%'), }; } render(data, target, w, h, chartOptions) { super.render(data,target,w,h,chartOptions); // options const options = this.getOptions(chartOptions); // container const svg = this.createSvg(target, w, h); function dragstarted() { const legendContainer = d3.select(this); legendContainer.attr('initialX', event.x); legendContainer.attr('initialY', event.y); } function dragged() { const legendContainer = d3.select(this); const diffY = event.y - parseFloat(legendContainer.attr('initialY')); if (isNaN(diffY)) { return false; } legendContainer.attr('transform', `translate( ${parseFloat(legendContainer.attr('initialPositionX'))}, ${parseFloat(legendContainer.attr('initialPositionY')) + diffY} )`); } function dragended() { const legendContainer = d3.select(this); legendContainer.transition() .duration(300) .attr('transform', `translate( ${legendContainer.attr('initialPositionX')}, ${legendContainer.attr('initialPositionY')} )`); } let total = 0; data.forEach((d) => { total += +d.value; }); const tooltipBuilder = this.donutDefaultTooltip( (d) => d.label, (d) => numeral(d.value).format('0,0'), (d) => this.formatters.formatpercent(total != 0 ? d.value / total : 0.0) ); this.useTip(tip => tip.attr('class', 'd3-tip') .direction('s') .offset([3, 0]) .html(tooltipBuilder)); if (data.length > 0) { const vis = svg .append('g') .attr('id', 'chart'); // legend const drag = d3.drag() .on('drag', dragged) .on('start', dragstarted) .on('end', dragended); const legend = svg.append('g') .attr('class', 'legend') .call(drag); legend.selectAll('rect') .data(data) .enter() .append('rect') .attr('x', 0) .attr('y', (d, i) => i * 15) .attr('width', 10) .attr('height', 10) .style('fill', (d) => options.colors(d.id)); let legendWidth = 0; const textDisplace = 12; const legendItems = legend.selectAll('g.legend-item') .data(data) .enter() .append('g') .attr('class', 'legend-item'); legendItems .append('text') .attr('x', textDisplace) .attr('y', (d, i) => (i * 15) + 9) .text(d => d.label); legendItems .append('title') .attr('x', textDisplace) .attr('y', (d, i) => (i * 15) + 9) .text(d => d.label); legendItems.each(function() { const legendItemWidth = this.getBBox().width; if (legendItemWidth > legendWidth && legendWidth + legendItemWidth < w * 0.75) { legendWidth = legendItemWidth; } }); legendWidth += textDisplace; legend .attr('transform', `translate( ${w - legendWidth - options.margins.right}, ${options.margins.top} )`) .attr('initialPositionX', w - legendWidth - options.margins.right) .attr('initialPositionY', options.margins.top); vis .attr('transform', `translate(${(w - legendWidth) / 2}, ${h / 2})`); const or = Math.min(h, w-legendWidth) / 2 - options.margins.top; const ir = Math.min(h, w-legendWidth) / 6 - options.margins.top; const arc = d3.arc() .innerRadius(ir) .outerRadius(or); const pie = d3.pie() // this will create arc data for us given a list of values .value((d) => { return d.value > 0 ? Math.max(d.value, total * .015) : 0; // we want slices to appear if they have data, so we return a minimum of // 1.5% of the overall total if the datapoint has a value > 0. }); // we must tell it out to access the value of each element in our data array const arcs = vis.selectAll('g.slice') // this selects all <g> elements with class slice (there aren't any yet) .data(pie(data)) // associate the generated pie data (an array of arcs, each having startAngle, endAngle and value properties) .enter() // this will create <g> elements for every 'extra' data element that should be associated with a selection. The result is creating a <g> for every object in the data array .append('g') // create a group to hold each slice (we will have a <path> and a <text> element associated with each slice) .attr('class', 'slice'); // allow us to style things in the slices (like text) arcs.append('path') .attr('fill', (d) => { return options.colors(d.data.id); }) // set the color for each slice to be chosen from the color function defined above .attr('stroke', '#fff') .attr('stroke-width', 5) .attr('title', d => d.label) .on('mouseover', d => this.tip.show(d.data, event.target)) .on('mouseout', d => this.tip.hide(d.data, event.target)) .attr('d', arc); // this creates the actual SVG path using the associated data (pie) with the arc drawing function } else { svg.append('text') .attr('transform', `translate(${w / 2}, ${h / 2})`) .style('text-anchor', 'middle') .text('No Data'); } } } return Donut; }); /* Copyright 2017 Observational Health Data Sciences and Informatics Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. Authors: Christopher Knoll, Alexander Saltykov */ define('atlascharts/histogram',["d3", "numeral", "./chart"], function(d3, numeral, Chart) { "use strict"; class Histogram extends Chart { static mapHistogram(histogramData) { // result is an array of arrays, each element in the array is another array containing // information about each bar of the histogram. const result = []; const offset = histogramData.OFFSET || 0; const intervalSize = histogramData.INTERVAL_SIZE; const tempData = this.normalizeDataframe(histogramData.DATA); for (let i = 0; i < histogramData.INTERVALS; i += 1) { const target = {}; target.x = offset + 1.0 * tempData.INTERVAL_INDEX[i] * intervalSize; // eslint-disable-line no-mixed-operators target.dx = intervalSize; target.y = tempData.COUNT_VALUE[i] || 0; result.push(target); } return result; } drawBoxplot(g, data, width, height) { const boxplot = g; const x = this.xScale; const whiskerHeight = height / 2; if (data.LIF !== data.q1) { // draw whisker boxplot.append('line') .attr('class', 'bar') .attr('x1', x(data.LIF)) .attr('y1', (height / 2) - (whiskerHeight / 2)) .attr('x2', x(data.LIF)) .attr('y2', (height / 2) + (whiskerHeight / 2)); boxplot.append('line') .attr('class', 'whisker') .attr('x1', x(data.LIF)) .attr('y1', height / 2) .attr('x2', x(data.q1)) .attr('y2', height / 2); } boxplot.append('rect') .attr('class', 'box') .attr('x', x(data.q1)) .attr('width', x(data.q3) - x(data.q1)) .attr('height', height); boxplot.append('line') .attr('class', 'median') .attr('x1', x(data.median)) .attr('y1', 0) .attr('x2', x(data.median)) .attr('y2', height); if (data.UIF !== data.q3) { // draw whisker boxplot.append('line') .attr('class', 'bar') .attr('x1', x(data.UIF)) .attr('y1', (height / 2) - (whiskerHeight / 2)) .attr('x2', x(data.UIF)) .attr('y2', (height / 2) + (whiskerHeight / 2)); boxplot.append('line') .attr('class', 'whisker') .attr('x1', x(data.q3)) .attr('y1', height / 2) .attr('x2', x(data.UIF)) .attr('y2', height / 2); } } render(chartData, target, w, h, chartOptions) { super.render(chartData, target, w, h, chartOptions); // options const defaults = { ticks: 10, yTicks: 4, yScale: d3.scaleLinear(), boxplotHeight: 10, getTooltipBuilder: null, }; const options = this.getOptions(defaults, chartOptions); // container const svg = this.createSvg(target, w, h); this.xScale = {}; // shared xScale for histogram and boxplot const data = chartData || []; // default to empty set if null is passed in const tooltipBuilder = typeof options.getTooltipBuilder === 'function' ? options.getTooltipBuilder(options) : d => numeral(d.y).format('0,0'); this.useTip((tip) => { tip.attr('class', 'd3-tip') .offset([-10, 0]) .html(tooltipBuilder); }); let xAxisLabelHeight = 0; let yAxisLabelWidth = 0; // apply labels (if specified) and offset margins accordingly if (options.xLabel) { const xAxisLabel = svg.append('g') .attr('transform', `translate(${w / 2}, ${h - options.margins.bottom})`); xAxisLabel.append('text') .attr('class', 'axislabel') .style('text-anchor', 'middle') .text(options.xLabel); const bbox = xAxisLabel.node().getBBox(); xAxisLabelHeight = bbox.height; } if (options.yLabel) { const yAxisLabel = svg.append('g') .attr( 'transform', `translate( ${options.margins.left}, ${((h - options.margins.bottom - options.margins.top) / 2) + options.margins.top} )`); yAxisLabel.append('text') .attr('class', 'axislabel') .attr('transform', 'rotate(-90)') .attr('y', 0) .attr('x', 0) .attr('dy', '1em') .style('text-anchor', 'middle') .text(options.yLabel); const bbox = yAxisLabel.node().getBBox(); yAxisLabelWidth = 1.5 * bbox.width; // width is calculated as // 1.5 * box height due to rotation anomolies that // cause the y axis label to appear shifted. } // calculate an intial width and height that does not take into account the tick text dimensions let width = w - options.margins.left - options.margins.right - yAxisLabelWidth; let height = h - options.margins.top - options.margins.bottom - xAxisLabelHeight; // define the intial scale (range will be updated after we determine the final dimensions) const x = this.xScale = d3.scaleLinear() .domain(options.xDomain || [ d3.min(data, d => d.x), d3.max(data, d => d.x + d.dx), ]) .range([0, width]); const xAxis = d3.axisBottom() .scale(x) .ticks(options.ticks) .tickFormat(options.xFormat); const y = options.yScale .domain([0, options.yMax || d3.max(data, d => d.y)]) .range([height, 0]); const yAxis = d3.axisLeft() .scale(y) .ticks(options.yTicks) .tickFormat(options.yFormat); // create temporary x axis const tempXAxis = svg.append('g').attr('class', 'axis'); tempXAxis.call(xAxis); // update width & height based on temp xaxis dimension and remove const xAxisHeight = Math.round(tempXAxis.node().getBBox().height); const xAxisWidth = Math.round(tempXAxis.node().getBBox().width); height -= xAxisHeight; width -= Math.max(0, (xAxisWidth - width)); // trim width if // xAxisWidth bleeds over the allocated width. tempXAxis.remove(); // create temporary y axis const tempYAxis = svg.append('g').attr('class', 'axis'); tempYAxis.call(yAxis); // update height based on temp xaxis dimension and remove const yAxisWidth = Math.round(tempYAxis.node().getBBox().width); width -= yAxisWidth; tempYAxis.remove(); if (options.boxplot) { height -= 12; // boxplot takes up 12 vertical space const boxplotG = svg.append('g') .attr('class', 'boxplot') .attr( 'transform', `translate(${(options.margins.left + yAxisLabelWidth + yAxisWidth)}, ${(options.margins.top + height + xAxisHeight)})` ); this.drawBoxplot(boxplotG, options.boxplot, width, 8); } // reset axis ranges x.range([0, width]); y.range([height, 0]); const hist = svg.append('g') .attr('transform', `translate( ${options.margins.left + yAxisLabelWidth + yAxisWidth}, ${options.margins.top})` ); const bar = hist.selectAll('.bar') .data(data) .enter().append('g') .attr('class', 'bar') .attr('transform', d => `translate(${x(d.x)}, ${y(d.y)})`) .on('mouseover', d => this.tip.show(d, event.target)) .on('mouseout', d => this.tip.hide(d, event.target)); bar.append('rect') .attr('x', 1) .attr('width', d => Math.max((x(d.x + d.dx) - x(d.x) - 1), 0.5)) .attr('height', d => height - y(d.y)); hist.append('g') .attr('class', 'x axis') .attr('transform', `translate(0, ${height})`) .call(xAxis); hist.append('g') .attr('class', 'y axis') .attr('transform', 'translate(0, 0)') .call(yAxis); } } return Histogram; }); /* Copyright 2017 Observational Health Data Sciences and Informatics Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. Authors: Christopher Knoll */ define('atlascharts/horizontal-boxplot',["d3", "./boxplot"], function(d3, Boxplot) { "use strict"; class HorizontalBoxplot extends Boxplot { render(data, target, w, h, chartOptions) { // options const defaults = { showXAxis: true, showMinMarkers: true, showMaxMarkers: true, boxHeight: 10, valueFormatter: this.formatters.formatSI(3), margins: { top: 0, right: 0, bottom: 0, left: 4, } }; const options = this.getOptions(defaults, chartOptions); let height = h < data.length * options.boxHeight ? data.length * options.boxHeight : h // container const svg = this.createSvg(target, w, height); const valueFormatter = options.valueFormatter; this.useTip(this.defaultTip, options); // assign a category if it is absent data.forEach(d => d.Category = d.Category || "Default"); let width = w - options.margins.left - options.margins.right; height = height - options.margins.top - options.margins.bottom; // the orientaiton of this plot is horizontal, where the x axis will contain the units in the distrubiton, and the y axis will be the different categories of data // define the intial scale (range will be updated after we determine the final dimensions) const x = d3.scaleLinear() .range([0, width]) .domain([options.xMin || d3.min(data, d => d.min), options.xMax || d3.max(data, d => d.max)]) .nice(); const y = d3.scaleBand() .range([0, height]) .round(1.0 / data.length) .domain(data.map(d => d.Category)) const xAxis = d3.axisBottom() .scale(x) .tickFormat(valueFormatter) const yAxis = d3.axisLeft() .scale(y); const colors = d3.scaleOrdinal().domain(data.map(d => d.Category)) .range(["#ff9315", "#0d61ff", "gold", "blue", "green", "red", "black", "orange", "brown","grey", "slateblue", "grey1", "darkgreen" ]) let xAxisHeight = 0, xAxisWidth = xAxisHeight; if (options.showXAxis) { // create temporary x axis const tempXAxis = svg.append('g').attr('class', 'axis'); tempXAxis.call(xAxis); // update width & height based on temp xaxis dimension and remove xAxisHeight = Math.round(tempXAxis.node().getBBox().height) + 2; xAxisWidth = Math.round(tempXAxis.node().getBBox().width) + 4; height -= xAxisHeight; width -= Math.max(0, (xAxisWidth - width)); // trim width if // xAxisWidth bleeds over the allocated width. tempXAxis.remove(); } let yAxisWidth = 0; if (options.showYAxis) { // create temporary y axis const tempYAxis = svg.append('g').attr('class', 'axis'); tempYAxis.call(yAxis); // update height based on temp xaxis dimension and remove yAxisWidth = Math.round(tempYAxis.node().getBBox().width); width -= yAxisWidth; tempYAxis.remove(); } const boxHeight = options.boxHeight; let boxOffset = (y.bandwidth() / 2 - boxHeight/2 ); let whiskerHeight = boxHeight / 2; let endMarkerSize = whiskerHeight / 10; if (options.showMinMarkers) { if (endMarkerSize > yAxisWidth) width -= 2 * endMarkerSize - yAxisWidth; // subtract from width any endMarkerSize's exceess over the yAxis Width. else width -= endMarkerSize; // subtract only the right side's end-marker width. } // reset axis ranges x.range([0, width]); y.range([height, 0]); const chart = svg.append('g') .attr('transform', `translate( ${options.margins.left + Math.max(yAxisWidth, endMarkerSize)}, ${options.margins.top} )`); // draw main box and whisker plots const boxplots = chart.selectAll('.boxplot') .data(data) .enter().append('g') .attr('class', 'boxplot') .attr('transform', d => `translate(0, ${y(d.Category)})`); const self = this; // set up scale for drawing box height const boxScale = d3.scaleLinear() .range([boxHeight/2, 0]) .domain([0,boxHeight]); // for each g element (containing the boxplot render surface), draw the whiskers, bars and rects boxplots.each(function (boxplotData) { const boxplot = d3.select(this); const d = boxplotData; const boxplotContainer = boxplot.append('g') .attr('transform', () => `translate(0, ${boxOffset})`) .append('g') .datum( boxplotData) if (d.LIF != d.q1) { // draw whisker boxplotContainer.append('line') .attr('class', 'bar') .attr('x1', x(d.LIF)) .attr('y1', boxScale(whiskerHeight * 1.5)) .attr('x2', x(d.LIF)) .attr('y2', boxScale(whiskerHeight * 0.5)) .style("stroke", colors(d.Category)); boxplotContainer.append('line') .attr('class', 'whisker') .attr('x1', x(d.LIF)) .attr('y1', boxScale(whiskerHeight)) .attr('x2', x(d.q1)) .attr(