@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
JavaScript
/*
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(