diff --git a/src/style.js b/src/style.js
index 903c3fdf60..d77bd0f1b8 100644
--- a/src/style.js
+++ b/src/style.js
@@ -47,7 +47,8 @@ export function styles(
imageFilter,
paintOrder,
pointerEvents,
- shapeRendering
+ shapeRendering,
+ channels
},
{
ariaLabel: cariaLabel,
@@ -81,9 +82,9 @@ export function styles(
// default fill becomes none. Similarly for marks that stroke by stroke, the
// default stroke only applies if the fill is (constant) none.
if (isNoneish(defaultFill)) {
- if (!isNoneish(defaultStroke) && !isNoneish(fill)) defaultStroke = "none";
+ if (!isNoneish(defaultStroke) && (!isNoneish(fill) || channels?.fill)) defaultStroke = "none";
} else {
- if (isNoneish(defaultStroke) && !isNoneish(stroke)) defaultFill = "none";
+ if (isNoneish(defaultStroke) && (!isNoneish(stroke) || channels?.stroke)) defaultFill = "none";
}
const [vfill, cfill] = maybeColorChannel(fill, defaultFill);
diff --git a/src/transforms/hexbin.js b/src/transforms/hexbin.js
index d2ff05e4e1..5952951352 100644
--- a/src/transforms/hexbin.js
+++ b/src/transforms/hexbin.js
@@ -1,4 +1,4 @@
-import {isNoneish, map, number, valueof} from "../options.js";
+import {map, number, valueof} from "../options.js";
import {applyPosition} from "../projection.js";
import {sqrt3} from "../symbol.js";
import {initializer} from "./basic.js";
@@ -13,17 +13,19 @@ export const ox = 0.5,
oy = 0;
export function hexbin(outputs = {fill: "count"}, {binWidth, ...options} = {}) {
+ const {z} = options;
+
// TODO filter e.g. to show empty hexbins?
// TODO disallow x, x1, x2, y, y1, y2 reducers?
binWidth = binWidth === undefined ? 20 : number(binWidth);
outputs = maybeOutputs(outputs, options);
- // A fill output means a fill channel, and hence the stroke should default to
- // none (assuming a mark that defaults to fill and no stroke, such as dot).
- // Note that it’s safe to mutate options here because we just created it with
- // the rest operator above.
- const {z, fill, stroke} = options;
- if (stroke === undefined && isNoneish(fill) && hasOutput(outputs, "fill")) options.stroke = "none";
+ // A fill output means a fill channel; declaring the channel here instead of
+ // waiting for the initializer allows the mark constructor to determine that
+ // the stroke should default to none (assuming a mark that defaults to fill
+ // and no stroke, such as dot). Note that it’s safe to mutate options here
+ // because we just created it with the rest operator above.
+ if (hasOutput(outputs, "fill")) options.channels = {...options.channels, fill: {value: []}};
// Populate default values for the r and symbol options, as appropriate.
if (options.symbol === undefined) options.symbol = "hexagon";
diff --git a/test/output/tipHexbinExplicit.svg b/test/output/tipHexbinExplicit.svg
new file mode 100644
index 0000000000..f4ca6b4cc8
--- /dev/null
+++ b/test/output/tipHexbinExplicit.svg
@@ -0,0 +1,306 @@
+
\ No newline at end of file
diff --git a/test/plots/tip.ts b/test/plots/tip.ts
index 35f2410da9..417a740df8 100644
--- a/test/plots/tip.ts
+++ b/test/plots/tip.ts
@@ -123,6 +123,19 @@ export async function tipHexbin() {
return Plot.hexagon(olympians, Plot.hexbin({r: "count"}, {x: "weight", y: "height", tip: true})).plot();
}
+// Normally you would slap a tip: true on the hexagon, as above, but here we
+// want to test that the hexbin transform isn’t applying an erroneous stroke:
+// none to the tip options (which would change the tip appearance).
+export async function tipHexbinExplicit() {
+ const olympians = await d3.csv("data/athletes.csv", d3.autoType);
+ return Plot.plot({
+ marks: [
+ Plot.hexagon(olympians, Plot.hexbin({fill: "count"}, {x: "weight", y: "height"})),
+ Plot.tip(olympians, Plot.pointer(Plot.hexbin({fill: "count"}, {x: "weight", y: "height"})))
+ ]
+ });
+}
+
export async function tipLine() {
const aapl = await d3.csv("data/aapl.csv", d3.autoType);
return Plot.lineY(aapl, {x: "Date", y: "Close", tip: true}).plot();