Skip to content

Commit 2ef9aa4

Browse files
committed
handle collapsed domains
1 parent 4dabda0 commit 2ef9aa4

File tree

9 files changed

+116
-20
lines changed

9 files changed

+116
-20
lines changed

src/mark.js

+15
Original file line numberDiff line numberDiff line change
@@ -340,3 +340,18 @@ export function isTemporal(values) {
340340
return value instanceof Date;
341341
}
342342
}
343+
344+
// Certain marks have special behavior if a scale is collapsed, i.e. if the
345+
// domain is degenerate and represents only a single value such as [3, 3]; for
346+
// example, a rect will span the full extent of the chart along a collapsed
347+
// dimension (whereas a dot will simply be drawn in the center).
348+
export function isCollapsed(scale) {
349+
const domain = scale.domain();
350+
const value = scale(domain[0]);
351+
for (let i = 1, n = domain.length; i < n; ++i) {
352+
if (scale(domain[i]) - value) {
353+
return false;
354+
}
355+
}
356+
return true;
357+
}

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, maybeColor, title, maybeNumber} from "../mark.js";
3+
import {Mark, number, maybeColor, title, maybeNumber, isCollapsed} from "../mark.js";
44
import {Style, applyDirectStyles, applyIndirectStyles, applyTransform, impliedString, applyAttr} from "../style.js";
55
import {maybeStackX, maybeStackY} from "../transforms/stack.js";
66

@@ -116,13 +116,13 @@ export class BarX extends AbstractBar {
116116
_positions({x1: X1, x2: X2, y: Y}) {
117117
return [X1, X2, Y];
118118
}
119-
_x(scales, {x1: X1, x2: X2}) {
119+
_x({x}, {x1: X1, x2: X2}, {marginLeft}) {
120120
const {insetLeft} = this;
121-
return i => Math.min(X1[i], X2[i]) + insetLeft;
121+
return isCollapsed(x) ? marginLeft + insetLeft : i => Math.min(X1[i], X2[i]) + insetLeft;
122122
}
123-
_width(scales, {x1: X1, x2: X2}) {
123+
_width({x}, {x1: X1, x2: X2}, {marginRight, marginLeft, width}) {
124124
const {insetLeft, insetRight} = this;
125-
return i => Math.max(0, Math.abs(X2[i] - X1[i]) - insetLeft - insetRight);
125+
return isCollapsed(x) ? width - marginRight - marginLeft - insetLeft - insetRight : i => Math.max(0, Math.abs(X2[i] - X1[i]) - insetLeft - insetRight);
126126
}
127127
}
128128

@@ -144,13 +144,13 @@ export class BarY extends AbstractBar {
144144
_positions({y1: Y1, y2: Y2, x: X}) {
145145
return [Y1, Y2, X];
146146
}
147-
_y(scales, {y1: Y1, y2: Y2}) {
147+
_y({y}, {y1: Y1, y2: Y2}, {marginTop}) {
148148
const {insetTop} = this;
149-
return i => Math.min(Y1[i], Y2[i]) + insetTop;
149+
return isCollapsed(y) ? marginTop + insetTop : i => Math.min(Y1[i], Y2[i]) + insetTop;
150150
}
151-
_height(scales, {y1: Y1, y2: Y2}) {
151+
_height({y}, {y1: Y1, y2: Y2}, {marginTop, marginBottom, height}) {
152152
const {insetTop, insetBottom} = this;
153-
return i => Math.max(0, Math.abs(Y2[i] - Y1[i]) - insetTop - insetBottom);
153+
return isCollapsed(y) ? height - marginTop - marginBottom - insetTop - insetBottom : i => Math.max(0, Math.abs(Y2[i] - Y1[i]) - insetTop - insetBottom);
154154
}
155155
}
156156

src/marks/rect.js

+7-6
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, maybeColor, title, maybeNumber} from "../mark.js";
3+
import {Mark, number, maybeColor, title, maybeNumber, isCollapsed} from "../mark.js";
44
import {Style, applyDirectStyles, applyIndirectStyles, applyTransform, impliedString, applyAttr} from "../style.js";
55
import {maybeStackX, maybeStackY} from "../transforms/stack.js";
66

@@ -63,7 +63,8 @@ export class Rect extends Mark {
6363
render(
6464
I,
6565
{x, y},
66-
{x1: X1, y1: Y1, x2: X2, y2: Y2, title: L, fill: F, fillOpacity: FO, stroke: S, strokeOpacity: SO}
66+
{x1: X1, y1: Y1, x2: X2, y2: Y2, title: L, fill: F, fillOpacity: FO, stroke: S, strokeOpacity: SO},
67+
{marginTop, marginRight, marginBottom, marginLeft, width, height}
6768
) {
6869
const {rx, ry} = this;
6970
const index = filter(I, X1, Y2, X2, Y2, F, FO, S, SO);
@@ -74,10 +75,10 @@ export class Rect extends Mark {
7475
.data(index)
7576
.join("rect")
7677
.call(applyDirectStyles, this)
77-
.attr("x", i => Math.min(X1[i], X2[i]) + this.insetLeft)
78-
.attr("y", i => Math.min(Y1[i], Y2[i]) + this.insetTop)
79-
.attr("width", i => Math.max(0, Math.abs(X2[i] - X1[i]) - this.insetLeft - this.insetRight))
80-
.attr("height", i => Math.max(0, Math.abs(Y1[i] - Y2[i]) - this.insetTop - this.insetBottom))
78+
.attr("x", isCollapsed(x) ? marginLeft + this.insetLeft : i => Math.min(X1[i], X2[i]) + this.insetLeft)
79+
.attr("y", isCollapsed(y) ? marginTop + this.insetTop : i => Math.min(Y1[i], Y2[i]) + this.insetTop)
80+
.attr("width", isCollapsed(x) ? width - marginLeft - marginRight - this.insetLeft - this.insetRight : i => Math.max(0, Math.abs(X2[i] - X1[i]) - this.insetLeft - this.insetRight))
81+
.attr("height", isCollapsed(y) ? height - marginTop - marginBottom - this.insetTop - this.insetBottom : i => Math.max(0, Math.abs(Y1[i] - Y2[i]) - this.insetTop - this.insetBottom))
8182
.call(applyAttr, "fill", F && (i => F[i]))
8283
.call(applyAttr, "fill-opacity", FO && (i => FO[i]))
8384
.call(applyAttr, "stroke", S && (i => S[i]))

src/marks/rule.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, identity, maybeColor, title, number} from "../mark.js";
3+
import {Mark, identity, maybeColor, title, number, isCollapsed} from "../mark.js";
44
import {Style, applyDirectStyles, applyIndirectStyles, applyTransform, applyAttr} from "../style.js";
55

66
export class RuleX extends Mark {
@@ -50,8 +50,8 @@ export class RuleX extends Mark {
5050
.call(applyDirectStyles, this)
5151
.attr("x1", X ? i => X[i] : (marginLeft + width - marginRight) / 2)
5252
.attr("x2", X ? i => X[i] : (marginLeft + width - marginRight) / 2)
53-
.attr("y1", Y1 ? i => Y1[i] + this.insetTop : marginTop + this.insetTop)
54-
.attr("y2", Y2 ? (y.bandwidth ? i => Y2[i] + y.bandwidth() - this.insetBottom : i => Y2[i] - this.insetBottom) : height - marginBottom - this.insetBottom)
53+
.attr("y1", Y1 && !isCollapsed(y) ? i => Y1[i] + this.insetTop : marginTop + this.insetTop)
54+
.attr("y2", Y2 && !isCollapsed(y) ? (y.bandwidth ? i => Y2[i] + y.bandwidth() - this.insetBottom : i => Y2[i] - this.insetBottom) : height - marginBottom - this.insetBottom)
5555
.call(applyAttr, "stroke", S && (i => S[i]))
5656
.call(title(L)))
5757
.node();
@@ -103,8 +103,8 @@ export class RuleY extends Mark {
103103
.data(index)
104104
.join("line")
105105
.call(applyDirectStyles, this)
106-
.attr("x1", X1 ? i => X1[i] + this.insetLeft : marginLeft + this.insetLeft)
107-
.attr("x2", X2 ? (x.bandwidth ? i => X2[i] + x.bandwidth() - this.insetRight : i => X2[i] - this.insetRight) : width - marginRight - this.insetRight)
106+
.attr("x1", X1 && !isCollapsed(x) ? i => X1[i] + this.insetLeft : marginLeft + this.insetLeft)
107+
.attr("x2", X2 && !isCollapsed(x) ? (x.bandwidth ? i => X2[i] + x.bandwidth() - this.insetRight : i => X2[i] - this.insetRight) : width - marginRight - this.insetRight)
108108
.attr("y1", Y ? i => Y[i] : (marginTop + height - marginBottom) / 2)
109109
.attr("y2", Y ? i => Y[i] : (marginTop + height - marginBottom) / 2)
110110
.call(applyAttr, "stroke", S && (i => S[i]))

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
@@ -87,6 +87,8 @@ export {default as sfTemperatureBandArea} from "./sf-temperature-band-area.js";
8787
export {default as simpsonsRatings} from "./simpsons-ratings.js";
8888
export {default as simpsonsRatingsDots} from "./simpsons-ratings-dots.js";
8989
export {default as simpsonsViews} from "./simpsons-views.js";
90+
export {default as singleValueBar} from "./single-value-bar.js";
91+
export {default as singleValueBin} from "./single-value-bin.js";
9092
export {default as stackedBar} from "./stacked-bar.js";
9193
export {default as stocksIndex} from "./stocks-index.js";
9294
export {default as travelersYearOverYear} from "./travelers-year-over-year.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)