Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 2 additions & 3 deletions src/dimensions.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ export function createDimensions(scales, marks, options = {}) {
// specified explicitly, adjust the automatic height accordingly.
let {
width = 640,
height = autoHeight(scales, marks, options, {
height = autoHeight(scales, options, {
width,
marginTopDefault,
marginRightDefault,
Expand Down Expand Up @@ -89,14 +89,13 @@ export function createDimensions(scales, marks, options = {}) {

function autoHeight(
{x, y, fy, fx},
marks,
{projection, aspectRatio},
{width, marginTopDefault, marginRightDefault, marginBottomDefault, marginLeftDefault}
) {
const nfy = fy ? fy.scale.domain().length : 1;

// If a projection is specified, use its natural aspect ratio (if known).
const ar = projectionAspectRatio(projection, marks);
const ar = projectionAspectRatio(projection);
if (ar) {
const nfx = fx ? fx.scale.domain().length : 1;
const far = ((1.1 * nfy - 0.1) / (1.1 * nfx - 0.1)) * ar; // 0.1 is default facet padding
Expand Down
2 changes: 1 addition & 1 deletion src/marks/geo.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ export class Geo extends Mark {
super(
data,
{
geometry: {value: options.geometry},
geometry: {value: options.geometry, scale: "projection"},
r: {value: vr, scale: "r", filter: positive, optional: true}
},
withDefaultSort(options),
Expand Down
44 changes: 30 additions & 14 deletions src/plot.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {axisFx, axisFy, axisX, axisY, gridFx, gridFy, gridX, gridY} from "./mark
import {frame} from "./marks/frame.js";
import {tip} from "./marks/tip.js";
import {arrayify, isColor, isIterable, isNone, isScaleOptions, map, yes, maybeIntervalTransform} from "./options.js";
import {createProjection} from "./projection.js";
import {createProjection, getGeometryChannels, hasProjection} from "./projection.js";
import {createScales, createScaleFunctions, autoScaleRange, exposeScales} from "./scales.js";
import {innerDimensions, outerDimensions} from "./scales.js";
import {position, registry as scaleRegistry} from "./scales/index.js";
Expand Down Expand Up @@ -48,8 +48,8 @@ export function plot(options = {}) {

// Compute a Map from scale name to an array of associated channels.
const channelsByScale = new Map();
if (topFacetState) addScaleChannels(channelsByScale, [topFacetState]);
addScaleChannels(channelsByScale, facetStateByMark);
if (topFacetState) addScaleChannels(channelsByScale, [topFacetState], options);
addScaleChannels(channelsByScale, facetStateByMark, options);

// Add implicit axis marks. Because this happens after faceting (because it
// depends on whether faceting is present), we must initialize the facet state
Expand Down Expand Up @@ -139,7 +139,7 @@ export function plot(options = {}) {
}

// Initalize the scales and dimensions.
const scaleDescriptors = createScales(addScaleChannels(channelsByScale, stateByMark), options);
const scaleDescriptors = createScales(addScaleChannels(channelsByScale, stateByMark, options), options);
const scales = createScaleFunctions(scaleDescriptors);
const dimensions = createDimensions(scaleDescriptors, marks, options);

Expand Down Expand Up @@ -217,8 +217,8 @@ export function plot(options = {}) {
// reinitialization. Preserve existing scale labels, if any.
if (newByScale.size) {
const newChannelsByScale = new Map();
addScaleChannels(newChannelsByScale, stateByMark, (key) => newByScale.has(key));
addScaleChannels(channelsByScale, stateByMark, (key) => newByScale.has(key));
addScaleChannels(newChannelsByScale, stateByMark, options, (key) => newByScale.has(key));
addScaleChannels(channelsByScale, stateByMark, options, (key) => newByScale.has(key));
const newScaleDescriptors = inheritScaleLabels(createScales(newChannelsByScale, options), scaleDescriptors);
const newScales = createScaleFunctions(newScaleDescriptors);
Object.assign(scaleDescriptors, newScaleDescriptors);
Expand Down Expand Up @@ -410,21 +410,36 @@ function inferChannelScales(channels) {
}
}

function addScaleChannels(channelsByScale, stateByMark, filter = yes) {
function addScaleChannels(channelsByScale, stateByMark, options, filter = yes) {
for (const {channels} of stateByMark.values()) {
for (const name in channels) {
const channel = channels[name];
const {scale} = channel;
if (scale != null && filter(scale)) {
const scaleChannels = channelsByScale.get(scale);
if (scaleChannels !== undefined) scaleChannels.push(channel);
else channelsByScale.set(scale, [channel]);
// Geo marks affect the default x and y domains if there is no
// projection. Skip this (as an optimization) when a projection is
// specified, or when the domains for x and y are specified.
if (scale === "projection") {
if (!hasProjection(options)) {
const [x, y] = getGeometryChannels(channel);
if (options.x?.domain === undefined) addScaleChannel(channelsByScale, "x", x);
if (options.y?.domain === undefined) addScaleChannel(channelsByScale, "y", y);
Comment thread
mbostock marked this conversation as resolved.
Outdated
}
} else {
addScaleChannel(channelsByScale, scale, channel);
}
}
}
}
return channelsByScale;
}

function addScaleChannel(channelsByScale, scale, channel) {
const scaleChannels = channelsByScale.get(scale);
if (scaleChannels !== undefined) scaleChannels.push(channel);
else channelsByScale.set(scale, [channel]);
}

// Returns the facet groups, and possibly fx and fy channels, associated with
// the top-level facet option {data, x, y}.
function maybeTopFacet(facet, options) {
Expand Down Expand Up @@ -518,8 +533,8 @@ function inferAxes(marks, channelsByScale, options) {
} = options;

// Disable axes if the corresponding scale is not present.
if (projection || (!isScaleOptions(x) && !hasScaleChannel("x", marks))) xAxis = xGrid = null;
if (projection || (!isScaleOptions(y) && !hasScaleChannel("y", marks))) yAxis = yGrid = null;
if (projection || (!isScaleOptions(x) && !hasPositionChannel("x", marks))) xAxis = xGrid = null;
if (projection || (!isScaleOptions(y) && !hasPositionChannel("y", marks))) yAxis = yGrid = null;
if (!channelsByScale.has("fx")) fxAxis = fxGrid = null;
if (!channelsByScale.has("fy")) fyAxis = fyGrid = null;

Expand Down Expand Up @@ -647,10 +662,11 @@ function hasAxis(marks, k) {
return marks.some((m) => m.ariaLabel?.startsWith(prefix));
}

function hasScaleChannel(k, marks) {
function hasPositionChannel(k, marks) {
for (const mark of marks) {
for (const key in mark.channels) {
if (mark.channels[key].scale === k) {
const {scale} = mark.channels[key];
if (scale === k || scale === "projection") {
return true;
}
}
Expand Down
39 changes: 33 additions & 6 deletions src/projection.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
geoOrthographic,
geoPath,
geoStereographic,
geoStream,
geoTransform,
geoTransverseMercator
} from "d3";
Expand Down Expand Up @@ -222,17 +223,28 @@ export function project(cx, cy, values, projection) {
}
}

// Returns true if a projection was specified. This should match the logic of
// createProjection above, and is called before we construct the projection.
// (Though note that we ignore the edge case where the projection initializer
// may return null.)
export function hasProjection({projection} = {}) {
if (projection == null) return false;
if (typeof projection.stream === "function") return true;
if (isObject(projection)) projection = projection.type;
return projection != null;
}

// When a named projection is specified, we can use its natural aspect ratio to
// determine a good value for the projection’s height based on the desired
// width. When we don’t have a way to know, the golden ratio is our best guess.
// Due to a circular dependency (we need to know the height before we can
// construct the projection), we have to test the raw projection option rather
// than the materialized projection; therefore we must be extremely careful that
// the logic of this function exactly matches Projection above!
export function projectionAspectRatio(projection, marks) {
// the logic of this function exactly matches createProjection above!
export function projectionAspectRatio(projection) {
if (typeof projection?.stream === "function") return defaultAspectRatio;
if (isObject(projection)) projection = projection.type;
if (projection == null) return hasGeometry(marks) ? defaultAspectRatio : undefined;
if (projection == null) return;
if (typeof projection !== "function") {
const {aspectRatio} = namedProjection(projection);
if (aspectRatio) return aspectRatio;
Expand All @@ -254,7 +266,22 @@ export function applyPosition(channels, scales, {projection}) {
return position;
}

function hasGeometry(marks) {
for (const mark of marks) if (mark.channels.geometry) return true;
return false;
export function getGeometryChannels(channel) {
const X = [];
const Y = [];
const x = {scale: "x", value: X};
const y = {scale: "y", value: Y};
const sink = {
point(x, y) {
X.push(x);
Y.push(y);
},
lineStart() {},
lineEnd() {},
polygonStart() {},
polygonEnd() {},
sphere() {}
};
for (const object of channel.value) geoStream(object, sink);
return [x, y];
}
6 changes: 5 additions & 1 deletion src/scales/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ export const opacity = Symbol("opacity");
// Symbol scales have a default range of categorical symbols.
export const symbol = Symbol("symbol");

// There isn’t really a projection scale; this represents x and y for geometry.
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 @@ -34,5 +37,6 @@ export const registry = new Map([
["color", color],
["opacity", opacity],
["symbol", symbol],
["length", length]
["length", length],
["projection", projection]
]);
65 changes: 65 additions & 0 deletions test/output/geoLine.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading