Public
Edited
Jun 23, 2023
Importers
Insert cell
Insert cell
example = Piano([0, 7, 9, 4, 11, 6].map(x => x + 60), {start: 48, end: 88})
Insert cell
Piano = function(notes, options) {
return diagram(makePiano(notes, options));
}
Insert cell
P = function(notes, options) {
return diagram(makePiano(notes, options));
}
Insert cell
function diagram(piano) {
const middleC = 60;
const selectedColor = "#f1ff5e";
const border = 2;
const height = piano.filter(key => key.type=="white")[0].coord.y.max + 2 * border
const svg = d3.create("svg")
.attr("width", width)
.attr("height", height);

// Draw white keys first: cannot set z-index for svg
svg.selectAll("rect.white").data(piano.filter(key => key.type=="white"))
.enter()
.append("rect")
.attr("x", key => key.coord.x.min + border)
.attr("y", key => key.coord.y.min + border)
.attr("width", key => key.coord.x.max - key.coord.x.min)
.attr("height", key => key.coord.y.max - key.coord.y.min)
.attr("stroke", "#444")
.attr("fill", key => key.selected ? selectedColor : "#fff")
.attr("stroke-width", border);

// Draw dot for middle c
svg.selectAll("rect.white").data(piano.filter(key => key.num === middleC))
.enter()
.append("circle")
.attr("cx", key => key.coord.x.min + border + (key.coord.x.max - key.coord.x.min) / 2)
.attr("cy", key => key.coord.y.max - 8)
.attr("r", 3)
.attr("fill", "#999")

// Draw black keys
svg.selectAll("rect.black").data(piano.filter(key => key.type=="black"))
.enter()
.append("rect")
.attr("x", key => key.coord.x.min + border)
.attr("y", 0 + border)
.attr("width", key => key.coord.x.max - key.coord.x.min)
.attr("height", key => key.coord.y.max - key.coord.y.min)
.attr("stroke", "#444")
.attr("fill", key => key.selected ? selectedColor : "#444")
.attr("stroke-width", border);
return svg.node();
}
Insert cell
Insert cell
// Create a piano object that contains informations to draw piano keyboard
function makePiano(selected=[], opt) {
const widthRatio=0.625;
const heightRatio=0.667;

const defaults = {
start: 29,
end: 101
};

let options = Object.assign({}, defaults, opt);

const lowestNote = options.start;
const highestNote = options.end;

const numKeys = highestNote - lowestNote + 1;
const isWhite = num => ![1, 4, 6, 9, 11].includes((num + lowestNote + 3) % 12);

const numLowerWhites = keyIndex => (
d3.range(keyIndex).reduce((acc, val) => (isWhite(val) ? acc+1 : acc), 0)
);
// Total # of white keys
const numWhiteKeys = numLowerWhites(numKeys);

// Dimension of keys
const height = 70;
const whiteWidth = height / 4;
const blackWidth = whiteWidth * widthRatio;

return d3.range(numKeys).map(i => {
const num = i+lowestNote
const offset = isWhite(i) ? 0 : -blackWidth/2;
return {
index: i,
num,
type: isWhite(i) ? "white" : "black",
// Piano coordinate: x: lower to higher key, y: near to far
coord: {
x: {
min: whiteWidth * numLowerWhites(i) + offset,
max: whiteWidth * numLowerWhites(i) + offset + (isWhite(i) ? whiteWidth : blackWidth)
},
y: {
min: isWhite(i) ? 0 : (1 - heightRatio) * height,
max: height
}
},
selected: selected.includes(i+lowestNote),
};
})
}
Insert cell
Insert cell
function getNode(n, v) {
n = document.createElementNS("http://www.w3.org/2000/svg", n);
for (var p in v)
n.setAttributeNS(null, p, v[p]);
return n
}
Insert cell
// svg.appendChild( getNode('rect', { width:200, height:20, fill:'#ff0000' }) );

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