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

One platform to build and deploy the best data apps

Experiment and prototype by building visualizations in live JavaScript notebooks. Collaborate with your team and decide which concepts to build out.
Use Observable Framework to build data apps locally. Use data loaders to build in any language or library, including Python, SQL, and R.
Seamlessly deploy to Observable. Test before you ship, use automatic deploy-on-commit, and ensure your projects are always up-to-date.
Learn more