UNPKG

@tanstack/charts

Version:

A chart grammar for TypeScript and JavaScript. Marks consume your data directly, channels describe visual encodings, and the engine compiles them into a renderer-neutral keyed scene. TanStack's compact scales cover common numeric and categorical mappings.

181 lines (180 loc) 5.93 kB
import { geoCentroid, geoPath } from "d3-geo"; import { channelValues, inferredKeyValues, isChartKey, isNonnegativeFiniteNumber, visualValue } from "./mark.js"; import { createMarkWithScaleValues } from "./mark-with-scale-values.js"; import { resolveNumericScale } from "./scale-input.js"; import { valueKey } from "./scales.js"; function geoShape(source, options) { const data = Array.isArray(source) ? source : Array.from(source); return createMarkWithScaleValues( ({ markIndex }) => { const id = options.id ?? `geo-shape-${markIndex}`; const colorValues = channelValues(data, options.color, () => null); const keys = inferredKeyValues(data, options.key); const rawRadii = typeof options.r === "number" ? data.map(() => options.r) : channelValues(data, options.r, () => 4.5); const radiusMapper = resolveNumericScale(options.rScale, rawRadii); const radii = radiusMapper ? rawRadii.map( (value) => isNonnegativeFiniteNumber(value) ? radiusMapper(value) : Number.NaN ) : rawRadii; return { id, channels: { color: { scale: "color", values: colorValues.filter(isChartKey) } }, render: ({ chart, color: resolveColor }) => { const projection = resolveGeoProjection( options.projection, chart, data ); const path = geoPath(projection); const nodes = []; const points = []; data.forEach((datum, datumIndex) => { const radius = radii[datumIndex]; if (!isNonnegativeFiniteNumber(radius)) return; path.pointRadius(radius); const pathData = path(datum); if (typeof pathData !== "string" || pathData.length === 0) return; const group = colorValues[datumIndex] ?? null; const color = resolveColor(group); const paint = geoPaint(datum); const fill = visualValue( options.fill, datum, datumIndex, data, paint & 2 ? color : "none" ); const stroke = visualValue( options.stroke, datum, datumIndex, data, paint & 1 ? color : "none" ); const key = `${id}:${valueKey(keys[datumIndex])}`; nodes.push({ kind: "area", key, points: [], path: pathData, style: { fill, fillOpacity: options.fillOpacity, stroke, strokeOpacity: options.strokeOpacity, strokeWidth: options.strokeWidth, strokeDasharray: options.strokeDasharray, opacity: options.opacity, lineJoin: "round" } }); const [x, y] = path.centroid(datum); const [longitude, latitude] = options.anchor?.(datum, { index: datumIndex, data }) ?? geoCentroid(datum); if (!Number.isFinite(x) || !Number.isFinite(y) || !Number.isFinite(longitude) || !Number.isFinite(latitude)) { return; } points.push({ key, markId: id, group: null, groupLabel: id, datum, datumIndex, xValue: longitude, yValue: latitude, x, y, color: paint === 1 ? stroke : fill }); }); return { nodes: [ { kind: "group", key: id, className: options.className ? `ts-chart__geo ${options.className}` : "ts-chart__geo", ariaHidden: true, children: nodes } ], points }; } }; }, options.motion ); } const sphere = { type: "Sphere" }; function resolveGeoProjection(input, chart, data) { if (typeof input === "function") return input({ chart, data }); const projection = input.type(); const fit = input.fit === "sphere" ? sphere : input.fit === "data" ? geoDataGeometry(data) : input.fit; if (!fit) return projection; const inset = input.inset !== void 0 && Number.isFinite(input.inset) ? Math.min( Math.max(0, input.inset), Math.max(0, (Math.min(chart.width, chart.height) - 1) / 2) ) : 0; return projection.fitExtent( [ [chart.x + inset, chart.y + inset], [chart.x + chart.width - inset, chart.y + chart.height - inset] ], fit ); } function geoDataGeometry(data) { const geometries = []; for (const datum of data) collectGeometries(datum, geometries); if (geometries.length === 0) return null; if (geometries.length === 1) return geometries[0] ?? null; return { type: "GeometryCollection", geometries }; } function collectGeometries(object, output) { if ("geometry" in object) { if (object.geometry) collectGeometries(object.geometry, output); return; } if ("features" in object) { for (const feature of object.features) collectGeometries(feature, output); return; } if ("geometries" in object) { for (const geometry of object.geometries) { collectGeometries(geometry, output); } return; } output.push(object); } function geoPaint(object) { if ("geometry" in object) { return object.geometry ? geoPaint(object.geometry) : 0; } if ("features" in object) { return object.features.reduce( (paint, feature) => paint | geoPaint(feature), 0 ); } if ("geometries" in object) { return object.geometries.reduce( (paint, geometry) => paint | geoPaint(geometry), 0 ); } return object.type === "LineString" || object.type === "MultiLineString" ? 1 : 2; } export { geoShape };