Skip to content

Commit

Permalink
[geo] add <CustomProjection />. fixes airbnb#433
Browse files Browse the repository at this point in the history
  • Loading branch information
hshoff committed Mar 14, 2019
1 parent 2330007 commit 11c2b4e
Show file tree
Hide file tree
Showing 8 changed files with 335 additions and 3 deletions.
22 changes: 22 additions & 0 deletions packages/vx-demo/components/gallery.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import Voronoi from './tiles/voronoi';
import Legends from './tiles/legends';
import BoxPlot from './tiles/boxplot';
import GeoMercator from './tiles/geo-mercator';
import GeoCustom from './tiles/geo-custom';
import Network from './tiles/network';
import Streamgraph from './tiles/streamgraph';
import Pack from './tiles/pack';
Expand Down Expand Up @@ -558,6 +559,26 @@ export default class Gallery extends React.Component {
</Link>
</Tilt>

<Tilt className="tilt" options={{ max: 8, scale: 1 }}>
<Link prefetch href="/geo-custom">
<div className="gallery-item" style={{ background: '#252b7e' }}>
<div className="image">
<ParentSize>
{({ width, height }) => (
<GeoCustom width={width} height={height + detailsHeight} />
)}
</ParentSize>
</div>
<div className="details" style={{ color: '#019ece' }}>
<div className="title">Geo</div>
<div className="description">
<pre>{'<Geo.CustomProjection />'}</pre>
</div>
</div>
</div>
</Link>
</Tilt>

<Tilt className="tilt" options={{ max: 8, scale: 1 }}>
<Link prefetch href="/network">
<div className="gallery-item" style={{ background: '#272b4d' }}>
Expand Down Expand Up @@ -922,6 +943,7 @@ export default class Gallery extends React.Component {
</Link>
</Tilt>
<div className="gallery-item placeholder" />
<div className="gallery-item placeholder" />
</div>

<div>
Expand Down
144 changes: 144 additions & 0 deletions packages/vx-demo/components/tiles/geo-custom.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
import React from 'react';
import * as topojson from 'topojson-client';
import { scaleQuantize } from '@vx/scale';
import { CustomProjection, Graticule } from '@vx/geo';
import {
geoConicConformal,
geoTransverseMercator,
geoNaturalEarth1,
geoConicEquidistant,
geoOrthographic,
geoStereographic
} from 'd3-geo';
import topology from '../../static/vx-geo/world-topo.json';

const bg = '#252b7e';
const purple = '#201c4e';

const world = topojson.feature(topology, topology.objects.units);
const color = scaleQuantize({
domain: [
Math.min(...world.features.map(f => f.geometry.coordinates.length)),
Math.max(...world.features.map(f => f.geometry.coordinates.length))
],
range: [
'#019ece',
'#f4448b',
'#fccf35',
'#82b75d',
'#b33c88',
'#fc5e2f',
'#f94b3a',
'#f63a48',
'#dde1fe',
'#8993f9',
'#b6c8fb',
'#65fe8d'
]
});

export default class GeoCustom extends React.Component {
constructor(props) {
super(props);
this.state = { projection: geoConicConformal, scaleFactor: 630 };
this.handleSelect = this.handleSelect.bind(this);
this.handleSlider = this.handleSlider.bind(this);
}

handleSelect(event) {
const { value } = event.target;
let nextProjection;
if (value === '1') {
nextProjection = geoConicConformal;
} else if (value === '2') {
nextProjection = geoTransverseMercator;
} else if (value === '3') {
nextProjection = geoNaturalEarth1;
} else if (value === '4') {
nextProjection = geoOrthographic;
} else if (value === '5') {
nextProjection = geoStereographic;
} else if (value === '6') {
nextProjection = geoConicEquidistant;
}
this.setState({ projection: nextProjection });
}

handleSlider(event) {
const { value } = event.target;
this.setState({ scaleFactor: value });
}

render() {
const { width, height, events = false } = this.props;
const { projection, scaleFactor } = this.state;
if (width < 10) return <div />;

const centerX = width / 2;
const centerY = height / 2;
const scale = (width / scaleFactor) * 100;

return (
<div>
<svg width={width} height={height}>
<rect x={0} y={0} width={width} height={height} fill={bg} rx={14} />
<CustomProjection
projection={projection}
data={world.features}
scale={scale}
translate={[centerX, centerY]}
>
{customProjection => {
return (
<g>
<Graticule graticule={g => customProjection.path(g)} stroke={purple} />
{customProjection.features.map((feature, i) => {
const { feature: f } = feature;
return (
<path
key={`map-feature-${i}`}
d={feature.path}
fill={color(f.geometry.coordinates.length)}
stroke={bg}
strokeWidth={0.5}
onClick={event => {
if (!events) return;
alert(`Clicked: ${f.properties.name} (${f.id})`);
}}
/>
);
})}
</g>
);
}}
</CustomProjection>
</svg>
<div>
<label>
projection:{' '}
<select onChange={this.handleSelect}>
<option value={1}>geoConicConformal</option>
<option value={2}>geoTransverseMercator</option>
<option value={3}>geoNaturalEarth1</option>
<option value={4}>geoOrthographic</option>
<option value={5}>geoStereographic</option>
<option value={6}>geoConicEquidistant</option>
</select>
</label>
<label>
{' '}
scale factor:{' '}
<input
onChange={this.handleSlider}
type="range"
defaultValue={scaleFactor}
max="1000"
min="100"
step={10}
/>
</label>
</div>
</div>
);
}
}
2 changes: 1 addition & 1 deletion packages/vx-demo/components/tiles/geo-mercator.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ export default ({ width, height, events = false }) => {

const centerX = width / 2;
const centerY = height / 2;
const scale = width / 630 * 100;
const scale = (width / 630) * 100;

return (
<svg width={width} height={height}>
Expand Down
1 change: 1 addition & 0 deletions packages/vx-demo/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@
"d3-array": "^1.1.1",
"d3-collection": "^1.0.4",
"d3-format": "^1.2.0",
"d3-geo": "^1.11.3",
"d3-hierarchy": "^1.1.4",
"d3-interpolate": "^1.1.5",
"d3-scale": "^1.0.6",
Expand Down
154 changes: 154 additions & 0 deletions packages/vx-demo/pages/geo-custom.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
import React from 'react';
import Show from '../components/show';
import GeoCustom from '../components/tiles/geo-custom';

export default () => {
return (
<Show events component={GeoCustom} title="Geo Custom">
{`import React from 'react';
import * as topojson from 'topojson-client';
import { scaleQuantize } from '@vx/scale';
import { CustomProjection, Graticule } from '@vx/geo';
import {
geoConicConformal,
geoTransverseMercator,
geoNaturalEarth1,
geoConicEquidistant,
geoOrthographic,
geoStereographic
} from 'd3-geo';
import topology from '../../static/vx-geo/world-topo.json';
const bg = '#252b7e';
const purple = '#201c4e';
const world = topojson.feature(topology, topology.objects.units);
const color = scaleQuantize({
domain: [
Math.min(...world.features.map(f => f.geometry.coordinates.length)),
Math.max(...world.features.map(f => f.geometry.coordinates.length))
],
range: [
'#019ece',
'#f4448b',
'#fccf35',
'#82b75d',
'#b33c88',
'#fc5e2f',
'#f94b3a',
'#f63a48',
'#dde1fe',
'#8993f9',
'#b6c8fb',
'#65fe8d'
]
});
export default class GeoCustom extends React.Component {
constructor(props) {
super(props);
this.state = { projection: geoConicConformal, scaleFactor: 630 };
this.handleSelect = this.handleSelect.bind(this);
this.handleSlider = this.handleSlider.bind(this);
}
handleSelect(event) {
const { value } = event.target;
let nextProjection;
if (value === '1') {
nextProjection = geoConicConformal;
} else if (value === '2') {
nextProjection = geoTransverseMercator;
} else if (value === '3') {
nextProjection = geoNaturalEarth1;
} else if (value === '4') {
nextProjection = geoOrthographic;
} else if (value === '5') {
nextProjection = geoStereographic;
} else if (value === '6') {
nextProjection = geoConicEquidistant;
}
this.setState({ projection: nextProjection });
}
handleSlider(event) {
const { value } = event.target;
this.setState({ scaleFactor: value });
}
render() {
const { width, height, events = false } = this.props;
const { projection, scaleFactor } = this.state;
const centerX = width / 2;
const centerY = height / 2;
const scale = (width / scaleFactor) * 100;
return (
<div>
<svg width={width} height={height}>
<rect x={0} y={0} width={width} height={height} fill={bg} rx={14} />
<CustomProjection
projection={projection}
data={world.features}
scale={scale}
translate={[centerX, centerY]}
>
{customProjection => {
return (
<g>
<Graticule graticule={g => customProjection.path(g)} stroke={purple} />
{customProjection.features.map((feature, i) => {
const { feature: f } = feature;
return (
<path
key={\`map-feature-\${i}\`}
d={feature.path}
fill={color(f.geometry.coordinates.length)}
stroke={bg}
strokeWidth={0.5}
onClick={event => {
alert(\`Clicked: \${f.properties.name} (\${f.id})\`);
}}
/>
);
})}
</g>
);
}}
</CustomProjection>
</svg>
<div>
<label>
projection:{' '}
<select onChange={this.handleSelect}>
<option value={1}>geoConicConformal</option>
<option value={2}>geoTransverseMercator</option>
<option value={3}>geoNaturalEarth1</option>
<option value={4}>geoOrthographic</option>
<option value={5}>geoStereographic</option>
<option value={6}>geoConicEquidistant</option>
</select>
</label>
<label>
{' '}
scale factor:{' '}
<input
onChange={this.handleSlider}
type="range"
defaultValue={scaleFactor}
max="1000"
min="100"
step={10}
/>
</label>
</div>
</div>
);
}
}
`}
</Show>
);
};
1 change: 1 addition & 0 deletions packages/vx-geo/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,5 @@ export { default as Mercator } from './projections/Mercator';
export { default as Orthographic } from './projections/Orthographic';
export { default as NaturalEarth } from './projections/NaturalEarth';
export { default as EqualEarth } from './projections/EqualEarth';
export { default as CustomProjection } from './projections/CustomProjection';
export { default as Graticule } from './graticule/Graticule';
9 changes: 9 additions & 0 deletions packages/vx-geo/src/projections/CustomProjection.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import React from 'react';
import Projection from './Projection';

/**
* All props pass through to `<Projection projection={customProjection} {...props} />`
*/
export default function CustomProjection(props) {
return <Projection {...props} />;
}
5 changes: 3 additions & 2 deletions packages/vx-geo/src/projections/Projection.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ const projectionMapping = {

Projection.propTypes = {
data: PropTypes.array.isRequired,
projection: PropTypes.string,
projection: PropTypes.oneOfType([PropTypes.string, PropTypes.func]),
projectionFunc: PropTypes.func,
clipAngle: PropTypes.number,
clipExtent: PropTypes.array,
Expand Down Expand Up @@ -67,7 +67,8 @@ export default function Projection({
children,
...restProps
}) {
const currProjection = projectionMapping[projection]();
const maybeCustomProjection = projectionMapping[projection] || projection;
const currProjection = maybeCustomProjection();

if (clipAngle) currProjection.clipAngle(clipAngle);
if (clipExtent) currProjection.clipExtent(clipExtent);
Expand Down

0 comments on commit 11c2b4e

Please sign in to comment.