diff --git a/CHANGES.md b/CHANGES.md index 345758a0b6c..2af11af4da0 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -3,6 +3,7 @@ #### next release (8.7.5) - TSify some `js` and `jsx` files and provide `.d.ts` ambient type files for a few others. This is so that running `tsc` on an external project that imports Terria code will typecheck successfully. +- Upgraded a bunch of d3 dependencies for fixing security errors. - [The next improvement] #### 8.7.4 - 2024-06-07 diff --git a/lib/Charts/BarChart.js b/lib/Charts/BarChart.js deleted file mode 100644 index fb7ffbd20dd..00000000000 --- a/lib/Charts/BarChart.js +++ /dev/null @@ -1,63 +0,0 @@ -"use strict"; - -import d3Sync from "./d3Sync"; - -import BaseChart from "./BaseChart"; - -const barWidth = 6; - -class BarChart extends BaseChart { - render(chart, chartData, renderContext) { - const { scales, state, chartTransition } = renderContext; - const chartDataSets = state.data.filter( - (data) => data.renderer instanceof BarChart - ); - const offset = - (barWidth * chartDataSets.length) / 2 - - chartDataSets.indexOf(chartData) * barWidth; - const sx = scales.x, - sy = scales.y[chartData.units], - color = chartData.color || "white"; - - // preprocess for negative data; rect does not seem to like negative values. - const points = chartData.points.map((p) => { - const point = { - x: sx(p.x) - offset, - y: sy(p.y), - dx: barWidth, - dy: sy(0) - sy(p.y) - }; - if (point.dy < 0) { - point.y += point.dy; - point.dy *= -1; - } - return point; - }); - - d3Sync( - chart, - points, - "rect", - (rect) => - rect - .attr("x", (p) => p.x) - .attr("y", (p) => p.y) - .attr("width", (p) => p.dx) - .attr("height", (p) => p.dy) - .style("fill", color) - .style("stroke", "none"), - chartTransition - ); - } - - getXpadding(chartData, allChartData, allRenderers) { - return ( - (barWidth * - allRenderers.filter((renderer) => renderer instanceof BarChart) - .length) / - 2 - ); - } -} - -module.exports = BarChart; diff --git a/lib/Charts/BaseChart.js b/lib/Charts/BaseChart.js deleted file mode 100644 index 1728a37b168..00000000000 --- a/lib/Charts/BaseChart.js +++ /dev/null @@ -1,43 +0,0 @@ -"use strict"; - -import TerriaError from "../Core/TerriaError"; - -class BaseChart { - constructor() { - this.id = null; - } - - /** - * Must implement this for new chart types. - * - * @param {Object} chart D3Selection containing group in which the chartData elements will be charted. - * @param {Object} chartData ChartData object to be charted. - * @param {ChartRenderer} renderContext Object containing various helper items which you may need for plotting. - // * @param {DOMElement} renderContext.container The DOM container in which to place the chart. - // * @param {Object} renderContext.state The state of the chart. - // * @param {Object} renderContext.size ... - // * @param {Object} renderContext.margin ... - // * @param {Object} renderContext.scales Chart x and y Scales. - // * @param {D3Scake} renderContext.scales.x X scale - // * @param {Object} renderContext.scales.y Map mapping y axis units to Scakes - // * @param {Array} renderContext.units Array of units - // * @param {D3Selection} renderContext.chart Chart Area - // * @param {D3Selection} renderContext.chartPlotContainer Content area under which all ChartData are rendered. - // * @param {String} renderContext.chartTransform Transform which positions sub-elements where they should be. If you are using chart or renderContext.chart or renderContext.chartPlotContainer, they are already transformed using this. I.e. there is no need to transform again. But if you are adding a high level object, such as under renderContext.chartSVGContainer, this could be helpful. - // * @param {D3Transition} renderContext.chartTransition Transition for animations - // * @param {D3Selection} renderContext.chartSVGContainer The top SVG element. - * - */ - render(chart, chartData, renderContext) { - throw new TerriaError("Not implemented"); - } - - /** - * Override this if you need a little extra space around your x axis for your chart/visualisation. - */ - getXpadding(chartData, allChartData, allRenderers) { - return 0; - } -} - -export default BaseChart; diff --git a/lib/Charts/ChartData.ts b/lib/Charts/ChartData.ts index 1902ab4f0cf..302c75e0e06 100644 --- a/lib/Charts/ChartData.ts +++ b/lib/Charts/ChartData.ts @@ -1,181 +1,4 @@ -import { min as d3ArrayMin, max as d3ArrayMax } from "d3-array"; -import defaultValue from "terriajs-cesium/Source/Core/defaultValue"; - export interface ChartPoint { readonly x: number | Date; readonly y: number; } - -export interface ChartDataOptions { - /** - * The array of points. Each point should have the format {x: X, y: Y}. - */ - readonly points: readonly ChartPoint[]; - - /** - * Unique ID for this set of points. - */ - readonly id?: string; - - /** - * Name of the category for this set of points, e.g. the source catalog item. - */ - readonly categoryName?: string; - - /** - * Name for this set of points. - */ - readonly name?: string; - - /** - * Units of this set of points. - */ - readonly units?: string; - - /** - * A function that returns CSS color code for this set of points. - * - * We use a function instead of an immediate value so that colors can be - * assigned lazily; only when it is required. - */ - readonly getColor: () => string | undefined; - - /** - * Minimum value for Y axis to display, overriding minimum value in data. - */ - readonly yAxisMin?: number; - - /** - * Maximum value for Y axis to display, overriding maximum value in data. - */ - readonly yAxisMax?: number; - - /** - * Chart type. If you want these points rendered with a certain way. Leave empty for auto detection. - */ - readonly type?: string; - - /** - * Click handler (called with (x, y) in data units) if some special behaviour is required for clicking. - */ - readonly onClick?: (x: number, y: number) => void; - - /** - * Request that the chart be scaled so that this series can be shown entirely. - */ - readonly showAll?: boolean; -} - -/** - * A container to pass data to a d3 chart: a single series of data points. - * For documentation on the custom tag, see lib/Models/registerCustomComponentTypes.js. - * - * @param {ChartDataOptions} [options] Further parameters. - */ -export default class ChartData { - /** - * The array of points. Each point should have the format {x: X, y: Y}. - */ - readonly points: readonly ChartPoint[]; - - /** - * A selected point from the array above. Used internally by charting functions for hover/clicking functionality. - */ - readonly point: ChartPoint | undefined; - - /** - * Unique id for this set of points. - */ - readonly id: string | undefined; - - /** - * Name of the category for this set of points., eg. the source catalog item. - */ - readonly categoryName: string | undefined; - - /** - * Name for this set of points. - */ - readonly name: string | undefined; - - /** - * Units of this set of points. - */ - readonly units: string | undefined; - - /** - * A function that returns CSS color code for this set of points. - * - * We use a function instead of an immediate value so that colors can be - * assigned lazily; only when it is required. - */ - readonly getColor: () => string | undefined; - - /** - * Minimum value for y axis to display, overriding minimum value in data. - */ - readonly yAxisMin: number | undefined; - - /** - * Maximum value for y axis to display, overriding maximum value in data. - */ - readonly yAxisMax: number | undefined; - - /** - * Chart type. If you want these points to be rendered with a certain way. Leave empty for auto detection. - */ - readonly type: string | undefined; - - /** - * Click handler (called with (x, y) in data units) if some special behaviour is required on clicking. - */ - readonly onClick: ((x: number, y: number) => void) | undefined; - - /** - * Request that the chart be scaled so that this series can be shown entirely. - * @default true - */ - readonly showAll: boolean; - - readonly yAxisWidth: number; - - constructor(options: ChartDataOptions) { - this.points = options.points; - this.point = undefined; - this.id = options.id; - this.categoryName = options.categoryName; - this.name = options.name; - this.units = options.units; - this.getColor = options.getColor; - this.yAxisMin = options.yAxisMin; - this.yAxisMax = options.yAxisMax; - this.type = options.type; - this.onClick = options.onClick; - this.showAll = defaultValue(options.showAll, true); - this.yAxisWidth = 40; - } - - /** - * Calculates the min and max x and y of the points. - * If there are no points, returns undefined. - * @return {Object} An object {x: [xmin, xmax], y: [ymin, ymax]}. - */ - getDomain(): - | { x: [number | Date, number | Date]; y: [number, number] } - | undefined { - const points = this.points; - if (points.length === 0) { - return undefined; - } - return { - x: [ - d3ArrayMin(points, (point) => point.x)!, - d3ArrayMax(points, (point) => point.x)! - ], - y: [ - d3ArrayMin(points, (point) => point.y)!, - d3ArrayMax(points, (point) => point.y)! - ] - }; - } -} diff --git a/lib/Charts/LineChart.js b/lib/Charts/LineChart.js deleted file mode 100644 index e2137f8599f..00000000000 --- a/lib/Charts/LineChart.js +++ /dev/null @@ -1,59 +0,0 @@ -"use strict"; - -import { line as d3Line } from "d3-shape"; -import BaseChart from "./BaseChart"; -import d3Sync from "./d3Sync"; -import { select as d3Select } from "d3-selection"; -import defined from "terriajs-cesium/Source/Core/defined"; - -class LineChart extends BaseChart { - constructor() { - super(); - this.path = null; - this.chartData = null; - this.sy = null; - } - - render(chart, chartData, renderContext) { - const { chartTransform, scales } = renderContext; - const sx = scales.x, - sy = scales.y[chartData.units], - color = chartData.color || "white"; - // If there are undefined or null y-values, just ignore them. This works well for initial and final undefined values, - // and simply interpolates over intermediate ones. This may not be what we want. - this.path = d3Line() // NOTE: it was originally 'basic', which is not an interpolation - .x((d) => sx(d.x)) - .y((d) => sy(d.y))(chartData.points.filter((point) => defined(point.y))); - // consider calling .defined() to allow discontinuous graphs - - this.chartData = chartData; - this.sy = sy; - - d3Sync( - chart, - [chartData], - "path", - (line, enter) => { - line.attr("d", this.path).style("fill", "none").style("stroke", color); - - // Can't do this on a transition call... - if (typeof line.classed === "function") { - line.classed("line", true); - } - }, - chartTransform - ); - } - - zoomOnAxisX(newScaleX) { - this.path = d3Line() - .x((d) => newScaleX(d.x)) - .y((d) => this.sy(d.y))( - this.chartData.points.filter((point) => defined(point.y)) - ); - - d3Select(`#${this.id}`).selectAll("path").attr("d", this.path); - } -} - -module.exports = LineChart; diff --git a/lib/Charts/MomentChart.js b/lib/Charts/MomentChart.js deleted file mode 100644 index 5fd0852d68f..00000000000 --- a/lib/Charts/MomentChart.js +++ /dev/null @@ -1,53 +0,0 @@ -"use strict"; - -import BaseChart from "./BaseChart"; -import d3Sync from "./d3Sync"; -import { select as d3Select } from "d3-selection"; - -class MomentChart extends BaseChart { - constructor() { - super(); - } - - render(chart, chartData, { chartTransform, scales, size }) { - const n = chartData.points.length; - // spacing is the number of pixels per data point. When it's very low, we want to make the lines - // thinner and lighter - const spacing = - (scales.x(chartData.points[n - 1].x) - scales.x(chartData.points[0].x)) / - n; - d3Sync(chart, chartData.points, "rect", function (el, isNew) { - el.attr("class", (d, i) => `dataIndex-${i}`) - .style("fill", chartData.color || "turquoise") - .attr("fill-opacity", function (d, i) { - return i === chartData.selectedIndex ? 1 : spacing > 3 ? 0.4 : 0.2; - }) - .attr("x", (d) => scales.x(d.x)) - .attr("y", 0) - .attr("width", function (d, i) { - return i === chartData.selectedIndex ? 3 : 1; - // return spacing > 20 ? 3 : 1; - }) - .attr("height", size.plotHeight) - .attr("cursor", "pointer"); - }); - } - - highlightMoment(selectedIndex) { - d3Select(`#${this.id}`) - .selectAll("rect") - .style("fill-opacity", 0.2) - .style("width", 1) - .filter(`.dataIndex-${selectedIndex}`) - .style("fill-opacity", 1) - .style("width", 3); - } - - zoomOnAxisX(newScaleX) { - d3Select(`#${this.id}`) - .selectAll("rect") - .attr("x", (d) => newScaleX(d.x)); - } -} - -module.exports = MomentChart; diff --git a/lib/Charts/MomentChartPoints.js b/lib/Charts/MomentChartPoints.js deleted file mode 100644 index 90b7a5c6522..00000000000 --- a/lib/Charts/MomentChartPoints.js +++ /dev/null @@ -1,205 +0,0 @@ -"use strict"; - -import BaseChart from "./BaseChart"; -import d3Sync from "./d3Sync"; -import { select as d3Select } from "d3-selection"; -import { interpolateNumber as d3InterpolateNumber } from "d3-interpolate"; - -const defaultMarkerSizeSmall = 2; -const defaultMarkerSizeLarge = 5; - -const selectedMarkerSizeSmall = 5; -const selectedMarkerSizeLarge = 8; - -class MomentChartPoints extends BaseChart { - constructor() { - super(); - this.alternateYAxis = null; - this._alternateYAxisFilteredPoints = null; - this.chartData = null; - this.yMin = null; - this.yMax = null; - this.lastKnownXScale = null; - } - - render(chart, chartData, { chartTransform, scales, size }, alternateYAxis) { - if (alternateYAxis !== null) { - this.alternateYAxis = alternateYAxis.firstY; - this._alternateYAxisFilteredPoints = alternateYAxis.firstY.points.filter( - (p) => p.y !== null - ); - this.yMin = alternateYAxis.yMin; - this.yMax = alternateYAxis.yMax; - } else { - this.alternateYAxis = null; - this._alternateYAxisFilteredPoints = null; - this.yMin = 0; - this.yMax = 1; - } - - this.chartData = chartData; - const ch = this; - - this.lastKnownXScale = scales.x; - const spacing = this._calculateSpacing(); - - d3Sync( - chart, - chartData.points, - "circle", - (el, isNew) => - el - .attr("class", (d, i) => `dataIndex-${i}`) - .style("fill", chartData.color || "turquoise") - .attr("r", function (d, i) { - if (i === chartData.selectedIndex) - return spacing > defaultMarkerSizeSmall - ? selectedMarkerSizeLarge - : selectedMarkerSizeSmall; - return spacing > defaultMarkerSizeSmall - ? defaultMarkerSizeLarge - : defaultMarkerSizeSmall; - }) - .style("fill-opacity", (d, i) => - i === chartData.selectedIndex ? 1 : 0.3 - ) - .attr("cx", (d) => scales.x(d.x)) - .attr("cy", function (d) { - if (ch.alternateYAxis === null) return size.plotHeight / 2; - const xIndex = ch._findNearestX(d.x); - if ( - xIndex === null || - xIndex === ch._alternateYAxisFilteredPoints.length - 2 || - xIndex === -1 - ) - return size.plotHeight / 2; - - // Get the location that the marker should live along the x axis - const xAsPercentage = ch._getXAsPercentage( - d.x, - ch._alternateYAxisFilteredPoints[xIndex].x, - ch._alternateYAxisFilteredPoints[xIndex + 1].x - ); - - // find a y value based on the x - const interNum = d3InterpolateNumber( - ch._alternateYAxisFilteredPoints[xIndex].y, - ch._alternateYAxisFilteredPoints[xIndex + 1].y - ); - const calculatedRawY = interNum(xAsPercentage); - - // Because the line the dots are being mapped against may not have the widest range - // we need to adjust it's y position in the chart - const chartHeightInterpolator = d3InterpolateNumber( - 0, - size.plotHeight - ); - const newMin = chartHeightInterpolator( - ch._calcPercentageBetweenMinAndMax( - ch.alternateYAxis.yAxisMin, - ch.yMin, - ch.yMax - ) - ); - const newMax = chartHeightInterpolator( - ch._calcPercentageBetweenMinAndMax( - ch.alternateYAxis.yAxisMax, - ch.yMin, - ch.yMax - ) - ); - - return ( - size.plotHeight - - ch._getYAsPercentage( - calculatedRawY, - newMin, - newMax, - ch.alternateYAxis.yAxisMin, - ch.alternateYAxis.yAxisMax - ) - ); - }), - chartTransform - ); - } - - zoomOnAxisX(newScaleX) { - this.lastKnownXScale = newScaleX; - - const spacing = this._calculateSpacing(); - const chartData = this.chartData; - d3Select(`#${this.id}`) - .selectAll("circle") - .attr("cx", (d) => newScaleX(d.x)) - .attr("r", function (d, i) { - if (i === chartData.selectedIndex) - return spacing > defaultMarkerSizeSmall - ? selectedMarkerSizeLarge - : selectedMarkerSizeSmall; - return spacing > defaultMarkerSizeSmall - ? defaultMarkerSizeLarge - : defaultMarkerSizeSmall; - }); - } - - highlightMoment(selectedIndex) { - const spacing = this._calculateSpacing(); - this.chartData.selectedIndex = selectedIndex; - d3Select(`#${this.id}`) - .selectAll("circle") - .style("fill-opacity", 0.3) - .attr("r", (d) => - spacing > defaultMarkerSizeSmall - ? defaultMarkerSizeLarge - : defaultMarkerSizeSmall - ) - .filter(`.dataIndex-${selectedIndex}`) - .style("fill-opacity", 1) - .attr("r", (d) => - spacing > defaultMarkerSizeSmall - ? selectedMarkerSizeLarge - : selectedMarkerSizeSmall - ); - } - - _calculateSpacing() { - const n = this.chartData.points.length; - return ( - (this.lastKnownXScale(this.chartData.points[n - 1].x) - - this.lastKnownXScale(this.chartData.points[0].x)) / - n - ); - } - - _getXAsPercentage(x, startX, endX) { - const unixStart = new Date(startX).getTime(); - const unixEnd = new Date(endX).getTime(); - return (new Date(x).getTime() - unixStart) / (unixEnd - unixStart); - } - - // https://stackoverflow.com/a/31687097/1979085 - _getYAsPercentage(fauxY, minAllowed, maxAllowed, min, max) { - return ( - ((maxAllowed - minAllowed) * (fauxY - min)) / (max - min) + minAllowed - ); - } - - _calcPercentageBetweenMinAndMax(x, min, max) { - return (x - min) / (max - min); - } - - _findNearestX(x) { - const xToSearchFor = new Date(x).getTime(); - for (var i = 0; i < this._alternateYAxisFilteredPoints.length; i++) { - if ( - new Date(this._alternateYAxisFilteredPoints[i].x).getTime() >= - xToSearchFor - ) - return i - 1; - } - return null; - } -} - -module.exports = MomentChartPoints; diff --git a/lib/Charts/Scales.js b/lib/Charts/Scales.js deleted file mode 100644 index a25ca48fb72..00000000000 --- a/lib/Charts/Scales.js +++ /dev/null @@ -1,193 +0,0 @@ -"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 uniq from "lodash-es/uniq"; - -const unknown = undefined; - -const Scales = { - /** - * Calculates the appropriate d3 scales. - * - * @param {Size} size Dimensions of plot area, as returned by Size class. - * @param {Object} [domain] Optional, object containing [minimum, maximum] arrays for each axis. - * @param {Number[]} domain.x [x-minimum, x-maximum]. - * @param {Object} domain.y An object whose keys are units ("undefined" for unknown), values being [y-minimum, y-maximum]. - * @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} {x, y: { [unit1], [unit2] ... }} where values are D3-scale objects. - */ - calculate(size, domain, data, xPadding = 0) { - const allUnits = uniq(data.map((line) => line.units || unknown)); - function computeDomain() { - if (data[0].points.length === 0) { - return; - } - // if there is at least one domain with "showAll", filter the others out. - const keepAll = data.filter((series) => series.showAll).length === 0; - - const filteredData = data.filter((d) => { - return d.points.length > 0; - }); - - // domains is an Array of the domains for each data element, with the units. - const domains = filteredData.map(function (series) { - const seriesD = series.getDomain(); - series.yAxisMin = seriesD.y[0]; - series.yAxisMax = seriesD.y[1]; - return { - units: series.units || unknown, - domain: seriesD - }; - }); - const importantDomains = domains.filter( - (d, i) => keepAll || filteredData[i].showAll - ); - - // domain.x is a simple [minx, maxx] array. - // domain.y is an object with keys being the different units, and values being [miny, maxy]. - const domain = { - x: [ - d3ArrayMin( - importantDomains.map((l) => l.domain.x), - (x) => x[0] - ), - d3ArrayMax( - importantDomains.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 (Object.prototype.hasOwnProperty.call(domain.y, theseUnits)) { - const thisYDomain = domain.y[theseUnits]; - // If the y-domain is positive and could reasonably be expanded to include zero, 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 expanded to include zero, 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) - ); - if (isFinite(yAxisMin) && thisYDomain[0] < yAxisMin) { - thisYDomain[0] = yAxisMin; - } - - const yAxisMax = Math.max.apply( - Math, - dataWithTheseUnits - .filter((d) => defined(d.yAxisMax)) - .map((d) => d.yAxisMax) - ); - if (isFinite(yAxisMax) && thisYDomain[1] > yAxisMax) { - thisYDomain[1] = yAxisMax; - } - } - } - return domain; - } - - function computeXScale() { - const xScale = - domain.x[0] instanceof Date ? d3ScaleTime() : d3ScaleLinear(); - return xScale.range([xPadding, size.width - xPadding]).domain(domain.x); - } - - function computeYScales() { - // 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 (const theseUnits in domain.y) { - if (Object.prototype.hasOwnProperty.call(domain.y, theseUnits)) { - const thisYDomain = domain.y[theseUnits]; - yScales[theseUnits] = d3ScaleLinear() - .range([size.plotHeight, 0]) - .domain(thisYDomain); - } - } - return yScales; - } - - if (data.length === 0) { - return; - } - if (!defined(domain)) { - domain = computeDomain(); - if (!domain) { - return; - } - } - return { x: computeXScale(), y: computeYScales() }; - }, - - 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; - } -}; - -export default Scales; diff --git a/lib/Charts/Size.js b/lib/Charts/Size.js deleted file mode 100644 index 2b5df1f7f2e..00000000000 --- a/lib/Charts/Size.js +++ /dev/null @@ -1,44 +0,0 @@ -"use strict"; - -import defaultValue from "terriajs-cesium/Source/Core/defaultValue"; -import defined from "terriajs-cesium/Source/Core/defined"; -import Title from "./Title"; -const defaultXAxisHeight = 14; // The default height of the x-axis itself, ie. the numbering of the ticks. -const defaultXAxisLabelHeight = 20; // The default additional height of the x-axis label, eg. "time". - -const yAxisWidth = 45; - -const Size = { - calculate(element, margin, state, numberOfYAxes) { - const xAxisHeight = defaultValue(state.xAxisHeight, defaultXAxisHeight); - const xAxisLabelHeight = defaultValue( - state.xAxisLabelHeight, - defaultXAxisLabelHeight - ); - const yAxesWidth = numberOfYAxes * yAxisWidth; - const titleHeight = Title.getHeight(state.titleSettings); - const width = element.offsetWidth - margin.left - margin.right - yAxesWidth; - const height = - element.offsetHeight - margin.top - margin.bottom - titleHeight; - const heightMinusXAxisLabelHeight = - height - - (defined(state.axisLabel) && defined(state.axisLabel.x) - ? xAxisLabelHeight - : 0); - const plotHeight = - heightMinusXAxisLabelHeight - (state.mini ? 0 : xAxisHeight); - return { - width: width, - yAxesWidth: yAxesWidth, - height: height, - heightMinusXAxisLabelHeight: heightMinusXAxisLabelHeight, - plotHeight: plotHeight, - xAxisHeight: xAxisHeight, - xAxisLabelHeight: xAxisLabelHeight - }; - }, - - yAxisWidth: yAxisWidth -}; - -export default Size; diff --git a/lib/Charts/Title.js b/lib/Charts/Title.js deleted file mode 100644 index 6657b4c0e25..00000000000 --- a/lib/Charts/Title.js +++ /dev/null @@ -1,113 +0,0 @@ -"use strict"; - -import { nest as d3Nest } from "d3-collection"; - -import defaultValue from "terriajs-cesium/Source/Core/defaultValue"; -import defined from "terriajs-cesium/Source/Core/defined"; - -const defaultClassName = "base-chart-title"; -const defaultHeight = 30; // The additional height of the title, which may in fact be the legend. - -/** - * Handles the drawing of the chart title, which may be a string or a legend. - * - * @param {String} [titleSettings.type='string'] May be 'string' or 'legend'. - * @param {String} [titleSettings.title] For 'string'-type titles, the title. - * @param {String} [titleSettings.className] The className to use for the title DOM element. Defaults to 'base-chart-title'. - * @param {Number} [titleSettings.height=defaultTitleHeight] The height of the title bar. - */ -const Title = { - className(titleSettings) { - return titleSettings && titleSettings.className - ? titleSettings.className - : defaultClassName; - }, - - getHeight(titleSettings) { - return defaultValue( - defined(titleSettings) ? titleSettings.height : 0, - defaultHeight - ); - }, - - create(d3Element, titleSettings) { - // For a nicely centered title, use css: .chart-title {left: 0, right: 0, text-align: center;} and maybe {margin: 0 auto;}. - d3Element - .append("div") - .attr("class", Title.className(titleSettings)) - .style("opacity", 1e-6) - .style("position", "absolute"); - }, - - enterUpdateAndExit( - d3Element, - titleSettings, - margin, - catalogItems, - transitionDuration - ) { - // The title might be the legend, or a simple string. - const title = d3Element - .select("." + Title.className(titleSettings)) - .style("top", margin.top + "px"); - title - .transition() - .duration(transitionDuration) - .style("opacity", Title.getHeight(titleSettings) > 0 ? 1 : 1e-6); - if (defined(titleSettings)) { - let titleData = catalogItems; - if (titleSettings.type === "string") { - titleData = [{ id: "_string__", name: titleSettings.title }]; - } - - // in d3 v4, selection.data method returns new selections - // rather than modifying the selection in-place. - const titleComponents = title - .selectAll(".title-component") - .data(titleData, (d) => d.id); - // Check whether there are multiple category names and/or column names. - const numberOfCategories = d3Nest().key((d) => d.categoryName).length; - const numberOfColumnNames = d3Nest().key((d) => d.name).length; - // This is to only show the interesting parts of the name & categoryName in the title, - // similar to Tooltip.js. - const getName = function (d, index) { - if (!d.categoryName) { - return d.name; - } - if (numberOfColumnNames === 1) { - return d.categoryName + (index === 0 ? " " + d.name : ""); - } - if (numberOfCategories === 1) { - return (index === 0 ? d.categoryName + " " : "") + d.name; - } - if (d.name === d.categoryName) { - return d.categoryName; - } - return d.categoryName + " " + d.name; - }; - // Enter. - const addedTitleComponents = titleComponents - .enter() - .append("span") - .attr("class", "title-component"); - - if (titleSettings.type === "legend") { - addedTitleComponents.append("span").attr("class", "color"); - } - addedTitleComponents.append("span").attr("class", "name"); - // Enter and update. - const mergedTitleComponents = addedTitleComponents.merge(titleComponents); - mergedTitleComponents - .select(".color") - .style("background-color", (d) => d.color) - .style("border-radius", (d) => { - return d.type === "momentPoints" ? "50%" : "0"; - }); - mergedTitleComponents.select(".name").text(getName); - // Exit. - titleComponents.exit().remove(); - } - } -}; - -export default Title; diff --git a/lib/Charts/Tooltip.js b/lib/Charts/Tooltip.js deleted file mode 100644 index bc3adc1aff6..00000000000 --- a/lib/Charts/Tooltip.js +++ /dev/null @@ -1,278 +0,0 @@ -"use strict"; - -import { nest as d3Nest } from "d3-collection"; -import { - select as d3Select, - event as d3Event, - clientPoint as d3ClientPoint -} from "d3-selection"; -import { transition as d3Transition } from "d3-transition"; // eslint-disable-line no-unused-vars - -import defaultValue from "terriajs-cesium/Source/Core/defaultValue"; -import defined from "terriajs-cesium/Source/Core/defined"; - -import dateformat from "dateformat"; - -const defaultTooltipOffset = { - // The meaning of these offsets depend on the alignment. - top: 10, - right: 10, - bottom: 10, - left: 10 -}; -const defaultClassName = "base-chart-tooltip"; -const defaultId = "base-chart-tooltip-id"; -const showHideDuration = 250; - -/** - * Handles the drawing of the chart tooltip, which shows the values of the selected data in a legend. - * - * @param {String} [tooltipSettings.id] The id to use for the tooltip DOM element, defaults to 'base-chart-tooltip-id'. Do not change this after creation. - * @param {String} [tooltipSettings.className] The className to use for the tooltip DOM element, defaults to 'base-chart-tooltip'. Do not change this after creation. - * @param {String} [tooltipSettings.align] One of 'hover' (hover at the mouse position), 'left', 'right', 'prefer-right' (chooses left or right depending on mouse position). - * @param {Object} [tooltipSettings.offset] An object with top, left and right properties; these properties' meanings depend on the alignment above. - * With right/left alignment, the offset is relative to the svg. - */ -const Tooltip = { - defaultClassName: defaultClassName, - defaultId: defaultId, - - id(tooltipSettings) { - return defaultValue(tooltipSettings.id, defaultId); - }, - - select(tooltipSettings) { - return d3Select("#" + Tooltip.id(tooltipSettings)); - }, - - create(container, tooltipSettings) { - // Make the tooltip DOM element, invisible to start. - if (defined(tooltipSettings)) { - container - .append("div") - .attr("id", Tooltip.id(tooltipSettings)) - .attr( - "class", - defaultValue(tooltipSettings.className, defaultClassName) - ) - .style("opacity", 1e-6) - .style("position", "absolute") - .style("display", "none"); - } - }, - - destroy(tooltipSettings) { - // Remove the tooltip DOM element. - if (defined(tooltipSettings)) { - const id = Tooltip.id(tooltipSettings); - const tooltipElement = d3Select("#" + id).nodes(); - if (tooltipElement) { - d3Select("#" + id).remove(); - //NOTE: why not remove it directly like above? - // tooltipElement.parentElement.removeChild(tooltipElement); - } - } - }, - - singleRowHtml(color, name, value, units) { - if (value === null) return; - const styleAttribute = defined(color) - ? 'style="background-color: ' + color + '" ' - : ""; - - const formattedVal = isNaN(value) ? value : value.toFixed(2); - - return ` - - - - - ${name} - - - ${formattedVal} - ${units || ""} - - `; - }, - - html(selectedData, xLocation) { - let html; - const readableX = - typeof xLocation.getMonth === "function" - ? dateformat(xLocation, "dd/mm/yyyy, HH:MMTT") - : xLocation; - html = '

' + readableX + "

"; - - // If there is only one line showing, then label it with the category name, not the column name. - // Else, if there is only one column name (shared by all the categories), show the category names - // and don't show the column name. - // Else, if there is only one category name, then there is no need to show it. - // In general, show both, grouped by category name. - - // If there is only a moment dataset it's x values (a date will be shown) - if ( - selectedData.length === 1 && - (selectedData[0].type === "moment" || - selectedData[0].type === "momentPoints") - ) { - return html; - } else if (selectedData.length === 1) { - const onlyLine = selectedData[0]; - html += '

' + onlyLine.categoryName + "

"; - html += ""; - html += this.singleRowHtml( - onlyLine.color, - `${onlyLine.name}`, - onlyLine.type === "moment" || onlyLine.type === "momentPoints" - ? readableX - : onlyLine.point.y, - onlyLine.units - ); - html += "
"; - return html; - } - - // The next line turns [chartData1A, chartData2, chartData1B] into - // [{key: 'categoryName1', values: [chartData1A, chartData1B]}, {key: 'categoryName2', values: [chartData2]}]. - const dataGroupedByCategory = d3Nest() - .key((d) => d.categoryName) - .entries(selectedData); - // And similarly for the column names. - // const dataGroupedByName = d3Nest() - // .key(d => d.name) - // .entries(selectedData); - - // if (dataGroupedByName.length === 1) { - // // All lines have the same name. - // html += ''; - // dataGroupedByName[0].values.forEach(line => { - // html += this.singleRowHtml( - // line.color, - // line.categoryName, - // line.point.y, - // line.units - // ); - // }); - // html += "
"; - // return html; - // } - - dataGroupedByCategory.forEach((group) => { - if ( - group.values[0].type === "moment" || - group.values[0].type === "momentPoints" - ) { - return; - } - // if (dataGroupedByCategory.length > 1) { - // html += '

' + group.key + "

"; - // } - html += '

' + group.key + "

"; - html += ''; - group.values.forEach((line) => { - html += this.singleRowHtml( - line.color, - line.name, - line.point.y, - line.units - ); - }); - html += "
"; - }); - return html; - }, - - show(html, tooltipElement, tooltipSettings, boundingRect) { - tooltipElement - .html(html) - .style("display", "block") - .transition() - .duration(showHideDuration) - .style("opacity", 1) - .style("max-width", "300px") - .style("visibility", "visible"); - - const tooltipWidth = +tooltipElement.nodes()[0].offsetWidth; - const tooltipOffset = defaultValue( - tooltipSettings.offset, - defaultTooltipOffset - ); - let top, left, right; - - const clientPos = d3ClientPoint(tooltipElement.node().parentNode, d3Event); - const clientX = clientPos[0]; - const clientY = clientPos[1]; - - switch (tooltipSettings.align) { - case "left": - top = tooltipOffset.top; - left = tooltipOffset.left; - break; - case "right": - top = tooltipOffset.top; - right = tooltipOffset.right; - break; - case "prefer-right": { - // Only show on the left if we would be under the tooltip on the right, but not on the left. - top = tooltipOffset.top; - const leftEdgeWhenPositionedRight = - boundingRect.width - tooltipOffset.right - tooltipWidth; - const rightEdgeWhenPositionedLeft = tooltipOffset.left + tooltipWidth; - if ( - clientX >= leftEdgeWhenPositionedRight && - clientX > rightEdgeWhenPositionedLeft - ) { - left = tooltipOffset.left; - } else { - right = tooltipOffset.right; - } - break; - } - case "hover": - default: - top = d3Event.clientY - tooltipOffset.top; - left = d3Event.clientX + (-tooltipWidth - tooltipOffset.left); - break; - } - - const tooltipHeight = tooltipElement.node().offsetHeight; - - const possibleYClash = clientY < tooltipHeight + tooltipOffset.top; - if (possibleYClash) { - tooltipElement.style("bottom", "60px"); - tooltipElement.style("top", null); - } else { - tooltipElement.style("top", top + "px"); - tooltipElement.style("bottom", null); - } - - if (left !== undefined) { - tooltipElement.style("left", left + "px"); - } else { - tooltipElement.style("left", "auto"); - } - if (right !== undefined) { - tooltipElement.style("right", right + "px"); - } else { - tooltipElement.style("right", "auto"); - } - }, - - hide(tooltipElement) { - tooltipElement - .transition() - .duration(showHideDuration) - .style("opacity", 1e-6); - // visibility hidden cannot transition, and it is too flashy if you use it without. - // We need it because opacity=0 along can get in front of other elements and prevent the hover from working at all. - // So delay it until (and only if) the opacity has already done its job. - setTimeout(function () { - if (+tooltipElement.style("opacity") < 0.002) { - tooltipElement.style("visibility", "hidden"); - } - }, showHideDuration * 1.2); - } -}; - -export default Tooltip; diff --git a/lib/Charts/d3Sync.js b/lib/Charts/d3Sync.js deleted file mode 100644 index 3634a4cfd0e..00000000000 --- a/lib/Charts/d3Sync.js +++ /dev/null @@ -1,56 +0,0 @@ -"use strict"; - -const childNodesSelector = function () { - return this.childNodes; -}; - -/** - * Convenience wrapper that manages D3's enter/exit mechanics to synchronise an array of data - * with DOM elements. - * @param {Element} parent HTML element which will contain the nodes - * @param {Object[]} arrayData Data to be synchronised. - * @param {String} childElementTagName Name of HTML element to be created for each data point. - * @param {Function} updateCallBack Function called with (d3object, isNewElement). If it returns the d3object, - * an opacity transition will be applied. - * @param {Boolean} transition Parameter passed to d3.transition(). - */ -function d3Sync( - parent, - arrayData, - childElementTagName, - updateCallback, - transition = null -) { - // move stray elements that are not 'childElementTagName' to the bottom - // that way they can be removed with the fade out animation - parent - .selectAll(childNodesSelector) - .filter((d, i, nodes) => nodes[i].tagName !== childElementTagName) - .each(function () { - this.parentElement.appendChild(this); - }); - // synchronise intended elements - const existing = parent - .selectAll(childNodesSelector) - .data(arrayData, function (d) { - return d ? d.id || d : this.id; - }); // id hack for ChartData objects which get re-created each time there are any changes - const enter = existing.enter().append(childElementTagName); - const exit = existing.exit(); - if (transition) { - exit.transition(transition).style("opacity", 1e-2).remove(); - const entered = updateCallback(enter, true); - if (entered) { - // We don't want to randomly transition all attributes on new elements, because it looks bad. - // Instead, let's just transition opacity. - entered.style("opacity", 0).transition(transition).style("opacity", 1); - } - updateCallback(existing.transition(transition).style("opacity", 1), false); - } else { - exit.remove(); - updateCallback(enter, true); - updateCallback(existing, false); - } -} - -module.exports = d3Sync; diff --git a/lib/Charts/initializeChartTypes.js b/lib/Charts/initializeChartTypes.js deleted file mode 100644 index 6efa76d91e7..00000000000 --- a/lib/Charts/initializeChartTypes.js +++ /dev/null @@ -1,82 +0,0 @@ -"use strict"; - -import defined from "terriajs-cesium/Source/Core/defined"; - -import LineChart from "./LineChart"; -import BarChart from "./BarChart"; -import MomentChart from "./MomentChart"; -import MomentChartPoints from "./MomentChartPoints"; - -const chartType = { - line: new LineChart(), - bar: new BarChart(), - moment: new MomentChart(), - momentPoints: new MomentChartPoints() -}; - -function getChart(chartType) { - if (chartType === "line") return new LineChart(); - if (chartType === "bar") return new BarChart(); - if (chartType === "moment") return new MomentChart(); - if (chartType === "momentPoints") return new MomentChartPoints(); -} - -/** - * @param {ChartData} chartData ChartData object for which chart type needs to be determined. - */ -export function determineChartType(chartData) { - function pointsPerYear() { - const years = - (domain.x[1].getTime() - domain.x[0].getTime()) / - (3600 * 24 * 365.25 * 1000); - return numPoints / years; - } - - function pointsPerRangeUnit() { - const range = Math.floor(domain.x[1] - domain.x[0]); - return numPoints / range; - } - - const pointsWithoutY = () => - chartData.points.filter((p) => !defined(p.y)).length; - - // respect chartData.type - if (defined(chartType[chartData.type])) { - return getChart(chartData.type); - } - const numPoints = chartData.points.length; - if (numPoints === 0) return getChart("line"); - const domain = chartData.getDomain(); - const isDate = domain.x[0] instanceof Date; - try { - if ( - (isDate && pointsPerYear() < 1) || - (!isDate && pointsPerRangeUnit() < 1) || - pointsWithoutY() > 0 - ) { - return getChart("line"); - // return chartType.bar; // TODO restore when bar charts work perfectly. - } - } catch (e) { - console.error(e.stack); - } - return getChart("line"); -} - -/** - * singleton - */ -export function initializeChartData(chartData) { - if (chartData.renderer === undefined) { - chartData.renderer = determineChartType(chartData); - } -} - -/** - * Ensure that we have determined the chart type for every data item in our state. - */ -function initializeChartTypes(state) { - state.chartItems.forEach(initializeChartData); -} - -export default initializeChartTypes; diff --git a/lib/ReactViews/Custom/Chart/ZoomX.jsx b/lib/ReactViews/Custom/Chart/ZoomX.jsx index f2eef609237..b3bd31ff5cd 100644 --- a/lib/ReactViews/Custom/Chart/ZoomX.jsx +++ b/lib/ReactViews/Custom/Chart/ZoomX.jsx @@ -1,6 +1,6 @@ import { observer } from "mobx-react"; import { zoom as d3Zoom } from "d3-zoom"; -import { select as d3Select, event as d3Event } from "d3-selection"; +import { select as d3Select } from "d3-selection"; import PropTypes from "prop-types"; import React from "react"; @@ -20,9 +20,9 @@ class ZoomX extends React.Component { this.zoom = d3Zoom() .scaleExtent(props.scaleExtent) .translateExtent(props.translateExtent) - .on("zoom", () => - props.onZoom(d3Event.transform.rescaleX(this.props.initialScale)) - ); + .on("zoom", (event) => { + props.onZoom(event.transform.rescaleX(this.props.initialScale)); + }); } componentDidMount() { diff --git a/package.json b/package.json index 3670120de86..b6c7b40fe45 100644 --- a/package.json +++ b/package.json @@ -42,12 +42,11 @@ "@turf/meta": "^6.5.0", "@types/arcgis-rest-api": "^10.4.5", "@types/create-react-class": "^15.6.2", - "@types/d3-array": "^2.0.0", - "@types/d3-axis": "^1.0.12", + "@types/d3-array": "^3.2.1", "@types/d3-color": "^3.0.0", - "@types/d3-scale-chromatic": "^2.0.0", - "@types/d3-selection": "^1.4.1", - "@types/d3-transition": "^1.1.4", + "@types/d3-scale-chromatic": "^3.0.3", + "@types/d3-selection": "^3.0.10", + "@types/d3-transition": "^3.0.8", "@types/dateformat": "^3.0.1", "@types/dompurify": "^2.3.1", "@types/file-saver": "^1.3.0", @@ -102,19 +101,15 @@ "core-js": "^3.1.4", "css-loader": "^2.1.0", "css-modules-typescript-loader": "^2.0.4", - "d3-array": "^1.0.0", - "d3-axis": "^1.0.0", - "d3-collection": "^1.0.0", - "d3-color": "^3.0.1", - "d3-dispatch": "^1.0.5", - "d3-ease": "^1.0.5", - "d3-interpolate": "^1.3.2", - "d3-scale": "^2.2.2", - "d3-scale-chromatic": "^2.0.0", - "d3-selection": "^1.0.0", - "d3-shape": "^1.0.0", - "d3-transition": "^1.0.0", - "d3-zoom": "^1.8.3", + "d3-array": "^3.2.4", + "d3-color": "^3.1.0", + "d3-ease": "^3.0.1", + "d3-interpolate": "^3.0.1", + "d3-scale-chromatic": "^3.1.0", + "d3-selection": "^3.0.0", + "d3-shape": "^3.2.0", + "d3-transition": "^3.0.1", + "d3-zoom": "^3.0.0", "dateformat": "^3.0.3", "dompurify": "^2.3.3", "fetch-mock": "^9.11.0",