Public
Edited
Oct 17, 2023
1 fork
Importers
7 stars
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
{
const p1 = {x: 50, y: 50}
const p2 = {x: 250, y: 250}
return svg`
<svg width="${width}" height="300">
<path d="${ connector(p1, p2) }" fill="none" stroke="orange" stroke-width="4"/>
</svg>`
}
Insert cell
Insert cell
{
const context = DOM.context2d(width, 300)
const canvas = context.canvas
const p1 = {x: 50, y: 50}
const p2 = {x: 250, y: 250}
context.beginPath()
connector(p1, p2, {context})
context.lineWidth = 4
context.strokeStyle = "orange"
context.stroke()
return canvas
}
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
straight = (p1, p2, {context = d3.path(), start = true} = {}) => {
// the simplest connector: it links p1 to p2 with a straight segment
if(start)
context.moveTo(p1.x, p1.y)
context.lineTo(p2.x, p2.y)
return context
}
Insert cell
square = (p1, p2, {context = d3.path(), start = true, sweep = true} = {}) => {
// a connector that makes a square turn
if(start)
context.moveTo(p1.x, p1.y)
if(sweep) {
// first segment is horizontal
context.lineTo(p2.x, p1.y)
}
else {
// first segment is vertical
context.lineTo(p1.x, p2.y)
}
context.lineTo(p2.x, p2.y)
return context
}
Insert cell
beveled = (p1, p2, {context = d3.path(), start = true, sweep = true, corner_radius = 25} = {}) => {
// similar to the 'square' connector, but with a 45 degrees bevel

let r = clamped_radius(corner_radius, p1, p2)
let [rx, ry] = signed_radii(r, p1, p2)
if(start)
context.moveTo(p1.x, p1.y)

if(sweep) {
context.lineTo(p2.x-rx, p1.y)
context.lineTo(p2.x, p1.y+ry)
}
else {
context.lineTo(p1.x, p2.y-ry)
context.lineTo(p1.x+rx, p2.y)
}
context.lineTo(p2.x, p2.y)
return context
}
Insert cell
rounded = (p1, p2, {context = d3.path(), start = true, sweep = true, corner_radius = 25} = {}) => {
// similar to the 'beveled' connector, but with a rounded corner

let cw = clockwise(p1, p2) ? 0 : 1
let r = clamped_radius(corner_radius, p1, p2)
if(start)
context.moveTo(p1.x, p1.y)

if(sweep) {
context.arcTo(p2.x, p1.y, p2.x, p2.y, r)
}
else {
context.arcTo(p1.x, p2.y, p2.x, p2.y, r)
}
context.lineTo(p2.x, p2.y)
return context
}
Insert cell
xor = (a, b) => a ? !b : b
Insert cell
clockwise = (p1, p2) => xor(p2.x > p1.x, p2.y > p1.y) // curve is clockwise or counterclockwise according to the quadrant p2 lies in (using p1 as origin)
Insert cell
clamped_radius = (radius, p1, p2) => Math.min(radius, Math.abs(p2.x-p1.x), Math.abs(p2.y-p1.y)) // actual radius is constrained by the relative position of the two points
Insert cell
signed_radii = (r, p1, p2) => {
// radii need to change sign according to the relative displacement of points
const rx = p2.x > p1.x ? r : -r
const ry = p2.y > p1.y ? r : -r
return [rx, ry]
}
Insert cell
connectors = ({straight, square, beveled, rounded})
Insert cell
Insert cell
data = ({
nodes: [
{x: w/4, y: h/4},
{x: 3*w/4, y: 3*h/4}
],
links: [
{source: 0, target: 1}
]
})
Insert cell
Insert cell
Playground = ({value, connector, sweep, corner_radius} = {}) => {
const vis = d3.create('svg')
const node = vis.node()
// avoid unintended sharing of data structures
value = _.cloneDeep(value)
vis.classed('playground', true)
vis
.attr('width', w)
.attr('height', h)
vis.append('text')
.classed('title', true)
.text(connector)
.attr('x', 10)
.attr('y', h-20)
function update() {
vis.selectAll('.link')
.data(value.links)
.join('path')
.classed('link', true)
.attr('d', d => {
const p1 = value.nodes[d.source]
const p2 = value.nodes[d.target]
return connectors[connector](p1, p2, {sweep, corner_radius}).toString()
})

vis.selectAll('.node')
.data(d => value.nodes)
.join(
enter => enter.append('circle')
.classed('node', true)
.call(d3.drag()
.on('drag', (event, d) => {
// d3.pointer(event, vis) does not work...
d.x = event.x
d.y = event.y
set(node, value) // set this Input value and notify
})
)
)
.attr('r', 8)
.attr('cx', d => d.x)
.attr('cy', d => d.y)
}
Object.defineProperty(node, 'value', {
get() {
return value
},
set(v) {
value = v
update()
}
})
node.value = value
return node
}
Insert cell
ex1 = Playground({value: this ? this.value : data, connector: 'straight'})
Insert cell
ex2 = Playground({value: this ? this.value : data, connector: 'square', sweep: sweep})
Insert cell
ex3 = Playground({value: this ? this.value : data, connector: 'beveled', sweep: sweep, corner_radius: corner_radius})
Insert cell
ex4 = Playground({value: this ? this.value : data, connector: 'rounded', sweep: sweep, corner_radius: corner_radius})
Insert cell
multibind(ex1, ex2, ex3, ex4)
Insert cell
Insert cell
w = 300
Insert cell
h = w/1.618
Insert cell
html`<style>
.playground {
border: 4px solid white;
background: whitesmoke;
box-sizing: border-box;
}

.node {
fill: white;
stroke: black;
stroke-width: 4;
}
.node:hover {
cursor: move;
}

.link {
fill: none;
stroke: steelblue;
stroke-width: 4;
}

.title {
font-family: sans-serif;
fill: #777;
pointer-events: none;
}
</style>`
Insert cell
Insert cell
_ = require("lodash")
Insert cell
Insert cell
Insert cell
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