UNPKG

terriajs

Version:

Geospatial data visualization platform.

147 lines (127 loc) 7.05 kB
'use strict'; import {min as d3ArrayMin, max as d3ArrayMax} from 'd3-array'; import {scaleTime as d3ScaleTime, scaleLinear as d3ScaleLinear} from 'd3-scale'; import defined from 'terriajs-cesium/Source/Core/defined'; import getUniqueValues from '../Core/getUniqueValues'; const unknown = 'unknown'; const Scales = { /** * Calculates the appropriate d3 scales. * * @param {Object} size Size as returned by Size. * @param {Object} [domain] Domain with x and y properties. * @param {Number[]} domain.x [x-minimum, x-maximum]. * @param {Number[]} domain.y An object with keys being the different units, and values being [y-minimum, y-maximum] for those units. Use unknown if no units given. * @param {ChartData[]} data The data for each line. This is required to extract units. Also if no domain is provided, it is calculated from the data. * @return {Object} The appropriate d3 scales. */ calculate(size, domain, data) { let theseUnits; if (data.length === 0) { return; } const allUnits = getUniqueValues(data.map(line=>(line.units || unknown))); if (!defined(domain)) { if (data[0].points.length === 0) { return; } // domains is an Array of the domains for each data element, with the units. const domains = data.map(line=>{ return {units: line.units || unknown, domain: line.getDomain()}; }); // domain.x is a simple [minx, maxx] array. // domain.y is an object with keys being the different units, and values being [miny, maxy]. domain = { x: [d3ArrayMin(domains.map(l=>l.domain.x), x=>x[0]), d3ArrayMax(domains.map(l=>l.domain.x), x=>x[1])], y: {} }; allUnits.forEach(theUnits=>{ const domainsWithTheseUnits = domains.filter(l=>(l.units === theUnits)); domain.y[theUnits] = [ d3ArrayMin(domainsWithTheseUnits, d=>d.domain.y[0]), d3ArrayMax(domainsWithTheseUnits, d=>d.domain.y[1]) ]; }); for (const theseUnits in domain.y) { if (domain.y.hasOwnProperty(theseUnits)) { const thisYDomain = domain.y[theseUnits]; // If the y-domain is positive and could reasonably be displayed to include zero, expand it to do so. // (Eg. the range is 5 to 50, do it; if it is 5 to 8, do not. Set the boundary arbitrarily at 5 to 12.5, ie. 1:2.5.) if ((thisYDomain[0] > 0) && (thisYDomain[0] / thisYDomain[1] < 0.4)) { thisYDomain[0] = 0; } // If the y-domain is negative and could reasonably be displayed to include zero, expand it to do so. if ((thisYDomain[1] < 0) && (thisYDomain[0] / thisYDomain[1] < 0.4)) { thisYDomain[1] = 0; } const dataWithTheseUnits = data.filter(l=>(l.units === theseUnits)); // Override y-domain if user has requested it. const yAxisMin = Math.min.apply(Math, dataWithTheseUnits.filter(d => defined(d.yAxisMin)).map(d => d.yAxisMin)); const yAxisMax = Math.max.apply(Math, dataWithTheseUnits.filter(d => defined(d.yAxisMax)).map(d => d.yAxisMax)); if (yAxisMin === yAxisMin && yAxisMin !== Infinity && yAxisMin !== -Infinity && thisYDomain[0] < yAxisMin) { thisYDomain[0] = yAxisMin; } if (yAxisMax === yAxisMax && yAxisMax !== Infinity && yAxisMax !== -Infinity && thisYDomain[1] > yAxisMax) { thisYDomain[1] = yAxisMax; } } } } let xScale; if (domain.x[0] instanceof Date) { xScale = d3ScaleTime(); } else { xScale = d3ScaleLinear(); } xScale.range([0, size.width]).domain(domain.x); // The x-axis takes up plot space, if it is at the bottom of the plot (ie. if the y-domain is entirely positive), // but not if it is in the middle of the plot (ie. if the y-domain includes zero). // // For now, we assume that the x-axis will be displayed aligned with the first data series' y-scale. const mainYDomain = domain.y[allUnits[0]]; const yContainsZero = (mainYDomain[0] < 0 && mainYDomain[1] > 0); if (yContainsZero) { const yPositiveOnly = d3ScaleLinear() .range([size.plotHeight, 0]) .domain([0, mainYDomain[1]]); if (yPositiveOnly(mainYDomain[0]) < size.heightMinusXAxisLabelHeight) { // There's only a very small negative range. The y-axis is near the bottom of the panel. // The x-axis can be xAxisHeight from the bottom, and the negative part of the y-axis fits in the xAxisHeight. // We want to use this scale, but we need to expand its range and domain. To do this, just use plotHeight = yPositiveOnly(mainYDomain[0]). size.plotHeight = yPositiveOnly(mainYDomain[0]); } else { // There's a big negative range, so the y-axis is not near the bottom of the panel. size.plotHeight = size.heightMinusXAxisLabelHeight; } } else if (mainYDomain[0] < 0 && mainYDomain[1] < 0) { // If range is entirely negative, the x-axis is at the top of the plot, so doesn't take up any space. size.plotHeight = size.heightMinusXAxisLabelHeight; } const yScales = {}; for (theseUnits in domain.y) { if (domain.y.hasOwnProperty(theseUnits)) { const thisYDomain = domain.y[theseUnits]; yScales[theseUnits] = d3ScaleLinear() .range([size.plotHeight, 0]) .domain(thisYDomain); } } return {x: xScale, y: yScales}; }, unknownUnits: unknown, /** * Return the automatically-generated tick values, but with the last one removed if it is too close to the end. * @param {d3.scale} scale The scale along which to calculate the tick values. * @param {Integer} numberOfTicks Number of ticks. * @return {Array} Tick values. */ truncatedTickValues(scale, numberOfTicks) { const values = scale.ticks(numberOfTicks); const lastValue = values[values.length - 1]; if ((lastValue - scale.domain()[0]) / (scale.domain()[1] - scale.domain()[0]) > (1 - 0.4 / values.length)) { values.pop(); } return values; } }; module.exports = Scales;