Published unlisted
Edited
May 18, 2021
Importers
2 stars
Insert cell
Insert cell
Insert cell
import { penguins as data } from "@enjalot/palmer-penguins"
Insert cell
data
Insert cell
mutable debug2 = undefined
Insert cell
Plot.plot({
marks: [
plotsymbol(data, {
symbol: "island",
fill: "species",
x: "flipper_length_mm",
y: "body_mass_g",
r: 4
// stroke: "black",
// strokeWidth: d => (d.body_mass_g > 4000 ? 1 : .1)
})
]
})
Insert cell
Plot.plot({
marks: [
false &&
Plot.dot(data, {
x: "flipper_length_mm",
y: "body_mass_g",
fill: "body_mass_g",
fillOpacity: (d, i) => i / data.length,
strokeOpacity: (d, i) => 1 - i / data.length,
strokeWidth: d => d.body_mass_g / 3000,
r: d => d["body_mass_g"] ** 3
}),
plotsymbol(data, {
symbol: "island",
x: d => d.flipper_length_mm - 0.6,
y: "body_mass_g",
fill: "body_mass_g",
//fillOpacity: (d, i) => i / data.length,
//strokeOpacity: (d, i) => 1 - i / data.length,
//stroke: "black"
r: d => d["body_mass_g"] ** 3

// strokeWidth: d => (d.body_mass_g > 4000 ? 1 : .1)
}),
false &&
plotsquare(data, {
x: "flipper_length_mm",
y: "body_mass_g",
fill: "body_mass_g",
fillOpacity: (d, i) => i / data.length,
strokeOpacity: (d, i) => 1 - i / data.length,
strokeWidth: d => d.body_mass_g / 3000,
r: d => d["body_mass_g"] ** 3
}),
false &&
plotdot(data, {
x: d => d.flipper_length_mm + .6,
y: "body_mass_g",
fill: "body_mass_g",
fillOpacity: (d, i) => i / data.length,
strokeOpacity: (d, i) => 1 - i / data.length,
stroke: "black",
r: d => d["body_mass_g"] ** 3

// strokeWidth: d => (d.body_mass_g > 4000 ? 1 : .1)
}),

false &&
plottriangle(data, {
x: d => d.flipper_length_mm + 1.2,
y: "body_mass_g",
fill: "body_mass_g",
fillOpacity: (d, i) => i / data.length,
strokeOpacity: (d, i) => 1 - i / data.length,
stroke: "black",
r: d => d["body_mass_g"] ** 3

// strokeWidth: d => (d.body_mass_g > 4000 ? 1 : .1)
})
].filter(d => d)
})
Insert cell
mutable debug = undefined
Insert cell
plotdot = {
class PlotMark extends PlotSVGMark {
constructor(data, options) {
super(data, {
nodeType: "svg:circle",
...options
});
}

render(I, { x, y, r }, { x: X, y: Y, r: R }) {
const g = super.render.apply(this, arguments);
select(g)
.selectChildren()
.attr("cx", i => (X[i]))
.attr("cy", i => (Y[i]))
.attr("r", R ? i => (R[i]) : this.r);
return g;
}
}

return function() {
return new PlotMark(...arguments);
};
}
Insert cell
plotsquare = {
class PlotMark extends PlotSVGMark {
constructor(data, options) {
super(data, {
nodeType: "svg:rect",
...options
});
}

render(I, { x, y, r }, { x: X, y: Y, r: R }) {
// ratio for a circle of radius r and a square of side w with the same surface
const k = Math.sqrt(Math.PI);

const g = super.render.apply(this, arguments);
select(g)
.selectChildren()
.attr("x", i => (X[i]) - .5 * k * (R ? (R[i]) : this.r))
.attr("y", i => (Y[i]) - .5 * k * (R ? (R[i]) : this.r))
.attr("width", R ? i => k * (R[i]) : k * this.r)
.attr("height", R ? i => k * (R[i]) : k * this.r);

return g;
}
}

return function() {
return new PlotMark(...arguments);
};
}
Insert cell
plottriangle = {
class PlotMark extends PlotSVGMark {
constructor(data, options) {
super(data, {
nodeType: "svg:path",
...options
});
}

render(I, { x, y, r }, { x: X, y: Y, r: R }) {
const shape = d3.symbol(d3.symbolTriangle);

const g = super.render.apply(this, arguments);
select(g)
.selectChildren()
.attr("transform", i => `translate(${(X[i])},${(Y[i])})`)
.attr(
"d",
R ? i => shape.size((R[i]) ** 2)() : shape.size(this.r ** 2)()
);

return g;
}
}

return function() {
return new PlotMark(...arguments);
};
}
Insert cell
plotsymbol = {
class PlotMark extends PlotSVGMark {
constructor(data, options) {
super(data, {
nodeType: "svg:path",
...options
});
this.symbol = maybeSymbol(options.symbol);
this.symbolScale =
options.symbolScale ||
d3.scaleOrdinal(
["circle", "cross", "diamond", "square", "star", "triangle", "wye"],
[
d3.symbolCircle,
d3.symbolCross,
d3.symbolDiamond,
d3.symbolSquare,
d3.symbolStar,
d3.symbolTriangle,
d3.symbolWye
]
);
}

render(I, { x, y, r }, { x: X, y: Y, r: R }) {
const symbol = d3.symbol();

const syscale = this.symbolScale;
const SY =
typeof this.symbol === "function" &&
this.data.map(this.symbol).map(syscale);
if (typeof this.symbol === "string") symbol.type(syscale(this.symbol));
mutable debug2 = SY;

const g = super.render.apply(this, arguments);
let t;
select(g)
.selectChildren()
.attr("transform", i => `translate(${(X[i])},${(Y[i])})`)
.attr(
"d",
SY
? i =>
symbol
.size(Math.PI * (t = R ? (R[i]) : this.r) * t)
.type(SY[i])()
: R
? i => symbol.size(Math.PI * (t = (R[i])) * t)()
: symbol.size(Math.PI * (t = this.r) * t)()
);

return g;
}
}

return function() {
return new PlotMark(...arguments);
};
}
Insert cell
class PlotSVGMark extends Plot.Mark {
constructor(
data,
{
nodeType,
x,
x1,
x2,
y,
y1,
y2,
z,
r,
title,
fill,
fillOpacity,
stroke,
strokeOpacity,
strokeWidth,
transform,
...style
} = {}
) {
const [vr, cr] = maybeNumber(r, 3);
const [vfill, cfill] = maybeColor(fill, "none");
const [vstroke, cstroke] = maybeColor(
stroke,
cfill === "none" ? "currentColor" : "none"
);
const [vstrokewidth, cstrokewidth] = maybeNumber(strokeWidth, 1);
const [vfillopacity, cfillopacity] = maybeNumber(fillOpacity, 1);
const [vstrokeopacity, cstrokeopacity] = maybeNumber(strokeOpacity, 1);
super(
data,
[
{ name: "x", value: x, scale: "x", optional: true },
{ name: "x1", value: x1, scale: "x", optional: true },
{ name: "x2", value: x2, scale: "x", optional: true },
{ name: "y", value: y, scale: "y", optional: true },
{ name: "y1", value: y1, scale: "y", optional: true },
{ name: "y2", value: y2, scale: "y", optional: true },
{ name: "z", value: z, optional: true },
{ name: "r", value: vr, scale: "r", optional: true },
{ name: "title", value: title, optional: true },
{ name: "fill", value: vfill, scale: "color", optional: true },
{ name: "stroke", value: vstroke, scale: "color", optional: true },
{
name: "strokeWidth",
value: vstrokewidth,
// scale: "strokewidth", // 🌶 channel works, but not the scale
optional: true
},
{
name: "fillOpacity",
value: vfillopacity,
// scale: "fillopacity",
optional: true
},
{
name: "strokeOpacity",
value: vstrokeopacity,
// scale: "fillopacity",
optional: true
}
],
transform
);
this.nodeType = nodeType;
this.r = cr;
Style(this, {
fill: cfill,
fillOpacity: cfillopacity,
strokeOpacity: cstrokeopacity,
stroke: cstroke,
strokeWidth:
cstroke === "none"
? undefined
: cstrokewidth === undefined
? 1.5
: cstrokewidth,
...style
});
}
render(
I,
{ x, y, r, color, strokewidth },
{
x: X,
x1: X1,
x2: X2,
y: Y,
y1: Y1,
y2: Y2,
z: Z,
r: R,
title: L,
fill: F,
fillOpacity: FO,
stroke: S,
strokeOpacity: SO,
strokeWidth: SW
}
) {
const nodeType = this.nodeType;
let index = filter(I, X, X1, X2, Y, Y1, Y2);

mutable debug2 = { I, index, X, X1 };

if (R) index = index.filter(i => positive(R[i]));
if (Z) index.sort((i, j) => ascending(Z[i], Z[j]));
return create("svg:g")
.call(applyIndirectStyles, this)
.call(applyTransform, x, y, 0.5, 0.5)
.call(g =>
g
.selectAll()
.data(index)
.join(nodeType)
.call(applyDirectStyles, this)
.attr("fill", F && (i => (F[i])))
.attr("fill-opacity", FO && (i => clamp(FO[i], 0, 1)))
.attr("stroke", S && (i => (S[i])))
.attr("stroke-opacity", SO && (i => clamp(SO[i], 0, 1)))
.attr("stroke-width", SW && (i => Math.max(0, SW[i])))
.call(title(L))
)
.node();
}
}
Insert cell
md`## Expose methods?`
Insert cell
function maybeSymbol(value, defaultValue) {
const symbols = new Set([
"circle",
"cross",
"diamond",
"square",
"star",
"triangle",
"wye"
]);
return value == null
? defaultValue
: symbols.has(value)
? value
: field(value);
}
Insert cell
field = label => Object.assign(d => d[label], { label })
Insert cell
function maybeColor(value, defaultValue) {
if (value === undefined) value = defaultValue;
return value === null ? [undefined, "none"]
: typeof value === "string" && (colors.has(value) || color(value)) ? [undefined, value]
: [value, undefined];
}
Insert cell
// Similar to maybeColor, this tests whether the given value is a number
// indicating a constant, and otherwise assumes that it’s a channel value.
function maybeNumber(value, defaultValue) {
if (value === undefined) value = defaultValue;
return value === null || typeof value === "number" ? [undefined, value]
: [value, undefined];
}
Insert cell
// The value may be defined as a string or function, rather than an object with
// a value property. TODO Allow value to be specified as array, too? This would
// require promoting the array to an accessor for compatibility with d3.bin.
function maybeValue(x) {
return typeof x === "string" || typeof x === "function" ? {value: x} : x;
}
Insert cell
// If the channel value is specified as a string, indicating a named field, this
// wraps the specified function f with another function with the corresponding
// label property, such that the associated axis inherits the label by default.
function maybeLabel(f, value) {
const label =
typeof value === "string"
? value
: typeof value === "function"
? value.label
: undefined;
return label === undefined ? f : Object.assign(d => f(d), { label });
}
Insert cell
// Applies the specified titles via selection.call.
function title(L) {
return L
? selection =>
selection
.filter(i => nonempty(L[i]))
.append("title")
.text(i => L[i])
: () => {};
}
Insert cell
// title for groups (lines, areas).
function titleGroup(L) {
return L
? selection =>
selection
.filter(([i]) => nonempty(L[i]))
.append("title")
.text(([i]) => L[i])
: () => {};
}
Insert cell
colors = new Set(["currentColor", "none"])
Insert cell
function nonempty(x) {
return x != null && x + "" !== "";
}
Insert cell
function filter(index, ...channels) {
for (const c of channels) {
if (c) index = index.filter(i => defined(c[i]));
}
return index;
}
Insert cell
function defined(x) {
return x != null && !Number.isNaN(x);
}
Insert cell
function positive(x) {
return x > 0 ? x : NaN;
}
Insert cell
function Style(mark, {
fill,
fillOpacity,
stroke,
strokeWidth,
strokeOpacity,
strokeLinejoin,
strokeLinecap,
strokeMiterlimit,
strokeDasharray,
mixBlendMode
} = {}) {
mark.fill = impliedString(fill, "currentColor");
mark.fillOpacity = impliedNumber(fillOpacity, 1);
mark.stroke = impliedString(stroke, "none");
mark.strokeWidth = impliedNumber(strokeWidth, 1);
mark.strokeOpacity = impliedNumber(strokeOpacity, 1);
mark.strokeLinejoin = impliedString(strokeLinejoin, "miter");
mark.strokeLinecap = impliedString(strokeLinecap, "butt");
mark.strokeMiterlimit = impliedNumber(strokeMiterlimit, 1);
mark.strokeDasharray = string(strokeDasharray);
mark.mixBlendMode = impliedString(mixBlendMode, "normal");
}
Insert cell
function applyIndirectStyles(selection, mark) {
applyAttr(selection, "fill", mark.fill);
applyAttr(selection, "fill-opacity", mark.fillOpacity);
applyAttr(selection, "stroke", mark.stroke);
applyAttr(selection, "stroke-width", mark.strokeWidth);
applyAttr(selection, "stroke-opacity", mark.strokeOpacity);
applyAttr(selection, "stroke-linejoin", mark.strokeLinejoin);
applyAttr(selection, "stroke-linecap", mark.strokeLinecap);
applyAttr(selection, "stroke-miterlimit", mark.strokeMiterlimit);
applyAttr(selection, "stroke-dasharray", mark.strokeDasharray);
}
Insert cell
function applyDirectStyles(selection, mark) {
applyStyle(selection, "mix-blend-mode", mark.mixBlendMode);
}
Insert cell
function applyAttr(selection, name, value) {
if (value != null) selection.attr(name, value);
}
Insert cell
function applyStyle(selection, name, value) {
if (value != null) selection.style(name, value);
}
Insert cell
function applyTransform(selection, x, y, tx = 0, ty = 0) {
if (x.bandwidth) tx += x.bandwidth() / 2;
if (y.bandwidth) ty += y.bandwidth() / 2;
selection.attr("transform", `translate(${tx},${ty})`);
}
Insert cell
Insert cell
function impliedString(value, impliedValue) {
if ((value = string(value)) !== impliedValue) return value;
}
Insert cell
function impliedNumber(value, impliedValue) {
if ((value = number(value)) !== impliedValue) return value;
}
Insert cell
string = x => (x == null ? undefined : x + "")
Insert cell
number = x => (x == null ? undefined : +x)
Insert cell
color = d3.color
Insert cell
create = d3.create
Insert cell
select = d3.select
Insert cell
ascending = d3.ascending
Insert cell
clamp = (x, lo, hi) => (x < lo ? lo : x > hi ? hi : x)
Insert cell

Purpose-built for displays of data

Observable is your go-to platform for exploring data and creating expressive data visualizations. Use reactive JavaScript notebooks for prototyping and a collaborative canvas for visual data exploration and dashboard creation.
Learn more