Skip to content

Commit

Permalink
rebase plot.carto
Browse files Browse the repository at this point in the history
  • Loading branch information
Fil committed Dec 31, 2020
1 parent 375cdc8 commit 97e63d1
Show file tree
Hide file tree
Showing 10 changed files with 5,226 additions and 1 deletion.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
"d3-array": "^2.8.0",
"d3-axis": "^2.0.0",
"d3-color": "^2.0.0",
"d3-geo": "2",
"d3-interpolate": "^2.0.1",
"d3-scale": "^3.2.3",
"d3-scale-chromatic": "^2.0.0",
Expand Down
1 change: 1 addition & 0 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ export {AxisX, AxisY} from "./marks/axis.js";
export {BarX, BarY, barX, barY} from "./marks/bar.js";
export {bin, binX, binY} from "./marks/bin.js";
export {Cell, cell, cellX, cellY} from "./marks/cell.js";
export {Carto, carto} from "./marks/carto.js";
export {Dot, dot, dotX, dotY} from "./marks/dot.js";
export {group, groupX, groupY} from "./marks/group.js";
export {Line, line, lineX, lineY} from "./marks/line.js";
Expand Down
88 changes: 88 additions & 0 deletions src/marks/carto.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import {ascending} from "d3-array";
import {geoPath} from "d3-geo";
import {create} from "d3-selection";
import {identity} from "../mark.js";
import {filter, nonempty, positive} from "../defined.js";
import {Mark, number, maybeColor, maybeNumber} from "../mark.js";
import {Style, applyDirectStyles, applyIndirectStyles} from "../style.js";

export class Carto extends Mark {
constructor(
data,
{
feature = identity,
z,
r,
fill,
stroke,
title,
insetTop = 0,
insetRight = 0,
insetBottom = 0,
insetLeft = 0,
transform,
...style
} = {}
) {
const [vr, cr = vr == null ? 3 : undefined] = maybeNumber(r);
const [vfill, cfill = vfill == null ? "none" : undefined] = maybeColor(fill);
const [vstroke, cstroke = vstroke == null && cfill === "none" ? "currentColor" : undefined] = maybeColor(stroke);
super(
data,
[
{name: "d", value: feature, scale: "projection", label: feature.label},
{name: "z", value: z, optional: true},
{name: "r", value: vr, scale: "r", optional: true},
{name: "fill", value: vfill, scale: "color", optional: true},
{name: "stroke", value: vstroke, scale: "color", optional: true},
{name: "title", value: title, optional: true}
],
transform
);
Style(this, {fill: cfill, stroke: cstroke, ...style});
this.r = cr;
this.insetTop = number(insetTop);
this.insetRight = number(insetRight);
this.insetBottom = number(insetBottom);
this.insetLeft = number(insetLeft);
}
render(
I,
{color, r, projection},
{d: D, z: Z, r: R, fill: F, stroke: S, strokeWidth: LW, title: L},
{height, marginBottom, marginLeft, marginRight, marginTop, width}
) {
let index = filter(I, D, F, S);
if (R) index = index.filter(i => positive(R[i]));
if (Z) index.sort((i, j) => ascending(Z[i], Z[j]));

if (projection.fitFeature) {
projection.fitExtent([
[marginLeft, marginTop],
[width - marginRight, height - marginBottom]
], projection.fitFeature);
delete projection.fitFeature;
}
const path = geoPath(projection).pointRadius(this.r);

return create("svg:g")
.call(applyIndirectStyles, this)
.call(g => g.selectAll()
.data(I)
.join("path")
.call(applyDirectStyles, this)
.attr("d", i => (R ? path.pointRadius(r(R[i])) : path)(D[i]))
.attr("fill", F && (i => color(F[i])))
.attr("stroke", S && (i => color(S[i])))
.attr("strokeWidth", LW && (i => LW[i]))
.call(L ? path => path
.filter(i => nonempty(L[i]))
.append("title")
.text(i => L[i]) : () => {}))
.node();
}
}

export function carto(data, options) {
return new Carto(data, options);
}
5 changes: 4 additions & 1 deletion src/scales.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import {registry, position, radius} from "./scales/index.js";
import {registry, projection, position, radius} from "./scales/index.js";
import {ScaleDiverging, ScaleLinear, ScalePow, ScaleLog, ScaleSymlog} from "./scales/quantitative.js";
import {ScaleTime, ScaleUtc} from "./scales/temporal.js";
import {ScaleOrdinal, ScalePoint, ScaleBand} from "./scales/ordinal.js";
import {ScaleProjection} from "./scales/projection.js";

export function Scales(channels, {inset, round, nice, align, padding, ...options} = {}) {
const scales = {};
Expand Down Expand Up @@ -43,6 +44,7 @@ function Scale(key, channels = [], options = {}) {
case "time": return ScaleTime(key, channels, options);
case "point": return ScalePoint(key, channels, options);
case "band": return ScaleBand(key, channels, options);
case "projection": return ScaleProjection(key, channels, options);
case undefined: return;
default: throw new Error(`unknown scale type: ${options.type}`);
}
Expand All @@ -58,6 +60,7 @@ function inferScaleType(key, channels, {type, domain, range}) {
return type;
}
if (registry.get(key) === radius) return "sqrt";
if (registry.get(key) === projection) return "projection";
for (const {type} of channels) if (type !== undefined) return type;
if ((domain || range || []).length > 2) return inferOrdinalType(key);
if (domain !== undefined) {
Expand Down
4 changes: 4 additions & 0 deletions src/scales/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ export const color = Symbol("color");
// default domain from 0 to the median first quartile of associated channels.
export const radius = Symbol("radius");

// ???
export const projection = Symbol("projection");

// TODO Rather than hard-coding the list of known scale names, collect the names
// and categories for each plot specification, so that custom marks can register
// custom scales.
Expand All @@ -20,5 +23,6 @@ export const registry = new Map([
["fx", position],
["fy", position],
["r", radius],
["projection", projection],
["color", color]
]);
19 changes: 19 additions & 0 deletions src/scales/projection.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import {geoMercator, geoOrthographic, geoEqualEarth} from "d3-geo";

// TODO Allow this to be extended.
const projections = new Map([
["mercator", geoMercator],
["orthographic", geoOrthographic],
["equalEarth", geoEqualEarth]
]);

export function ScaleProjection(key, channels, { projection, rotate, precision }) {
// TODO: use the channels to set up the projection's extent
// note: we don't yet know the canvas dimensions…
projection = typeof projection === "function" ? projection
: (projections.get(projection) || geoMercator)();
if (rotate && projection.rotate) projection.rotate(rotate);
if (precision && projection.precision) projection.precision(precision);
projection.fitFeature = channels[0].value[0] || {type:"Sphere"};
return {type: "projection", scale: projection};
}
26 changes: 26 additions & 0 deletions test/base-map.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<!DOCTYPE html>
<meta charset="utf-8">
<body>
<script src="https://cdn.jsdelivr.net/npm/d3@6/dist/d3.js"></script>
<script src="../dist/@observablehq/plot.umd.js"></script>
<script>

const url = "https://unpkg.com/[email protected]/world/110m_land.geojson";

(async function() {
const world = await d3.json(url);

document.body.appendChild(Plot.plot({
marks: [
// 🚀 the very first mark defines the region of interest (TODO: design this)
Plot.carto([{type: "Sphere"}], { fill: "none" }),
Plot.carto([world], { fill: "black" }),
Plot.carto([{type: "Sphere"}], { strokeWidth: 2 }),
],
width: 600,
height: 600,
}));

})();

</script>
36 changes: 36 additions & 0 deletions test/choropleth.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<!DOCTYPE html>
<meta charset="utf-8">
<body>
<script src="https://cdn.jsdelivr.net/npm/d3@6/dist/d3.js"></script>
<script src="../dist/@observablehq/plot.umd.js"></script>
<script>

const url = "https://unpkg.com/[email protected]/world/110m_countries.geojson";

(async function() {
const world = await d3.json(url);

document.body.appendChild(Plot.plot({
projection: {
projection: "equalEarth",
rotate: [-10, 0]
},
color: { scheme: "rdylgn", },
marks: [
Plot.carto([{type: "Sphere"}], { fill: "lightblue" }),
Plot.carto([d3.geoGraticule10()], { stroke: "#fff", strokeWidth: .25 }),
Plot.carto(world.features,
{
fill: d => d.properties.income_grp,
strokeWidth: .25,
stroke: "#000"
}),
Plot.carto([{type: "Sphere"}], { strokeWidth: 2 }),
],
width: document.body.offsetWidth,
height: document.body.offsetWidth / 2,
}));

})();

</script>
46 changes: 46 additions & 0 deletions test/cities-5000.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
<!DOCTYPE html>
<meta charset="utf-8">
<body>
<script src="https://cdn.jsdelivr.net/npm/d3@6/dist/d3.js"></script>
<script src="../dist/@observablehq/plot.umd.js"></script>
<script>

const url = "https://unpkg.com/[email protected]/world/110m_countries.geojson";

(async function() {
const world = await d3.json(url);
const cities = await d3.csv("data/cities-5000.csv");

document.body.appendChild(Plot.plot({
projection: {
projection: "equalEarth",
rotate: [-10, 0]
},
r: { domain: [0, 5e6] },
marks: [
// 🚀 the very first mark defines the region of interest (TODO: design this)
Plot.carto([{type: "Sphere"}], { fill: "none" }),
Plot.carto([world], { fill: "black" }),
Plot.carto(cities, {
transform: data => data.map(({lng, lat, ...properties}) => ({
type: "Point", coordinates: [lng, lat], properties
})),
fill: "orange",
title: d => d.properties.city,
r: d => d.properties.population
}),
Plot.carto([{type: "Sphere"}], { strokeWidth: 2 }),
],
width: 600,
height: 600,
marginLeft: 1,
marginRight: 1,
marginTop: 1,
marginBottom: 1,
width: document.body.offsetWidth,
height: document.body.offsetWidth / 2,
}));

})();

</script>
Loading

0 comments on commit 97e63d1

Please sign in to comment.