Skip to content

Commit 29fdc97

Browse files
committed
handle collapsed domains
1 parent 149a295 commit 29fdc97

File tree

9 files changed

+120
-31
lines changed

9 files changed

+120
-31
lines changed

src/mark.js

+15
Original file line numberDiff line numberDiff line change
@@ -326,3 +326,18 @@ export function isTemporal(values) {
326326
return value instanceof Date;
327327
}
328328
}
329+
330+
// Certain marks have special behavior if a scale is collapsed, i.e. if the
331+
// domain is degenerate and represents only a single value such as [3, 3]; for
332+
// example, a rect will span the full extent of the chart along a collapsed
333+
// dimension (whereas a dot will simply be drawn in the center).
334+
export function isCollapsed(scale) {
335+
const domain = scale.domain();
336+
const value = scale(domain[0]);
337+
for (let i = 1, n = domain.length; i < n; ++i) {
338+
if (scale(domain[i]) - value) {
339+
return false;
340+
}
341+
}
342+
return true;
343+
}

src/marks/bar.js

+9-9
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import {create} from "d3";
22
import {filter} from "../defined.js";
3-
import {Mark, number} from "../mark.js";
3+
import {Mark, number, isCollapsed} from "../mark.js";
44
import {applyDirectStyles, applyIndirectStyles, applyTransform, impliedString, applyAttr, applyChannelStyles} from "../style.js";
55
import {maybeStackX, maybeStackY} from "../transforms/stack.js";
66

@@ -75,13 +75,13 @@ export class BarX extends AbstractBar {
7575
_positions({x1: X1, x2: X2, y: Y}) {
7676
return [X1, X2, Y];
7777
}
78-
_x(scales, {x1: X1, x2: X2}) {
78+
_x({x}, {x1: X1, x2: X2}, {marginLeft}) {
7979
const {insetLeft} = this;
80-
return i => Math.min(X1[i], X2[i]) + insetLeft;
80+
return isCollapsed(x) ? marginLeft + insetLeft : i => Math.min(X1[i], X2[i]) + insetLeft;
8181
}
82-
_width(scales, {x1: X1, x2: X2}) {
82+
_width({x}, {x1: X1, x2: X2}, {marginRight, marginLeft, width}) {
8383
const {insetLeft, insetRight} = this;
84-
return i => Math.max(0, Math.abs(X2[i] - X1[i]) - insetLeft - insetRight);
84+
return isCollapsed(x) ? width - marginRight - marginLeft - insetLeft - insetRight : i => Math.max(0, Math.abs(X2[i] - X1[i]) - insetLeft - insetRight);
8585
}
8686
}
8787

@@ -104,13 +104,13 @@ export class BarY extends AbstractBar {
104104
_positions({y1: Y1, y2: Y2, x: X}) {
105105
return [Y1, Y2, X];
106106
}
107-
_y(scales, {y1: Y1, y2: Y2}) {
107+
_y({y}, {y1: Y1, y2: Y2}, {marginTop}) {
108108
const {insetTop} = this;
109-
return i => Math.min(Y1[i], Y2[i]) + insetTop;
109+
return isCollapsed(y) ? marginTop + insetTop : i => Math.min(Y1[i], Y2[i]) + insetTop;
110110
}
111-
_height(scales, {y1: Y1, y2: Y2}) {
111+
_height({y}, {y1: Y1, y2: Y2}, {marginTop, marginBottom, height}) {
112112
const {insetTop, insetBottom} = this;
113-
return i => Math.max(0, Math.abs(Y2[i] - Y1[i]) - insetTop - insetBottom);
113+
return isCollapsed(y) ? height - marginTop - marginBottom - insetTop - insetBottom : i => Math.max(0, Math.abs(Y2[i] - Y1[i]) - insetTop - insetBottom);
114114
}
115115
}
116116

src/marks/rect.js

+5-5
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import {create} from "d3";
22
import {filter} from "../defined.js";
3-
import {Mark, number} from "../mark.js";
3+
import {Mark, number, isCollapsed} from "../mark.js";
44
import {applyDirectStyles, applyIndirectStyles, applyTransform, impliedString, applyAttr, applyChannelStyles} from "../style.js";
55
import {maybeStackX, maybeStackY} from "../transforms/stack.js";
66

@@ -51,10 +51,10 @@ export class Rect extends Mark {
5151
.data(index)
5252
.join("rect")
5353
.call(applyDirectStyles, this)
54-
.attr("x", X1 && X2 ? i => Math.min(X1[i], X2[i]) + insetLeft : marginLeft + insetLeft)
55-
.attr("y", Y1 && Y2 ? i => Math.min(Y1[i], Y2[i]) + insetTop : marginTop + insetTop)
56-
.attr("width", X1 && X2 ? i => Math.max(0, Math.abs(X2[i] - X1[i]) - insetLeft - insetRight) : width - marginRight - marginLeft - insetRight - insetLeft)
57-
.attr("height", Y1 && Y2 ? i => Math.max(0, Math.abs(Y1[i] - Y2[i]) - insetTop - insetBottom) : height - marginTop - marginBottom - insetTop - insetBottom)
54+
.attr("x", X1 && X2 && !isCollapsed(x) ? i => Math.min(X1[i], X2[i]) + insetLeft : marginLeft + insetLeft)
55+
.attr("y", Y1 && Y2 && !isCollapsed(y) ? i => Math.min(Y1[i], Y2[i]) + insetTop : marginTop + insetTop)
56+
.attr("width", X1 && X2 && !isCollapsed(x) ? i => Math.max(0, Math.abs(X2[i] - X1[i]) - insetLeft - insetRight) : width - marginRight - marginLeft - insetRight - insetLeft)
57+
.attr("height", Y1 && Y2 && !isCollapsed(y) ? i => Math.max(0, Math.abs(Y1[i] - Y2[i]) - insetTop - insetBottom) : height - marginTop - marginBottom - insetTop - insetBottom)
5858
.call(applyAttr, "rx", rx)
5959
.call(applyAttr, "ry", ry)
6060
.call(applyChannelStyles, channels))

src/marks/rule.js

+11-17
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import {create} from "d3";
22
import {filter} from "../defined.js";
3-
import {Mark, identity, number} from "../mark.js";
3+
import {Mark, identity, number, isCollapsed} from "../mark.js";
44
import {applyDirectStyles, applyIndirectStyles, applyTransform, applyChannelStyles} from "../style.js";
55

66
const defaults = {
@@ -31,13 +31,10 @@ export class RuleX extends Mark {
3131
this.insetTop = number(insetTop);
3232
this.insetBottom = number(insetBottom);
3333
}
34-
render(
35-
I,
36-
{x, y},
37-
channels,
38-
{width, height, marginTop, marginRight, marginLeft, marginBottom}
39-
) {
34+
render(I, {x, y}, channels, dimensions) {
4035
const {x: X, y1: Y1, y2: Y2} = channels;
36+
const {width, height, marginTop, marginRight, marginLeft, marginBottom} = dimensions;
37+
const {insetTop, insetBottom} = this;
4138
const index = filter(I, X, Y1, Y2);
4239
return create("svg:g")
4340
.call(applyIndirectStyles, this)
@@ -48,8 +45,8 @@ export class RuleX extends Mark {
4845
.call(applyDirectStyles, this)
4946
.attr("x1", X ? i => X[i] : (marginLeft + width - marginRight) / 2)
5047
.attr("x2", X ? i => X[i] : (marginLeft + width - marginRight) / 2)
51-
.attr("y1", Y1 ? i => Y1[i] + this.insetTop : marginTop + this.insetTop)
52-
.attr("y2", Y2 ? (y.bandwidth ? i => Y2[i] + y.bandwidth() - this.insetBottom : i => Y2[i] - this.insetBottom) : height - marginBottom - this.insetBottom)
48+
.attr("y1", Y1 && !isCollapsed(y) ? i => Y1[i] + insetTop : marginTop + insetTop)
49+
.attr("y2", Y2 && !isCollapsed(y) ? (y.bandwidth ? i => Y2[i] + y.bandwidth() - insetBottom : i => Y2[i] - insetBottom) : height - marginBottom - insetBottom)
5350
.call(applyChannelStyles, channels))
5451
.node();
5552
}
@@ -78,13 +75,10 @@ export class RuleY extends Mark {
7875
this.insetRight = number(insetRight);
7976
this.insetLeft = number(insetLeft);
8077
}
81-
render(
82-
I,
83-
{x, y},
84-
channels,
85-
{width, height, marginTop, marginRight, marginLeft, marginBottom}
86-
) {
78+
render(I, {x, y}, channels, dimensions) {
8779
const {y: Y, x1: X1, x2: X2} = channels;
80+
const {width, height, marginTop, marginRight, marginLeft, marginBottom} = dimensions;
81+
const {insetLeft, insetRight} = this;
8882
const index = filter(I, Y, X1, X2);
8983
return create("svg:g")
9084
.call(applyIndirectStyles, this)
@@ -93,8 +87,8 @@ export class RuleY extends Mark {
9387
.data(index)
9488
.join("line")
9589
.call(applyDirectStyles, this)
96-
.attr("x1", X1 ? i => X1[i] + this.insetLeft : marginLeft + this.insetLeft)
97-
.attr("x2", X2 ? (x.bandwidth ? i => X2[i] + x.bandwidth() - this.insetRight : i => X2[i] - this.insetRight) : width - marginRight - this.insetRight)
90+
.attr("x1", X1 && !isCollapsed(x) ? i => X1[i] + insetLeft : marginLeft + insetLeft)
91+
.attr("x2", X2 && !isCollapsed(x) ? (x.bandwidth ? i => X2[i] + x.bandwidth() - insetRight : i => X2[i] - insetRight) : width - marginRight - insetRight)
9892
.attr("y1", Y ? i => Y[i] : (marginTop + height - marginBottom) / 2)
9993
.attr("y2", Y ? i => Y[i] : (marginTop + height - marginBottom) / 2)
10094
.call(applyChannelStyles, channels))

test/output/singleValueBar.svg

+18
Loading

test/output/singleValueBin.svg

+45
Loading

test/plots/index.js

+2
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,8 @@ export {default as sfTemperatureBandArea} from "./sf-temperature-band-area.js";
8888
export {default as simpsonsRatings} from "./simpsons-ratings.js";
8989
export {default as simpsonsRatingsDots} from "./simpsons-ratings-dots.js";
9090
export {default as simpsonsViews} from "./simpsons-views.js";
91+
export {default as singleValueBar} from "./single-value-bar.js";
92+
export {default as singleValueBin} from "./single-value-bin.js";
9193
export {default as softwareVersions} from "./software-versions.js";
9294
export {default as stackedBar} from "./stacked-bar.js";
9395
export {default as stackedRect} from "./stacked-rect.js";

test/plots/single-value-bar.js

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import * as Plot from "@observablehq/plot";
2+
3+
export default async function() {
4+
return Plot.plot({
5+
marks: [
6+
Plot.barY({length: 1}, {x: ["foo"], y1: [0], y2: [0]}),
7+
Plot.ruleX(["foo"], {stroke: "red", y1: [0], y2: [0]})
8+
]
9+
});
10+
}

test/plots/single-value-bin.js

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import * as Plot from "@observablehq/plot";
2+
3+
export default async function() {
4+
return Plot.rectY([3], Plot.binX()).plot();
5+
}

0 commit comments

Comments
 (0)