diff --git a/jest.config.js b/jest.config.js index 50e2bccd7..bd632fc5b 100644 --- a/jest.config.js +++ b/jest.config.js @@ -44,6 +44,6 @@ module.exports = { verbose: false, testPathIgnorePatterns: ['/packages/visx-demo'], transformIgnorePatterns: [ - 'node_modules/(?!(d3-(array|color|format|interpolate|scale|time|time-format)|internmap)/)', + 'node_modules/(?!(d3-(array|color|format|interpolate|scale|time|time-format)|delaunator|internmap|robust-predicates)/)', ], }; diff --git a/packages/visx-delaunay/.npmrc b/packages/visx-delaunay/.npmrc new file mode 100644 index 000000000..9cf949503 --- /dev/null +++ b/packages/visx-delaunay/.npmrc @@ -0,0 +1 @@ +package-lock=false \ No newline at end of file diff --git a/packages/visx-delaunay/Readme.md b/packages/visx-delaunay/Readme.md new file mode 100644 index 000000000..0fd48f47a --- /dev/null +++ b/packages/visx-delaunay/Readme.md @@ -0,0 +1,67 @@ +# @visx/delaunay + + + + + +## Overview + +A Voronoi diagram partitions a two-dimensional plane into regions based on a set of input points. +Each unique input point maps to a corresponding region, where each region represents _all points +that are closer to the input point than to any other input point_. + +Not only are Voronoi diagrams 😍, but they can be used to +[improve the interactive experience of a visualization](https://www.visualcinnamon.com/2015/07/voronoi.html). +This is most often accomplished by overlaying an invisible voronoi grid on top of the visualization +to increase the target area of interaction sites such as points on a scatter plot. + +The `@visx/delaunay` package provides a wrapper around the existing +[d3-delaunay](https://github.com/d3/d3-delaunay) package with some `react`-specific utilities. + +## Installation + +``` +npm install --save @visx/delaunay +``` + +## Usage + +The `@visx/delaunay` package exports a wrapped version of the d3 `voronoi` and `delaunay` layouts for flexible usage, +as well as a `` component for rendering Voronoi and Delaunay regions. + +```js +import { voronoi, Polygon } from '@visx/delaunay'; + +const points = Array(n).fill(null).map(() => ({ + x: Math.random() * innerWidth, + y: Math.random() * innerHeight, +})); + +// width + height set an extent on the voronoi +// x + y set relevant accessors depending on the shape of your data +const voronoiDiagram = voronoi({ + data: points, + x: d => d.x, + y: d => d.y, + width, + height, +}); + +const polygons = Array.from(voronoiDiagram.cellPolygons()); + +return ( + + + {polygons.map((polygon) => ( + + ))} + {points.map(({ x, y }) => ( + + )} + + +) +``` + +Additional information about the voronoi diagram API can be found in the +[d3-delaunay documentation](https://github.com/d3/d3-delaunay#voronoi). diff --git a/packages/visx-delaunay/package.json b/packages/visx-delaunay/package.json new file mode 100644 index 000000000..fb5f793df --- /dev/null +++ b/packages/visx-delaunay/package.json @@ -0,0 +1,42 @@ +{ + "name": "@visx/delaunay", + "version": "1.0.0", + "description": "visx delaunay", + "sideEffects": false, + "main": "lib/index.js", + "module": "esm/index.js", + "types": "lib/index.d.ts", + "files": [ + "lib", + "esm" + ], + "repository": { + "type": "git", + "url": "git+https://github.com/airbnb/visx.git" + }, + "keywords": [ + "visx", + "react", + "d3", + "visualizations", + "charts" + ], + "author": "@SheaJanke", + "license": "MIT", + "bugs": { + "url": "https://github.com/airbnb/visx/issues" + }, + "homepage": "https://github.com/airbnb/visx#readme", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@types/react": "*", + "@visx/vendor": "3.2.0", + "classnames": "^2.3.1", + "prop-types": "^15.6.1" + }, + "peerDependencies": { + "react": "^16.3.0-0 || ^17.0.0-0 || ^18.0.0-0" + } +} diff --git a/packages/visx-delaunay/src/components/Polygon.tsx b/packages/visx-delaunay/src/components/Polygon.tsx new file mode 100644 index 000000000..c0d14d4e0 --- /dev/null +++ b/packages/visx-delaunay/src/components/Polygon.tsx @@ -0,0 +1,24 @@ +import React from 'react'; +import cx from 'classnames'; + +export type PolygonProps = { + /** Override render function which is provided polygon and generated path. */ + children?: ({ path, polygon }: { path: string; polygon: [number, number][] }) => React.ReactNode; + /** className to apply to path element. */ + className?: string; + /** Array of coordinate arrays for the polygon (e.g., [[x,y], [x1,y1], ...]), used to generate polygon path. */ + polygon?: [number, number][]; +}; + +export default function Polygon({ + polygon, + className, + children, + ...restProps +}: PolygonProps & Omit, keyof PolygonProps>) { + if (!polygon) return null; + const path = `M${polygon.join('L')}Z`; + if (children) return <>{children({ path, polygon })}; + + return ; +} diff --git a/packages/visx-delaunay/src/delaunay.ts b/packages/visx-delaunay/src/delaunay.ts new file mode 100644 index 000000000..de9a4244f --- /dev/null +++ b/packages/visx-delaunay/src/delaunay.ts @@ -0,0 +1,17 @@ +import { Delaunay } from '@visx/vendor/d3-delaunay'; + +interface Config { + /** The data for the delaunay triangulation */ + data?: Datum[]; + /** Set the x-value accessor function for the delaunay triangulation. */ + x: (d: Datum) => number; + /** Set the y-value accessor function for the delaunay triangulation. */ + y: (d: Datum) => number; +} + +/** + * Returns a configured d3 delaunay triangulation. See d3-delaunay for the complete API reference. + */ +export default function delaunay({ data = [], x, y }: Config) { + return Delaunay.from(data, x, y); +} diff --git a/packages/visx-delaunay/src/index.ts b/packages/visx-delaunay/src/index.ts new file mode 100644 index 000000000..0cbb1a47f --- /dev/null +++ b/packages/visx-delaunay/src/index.ts @@ -0,0 +1,3 @@ +export { default as delaunay } from './delaunay'; +export { default as voronoi } from './voronoi'; +export { default as Polygon } from './components/Polygon'; diff --git a/packages/visx-delaunay/src/voronoi.ts b/packages/visx-delaunay/src/voronoi.ts new file mode 100644 index 000000000..d8e870df0 --- /dev/null +++ b/packages/visx-delaunay/src/voronoi.ts @@ -0,0 +1,30 @@ +import { Delaunay } from '@visx/vendor/d3-delaunay'; + +const CLIP_PADDING = 1; + +interface Config { + /** The data for the voronoi diagram */ + data?: Datum[]; + /** The total width of the voronoi diagram. */ + width?: number; + /** The total width of the voronoi diagram. */ + height?: number; + /** Set the x-value accessor function for the voronoi diagram. */ + x: (d: Datum) => number; + /** Set the y-value accessor function for the voronoi diagram. */ + y: (d: Datum) => number; +} + +/** + * Returns a configured d3 voronoi diagram for the given data. See d3-delaunay + * for the complete API reference. + */ +export default function voronoi({ data = [], width = 0, height = 0, x, y }: Config) { + const delaunay = Delaunay.from(data, x, y); + return delaunay.voronoi([ + -CLIP_PADDING, + -CLIP_PADDING, + width + CLIP_PADDING, + height + CLIP_PADDING, + ]); +} diff --git a/packages/visx-delaunay/test/Polygon.test.tsx b/packages/visx-delaunay/test/Polygon.test.tsx new file mode 100644 index 000000000..73918e176 --- /dev/null +++ b/packages/visx-delaunay/test/Polygon.test.tsx @@ -0,0 +1,35 @@ +import React from 'react'; +import { shallow } from 'enzyme'; + +import { Polygon } from '../src'; + +describe('', () => { + const polygon: [number, number][] = new Array(3).fill(null).map((_, i) => [i, i]); + + const props = { polygon }; + + test('it should be defined', () => { + expect(Polygon).toBeDefined(); + }); + + test('it should not render without a polygon', () => { + const wrapper = shallow(); + expect(wrapper.type()).toBeNull(); + }); + + test('it should render a path', () => { + const wrapper = shallow(); + expect(wrapper.find('path')).toHaveLength(1); + }); + + test('it should set a d attribute based on the polygon prop', () => { + const wrapper = shallow(); + const d = 'M0,0L1,1L2,2Z'; + expect(wrapper.find('path').props().d).toEqual(d); + }); + + test('it should add extra (non-func) props to the path element', () => { + const wrapper = shallow(); + expect(wrapper.find('path').props().fill).toBe('orange'); + }); +}); diff --git a/packages/visx-delaunay/test/delaunay.test.ts b/packages/visx-delaunay/test/delaunay.test.ts new file mode 100644 index 000000000..e5bd5fd30 --- /dev/null +++ b/packages/visx-delaunay/test/delaunay.test.ts @@ -0,0 +1,26 @@ +import { delaunay } from '../src'; + +const data = [ + { x: 10, y: 10 }, + { x: 10, y: 20 }, + { x: 20, y: 20 }, + { x: 20, y: 10 }, +]; + +describe('delaunay', () => { + test('it should be defined', () => { + expect(delaunay).toBeDefined(); + }); + + test('it should find closest point', () => { + const delaunayDiagram = delaunay({ data, x: (d) => d.x, y: (d) => d.y }); + expect(delaunayDiagram.find(9, 11)).toBe(0); + expect(delaunayDiagram.find(11, 19)).toBe(1); + expect(delaunayDiagram.find(21, 19)).toBe(2); + }); + + test('the delaunay triagulation of a square should contain two triangles', () => { + const delaunayDiagram = delaunay({ data, x: (d) => d.x, y: (d) => d.y }); + expect(Array.from(delaunayDiagram.trianglePolygons())).toHaveLength(2); + }); +}); diff --git a/packages/visx-delaunay/test/tsconfig.json b/packages/visx-delaunay/test/tsconfig.json new file mode 100644 index 000000000..a59615e52 --- /dev/null +++ b/packages/visx-delaunay/test/tsconfig.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "composite": false, + "emitDeclarationOnly": false, + "noEmit": true, + "rootDir": "." + }, + "extends": "../../../tsconfig.options.json", + "include": ["**/*", "../types/**/*", "../../../types/**/*"], + "references": [ + { + "path": ".." + } + ] +} diff --git a/packages/visx-delaunay/test/voronoi.test.ts b/packages/visx-delaunay/test/voronoi.test.ts new file mode 100644 index 000000000..bd60180d1 --- /dev/null +++ b/packages/visx-delaunay/test/voronoi.test.ts @@ -0,0 +1,29 @@ +import { voronoi } from '../src'; + +const x = () => 123; +const y = () => 123; + +describe('voronoi', () => { + test('it should be defined', () => { + expect(voronoi).toBeDefined(); + }); + + test('width and height params should define extent', () => { + const width = 17; + const height = 99; + const v = voronoi({ width, height, x, y }); + expect(v.xmin).toBe(-1); + expect(v.ymin).toBe(-1); + expect(v.xmax).toEqual(width + 1); + expect(v.ymax).toEqual(height + 1); + }); + + test('100 random points should give 100 cell polygons', () => { + const data = new Array(100).fill(null).map(() => ({ + x: Math.random(), + y: Math.random(), + })); + const v = voronoi({ data, x: (d) => d.x, y: (d) => d.y }); + expect(Array.from(v.cellPolygons())).toHaveLength(100); + }); +}); diff --git a/packages/visx-delaunay/tsconfig.json b/packages/visx-delaunay/tsconfig.json new file mode 100644 index 000000000..484b21590 --- /dev/null +++ b/packages/visx-delaunay/tsconfig.json @@ -0,0 +1,22 @@ +{ + "compilerOptions": { + "declarationDir": "lib", + "outDir": "lib", + "rootDir": "src" + }, + "exclude": [ + "lib", + "test" + ], + "extends": "../../tsconfig.options.json", + "include": [ + "src/**/*", + "types/**/*", + "../../types/**/*" + ], + "references": [ + { + "path": "../visx-vendor" + } + ] +} \ No newline at end of file diff --git a/packages/visx-demo/package.json b/packages/visx-demo/package.json index b97cdba6a..eb729fdf1 100644 --- a/packages/visx-demo/package.json +++ b/packages/visx-demo/package.json @@ -40,6 +40,7 @@ "@visx/chord": "3.0.0", "@visx/clip-path": "3.0.0", "@visx/curve": "3.0.0", + "@visx/delaunay": "1.0.0", "@visx/drag": "3.0.1", "@visx/event": "3.0.1", "@visx/geo": "3.0.0", diff --git a/packages/visx-demo/public/static/docs/visx-demo.html b/packages/visx-demo/public/static/docs/visx-demo.html deleted file mode 100644 index 70129df3e..000000000 --- a/packages/visx-demo/public/static/docs/visx-demo.html +++ /dev/null @@ -1,45 +0,0 @@ - - - - - - - - - \ No newline at end of file diff --git a/packages/visx-demo/src/components/Gallery/DelaunayTriangulationTile.tsx b/packages/visx-demo/src/components/Gallery/DelaunayTriangulationTile.tsx new file mode 100644 index 000000000..82be75d45 --- /dev/null +++ b/packages/visx-demo/src/components/Gallery/DelaunayTriangulationTile.tsx @@ -0,0 +1,27 @@ +import React from 'react'; +import Delaunay, { + DelaunayTriangulationProps, +} from '../../sandboxes/visx-delaunay-triangulation/Example'; +import GalleryTile from '../GalleryTile'; + +export { default as packageJson } from '../../sandboxes/visx-delaunay-triangulation/package.json'; + +const tileStyles = { + background: 'black', + borderRadius: 14, + boxShadow: 'rgba(0, 0, 0, 0.1) 0px 1px 6px', +}; +const detailsStyles = { background: 'white', color: '#5B247A', borderRadius: '0 0 14px 14px' }; + +export default function DelaunayTriangulationTile() { + return ( + + title="Delaunay Triangulation" + description="" + exampleRenderer={Delaunay} + exampleUrl="/delaunay-triangulation" + tileStyles={tileStyles} + detailsStyles={detailsStyles} + /> + ); +} diff --git a/packages/visx-demo/src/components/Gallery/DelaunayVoronoiTile.tsx b/packages/visx-demo/src/components/Gallery/DelaunayVoronoiTile.tsx new file mode 100644 index 000000000..3d1d6f1ac --- /dev/null +++ b/packages/visx-demo/src/components/Gallery/DelaunayVoronoiTile.tsx @@ -0,0 +1,25 @@ +import React from 'react'; +import Voronoi, { VoronoiProps } from '../../sandboxes/visx-delaunay-voronoi/Example'; +import GalleryTile from '../GalleryTile'; + +export { default as packageJson } from '../../sandboxes/visx-delaunay-voronoi/package.json'; + +const tileStyles = { + background: '#eb6d88', + borderRadius: 14, + boxShadow: 'rgba(0, 0, 0, 0.1) 0px 1px 6px', +}; +const detailsStyles = { background: 'white', color: '#eb6d88', borderRadius: '0 0 14px 14px' }; + +export default function DelaunayTile() { + return ( + + title="Voronoi Overlay" + description="" + exampleRenderer={Voronoi} + exampleUrl="/delaunay-voronoi" + tileStyles={tileStyles} + detailsStyles={detailsStyles} + /> + ); +} diff --git a/packages/visx-demo/src/components/Gallery/index.tsx b/packages/visx-demo/src/components/Gallery/index.tsx index c941fb02a..d54d24e4b 100644 --- a/packages/visx-demo/src/components/Gallery/index.tsx +++ b/packages/visx-demo/src/components/Gallery/index.tsx @@ -15,6 +15,8 @@ import * as BarsTile from './BarsTile'; import * as BrushTile from './BrushTile'; import * as ChordTile from './ChordTile'; import * as CurvesTile from './CurvesTile'; +import * as DelaunayTile from './DelaunayTriangulationTile'; +import * as DelaunayVoronoiTile from './DelaunayVoronoiTile'; import * as DendrogramsTile from './DendrogramsTile'; import * as DotsTile from './DotsTile'; import * as DragIITile from './DragIITile'; @@ -44,7 +46,6 @@ import * as ThresholdTile from './ThresholdTile'; import * as TooltipTile from './TooltipTile'; import * as TreemapTile from './TreemapTile'; import * as TreesTile from './TreesTile'; -import * as VoronoiTile from './VoronoiTile'; import * as WordcloudTile from './WordcloudTile'; import * as XYChartTile from './XYChartTile'; import * as ZoomITile from './ZoomITile'; @@ -58,6 +59,8 @@ const tiltOptions = { max: 8, scale: 1 }; export const tiles = [ CurvesTile, BarsTile, + DelaunayTile, + DelaunayVoronoiTile, DotsTile, PatternsTile, AreaTile, @@ -98,7 +101,6 @@ export const tiles = [ TextTile, TooltipTile, TreesTile, - VoronoiTile, WordcloudTile, ]; diff --git a/packages/visx-demo/src/components/PackageList.tsx b/packages/visx-demo/src/components/PackageList.tsx index 4933234c0..4618c8187 100644 --- a/packages/visx-demo/src/components/PackageList.tsx +++ b/packages/visx-demo/src/components/PackageList.tsx @@ -159,6 +159,12 @@ export default function PackageList({ {!compact &&

Enable selection of a part of an interface

} +
  • + + delaunay + + {!compact &&

    Partition points in a chart to improve user interaction

    } +
  • drag diff --git a/packages/visx-demo/src/pages/delaunay-triangulation.tsx b/packages/visx-demo/src/pages/delaunay-triangulation.tsx new file mode 100644 index 000000000..1d2149011 --- /dev/null +++ b/packages/visx-demo/src/pages/delaunay-triangulation.tsx @@ -0,0 +1,26 @@ +import React from 'react'; +import DelaunayTriangulation from '../sandboxes/visx-delaunay-triangulation/Example'; +import packageJson from '../sandboxes/visx-delaunay-triangulation/package.json'; +import Show from '../components/Show'; +import DelaunayTriangulationSource from '!!raw-loader!../sandboxes/visx-delaunay-triangulation/Example'; + +function DelaunayTriangulationPage() { + return ( + + {DelaunayTriangulationSource} + + ); +} +export default DelaunayTriangulationPage; diff --git a/packages/visx-demo/src/pages/delaunay-voronoi.tsx b/packages/visx-demo/src/pages/delaunay-voronoi.tsx new file mode 100644 index 000000000..c31d3e40a --- /dev/null +++ b/packages/visx-demo/src/pages/delaunay-voronoi.tsx @@ -0,0 +1,26 @@ +import React from 'react'; +import VoronoiChart from '../sandboxes/visx-delaunay-voronoi/Example'; +import packageJson from '../sandboxes/visx-delaunay-voronoi/package.json'; +import Show from '../components/Show'; +import VoronoiChartSource from '!!raw-loader!../sandboxes/visx-delaunay-voronoi/Example'; + +function DelaunayVoronoiPage() { + return ( + + {VoronoiChartSource} + + ); +} +export default DelaunayVoronoiPage; diff --git a/packages/visx-demo/src/pages/docs/delaunay.tsx b/packages/visx-demo/src/pages/docs/delaunay.tsx new file mode 100644 index 000000000..b400c6743 --- /dev/null +++ b/packages/visx-demo/src/pages/docs/delaunay.tsx @@ -0,0 +1,24 @@ +import React from 'react'; +import DelaunayReadme from '!!raw-loader!../../../../visx-delaunay/Readme.md'; +import Polygon from '../../../../visx-delaunay/src/components/Polygon'; +import delaunay from '../../../../visx-delaunay/src/delaunay'; +import voronoi from '../../../../visx-delaunay/src/voronoi'; +import DocPage from '../../components/DocPage'; +import DelaunayTriangulationTile from '../../components/Gallery/DelaunayTriangulationTile'; +import DelaunayVoronoiTile from '../../components/Gallery/DelaunayVoronoiTile'; + +const components = [delaunay, voronoi, Polygon]; + +const examples = [DelaunayVoronoiTile, DelaunayTriangulationTile]; + +function DelaunayDocs() { + return ( + + ); +} +export default DelaunayDocs; diff --git a/packages/visx-demo/src/sandboxes/exampleToVisxDependencyLookup.ts b/packages/visx-demo/src/sandboxes/exampleToVisxDependencyLookup.ts index 82e8c8b2d..78dccd499 100644 --- a/packages/visx-demo/src/sandboxes/exampleToVisxDependencyLookup.ts +++ b/packages/visx-demo/src/sandboxes/exampleToVisxDependencyLookup.ts @@ -9,6 +9,8 @@ import barstackHorizontalPackageJson from './visx-barstack-horizontal/package.js import brushPackageJson from './visx-brush/package.json'; import chordPackageJson from './visx-chord/package.json'; import curvePackageJson from './visx-curve/package.json'; +import delaunayPackageJson from './visx-delaunay-triangulation/package.json'; +import delaunayVoronoiPackageJson from './visx-delaunay-voronoi/package.json'; import dendrogramPackageJson from './visx-dendrogram/package.json'; import dotsPackageJson from './visx-dots/package.json'; import dragIPackageJson from './visx-drag-i/package.json'; @@ -55,6 +57,8 @@ const examples = [ brushPackageJson, chordPackageJson, curvePackageJson, + delaunayPackageJson, + delaunayVoronoiPackageJson, dendrogramPackageJson, dotsPackageJson, dragIIPackageJson, diff --git a/packages/visx-demo/src/sandboxes/visx-delaunay-triangulation/Example.tsx b/packages/visx-demo/src/sandboxes/visx-delaunay-triangulation/Example.tsx new file mode 100644 index 000000000..72e13d20d --- /dev/null +++ b/packages/visx-demo/src/sandboxes/visx-delaunay-triangulation/Example.tsx @@ -0,0 +1,108 @@ +import React, { useState, useMemo, useRef } from 'react'; +import { Group } from '@visx/group'; +import { GradientPurpleTeal as Gradient } from '@visx/gradient'; +import { RectClipPath } from '@visx/clip-path'; +import { delaunay, Polygon } from '@visx/delaunay'; +import { localPoint } from '@visx/event'; +import { getSeededRandom } from '@visx/mock-data'; + +type Datum = { + x: number; + y: number; + id: string; +}; + +const seededRandom = getSeededRandom(0.88); + +const data: Datum[] = new Array(150).fill(null).map(() => ({ + x: seededRandom(), + y: seededRandom(), + id: Math.random().toString(36).slice(2), +})); + +const defaultMargin = { + top: 16, + left: 16, + right: 16, + bottom: 92, +}; + +export type DelaunayTriangulationProps = { + width: number; + height: number; + margin?: { top: number; right: number; bottom: number; left: number }; +}; + +function Example({ width, height, margin = defaultMargin }: DelaunayTriangulationProps) { + const innerWidth = width - margin.left - margin.right; + const innerHeight = height - margin.top - margin.bottom; + + const delaunayDiagram = useMemo( + () => + delaunay({ + data, + x: (d) => d.x * innerWidth, + y: (d) => d.y * innerHeight, + }), + [innerWidth, innerHeight], + ); + const triangles = Array.from(delaunayDiagram.trianglePolygons()); + + const svgRef = useRef(null); + const [hoveredId, setHoveredId] = useState(null); + + return width < 10 ? null : ( +
    + + + + { + if (!svgRef.current) return; + + // find the nearest point to the current mouse position. + const point = localPoint(svgRef.current, event); + if (!point) return; + + const closest = delaunayDiagram.find(point.x - margin.left, point.y - margin.top); + setHoveredId(data[closest].id); + }} + onMouseLeave={() => { + setHoveredId(null); + }} + > + {triangles.map((triangle, i) => ( + + ))} + {data.map(({ x, y, id }) => ( + + ))} + + + +
    + ); +} + +export default Example; diff --git a/packages/visx-demo/src/sandboxes/visx-delaunay-triangulation/index.tsx b/packages/visx-demo/src/sandboxes/visx-delaunay-triangulation/index.tsx new file mode 100644 index 000000000..537d244f5 --- /dev/null +++ b/packages/visx-demo/src/sandboxes/visx-delaunay-triangulation/index.tsx @@ -0,0 +1,12 @@ +import React from 'react'; +import { createRoot } from 'react-dom/client'; +import ParentSize from '@visx/responsive/lib/components/ParentSize'; + +import Example from './Example'; +import './sandbox-styles.css'; + +const root = createRoot(document.getElementById('root')!); + +root.render( + {({ width, height }) => }, +); diff --git a/packages/visx-demo/src/sandboxes/visx-delaunay-triangulation/package.json b/packages/visx-demo/src/sandboxes/visx-delaunay-triangulation/package.json new file mode 100644 index 000000000..a917a1beb --- /dev/null +++ b/packages/visx-demo/src/sandboxes/visx-delaunay-triangulation/package.json @@ -0,0 +1,30 @@ +{ + "name": "@visx/demo-delaunay-triangulation", + "description": "Standalone visx delaunay triangulation demo.", + "main": "index.tsx", + "private": true, + "dependencies": { + "@babel/runtime": "^7.8.4", + "@types/react": "^18", + "@types/react-dom": "^18", + "@visx/clip-path": "latest", + "@visx/delaunay": "latest", + "@visx/event": "latest", + "@visx/gradient": "latest", + "@visx/group": "latest", + "@visx/mock-data": "latest", + "@visx/responsive": "latest", + "react": "^18", + "react-dom": "^18", + "react-scripts-ts": "3.1.0", + "typescript": "^3" + }, + "keywords": [ + "visualization", + "d3", + "react", + "visx", + "delaunay", + "triangulation" + ] +} diff --git a/packages/visx-demo/src/sandboxes/visx-delaunay-triangulation/sandbox-styles.css b/packages/visx-demo/src/sandboxes/visx-delaunay-triangulation/sandbox-styles.css new file mode 100644 index 000000000..b91993723 --- /dev/null +++ b/packages/visx-demo/src/sandboxes/visx-delaunay-triangulation/sandbox-styles.css @@ -0,0 +1,8 @@ +html, +body, +#root { + height: 100%; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, + 'Open Sans', 'Helvetica Neue', sans-serif; + line-height: 2em; +} diff --git a/packages/visx-demo/src/sandboxes/visx-delaunay-voronoi/Example.tsx b/packages/visx-demo/src/sandboxes/visx-delaunay-voronoi/Example.tsx new file mode 100644 index 000000000..5f54485e1 --- /dev/null +++ b/packages/visx-demo/src/sandboxes/visx-delaunay-voronoi/Example.tsx @@ -0,0 +1,114 @@ +import React, { useState, useMemo, useRef } from 'react'; +import { Group } from '@visx/group'; +import { GradientOrangeRed, GradientPinkRed } from '@visx/gradient'; +import { RectClipPath } from '@visx/clip-path'; +import { voronoi, Polygon } from '@visx/delaunay'; +import { localPoint } from '@visx/event'; +import { getSeededRandom } from '@visx/mock-data'; + +type Datum = { + x: number; + y: number; + id: string; +}; + +const seededRandom = getSeededRandom(0.88); + +const data: Datum[] = new Array(150).fill(null).map(() => ({ + x: seededRandom(), + y: seededRandom(), + id: Math.random().toString(36).slice(2), +})); + +const defaultMargin = { + top: 0, + left: 0, + right: 0, + bottom: 76, +}; + +export type VoronoiProps = { + width: number; + height: number; + margin?: { top: number; right: number; bottom: number; left: number }; +}; + +function Example({ width, height, margin = defaultMargin }: VoronoiProps) { + const innerWidth = width - margin.left - margin.right; + const innerHeight = height - margin.top - margin.bottom; + + const voronoiDiagram = useMemo( + () => + voronoi({ + data, + x: (d) => d.x * innerWidth, + y: (d) => d.y * innerHeight, + width: innerWidth, + height: innerHeight, + }), + [innerWidth, innerHeight], + ); + + const svgRef = useRef(null); + const [hoveredId, setHoveredId] = useState(null); + const [neighborIds, setNeighborIds] = useState>(new Set()); + + return width < 10 ? null : ( + + + + + { + if (!svgRef.current) return; + + // find the nearest polygon to the current mouse position + const point = localPoint(svgRef.current, event); + if (!point) return; + + const closest = voronoiDiagram.delaunay.find(point.x, point.y); + // find neighboring polygons to hightlight + if (closest && data[closest].id !== hoveredId) { + const neighbors = Array.from(voronoiDiagram.neighbors(closest)); + setNeighborIds(new Set(neighbors.map((d) => data[d].id))); + setHoveredId(data[closest].id); + } + }} + onMouseLeave={() => { + setHoveredId(null); + setNeighborIds(new Set()); + }} + > + {data.map((d, i) => ( + + ))} + {data.map(({ x, y, id }) => ( + + ))} + + + ); +} + +export default Example; diff --git a/packages/visx-demo/src/sandboxes/visx-delaunay-voronoi/index.tsx b/packages/visx-demo/src/sandboxes/visx-delaunay-voronoi/index.tsx new file mode 100644 index 000000000..537d244f5 --- /dev/null +++ b/packages/visx-demo/src/sandboxes/visx-delaunay-voronoi/index.tsx @@ -0,0 +1,12 @@ +import React from 'react'; +import { createRoot } from 'react-dom/client'; +import ParentSize from '@visx/responsive/lib/components/ParentSize'; + +import Example from './Example'; +import './sandbox-styles.css'; + +const root = createRoot(document.getElementById('root')!); + +root.render( + {({ width, height }) => }, +); diff --git a/packages/visx-demo/src/sandboxes/visx-delaunay-voronoi/package.json b/packages/visx-demo/src/sandboxes/visx-delaunay-voronoi/package.json new file mode 100644 index 000000000..c56e3815e --- /dev/null +++ b/packages/visx-demo/src/sandboxes/visx-delaunay-voronoi/package.json @@ -0,0 +1,30 @@ +{ + "name": "@visx/demo-delaunay-voronoi", + "description": "Standalone @visx/delaunay voronoi demo.", + "main": "index.tsx", + "private": true, + "dependencies": { + "@babel/runtime": "^7.8.4", + "@types/react": "^18", + "@types/react-dom": "^18", + "@visx/clip-path": "latest", + "@visx/delaunay": "latest", + "@visx/event": "latest", + "@visx/gradient": "latest", + "@visx/group": "latest", + "@visx/mock-data": "latest", + "@visx/responsive": "latest", + "react": "^18", + "react-dom": "^18", + "react-scripts-ts": "3.1.0", + "typescript": "^3" + }, + "keywords": [ + "visualization", + "d3", + "react", + "visx", + "voronoi", + "delaunay" + ] +} diff --git a/packages/visx-demo/src/sandboxes/visx-delaunay-voronoi/sandbox-styles.css b/packages/visx-demo/src/sandboxes/visx-delaunay-voronoi/sandbox-styles.css new file mode 100644 index 000000000..b91993723 --- /dev/null +++ b/packages/visx-demo/src/sandboxes/visx-delaunay-voronoi/sandbox-styles.css @@ -0,0 +1,8 @@ +html, +body, +#root { + height: 100%; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, + 'Open Sans', 'Helvetica Neue', sans-serif; + line-height: 2em; +} diff --git a/packages/visx-demo/src/types/index.ts b/packages/visx-demo/src/types/index.ts index d6a8d06d0..d4255da8d 100644 --- a/packages/visx-demo/src/types/index.ts +++ b/packages/visx-demo/src/types/index.ts @@ -25,6 +25,7 @@ export type VisxPackage = | 'chord' | 'clip-path' | 'curve' + | 'delaunay' | 'drag' | 'event' | 'geo' diff --git a/packages/visx-demo/tsconfig.json b/packages/visx-demo/tsconfig.json index 45cf0de4c..585e8d841 100644 --- a/packages/visx-demo/tsconfig.json +++ b/packages/visx-demo/tsconfig.json @@ -51,6 +51,9 @@ { "path": "../visx-curve" }, + { + "path": "../visx-delaunay" + }, { "path": "../visx-drag" }, diff --git a/packages/visx-vendor/package.json b/packages/visx-vendor/package.json index acbde173c..e0e319736 100644 --- a/packages/visx-vendor/package.json +++ b/packages/visx-vendor/package.json @@ -15,6 +15,7 @@ "dependencies": { "@types/d3-array": "3.0.3", "@types/d3-color": "3.1.0", + "@types/d3-delaunay": "6.0.1", "@types/d3-format": "3.0.1", "@types/d3-interpolate": "3.0.1", "@types/d3-scale": "4.0.2", @@ -22,6 +23,7 @@ "@types/d3-time-format": "2.1.0", "d3-array": "3.2.1", "d3-color": "3.1.0", + "d3-delaunay": "6.0.2", "d3-format": "3.1.0", "d3-interpolate": "3.0.1", "d3-scale": "4.0.2", diff --git a/packages/visx-visx/package.json b/packages/visx-visx/package.json index 7c67f3ee9..cffd3d673 100644 --- a/packages/visx-visx/package.json +++ b/packages/visx-visx/package.json @@ -39,6 +39,7 @@ "@visx/brush": "3.2.0", "@visx/clip-path": "3.0.0", "@visx/curve": "3.0.0", + "@visx/delaunay": "1.0.0", "@visx/drag": "3.0.1", "@visx/event": "3.0.1", "@visx/geo": "3.0.0", diff --git a/packages/visx-visx/tsconfig.json b/packages/visx-visx/tsconfig.json index 2ec3dc58f..4d4c129e9 100644 --- a/packages/visx-visx/tsconfig.json +++ b/packages/visx-visx/tsconfig.json @@ -33,6 +33,9 @@ { "path": "../visx-curve" }, + { + "path": "../visx-delaunay" + }, { "path": "../visx-drag" }, diff --git a/yarn.lock b/yarn.lock index b2d65e91b..fea3903ad 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2683,6 +2683,11 @@ resolved "https://registry.yarnpkg.com/@types/d3-color/-/d3-color-3.1.0.tgz#6594da178ded6c7c3842f3cc0ac84b156f12f2d4" integrity sha512-HKuicPHJuvPgCD+np6Se9MQvS6OCbJmOjGvylzMJRlDwUXjKTTXs6Pwgk79O09Vj/ho3u1ofXnhFOaEWWPrlwA== +"@types/d3-delaunay@6.0.1": + version "6.0.1" + resolved "https://registry.yarnpkg.com/@types/d3-delaunay/-/d3-delaunay-6.0.1.tgz#006b7bd838baec1511270cb900bf4fc377bbbf41" + integrity sha512-tLxQ2sfT0p6sxdG75c6f/ekqxjyYR0+LwPrsO1mbC9YDBzPJhs2HbJJRrn8Ez1DBoHRo2yx7YEATI+8V1nGMnQ== + "@types/d3-format@3.0.1": version "3.0.1" resolved "https://registry.yarnpkg.com/@types/d3-format/-/d3-format-3.0.1.tgz#194f1317a499edd7e58766f96735bdc0216bb89d" @@ -5086,6 +5091,13 @@ d3-color@1: resolved "https://registry.yarnpkg.com/d3-color/-/d3-color-3.1.0.tgz#395b2833dfac71507f12ac2f7af23bf819de24e2" integrity sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA== +d3-delaunay@6.0.2: + version "6.0.2" + resolved "https://registry.yarnpkg.com/d3-delaunay/-/d3-delaunay-6.0.2.tgz#7fd3717ad0eade2fc9939f4260acfb503f984e92" + integrity sha512-IMLNldruDQScrcfT+MWnazhHbDJhcRJyOEBAJfwQnHle1RPh6WDuLvxNArUju2VSMSUuKlY5BGHRJ2cYyoFLQQ== + dependencies: + delaunator "5" + d3-dispatch@^1.0.3: version "1.0.6" resolved "https://registry.yarnpkg.com/d3-dispatch/-/d3-dispatch-1.0.6.tgz#00d37bcee4dd8cd97729dd893a0ac29caaba5d58" @@ -5372,6 +5384,13 @@ define-property@^2.0.2: is-descriptor "^1.0.2" isobject "^3.0.1" +delaunator@5: + version "5.0.0" + resolved "https://registry.yarnpkg.com/delaunator/-/delaunator-5.0.0.tgz#60f052b28bd91c9b4566850ebf7756efe821d81b" + integrity sha512-AyLvtyJdbv/U1GkiS6gUUzclRoAY4Gs75qkMygJJhU75LW4DNuSF2RMzpxs9jw9Oz1BobHjTdkG3zdP55VxAqw== + dependencies: + robust-predicates "^3.0.0" + delayed-stream@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" @@ -12093,6 +12112,11 @@ ripemd160@^2.0.0, ripemd160@^2.0.1: hash-base "^3.0.0" inherits "^2.0.1" +robust-predicates@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/robust-predicates/-/robust-predicates-3.0.1.tgz#ecde075044f7f30118682bd9fb3f123109577f9a" + integrity sha512-ndEIpszUHiG4HtDsQLeIuMvRsDnn8c8rYStabochtUeCvfuvNptb5TUbVD68LRAILPX7p9nqQGh4xJgn3EHS/g== + rst-selector-parser@^2.2.3: version "2.2.3" resolved "https://registry.yarnpkg.com/rst-selector-parser/-/rst-selector-parser-2.2.3.tgz#81b230ea2fcc6066c89e3472de794285d9b03d91"