Skip to content

Commit

Permalink
Create new @visx/delaunay package (#1678)
Browse files Browse the repository at this point in the history
* feat(delaunay): Initial setup for the new visx-delaunay package

* feat(delaunay): Working Voronoi polygon example

* feat(delaunay): Add delaunay triangulation example

* feat(delaunay): Add examples to gallery

* feat(delaunay): Add unit tests

* feat(delaunay): Fix demo pages

* feat(delaunay): Add margin to examples and fix lint errors

* feat(delaunay): Cleanup and renaming

* feat(delaunay); Update TS references

* feat(delaunay): Add ESM-only dependencies to next.config.ts

* feat(delaunay): Add ESM-only dependencies to next.config.ts

* Trigger build 1

* feat(delaunay): Address pull request comments

* Change d3-delaunay references to @visx/vendor package.

* Delete unnecessary yarn.lock file and visx-demo.html

* Update tsconfig references.

* Fix delaunay triangulation closest point calculation.

* Update packages sizes.

* Remove Voronoi tile from gallery and update jest config.

* Retrigger build
  • Loading branch information
SheaJanke authored Jul 11, 2023
1 parent 298b7bd commit 620ecd7
Show file tree
Hide file tree
Showing 37 changed files with 811 additions and 48 deletions.
2 changes: 1 addition & 1 deletion jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,6 @@ module.exports = {
verbose: false,
testPathIgnorePatterns: ['<rootDir>/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)/)',
],
};
1 change: 1 addition & 0 deletions packages/visx-delaunay/.npmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
package-lock=false
67 changes: 67 additions & 0 deletions packages/visx-delaunay/Readme.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
# @visx/delaunay

<a title="@visx/delaunay npm downloads" href="https://www.npmjs.com/package/@visx/delaunay">
<img src="https://img.shields.io/npm/dm/@visx/delaunay.svg?style=flat-square" />
</a>

## 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 `<Polygon />` 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 (
<svg>
<Group>
{polygons.map((polygon) => (
<Polygon key={...} polygon={polygon} />
))}
{points.map(({ x, y }) => (
<circle key={...} cx={x} cy={y} />
)}
</Group>
</svg>
)
```
Additional information about the voronoi diagram API can be found in the
[d3-delaunay documentation](https://github.com/d3/d3-delaunay#voronoi).
42 changes: 42 additions & 0 deletions packages/visx-delaunay/package.json
Original file line number Diff line number Diff line change
@@ -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"
}
}
24 changes: 24 additions & 0 deletions packages/visx-delaunay/src/components/Polygon.tsx
Original file line number Diff line number Diff line change
@@ -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<React.SVGProps<SVGPathElement>, keyof PolygonProps>) {
if (!polygon) return null;
const path = `M${polygon.join('L')}Z`;
if (children) return <>{children({ path, polygon })}</>;

return <path className={cx('visx-delaunay-polygon', className)} d={path} {...restProps} />;
}
17 changes: 17 additions & 0 deletions packages/visx-delaunay/src/delaunay.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { Delaunay } from '@visx/vendor/d3-delaunay';

interface Config<Datum> {
/** 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<Datum>({ data = [], x, y }: Config<Datum>) {
return Delaunay.from(data, x, y);
}
3 changes: 3 additions & 0 deletions packages/visx-delaunay/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export { default as delaunay } from './delaunay';
export { default as voronoi } from './voronoi';
export { default as Polygon } from './components/Polygon';
30 changes: 30 additions & 0 deletions packages/visx-delaunay/src/voronoi.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { Delaunay } from '@visx/vendor/d3-delaunay';

const CLIP_PADDING = 1;

interface Config<Datum> {
/** 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<Datum>({ data = [], width = 0, height = 0, x, y }: Config<Datum>) {
const delaunay = Delaunay.from(data, x, y);
return delaunay.voronoi([
-CLIP_PADDING,
-CLIP_PADDING,
width + CLIP_PADDING,
height + CLIP_PADDING,
]);
}
35 changes: 35 additions & 0 deletions packages/visx-delaunay/test/Polygon.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import React from 'react';
import { shallow } from 'enzyme';

import { Polygon } from '../src';

describe('<Polygon />', () => {
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(<Polygon />);
expect(wrapper.type()).toBeNull();
});

test('it should render a path', () => {
const wrapper = shallow(<Polygon {...props} />);
expect(wrapper.find('path')).toHaveLength(1);
});

test('it should set a d attribute based on the polygon prop', () => {
const wrapper = shallow(<Polygon {...props} />);
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(<Polygon {...props} fill="orange" />);
expect(wrapper.find('path').props().fill).toBe('orange');
});
});
26 changes: 26 additions & 0 deletions packages/visx-delaunay/test/delaunay.test.ts
Original file line number Diff line number Diff line change
@@ -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);
});
});
15 changes: 15 additions & 0 deletions packages/visx-delaunay/test/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"compilerOptions": {
"composite": false,
"emitDeclarationOnly": false,
"noEmit": true,
"rootDir": "."
},
"extends": "../../../tsconfig.options.json",
"include": ["**/*", "../types/**/*", "../../../types/**/*"],
"references": [
{
"path": ".."
}
]
}
29 changes: 29 additions & 0 deletions packages/visx-delaunay/test/voronoi.test.ts
Original file line number Diff line number Diff line change
@@ -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);
});
});
22 changes: 22 additions & 0 deletions packages/visx-delaunay/tsconfig.json
Original file line number Diff line number Diff line change
@@ -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"
}
]
}
1 change: 1 addition & 0 deletions packages/visx-demo/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
45 changes: 0 additions & 45 deletions packages/visx-demo/public/static/docs/visx-demo.html

This file was deleted.

Loading

0 comments on commit 620ecd7

Please sign in to comment.