terriajs
Version:
Geospatial data visualization platform.
140 lines (128 loc) • 4.06 kB
JSX
import { scaleLinear } from "@visx/scale";
import { interpolateNumber as d3InterpolateNumber } from "d3-interpolate";
import { computed } from "mobx";
import { observer } from "mobx-react";
import PropTypes from "prop-types";
import React from "react";
import Glyphs from "./Glyphs";
import { GlyphCircle } from "@visx/glyph";
@observer
class MomentPointsChart extends React.Component {
static propTypes = {
id: PropTypes.string.isRequired,
chartItem: PropTypes.object.isRequired,
basisItem: PropTypes.object,
basisItemScales: PropTypes.object,
scales: PropTypes.object.isRequired,
glyph: PropTypes.string
};
static defaultProps = {
glyph: "circle"
};
@computed
get points() {
const { chartItem, basisItem, basisItemScales, scales } = this.props;
if (basisItem) {
// We want to stick the chartItem points to the basis item, to do this we
// interpolate the chart item points to match the basis item points. This
// interpolation should not affect the scale of the chart item points.
const basisToSourceScale = scaleLinear({
domain: basisItemScales.y.domain(),
range: scales.y.domain()
});
const interpolatedPoints = chartItem.points.map((p) => ({
...p,
...interpolate(p, basisItem.points, basisToSourceScale)
}));
return interpolatedPoints;
}
return chartItem.points;
}
doZoom(scales) {
const points = this.points;
if (points.length === 0) {
return;
}
const glyphs = document.querySelectorAll(
`g#${this.props.id} > g.visx-glyph`
);
glyphs.forEach((glyph, i) => {
const point = points[i];
if (point) {
const left = scales.x(point.x);
const top = scales.y(point.y);
const scale = point.isSelected ? "scale(1.4, 1.4)" : "";
glyph.setAttribute("transform", `translate(${left}, ${top}) ${scale}`);
glyph.setAttribute("fill-opacity", point.isSelected ? 1.0 : 0.3);
}
});
}
render() {
const { id, chartItem, scales, glyph } = this.props;
const baseKey = `moment-point-${chartItem.categoryName}-${chartItem.name}`;
const fillColor = chartItem.getColor();
const isClickable = chartItem.onClick !== undefined;
const clickProps = (point) => {
if (isClickable) {
return {
pointerEvents: "all",
cursor: "pointer",
onClick: () => chartItem.onClick(point)
};
}
return {};
};
const Glyph = Glyphs[glyph] ?? GlyphCircle;
return (
<g id={id}>
<For each="p" index="i" of={this.points}>
<Glyph
key={`${baseKey}-${i}`}
left={scales.x(p.x)}
top={scales.y(p.y)}
size={100}
fill={fillColor}
fillOpacity={p.isSelected ? 1.0 : 0.3}
{...clickProps(p)}
/>
</For>
</g>
);
}
}
/** Interpolates the given source point {x, y} to the closet point in the `sortedPoints` array.
*
* The source point and `sortedBasisPoints` may be of different scale, so we use `basisToSourceScale`
* to generate a point in the original source items scale.
*/
function interpolate({ x, y }, sortedBasisPoints, basisToSourceScale) {
const closest = closestPointIndex(x, sortedBasisPoints);
if (closest === undefined) {
return { x, y };
}
const a = sortedBasisPoints[closest];
const b = sortedBasisPoints[closest + 1];
if (a === undefined || b === undefined) {
return { x, y };
}
const xAsPercentage =
(x.getTime() - a.x.getTime()) / (b.x.getTime() - a.x.getTime());
const interpolated = {
x,
y: d3InterpolateNumber(
basisToSourceScale(a.y),
basisToSourceScale(b.y)
)(xAsPercentage)
};
return interpolated;
}
function closestPointIndex(x, sortedPoints) {
for (let i = 0; i < sortedPoints.length; i++) {
if (sortedPoints[i].x.getTime() >= x.getTime()) {
if (i === 0) return 0;
return i - 1;
}
}
return;
}
export default MomentPointsChart;