Public
Edited
Mar 23, 2023
1 star
Insert cell
# Spiral treemap with consistent aspect ratios
Insert cell
chart = {
const svg = d3.create("svg")
.attr("viewBox", [0, 0, width, height])
.style("font", "10px sans-serif");

const containerWidth = 400
const containerHeight = 200
svg
.append('rect')
.attr('width', containerWidth)
.attr('height', containerHeight)
.attr('x', 0)
.attr('y', 0)
.attr('fill', 'none')
.attr('stroke', 'black')

const x0 = containerWidth / k
const y0 = x0 * containerHeight / containerWidth
const x1 = containerWidth - x0
const y1 = containerHeight - y0

svg
.append('rect')
.attr('width', x1 - x0)
.attr('height', y1 - y0)
.attr('x', x0)
.attr('y', y0)
.attr('fill', 'none')
.attr('stroke', 'red')
svg
.append('rect')
.attr('width', x1 - x0)
.attr('height', y1 - y0)
.attr('x', x0)
.attr('y', y0)
.attr('fill', 'none')
.attr('stroke', 'red')

const x0sample = x0
const y0sample = 0
const x1sample = containerWidth / k + x0
const y1sample = y0

svg
.append('rect')
.attr('width', x1sample - x0sample)
.attr('height', y1sample - y0sample)
.attr('x', x0sample)
.attr('y', y0sample)
.attr('fill', 'none')
.attr('stroke', 'blue')

// then we need to scale the first rectangle
svg
.append('rect')
.attr('width', containerWidth)
.attr('height', containerHeight)
.attr('x', 500)
.attr('y', 0)
.attr('fill', 'none')
.attr('stroke', 'black')

const x0Prime = containerWidth / k2
const y0Prime = x0Prime * containerHeight / containerWidth
const x1Prime = containerWidth - x0Prime
const y1Prime = containerHeight - y0Prime
const scaleX = d3.scaleLinear().domain([0, containerWidth]).range([x0Prime, x1Prime])
const scaleY = d3.scaleLinear().domain([0, containerHeight]).range([y0Prime, y1Prime])

svg
.append('rect')
.attr('width', scaleX(containerWidth)-scaleX(0))
.attr('height', scaleY(containerHeight) - scaleY(0))
.attr('x', scaleX(0)+500)
.attr('y', scaleY(0))
.attr('fill', 'none')
.attr('stroke', 'black')

svg
.append('rect')
.attr('width', scaleX(x1)-scaleX(x0))
.attr('height', scaleY(y1) - scaleY(y0))
.attr('x', scaleX(x0)+500)
.attr('y', scaleY(y0))
.attr('fill', 'none')
.attr('stroke', 'red')

svg
.append('rect')
.attr('width', scaleX(x1sample)-scaleX(x0sample))
.attr('height', scaleY(y1sample) - scaleY(y0sample))
.attr('x', scaleX(x0sample)+500)
.attr('y', scaleY(y0sample))
.attr('fill', 'none')
.attr('stroke', 'blue')

return svg.node()
}
Insert cell
height = 300
Insert cell
k = 4
Insert cell
k2 = 2 * (k+1)//6
Insert cell
getSizes = (k, x0, y0, x1, y1) => {
const segmentWidth = (x1-x0)/ (k+2)
const segmentHeight = segmentWidth * (y1-y0) / (x1-x0)
const kHorizontal = Math.ceil((y1-y0) / segmentHeight)
return [segmentWidth, segmentHeight, kHorizontal]
}
Insert cell
imagesSpiral = (parent, x0, y0, x1, y1) => {
const EAST = 0
const SOUTH = 1
const WEST = 2
const NORTH = 3
const aspectRatio = (x1-x0)/(y1-y0)

// place center image
let nodes = [...parent.children]
let node = null

let k = (!parent.depth) ? nodes[0].data.k : 0
let [segW, segH, kH] = (!parent.depth) ? getSizes(k,x0,y0,x1,y1) : [(x1-x0)/2, (y1-y0)/2, 2]
let [innerX0, innerY0] = [segW, segH]
let [innerX1, innerY1] = (!parent.depth) ? [innerX0 + k * segW, innerY0 + (kH - 2) * segH] : [segW, segH]
let i = -1
let maxDirection = { 0: 1, 1: 1, 2: 1, 3: 1 }
if (!parent.depth) {
nodes[0].x0 = innerX0
nodes[0].y0 = innerY0
nodes[0].x1 = innerX1
nodes[0].y1 = innerY1
i = 0 // we take the first image as a central one
maxDirection = { 0: k, 1: kH, 2: k, 3: kH }
}
let n = nodes.length
let direction = EAST
let segment = []
while (++i < n) {
node = nodes[i]
segment.push(node)

if (direction == EAST) {
if ( k > 0 ) {
node.x0 = (segment.length == 1) ? segW : segment[segment.length-2].x1
node.y0 = 0
node.x1 = node.x0 + segW
node.y1 = segH
} else {
node.x0 = (x1-x0)/2
node.y0 = 0
node.x1 = node.x0 + segW
node.y1 = (y1-y0)/2
}
} else if (direction == SOUTH) {
if (k > 0) {
node.x0 = innerX1
node.y0 = (segment.length == 1) ? 0 : segment[segment.length-2].y1
node.x1 = node.x0 + segW
node.y1 = node.y0 + segH
} else {
node.x0 = (x1-x0)/2
node.y0 = (y1-y0)/2
node.x1 = node.x0 + segW
node.y1 = node.y0 + segH
}
} else if (direction == WEST) {
if (k > 0) {
node.x0 = (segment.length == 1) ? innerX1 - segW : segment[segment.length-2].x0 - segW
node.y0 = y1 - y0 - segH
node.x1 = node.x0 + segW
node.y1 = node.y0 + segH
} else {
node.x0 = 0
node.y0 = (y1-y0)/2
node.x1 = (x1-x0)/2
node.y1 = node.y0 + segH
}
} else if (direction == NORTH) {
if (k > 0) {
node.x0 = 0
node.y0 = (segment.length == 1) ? innerY1 : segment[segment.length-2].y0 - segH
node.x1 = segW
node.y1 = node.y0 + segH
} else {
node.x0 = 0
node.y0 = 0
node.x1 = segW
node.y1 = segH
}
}

if (segment.length === maxDirection[direction]) {
direction = (direction + 1) % 4
segment.length = 0;

if (direction === EAST && i < nodes.length-1) {
const newK = nodes[i+1].data.k
const [newSegW, newSegH, newkH] = getSizes(newK,x0,y0,x1,y1)
segW = newSegW
segH = newSegH
kH = newkH
maxDirection = { 0: newK, 1: kH, 2: newK, 3: kH}
innerX0 = segW
innerY0 = segH
innerX1 = innerX0 + newK * segW
innerY1 = innerY0 + (kH - 2) * segH

const scaleX = d3.scaleLinear().domain([0, x1-x0]).range([segW, x1-x0-segW])
const scaleY = d3.scaleLinear().domain([0, y1-y0]).range([segH, y1-y0-segH])
for (let j=0; j<=i; j++) {
nodes[j].x0 = scaleX(nodes[j].x0)
nodes[j].y0 = scaleY(nodes[j].y0)
nodes[j].x1 = scaleX(nodes[j].x1)
nodes[j].y1 = scaleY(nodes[j].y1)
}
}
}
}
}
Insert cell
generateData = () => {
// { name: 100, size: 100, index: 0 }, { name: 121, size: 121, index: 1 },
let firstLevel = d3.range(5, 18).map((d,i) => (
{ name: Number(d*d).toString(), size: d*d, index: i, k: 2 }
))

const secondLevel = d3.range(0, 20).map((d,i) => (
{ name: Number(d*d).toString(), size: d*d, index: i, k: 4 }
))

const thirdLevel = d3.range(0, 52).map((d,i) => (
{ name: Number(d*d).toString(), size: d*d, index: i, k: 12 }
))

firstLevel = firstLevel.concat(secondLevel).concat(thirdLevel)
firstLevel[1].children = d3.range(100, 104).map((d,i) => (
{ name: Number(d*d).toString(), size: d*d, index: i, k: 2 }
))

firstLevel[3].children = d3.range(100, 104).map((d,i) => (
{ name: Number(d*d).toString(), size: d*d, index: i, k: 2 }
))

const root = { name: 'squared', index: 0, children: firstLevel }
return root
}
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
chart2 = {
const root = treemap(hierarchy(generateData()))

const svg = d3.create("svg")
.attr("viewBox", [0, 0, width, height])
.style("font", "10px sans-serif");
const cell = svg.selectAll("g")
.data(root.descendants())
.join("g")
.attr("transform", d => (d.depth > 1) ? `translate(${d.parent.x0 + d.x0},${d.parent.y0 + d.y0})` : `translate(${d.x0},${d.y0})`);

cell.append("rect")
.attr("id", d => d.data.id)
.attr("width", d => d.x1 - d.x0)
.attr("height", d => d.y1 - d.y0 )
.attr("stroke", d => (d.depth > 1) ? 'red' : 'black')
.attr("fill", 'white')
return svg.node()
}
Insert cell
data
Insert cell
data

Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
spiralDistance(points32)
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
points84Strategy1 = points84.map((p,i) => ({...p, k: spiralK("2mid1small", i)}))
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
spatialSpiral(points84.slice(0,61).map((p,i) => ({...p, k: spiralK("3mid", i)})), 3)
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
spiral(points200Strategy1.slice(0,181))
Insert cell
spiral(points200.map((p,i) => ({...p, k: spiralK("3midrestsmall", i)})))
Insert cell
spiral(points200Strategy1.slice(0,97))
Insert cell
spatialSpiral(points200Strategy1.slice(0,97), 4)
Insert cell
spatialSpiral(points200Strategy1.slice(0,201), 5)
Insert cell
spiralDistance(points200Strategy1.slice(0,201), 5)
Insert cell
t = getSpatiallyAwareArray(points84.map((p,i) => ({...p, k: spiralK("2mid1small", i)})), 3)
Insert cell
viewTreemap(hierarchy({ name: 'root', index: 0, children: [...t] }))
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
svg`<svg width="20" height="20">
<rect x="0" y="0" width="20px" height="20px" fill="red" />
<circle cx="10" cy="10" r="8" stroke="red" fill="blue"/>
</svg>`
Insert cell
svg`<svg width="${iconR}" height="${iconR}">
<circle cx="${iconR/2}" cy="${iconR/2}" r="${iconR/2-iconBorder}" stroke="red" stroke-width="${iconBorder}" fill="blue"/>
<line x1="0" y1="0" x2="${iconR}" y2="${iconR}" stroke="white" stroke-width="${strokeWidth}"/>
<line x1="0" y1="${iconR}" x2="${iconR}" y2="0" stroke="white" stroke-width="${strokeWidth}"/>
</svg>`
Insert cell
svg`<svg width="${iconR}" height="${iconR}">
<circle cx="${iconR/2}" cy="${iconR/2}" r="${iconR/2-iconBorder}" stroke="red" stroke-width="${iconBorder}" fill="blue"/>
<line x1="${iconR/2}" y1="0" x2="${iconR/2}" y2="${iconR}" stroke="white" stroke-width="${strokeWidth}"/>
<line x1="0" y1="${iconR/2}" x2="${iconR}" y2="${iconR/2}" stroke="white" stroke-width="${strokeWidth}"/>
</svg>`
Insert cell
iconR = 20
Insert cell
iconBorder = 2
Insert cell
strokeWidth = 1
Insert cell
tr=20
Insert cell
Insert cell
gridTreemap = hierarchy => d3.treemap()
.tile(d3.treemapResquarify)
.size([viewWidth, viewHeight])
.paddingOuter(0)
.round(true)(hierarchy)
Insert cell
gridLayout = (points) => {
const noVertical = 4
const noHorizontal = 4
const tileWidth = viewWidth / noHorizontal
const tileHeight = viewHeight / noVertical

console.log(tileWidth)
console.log(tileHeight)
const root = hierarchy({ name: 'root', index: 0, children: points.slice(0, noVertical * noHorizontal) })
let x = 0
let y = 0
root.children.forEach(child => {
if (x >= viewWidth) {
x = 0
y += tileHeight
}
child.x0 = x
child.x1 = child.x0 + tileWidth
child.y0 = y
child.y1 = child.y0 + tileHeight
x += tileWidth
})
console.log(root.children)

const svg = d3.create("svg")
.attr("viewBox", [0, 0, viewWidth, viewHeight])
.style("font", "10px sans-serif");
const cell = svg.selectAll("g")
.data(root.children)
.join("g")
.attr("transform", d => (d.depth > 1) ? `translate(${d.parent.x0 + d.x0},${d.parent.y0 + d.y0})` : `translate(${d.x0},${d.y0})`);

cell.append("rect")
.attr("id", d => d.data.id)
.attr("width", d => d.x1 - d.x0)
.attr("height", d => d.y1 - d.y0 )
.attr("stroke", d => 'black')
.attr("fill", d => d.data.color)

cell.append("text")
.attr("dx", d => (d.x1 - d.x0) / 2)
.attr("dy", d => (d.y1 - d.y0) / 2 + 3)
.attr("text-anchor", "middle")
.text(d => format(d.data.distance))
return svg.node()
}
Insert cell
gridLayout(points84)
Insert cell
{
const noVertical = 4
const noHorizontal = 4
const width = 40
const height = 40
const tileWidth = width / noHorizontal
const tileHeight = height / noVertical
const points = points84.slice(0,noVertical * noHorizontal)
const root = hierarchy({ name: 'root', index: 0, children: points})

let x = 0
let y = 0
root.children.forEach(child => {
if (x >= width) {
x = 0
y += tileHeight
}
child.x0 = x
child.x1 = child.x0 + tileWidth
child.y0 = y
child.y1 = child.y0 + tileHeight
x += tileWidth
})

root.children.forEach(child => {
})
return root.children
}
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