Public
Edited
Nov 23
2 forks
Importers
7 stars
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
wheel.setPlaces(
harmony[harmonyType](bins, firstColor, separation),
!(harmonyType == "complementary" || harmonyType == "analogous")
)
Insert cell
Insert cell
function wheelColors(
n = bins,
hs = hueShift,
sat = saturation,
lum = luminosity
) {
const hueShiftAngle = (360 / n) * hs;
return d3
.range(n)
.map((i) => `hsl(${(i / n) * 360 + hueShiftAngle},${sat}%,${lum}%)`);
}
Insert cell
function colorWheel(n = 24, options = {}) {
let {
margin = 20,
innerRadius = 100,
outerRadius = 200,
padAngle = 0.01,
padRadius = 100,
cornerRadius = 4
} = options;

let arcGenerator = d3
.arc()
.innerRadius(innerRadius)
.outerRadius(outerRadius)
.padAngle(padAngle)
.padRadius(padRadius)
.cornerRadius(cornerRadius);

let wc = wheelColors();

let arcData = d3.range(n).map((i) => ({
startAngle: ((i - 0.5) / n) * Math.PI * 2 + Math.PI / 2,
endAngle: ((i + 0.5) / n) * Math.PI * 2 + Math.PI / 2,
color: wc[i]
}));

let img = svg`<svg width=${margin * 2 + outerRadius * 4}
height=${
(margin + outerRadius) * 2
} style="background-color:${background}">`;

d3.select(img)
.append("g")
.attr(
"transform",
`translate(${margin + outerRadius},${margin + outerRadius})`
)
.selectAll("path")
.data(arcData)
.enter()
.append("path")
.attr("d", arcGenerator)
.attr("fill", (d) => d.color);

let placeMarks = d3
.select(img)
.append("g")
.attr("class", "places")
.attr(
"transform",
`translate(${margin + outerRadius},${margin + outerRadius})`
);

let placePolygon = d3
.select(img)
.append("g")
.attr("class", "placePolygon")
.attr(
"transform",
`translate(${margin + outerRadius},${margin + outerRadius})`
);

let placeSamples = d3
.select(img)
.append("g")
.attr("class", "placeSamples")
.attr(
"transform",
`translate(${margin * 2 + outerRadius * 3},${
margin + outerRadius
})scale(0.6)`
);
placeSamples
.append("rect")
.attr("fill", "none")
.attr("stroke", "black")
.attr("width", outerRadius * 2)
.attr("height", innerRadius * 2)
.attr("x", -outerRadius)
.attr("y", -innerRadius);

img.setPlaces = (places, close) => {
let pts = places.map((d) => [
((innerRadius + outerRadius) / 2) * Math.cos((d / n) * Math.PI * 2),
((innerRadius + outerRadius) / 2) * Math.sin((d / n) * Math.PI * 2)
]);
placeMarks.selectAll("circle").remove();
placeMarks
.selectAll("circle")
.data(pts)
.join("circle")
.attr("cx", (d) => d[0])
.attr("cy", (d) => d[1])
.attr("r", 5)
.attr("fill", "black");
placePolygon.selectAll("polygon,polyline").remove();
placePolygon
.append(close ? "polygon" : "polyline")
.attr("stroke", "black")
.attr("stroke-width", 3)
.attr("fill", "none")
.attr(
"points",
pts.map((d) => "" + d)
);
placeSamples.selectAll("circle").remove();
const np = places.length;
placeSamples
.selectAll("circle")
.data(places)
.enter()
.append("circle")
.attr("fill", (d) => arcData[d].color)
.attr("r", innerRadius * 0.5)
.attr("cx", (d, i) => ((i - (np - 1) / 2) / np) * outerRadius * 0.9);
};

img.getPalette = (places) => {
return places.map((d) => arcData[d].color);
};

return img;
}
Insert cell
harmony = ({
complementary: function (n, i) {
return [i % n, Math.round(i + n / 2) % n];
},
analogous: function (n, i, s) {
return [i % n, (i + s) % n, (i + 2 * s) % n];
},
triad: function (n, i) {
return [i % n, Math.round(i + n / 3) % n, Math.round(i + (2 * n) / 3) % n];
},
square: function (n, i) {
return [
i % n,
Math.round(i + n / 4) % n,
Math.round(i + (2 * n) / 4) % n,
Math.round(i + (3 * n) / 4) % n
];
},
rectangle: function (n, i, s = 1) {
return [
Math.round(i + n - s) % n,
Math.round(i + n + s) % n,
Math.round(i + n + n / 2 - s) % n,
Math.round(i + n + n / 2 + s) % n
];
},
"split-analogous": function (n, i, s = 1) {
return [
i % n,
Math.round(i + n + n / 2 - s) % n,
Math.round(i + n + n / 2) % n,
Math.round(i + n + n / 2 + s) % n
];
},
"split-complementary": function (n, i, s = 1) {
return [
i % n,
Math.round(i + n + n / 2 - s) % n,
Math.round(i + n + n / 2 + s) % n
];
}
})
Insert cell
//
// Returns a harmonic palette given a seed color and a harmony scheme
//
function harmonicColors(firstColor, harmonyType = "square", options = {}) {
let { bins = 24, separation = 1 } = options;
let { h, s, l } = d3.hsl(firstColor);
let sectorAngle = 360 / bins;
let i = Math.round(h / sectorAngle);
let hs = (h - i * sectorAngle) / sectorAngle;
let wheel = wheelColors(bins, hs, s * 100, l * 100);
return harmony[harmonyType](bins, i, separation).map((j) => wheel[j]);
}
Insert cell
paletteDisplay(
harmonicColors("steelblue", "split-analogous", { separation: 1 })
)
Insert cell
function paletteDisplay(palette, options = {}) {
let { width = 200, height = 50 } = options;
let ctx = DOM.context2d(width, height, 1);
let cellSize = width / palette.length;
palette.forEach((color, i) => {
ctx.fillStyle = color;
ctx.fillRect(i * cellSize, 0, cellSize, height);
});
return ctx.canvas;
}
Insert cell
d3.hsl("green").h
Insert cell
Insert cell
Insert cell
viewof firstColor = Inputs.range([0, bins - 1], {
label: "first color",
step: 1,
value: 0
})
Insert cell
viewof hueShift = Inputs.range([-0.5, 0.5], { step: 0.05, label: "Hue Shift" })
Insert cell
Insert cell
viewof luminosity = Inputs.range([0, 100], {
step: 1,
value: 50,
label: "Luminosity"
})
Insert cell
viewof background = {
let inp = html`<input type="color" id="bkg-color" value="#ffffff" />`;
let div = html`<form>
<label style="font: 13px/1.2 var(--sans-serif);width:120px;">Background </label>
${inp}
</form>`;
let cb = (e) => {
div.value = inp.value;
div.dispatchEvent(new Event("input"));
};
inp.oninput = cb;
cb();
return div;
}
Insert cell
viewof bins = Inputs.select([12, 24, 48], { label: "color bins", value: 24 })
Insert cell
viewof separation = Inputs.range([1, bins], {
label: "separation",
step: 1,
value: 1
})
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