Skip to content

Commit b6e4c64

Browse files
authored
handle collapsed domains (#470)
* handle collapsed domains * move isCollapsed * destructure style
1 parent 149a295 commit b6e4c64

12 files changed

+130
-45
lines changed

src/facet.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ class Facet extends Mark {
7272
}
7373
return {index, channels: [...channels, ...subchannels]};
7474
}
75-
render(index, scales, channels, dimensions, axes) {
75+
render(I, scales, channels, dimensions, axes) {
7676
const {marks, marksChannels, marksIndex, marksIndexByFacet} = this;
7777
const {fx, fy} = scales;
7878
const fyMargins = fy && {marginTop: 0, marginBottom: 0, height: fy.bandwidth()};

src/marks/bar.js

+9-8
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import {create} from "d3";
22
import {filter} from "../defined.js";
33
import {Mark, number} from "../mark.js";
4+
import {isCollapsed} from "../scales.js";
45
import {applyDirectStyles, applyIndirectStyles, applyTransform, impliedString, applyAttr, applyChannelStyles} from "../style.js";
56
import {maybeStackX, maybeStackY} from "../transforms/stack.js";
67

@@ -75,13 +76,13 @@ export class BarX extends AbstractBar {
7576
_positions({x1: X1, x2: X2, y: Y}) {
7677
return [X1, X2, Y];
7778
}
78-
_x(scales, {x1: X1, x2: X2}) {
79+
_x({x}, {x1: X1, x2: X2}, {marginLeft}) {
7980
const {insetLeft} = this;
80-
return i => Math.min(X1[i], X2[i]) + insetLeft;
81+
return isCollapsed(x) ? marginLeft + insetLeft : i => Math.min(X1[i], X2[i]) + insetLeft;
8182
}
82-
_width(scales, {x1: X1, x2: X2}) {
83+
_width({x}, {x1: X1, x2: X2}, {marginRight, marginLeft, width}) {
8384
const {insetLeft, insetRight} = this;
84-
return i => Math.max(0, Math.abs(X2[i] - X1[i]) - insetLeft - insetRight);
85+
return isCollapsed(x) ? width - marginRight - marginLeft - insetLeft - insetRight : i => Math.max(0, Math.abs(X2[i] - X1[i]) - insetLeft - insetRight);
8586
}
8687
}
8788

@@ -104,13 +105,13 @@ export class BarY extends AbstractBar {
104105
_positions({y1: Y1, y2: Y2, x: X}) {
105106
return [Y1, Y2, X];
106107
}
107-
_y(scales, {y1: Y1, y2: Y2}) {
108+
_y({y}, {y1: Y1, y2: Y2}, {marginTop}) {
108109
const {insetTop} = this;
109-
return i => Math.min(Y1[i], Y2[i]) + insetTop;
110+
return isCollapsed(y) ? marginTop + insetTop : i => Math.min(Y1[i], Y2[i]) + insetTop;
110111
}
111-
_height(scales, {y1: Y1, y2: Y2}) {
112+
_height({y}, {y1: Y1, y2: Y2}, {marginTop, marginBottom, height}) {
112113
const {insetTop, insetBottom} = this;
113-
return i => Math.max(0, Math.abs(Y2[i] - Y1[i]) - insetTop - insetBottom);
114+
return isCollapsed(y) ? height - marginTop - marginBottom - insetTop - insetBottom : i => Math.max(0, Math.abs(Y2[i] - Y1[i]) - insetTop - insetBottom);
114115
}
115116
}
116117

src/marks/frame.js

+7-10
Original file line numberDiff line numberDiff line change
@@ -22,20 +22,17 @@ export class Frame extends Mark {
2222
this.insetBottom = number(insetBottom);
2323
this.insetLeft = number(insetLeft);
2424
}
25-
render(
26-
index,
27-
scales,
28-
channels,
29-
{marginTop, marginRight, marginBottom, marginLeft, width, height}
30-
) {
25+
render(I, scales, channels, dimensions) {
26+
const {marginTop, marginRight, marginBottom, marginLeft, width, height} = dimensions;
27+
const {insetTop, insetRight, insetBottom, insetLeft} = this;
3128
return create("svg:rect")
3229
.call(applyIndirectStyles, this)
3330
.call(applyDirectStyles, this)
3431
.call(applyTransform, null, null, 0.5, 0.5)
35-
.attr("x", marginLeft + this.insetLeft)
36-
.attr("y", marginTop + this.insetTop)
37-
.attr("width", width - marginLeft - marginRight - this.insetLeft - this.insetRight)
38-
.attr("height", height - marginTop - marginBottom - this.insetTop - this.insetBottom)
32+
.attr("x", marginLeft + insetLeft)
33+
.attr("y", marginTop + insetTop)
34+
.attr("width", width - marginLeft - marginRight - insetLeft - insetRight)
35+
.attr("height", height - marginTop - marginBottom - insetTop - insetBottom)
3936
.node();
4037
}
4138
}

src/marks/rect.js

+5-4
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import {create} from "d3";
22
import {filter} from "../defined.js";
33
import {Mark, number} from "../mark.js";
4+
import {isCollapsed} from "../scales.js";
45
import {applyDirectStyles, applyIndirectStyles, applyTransform, impliedString, applyAttr, applyChannelStyles} from "../style.js";
56
import {maybeStackX, maybeStackY} from "../transforms/stack.js";
67

@@ -51,10 +52,10 @@ export class Rect extends Mark {
5152
.data(index)
5253
.join("rect")
5354
.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)
55+
.attr("x", X1 && X2 && !isCollapsed(x) ? i => Math.min(X1[i], X2[i]) + insetLeft : marginLeft + insetLeft)
56+
.attr("y", Y1 && Y2 && !isCollapsed(y) ? i => Math.min(Y1[i], Y2[i]) + insetTop : marginTop + insetTop)
57+
.attr("width", X1 && X2 && !isCollapsed(x) ? i => Math.max(0, Math.abs(X2[i] - X1[i]) - insetLeft - insetRight) : width - marginRight - marginLeft - insetRight - insetLeft)
58+
.attr("height", Y1 && Y2 && !isCollapsed(y) ? i => Math.max(0, Math.abs(Y1[i] - Y2[i]) - insetTop - insetBottom) : height - marginTop - marginBottom - insetTop - insetBottom)
5859
.call(applyAttr, "rx", rx)
5960
.call(applyAttr, "ry", ry)
6061
.call(applyChannelStyles, channels))

src/marks/rule.js

+11-16
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import {create} from "d3";
22
import {filter} from "../defined.js";
33
import {Mark, identity, number} from "../mark.js";
4+
import {isCollapsed} from "../scales.js";
45
import {applyDirectStyles, applyIndirectStyles, applyTransform, applyChannelStyles} from "../style.js";
56

67
const defaults = {
@@ -31,13 +32,10 @@ export class RuleX extends Mark {
3132
this.insetTop = number(insetTop);
3233
this.insetBottom = number(insetBottom);
3334
}
34-
render(
35-
I,
36-
{x, y},
37-
channels,
38-
{width, height, marginTop, marginRight, marginLeft, marginBottom}
39-
) {
35+
render(I, {x, y}, channels, dimensions) {
4036
const {x: X, y1: Y1, y2: Y2} = channels;
37+
const {width, height, marginTop, marginRight, marginLeft, marginBottom} = dimensions;
38+
const {insetTop, insetBottom} = this;
4139
const index = filter(I, X, Y1, Y2);
4240
return create("svg:g")
4341
.call(applyIndirectStyles, this)
@@ -48,8 +46,8 @@ export class RuleX extends Mark {
4846
.call(applyDirectStyles, this)
4947
.attr("x1", X ? i => X[i] : (marginLeft + width - marginRight) / 2)
5048
.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)
49+
.attr("y1", Y1 && !isCollapsed(y) ? i => Y1[i] + insetTop : marginTop + insetTop)
50+
.attr("y2", Y2 && !isCollapsed(y) ? (y.bandwidth ? i => Y2[i] + y.bandwidth() - insetBottom : i => Y2[i] - insetBottom) : height - marginBottom - insetBottom)
5351
.call(applyChannelStyles, channels))
5452
.node();
5553
}
@@ -78,13 +76,10 @@ export class RuleY extends Mark {
7876
this.insetRight = number(insetRight);
7977
this.insetLeft = number(insetLeft);
8078
}
81-
render(
82-
I,
83-
{x, y},
84-
channels,
85-
{width, height, marginTop, marginRight, marginLeft, marginBottom}
86-
) {
79+
render(I, {x, y}, channels, dimensions) {
8780
const {y: Y, x1: X1, x2: X2} = channels;
81+
const {width, height, marginTop, marginRight, marginLeft, marginBottom} = dimensions;
82+
const {insetLeft, insetRight} = this;
8883
const index = filter(I, Y, X1, X2);
8984
return create("svg:g")
9085
.call(applyIndirectStyles, this)
@@ -93,8 +88,8 @@ export class RuleY extends Mark {
9388
.data(index)
9489
.join("line")
9590
.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)
91+
.attr("x1", X1 && !isCollapsed(x) ? i => X1[i] + insetLeft : marginLeft + insetLeft)
92+
.attr("x2", X2 && !isCollapsed(x) ? (x.bandwidth ? i => X2[i] + x.bandwidth() - insetRight : i => X2[i] - insetRight) : width - marginRight - insetRight)
9893
.attr("y1", Y ? i => Y[i] : (marginTop + height - marginBottom) / 2)
9994
.attr("y2", Y ? i => Y[i] : (marginTop + height - marginBottom) / 2)
10095
.call(applyChannelStyles, channels))

src/marks/text.js

+2-6
Original file line numberDiff line numberDiff line change
@@ -45,13 +45,9 @@ export class Text extends Mark {
4545
this.dx = string(dx);
4646
this.dy = string(dy);
4747
}
48-
render(
49-
I,
50-
{x, y},
51-
channels,
52-
{width, height, marginTop, marginRight, marginBottom, marginLeft}
53-
) {
48+
render(I, {x, y}, channels, dimensions) {
5449
const {x: X, y: Y, rotate: R, text: T, fontSize: FS} = channels;
50+
const {width, height, marginTop, marginRight, marginBottom, marginLeft} = dimensions;
5551
const {rotate} = this;
5652
const index = filter(I, X, Y, R).filter(i => nonempty(T[i]));
5753
const cx = (marginLeft + width - marginRight) / 2;

src/scales.js

+15
Original file line numberDiff line numberDiff line change
@@ -129,3 +129,18 @@ export function applyScales(channels = [], scales) {
129129
}
130130
return values;
131131
}
132+
133+
// Certain marks have special behavior if a scale is collapsed, i.e. if the
134+
// domain is degenerate and represents only a single value such as [3, 3]; for
135+
// example, a rect will span the full extent of the chart along a collapsed
136+
// dimension (whereas a dot will simply be drawn in the center).
137+
export function isCollapsed(scale) {
138+
const domain = scale.domain();
139+
const value = scale(domain[0]);
140+
for (let i = 1, n = domain.length; i < n; ++i) {
141+
if (scale(domain[i]) - value) {
142+
return false;
143+
}
144+
}
145+
return true;
146+
}

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)