UNPKG

@ohdsi/atlascharts

Version:

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

476 lines (417 loc) 16.2 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. Authors: Christopher Knoll */ define(["d3", "./chart"], function(d3, Chart) { "use strict"; class Trellisline extends Chart { render(dataByTrellis, target, w, h, chartOptions) { super.render(dataByTrellis, target, w, h, chartOptions); // options const defaults = { trellisSet: d3.keys(dataByTrellis), yTicks: 4, xFormat: d3.format('d'), yFormat: d3.format('d'), interpolate: d3.curveLinear, }; const options = this.getOptions(defaults, chartOptions); // container const svg = this.createSvg(target, w, h); function mouseover() { gTrellis.selectAll('.g-end').style('display', 'none'); gTrellis.selectAll('.g-value').style('display', null); mousemove.call(this); } function mousemove() { const date = seriesScale.invert(d3.mouse(event.target)[0]); gTrellis.selectAll('.g-label-value.g-start').call(valueLabel, date); gTrellis.selectAll('.g-label-year.g-start').call(yearLabel, date); gTrellis.selectAll('.g-value').attr('transform', function (d) { const s = d.values; if (s) { const v = s[bisect(s, date, 0, s.length - 1)]; const yValue = (v.Y_PREVALENCE_1000PP === 0 || v.Y_PREVALENCE_1000PP) ? v.Y_PREVALENCE_1000PP : v.yPrevalence1000Pp; if (v && v.date) { return 'translate(' + seriesScale(v.date) + ',' + yScale(yValue) + ')'; } else { return 'translate(0,0);'; } } }); } function mouseout() { gTrellis.selectAll('.g-end').style('display', null); gTrellis.selectAll('.g-label-value.g-start').call(valueLabel, minDate); gTrellis.selectAll('.g-label-year.g-start').call(yearLabel, minDate); gTrellis.selectAll('.g-label-year.g-end').call(yearLabel, maxDate); gTrellis.selectAll('.g-value').style('display', 'none'); } function valueLabel(text, date) { const offsetScale = d3.scaleLinear().domain(seriesScale.range()); let items = []; const it = {}; text.each(function(d, idx) { const text = d3.select(this); const s = d.values; const i = bisect(s, date, 0, s.length - 1); const j = Math.round(i / (s.length - 1) * (s.length - 12)); const v = s[i]; if (v && v.date) { const x = seriesScale(v.date); var yValue = (v.Y_PREVALENCE_1000PP === 0 || v.Y_PREVALENCE_1000PP) ? v.Y_PREVALENCE_1000PP : v.yPrevalence1000Pp; const xPos = offsetScale.range([0, trellisScale.bandwidth()])(x); const yPos = yScale(d3.max(s.slice(j, j + 12), d => yValue)); const trellisName = v.TRELLIS_NAME || v.trellisName; if (trellisName) { !it[trellisName] && (it[trellisName] = []); const textAnchor = v.date.getTime() === maxDate.getTime() ? 'end' : v.date.getTime() === minDate.getTime() ? 'start' : 'start'; it[trellisName].push({ y: yPos, x: xPos, textAnchor, value: yValue, text, color: options.colors(d.key) }) } } }); Object.keys(it).forEach(k => { const items = it[k]; items.sort((a,b) => a.y - b.y); items.forEach((item, idx) => { if (idx > 0) { const last = items[idx-1].y; items[idx].y += Math.max(0, (last + 15) - items[idx].y); } }); const itemsLte20 = items.filter(item => item.value <= 20); items.forEach(item => { const { text, x, y, color, textAnchor, value } = item; text.text(options.yFormat(value)) .style('display', 'block') // .style('fill', color) .style('text-anchor', textAnchor) .attr('transform', `translate( ${textAnchor === 'start' ? x + 4 : x - 4}, ${value <= 20 && itemsLte20.length !== 1 ? y - 20 : y} )` ); }); }); } function yearLabel(text, date) { const offsetScale = d3.scaleLinear().domain(seriesScale.range()); // derive the x vale by using the first trellis/series set of values. // All series are assumed to contain the same domain of X values. const s = (dataByTrellis[0] && dataByTrellis[0].values[0] && dataByTrellis[0].values[0].values) || [], v = s[bisect(s, date, 0, s.length - 1)]; if (v && v.date) { const x = seriesScale(v.date); text.each(function (d) { d3.select(this) .text(v.date.getFullYear()) .attr('transform', `translate( ${offsetScale.range([0, trellisScale.bandwidth() - this.getComputedTextLength()])(x)}, ${height + 6} )` ) .style('display', null); }); } } function renderLegend(g) { let offset = 0; options.colors.domain().forEach((d) => { const legendItem = g.append('g').attr('class', 'trellisLegend'); const legendText = legendItem.append('text') .text(d); const textBBox = legendItem.node().getBBox(); legendText .attr('x', 12) .attr('y', textBBox.height); legendItem.append('line') .attr('x1', 0) .attr('y1', 10) .attr('x2', 10) .attr('y2', 10) .style('stroke', () => options.colors(d)); legendItem.attr('transform', `translate(${offset}, 0)`); offset += legendItem.node().getBBox().width + 5; }); } const bisect = d3.bisector(d => d.date).left; const minDate = d3.min(dataByTrellis, trellis => d3.min(trellis.values, series => d3.min(series.values, d => d.date ) ) ); const maxDate = d3.max(dataByTrellis, trellis => d3.max(trellis.values, series => d3.max(series.values, d => d.date ) ) ); const minY = d3.min(dataByTrellis, trellis => d3.min(trellis.values, series => d3.min(series.values, d => (d.Y_PREVALENCE_1000PP === 0 || d.Y_PREVALENCE_1000PP) ? d.Y_PREVALENCE_1000PP : d.yPrevalence1000Pp ) ) ); const maxY = d3.max(dataByTrellis, trellis => d3.max(trellis.values, series => d3.max(series.values, d => (d.Y_PREVALENCE_1000PP === 0 || d.Y_PREVALENCE_1000PP) ? d.Y_PREVALENCE_1000PP : d.yPrevalence1000Pp ) ) ); let seriesLabel; let seriesLabelHeight = 0; if (options.seriesLabel) { seriesLabel = svg.append('g'); seriesLabel.append('text') .attr('class', 'axislabel') .style('text-anchor', 'middle') .attr('dy', '.79em') .text(options.seriesLabel); if (seriesLabelHeight = seriesLabel.node()) { seriesLabelHeight = seriesLabel.node().getBBox().height + 10; } } let trellisLabel; let trellisLabelHeight = 0; if (options.trellisLabel) { trellisLabel = svg.append('g'); trellisLabel.append('text') .attr('class', 'axislabel') .style('text-anchor', 'middle') .attr('dy', '.79em') .text(options.trellisLabel); trellisLabelHeight = trellisLabel.node().getBBox().height + 10; } // simulate a single trellis heading let trellisHeading; let trellisHeadingHeight = 0; trellisHeading = svg.append('g') .attr('class', 'g-label-trellis'); trellisHeading.append('text') .text(options.trellisSet.join('')); trellisHeadingHeight = trellisHeading.node().getBBox().height + 10; trellisHeading.remove(); let yAxisLabel; let yAxisLabelWidth = 0; if (options.yLabel) { yAxisLabel = svg.append('g'); yAxisLabel.append('text') .attr('class', 'axislabel') .style('text-anchor', 'middle') .text(options.yLabel); yAxisLabelWidth = yAxisLabel.node().getBBox().height + 4; } // calculate an intial width and height that does not take into account the tick text dimensions let width = w - options.margins.left - yAxisLabelWidth - options.margins.right; let height = h - options.margins.top - trellisLabelHeight - trellisHeadingHeight- seriesLabelHeight - options.margins.bottom*2; const trellisScale = d3.scaleBand() .domain(options.trellisSet) .range([0, width]) .paddingOuter(0.2) .paddingInner(0.25); const seriesScale = d3.scaleTime() .domain([minDate, maxDate]) .range([0, trellisScale.bandwidth()]); const yScale = d3.scaleLinear() .domain([minY, maxY]) .range([height, 0]); const yAxis = d3.axisLeft() .scale(yScale) .tickFormat(options.yFormat) .ticks(options.yTicks); // create temporary x axis const xAxis = d3.axisBottom() .scale(seriesScale); 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; // trim width if xAxisWidth bleeds over the allocated width. width -= Math.max(0, (xAxisWidth - width)); tempXAxis.remove(); // create temporary y axis const tempYAxis = svg.append('g').attr('class', 'axis'); tempYAxis.call(yAxis); // update width based on temp yaxis dimension and remove const yAxisWidth = Math.round(tempYAxis.node().getBBox().width); width -= yAxisWidth; tempYAxis.remove(); // reset axis ranges trellisScale .range([0, width]) .paddingOuter(0.2) .paddingInner(0.25); seriesScale.range([0, trellisScale.bandwidth()]); yScale.range([height, 0]); if (options.trellisLabel) { trellisLabel.attr('transform', `translate( ${(width / 2) + options.margins.left}, ${options.margins.top} )`); } if (options.seriesLabel) { seriesLabel.attr('transform', `translate( ${(width / 2) + options.margins.left}, ${trellisLabelHeight + height + xAxisHeight + seriesLabelHeight + options.margins.top*2} )`); } if (options.yLabel) { yAxisLabel.attr('transform', `translate( ${options.margins.left}, ${(height / 2) + trellisLabelHeight + trellisHeadingHeight} )`); yAxisLabel.select('text') .attr('transform', 'rotate(-90)') .attr('y', 0) .attr('x', 0) .attr('dy', '1em'); } const seriesLine = d3.line() .x(d => seriesScale(d.date)) .y(d => yScale((d.Y_PREVALENCE_1000PP === 0 || d.Y_PREVALENCE_1000PP) ? d.Y_PREVALENCE_1000PP : d.yPrevalence1000Pp) ) .curve(options.interpolate); // when using d3selection.select instead of d3.select, d3.mouse will have a bug with undefined event const vis = d3.select(svg.node()).append('g') .attr('transform', d => `translate( ${yAxisLabelWidth + yAxisWidth + options.margins.left}, ${trellisLabelHeight} )` ); const gTrellis = vis.selectAll('.g-trellis') .data(trellisScale.domain()) .enter() .append('g') .attr('class', 'g-trellis') .attr('transform', d => `translate(${trellisScale(d)}, ${trellisHeadingHeight})` ); const seriesGuideXAxis = d3.axisBottom() .scale(seriesScale) .tickFormat('') .tickSize(-height); const seriesGuideYAxis = d3.axisLeft() .scale(yScale) .tickFormat('') .tickSize(-trellisScale.bandwidth()) .ticks(8); gTrellis.append('g') .attr('class', 'x-guide') .attr('transform', `translate(0, ${height})`) .call(seriesGuideXAxis); gTrellis.append('g') .attr('class', 'y-guide') .call(seriesGuideYAxis); const gSeries = gTrellis.selectAll('.g-series') .data((trellis) => { const seriesData = dataByTrellis.filter(e => e.key === trellis); if (seriesData.length > 0) return seriesData[0].values; else return []; }) .enter() .append('g') .attr('class', 'g-series lineplot'); gSeries.append('path') .attr('class', 'line') .attr('d', d => seriesLine(d.values.sort((a, b) => d3.ascending(a.date, b.date) )) ) .style('stroke', d => options.colors(d.key)); gSeries.append('circle') .attr('class', 'g-value') .attr('transform', (d) => { const v = d.values; if (v && v[v.length - 1] && v[v.length - 1].date && v[v.length - 1] && (v[v.length - 1].Y_PREVALENCE_1000PP || v[v.length - 1].yPrevalence1000Pp)) { const yValue = (v[v.length - 1].Y_PREVALENCE_1000PP === 0 || v[v.length - 1].Y_PREVALENCE_1000PP) ? v[v.length - 1].Y_PREVALENCE_1000PP : v[v.length - 1].yPrevalence1000Pp; return `translate(${seriesScale(v[v.length - 1].date)}, ${yScale(yValue)})`; } return 'translate(0, 0)'; }) .attr('r', 2.5) .style('display', 'none'); gSeries.append('text') .attr('class', 'g-label-value g-start') .call(valueLabel, minDate); gSeries.append('text') .attr('class', 'g-label-value g-end') .call(valueLabel, maxDate); gTrellis.append('text') .attr('class', 'g-label-year g-start') .attr('dy', '.71em') .call(yearLabel, minDate); gTrellis.append('text') .attr('class', 'g-label-year g-end') .attr('dy', '.71em') .call(yearLabel, maxDate); gTrellis.append('g') .attr('class', 'x axis') .append('line') .attr('x2', trellisScale.bandwidth()) .attr('y1', yScale(minY)) .attr('y2', yScale(minY)); gTrellis.append('g') .attr('class', 'g-label-trellis') .attr('transform', d => `translate(${trellisScale.bandwidth() / 2}, 0)` ) .append('text') .attr('dy', '-1em') .style('text-anchor', 'middle') .text(d => d); gTrellis.append('rect') .attr('class', 'g-overlay') .attr('x', -4) .attr('width', trellisScale.bandwidth() + 8) .attr('height', height + 18) .on('mouseover', mouseover) .on('mousemove', mousemove) .on('mouseout', mouseout); d3.select(gTrellis.nodes()[0]).append('g') .attr('class', 'y axis') .attr('transform', 'translate(-4,0)') .call(yAxis); const legendContainer = svg.append('g') .attr('transform', `translate(${options.margins.left}, ${options.margins.top})`); legendContainer.call(renderLegend); } } return Trellisline; });