UNPKG

terriajs

Version:

Geospatial data visualization platform.

209 lines 9.82 kB
import { createElement as _createElement } from "react"; import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; import { RectClipPath } from "@visx/clip-path"; import { localPoint } from "@visx/event"; import { GridRows } from "@visx/grid"; import { Group } from "@visx/group"; import { withParentSize } from "@visx/responsive"; import { scaleLinear, scaleTime } from "@visx/scale"; import groupBy from "lodash-es/groupBy"; import minBy from "lodash-es/minBy"; import { observer } from "mobx-react"; import { useEffect, useMemo, useState } from "react"; import Styles from "./bottom-dock-chart.scss"; import Legends from "./Legends"; import Tooltip from "./Tooltip"; import { Cursor, Plot, PointsOnMap, XAxis, YAxis } from "./utils"; import { ZoomX } from "./ZoomX"; const CHART_MIN_WIDTH = 110; const DEFAULT_GRID_COLOR = "#efefef"; const Y_AXIS_NUM_TICKS = 4; const Y_AXIS_TICK_LABEL_FONT_SIZE = 10; const _BottomDockChart = observer(({ chartItems, xAxis, parentWidth = 0, width, height, margin }) => { return (_jsx(Chart, { chartItems: chartItems, xAxis: xAxis, height: height, margin: margin, width: Math.max(CHART_MIN_WIDTH, width || parentWidth) })); }); export const BottomDockChart = withParentSize(_BottomDockChart); BottomDockChart.displayName = "BottomDockChart"; const DEFAULT_MARGIN = { left: 20, right: 30, top: 10, bottom: 50 }; const Chart = observer(({ chartItems: propsChartItems, xAxis, width, height, margin = DEFAULT_MARGIN }) => { const [zoomedXScale, setZoomedXScale] = useState(undefined); const [mouseCoords, setMouseCoords] = useState(undefined); const processedChartItems = useMemo(() => { return sortChartItemsByType(propsChartItems) .filter((chartItem) => chartItem.points.length > 0) .map((chartItem) => { return { ...chartItem, points: chartItem.points.slice().sort((p1, p2) => +p1.x - +p2.x) }; }); }, [propsChartItems]); const plotHeight = height - margin.top - margin.bottom - Legends.maxHeightPx; const yAxes = useMemo(() => { const range = [plotHeight, 0]; const chartItemsByUnit = groupBy(processedChartItems, "units"); return Object.entries(chartItemsByUnit).map(([units, chartItems]) => { return { units: units === "undefined" ? undefined : units, scale: scaleLinear({ domain: calculateDomainY(chartItems), range }), color: chartItems[0].getColor() }; }); }, [plotHeight, processedChartItems]); // We need to consider only the left most Y-axis as its label values appear // outside the chart plot area. The labels of inner y-axes appear inside // the plot area. const estimatedYAxesWidth = useMemo(() => { const leftmostYAxis = yAxes[0]; const maxLabelDigits = Math.max(0, ...leftmostYAxis.scale .ticks(Y_AXIS_NUM_TICKS) .map((n) => n.toString().length)); return maxLabelDigits * Y_AXIS_TICK_LABEL_FONT_SIZE; }, [yAxes]); const plotWidth = width - margin.left - margin.right - estimatedYAxesWidth; const adjustedMargin = useMemo(() => ({ ...margin, left: margin.left + estimatedYAxesWidth }), [estimatedYAxesWidth, margin]); const initialXScale = useMemo(() => { const params = { domain: calculateDomainX(processedChartItems), range: [0, plotWidth] }; if (xAxis.scale === "linear") return scaleLinear(params); else return scaleTime(params); }, [xAxis, processedChartItems, plotWidth]); const xScale = zoomedXScale || initialXScale; const initialScales = useMemo(() => processedChartItems.map((c) => ({ x: initialXScale, y: yAxes.find((y) => y.units === c.units).scale })), [processedChartItems, initialXScale, yAxes]); const zoomedScales = useMemo(() => processedChartItems.map((c) => ({ x: xScale, y: yAxes.find((y) => y.units === c.units).scale })), [processedChartItems, xScale, yAxes]); const pointsNearMouse = useMemo(() => { if (!mouseCoords) return []; return processedChartItems .map((chartItem) => ({ chartItem, point: findNearestPoint(chartItem.points, mouseCoords, xScale, 7) })) .filter(pointNotUndefined); }, [processedChartItems, mouseCoords, xScale]); const cursorX = pointsNearMouse.length > 0 ? xScale(pointsNearMouse[0].point.x) : mouseCoords?.x; const tooltip = useMemo(() => { const margin = adjustedMargin; const tooltip = { items: pointsNearMouse }; if (!mouseCoords || mouseCoords.x < plotWidth * 0.5) { tooltip.right = width - (plotWidth + margin.right); } else { tooltip.left = margin.left; } tooltip.bottom = height - (margin.top + plotHeight); return tooltip; }, [ adjustedMargin, pointsNearMouse, mouseCoords, width, plotWidth, height, plotHeight ]); const setMouseCoordsFromEvent = (event) => { const coords = localPoint(event.target.ownerSVGElement || event.target, event); if (!coords) return; setMouseCoords({ x: coords.x - adjustedMargin.left, y: coords.y - adjustedMargin.top }); }; useEffect(() => { setZoomedXScale(undefined); }, [processedChartItems]); if (processedChartItems.length === 0) return _jsx("div", { className: Styles.empty, children: "No data available" }); return (_jsxs(ZoomX, { surface: "#zoomSurface", initialScale: initialXScale, scaleExtent: [1, Infinity], translateExtent: [ [0, 0], [Infinity, Infinity] ], // Wrap setZoomedXScale in a function to ensure React stores the D3 scale function as a value. // If passed directly, React treats functions as state updaters, causing zoom to break. onZoom: (xScale) => setZoomedXScale(() => xScale), children: [_jsx(Legends, { width: plotWidth, chartItems: processedChartItems }), _jsxs("div", { style: { position: "relative" }, children: [_jsx("svg", { width: "100%", height: height, onMouseMove: setMouseCoordsFromEvent, onMouseLeave: () => setMouseCoords(undefined), children: _jsxs(Group, { left: adjustedMargin.left, top: adjustedMargin.top, children: [_jsx(RectClipPath, { id: "plotClip", width: plotWidth, height: plotHeight }), _jsx(XAxis, { top: plotHeight + 1, scale: xScale, label: xAxis.units || (xAxis.scale === "time" ? "Date" : "") }), yAxes.map((y, i) => (_createElement(YAxis, { ...y, key: `y-axis-${y.units}`, color: yAxes.length > 1 ? y.color : DEFAULT_GRID_COLOR, offset: i * 50 }))), yAxes.map((y) => (_jsx(GridRows, { width: plotWidth, height: plotHeight, scale: y.scale, numTicks: Y_AXIS_NUM_TICKS, stroke: yAxes.length > 1 ? y.color : DEFAULT_GRID_COLOR, lineStyle: { opacity: 0.3 } }, `grid-${y.units}`))), _jsxs("svg", { id: "zoomSurface", clipPath: "url(#plotClip)", pointerEvents: "all", children: [_jsx("rect", { width: plotWidth, height: plotHeight, fill: "transparent" }), cursorX && _jsx(Cursor, { x: cursorX, stroke: DEFAULT_GRID_COLOR }), _jsx(Plot, { chartItems: processedChartItems, initialScales: initialScales, zoomedScales: zoomedScales })] })] }) }), _jsx(Tooltip, { ...tooltip }), _jsx(PointsOnMap, { chartItems: processedChartItems })] })] })); }); Chart.displayName = "Chart"; // Type guard to filter ChartItems that don't produce a nearestPoint const pointNotUndefined = (itemPoint) => itemPoint.point !== undefined; /** * Sorts chartItems so that `momentPoints` are rendered on top then * `momentLines` and then any other types. * @param {ChartItem[]} chartItems array of chartItems to sort */ const sortChartItemsByType = (chartItems) => { return chartItems.slice().sort((a, b) => { if (a.type === "momentPoints") return 1; else if (b.type === "momentPoints") return -1; else if (a.type === "momentLines") return 1; else if (b.type === "momentLines") return -1; return 0; }); }; /** * Calculates a combined domain of all chartItems. * Convert Dates to numbers */ const calculateDomainX = (chartItems) => { const xmin = Math.min(...chartItems.map((c) => +c.domain.x[0])); const xmax = Math.max(...chartItems.map((c) => +c.domain.x[1])); return [xmin, xmax]; }; const calculateDomainY = (chartItems) => { const ymin = Math.min(...chartItems.map((c) => c.domain.y[0])); const ymax = Math.max(...chartItems.map((c) => c.domain.y[1])); return [ymin, ymax]; }; const findNearestPoint = (points, coords, xScale, maxDistancePx) => { function distance(coords, point) { // Works with numbers or Dates return point ? +coords.x - +xScale(point.x) : Infinity; } let left = 0; let right = points.length; let mid = 0; while (left !== right) { mid = left + Math.floor((right - left) / 2); const dist = distance(coords, points[mid]); if (dist === 0) { break; } else if (dist < 0) { right = mid; } else { left = mid + 1; } } const leftPoint = points[mid - 1]; const midPoint = points[mid]; const rightPoint = points[mid + 1]; const nearestPoint = minBy([leftPoint, midPoint, rightPoint], (p) => p ? Math.abs(distance(coords, p)) : Infinity); return nearestPoint !== undefined && Math.abs(distance(coords, nearestPoint)) <= maxDistancePx ? nearestPoint : undefined; }; //# sourceMappingURL=BottomDockChart.js.map