Skip to content

Commit 37a9502

Browse files
mbostockFil
andauthored
asymmetric insets (#567)
* asymmetric insets * document * test asymmetric insets * Update README * revert asymmetric inset tests Co-authored-by: Philippe Rivière <[email protected]>
1 parent 0cc04e8 commit 37a9502

File tree

4 files changed

+51
-21
lines changed

4 files changed

+51
-21
lines changed

README.md

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -225,7 +225,17 @@ The position scales (*x*, *y*, *fx*, and *fy*) support additional options:
225225
* *scale*.**inset** - inset the default range by the specified amount in pixels
226226
* *scale*.**round** - round the output value to the nearest integer (whole pixel)
227227

228-
The *scale*.inset option can provide “breathing room” to separate marks from axes or the plot’s edge. For example, in a scatterplot with a Plot.dot with the default 3-pixel radius and 1.5-pixel stroke width, an inset of 5 pixels prevents dots from overlapping with the axes. The *scale*.round option is useful for crisp edges by rounding to the nearest pixel boundary.
228+
The *x* scale supports asymmetric insets for more precision. Replace inset by:
229+
230+
* *scale*.**insetLeft** - insets the start of the default range by the specified number of pixels
231+
* *scale*.**insetRight** - insets the end of the default range by the specified number of pixels
232+
233+
Similarly, the *y* scale supports asymmetric insets with:
234+
235+
* *scale*.**insetTop** - insets the top of the default range by the specified number of pixels
236+
* *scale*.**insetBottom** - insets the bottom of the default range by the specified number of pixels
237+
238+
The inset scale options can provide “breathing room” to separate marks from axes or the plot’s edge. For example, in a scatterplot with a Plot.dot with the default 3-pixel radius and 1.5-pixel stroke width, an inset of 5 pixels prevents dots from overlapping with the axes. The *scale*.round option is useful for crisp edges by rounding to the nearest pixel boundary.
229239

230240
In addition to the generic *ordinal* scale type, which requires an explicit output range value for each input domain value, Plot supports special *point* and *band* scale types for encoding ordinal data as position. These scale types accept a [*min*, *max*] range similar to quantitative scales, and divide this continuous interval into discrete points or bands based on the number of distinct values in the domain (*i.e.*, the domain’s cardinality). If the associated marks have no effective width along the ordinal dimension — such as a dot, rule, or tick — then use a *point* scale; otherwise, say for a bar, use a *band* scale. In the image below, the top *x*-scale is a *point* scale while the bottom *x*-scale is a *band* scale; see [Plot: Scales](https://observablehq.com/@observablehq/plot-scales) for an interactive version.
231241

src/scales.js

Lines changed: 36 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -7,27 +7,51 @@ import {ScaleTime, ScaleUtc} from "./scales/temporal.js";
77
import {ScaleOrdinal, ScalePoint, ScaleBand} from "./scales/ordinal.js";
88
import {isOrdinal, isTemporal} from "./mark.js";
99

10-
export function Scales(channels, {inset, round, nice, align, padding, ...options} = {}) {
10+
export function Scales(channels, {
11+
inset: globalInset = 0,
12+
insetTop: globalInsetTop = globalInset,
13+
insetRight: globalInsetRight = globalInset,
14+
insetBottom: globalInsetBottom = globalInset,
15+
insetLeft: globalInsetLeft = globalInset,
16+
round,
17+
nice,
18+
align,
19+
padding,
20+
...options
21+
} = {}) {
1122
const scales = {};
1223
for (const key of registry.keys()) {
1324
const scaleChannels = channels.get(key);
1425
const scaleOptions = options[key];
1526
if (scaleChannels || scaleOptions) {
1627
const scale = Scale(key, scaleChannels, {
17-
inset: key === "x" || key === "y" ? inset : undefined, // not for facet
1828
round: registry.get(key) === position ? round : undefined, // only for position
1929
nice,
2030
align,
2131
padding,
2232
...scaleOptions
2333
});
2434
if (scale) {
25-
if (scaleOptions) { // populate generic scale options (percent, transform)
26-
let {percent, transform} = scaleOptions;
27-
if (transform == null) transform = undefined;
28-
else if (typeof transform !== "function") throw new Error("invalid scale transform");
29-
scale.percent = !!percent;
30-
scale.transform = transform;
35+
// populate generic scale options (percent, transform, insets)
36+
let {
37+
percent,
38+
transform,
39+
inset,
40+
insetTop = inset !== undefined ? inset : key === "y" ? globalInsetTop : 0, // not fy
41+
insetRight = inset !== undefined ? inset : key === "x" ? globalInsetRight : 0, // not fx
42+
insetBottom = inset !== undefined ? inset : key === "y" ? globalInsetBottom : 0, // not fy
43+
insetLeft = inset !== undefined ? inset : key === "x" ? globalInsetLeft : 0 // not fx
44+
} = scaleOptions || {};
45+
if (transform == null) transform = undefined;
46+
else if (typeof transform !== "function") throw new Error("invalid scale transform");
47+
scale.percent = !!percent;
48+
scale.transform = transform;
49+
if (key === "x" || key === "fx") {
50+
scale.insetLeft = +insetLeft;
51+
scale.insetRight = +insetRight;
52+
} else if (key === "y" || key === "fy") {
53+
scale.insetTop = +insetTop;
54+
scale.insetBottom = +insetBottom;
3155
}
3256
scales[key] = scale;
3357
}
@@ -46,9 +70,9 @@ export function autoScaleRange({x, y, fx, fy}, dimensions) {
4670

4771
function autoScaleRangeX(scale, dimensions) {
4872
if (scale.range === undefined) {
49-
const {inset = 0} = scale;
73+
const {insetLeft, insetRight} = scale;
5074
const {width, marginLeft = 0, marginRight = 0} = dimensions;
51-
scale.range = [marginLeft + inset, width - marginRight - inset];
75+
scale.range = [marginLeft + insetLeft, width - marginRight - insetRight];
5276
if (!isOrdinalScale(scale)) scale.range = piecewiseRange(scale);
5377
scale.scale.range(scale.range);
5478
}
@@ -57,9 +81,9 @@ function autoScaleRangeX(scale, dimensions) {
5781

5882
function autoScaleRangeY(scale, dimensions) {
5983
if (scale.range === undefined) {
60-
const {inset = 0} = scale;
84+
const {insetTop, insetBottom} = scale;
6185
const {height, marginTop = 0, marginBottom = 0} = dimensions;
62-
scale.range = [height - marginBottom - inset, marginTop + inset];
86+
scale.range = [height - marginBottom - insetBottom, marginTop + insetTop];
6387
if (isOrdinalScale(scale)) scale.range.reverse();
6488
else scale.range = piecewiseRange(scale);
6589
scale.scale.range(scale.range);

src/scales/ordinal.js

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,19 +8,17 @@ export function ScaleO(scale, channels, {
88
type,
99
domain = inferDomain(channels),
1010
range,
11-
reverse,
12-
inset = 0
11+
reverse
1312
}) {
1413
if (type === "categorical") type = "ordinal"; // shorthand for color schemes
1514
if (reverse) domain = reverseof(domain);
16-
inset = +inset;
1715
scale.domain(domain);
1816
if (range !== undefined) {
1917
// If the range is specified as a function, pass it the domain.
2018
if (typeof range === "function") range = range(domain);
2119
scale.range(range);
2220
}
23-
return {type, domain, range, scale, inset};
21+
return {type, domain, range, scale};
2422
}
2523

2624
export function ScaleOrdinal(key, channels, {

src/scales/quantitative.js

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -58,12 +58,10 @@ export function ScaleQ(key, scale, channels, {
5858
scheme,
5959
range = registry.get(key) === radius ? inferRadialRange(channels, domain) : registry.get(key) === opacity ? unit : undefined,
6060
interpolate = registry.get(key) === color ? (scheme == null && range !== undefined ? interpolateRgb : quantitativeScheme(scheme !== undefined ? scheme : type === "cyclical" ? "rainbow" : "turbo")) : round ? interpolateRound : interpolateNumber,
61-
reverse,
62-
inset = 0
61+
reverse
6362
}) {
6463
if (type === "cyclical" || type === "sequential") type = "linear"; // shorthand for color schemes
6564
reverse = !!reverse;
66-
inset = +inset;
6765

6866
// Sometimes interpolate is a named interpolator, such as "lab" for Lab color
6967
// space. Other times interpolate is a function that takes two arguments and
@@ -106,7 +104,7 @@ export function ScaleQ(key, scale, channels, {
106104
if (nice) scale.nice(nice === true ? undefined : nice);
107105
if (range !== undefined) scale.range(range);
108106
if (clamp) scale.clamp(clamp);
109-
return {type, domain, range, scale, interpolate, inset};
107+
return {type, domain, range, scale, interpolate};
110108
}
111109

112110
export function ScaleLinear(key, channels, options) {

0 commit comments

Comments
 (0)