Published
Edited
Dec 1, 2020
6 forks
Importers
30 stars
Insert cell
Insert cell
Insert cell
Insert cell
plot = (opt) => {
const [angleA, angleB, angleC] = [270, 150, 30].map(d => [Math.cos(d * rad), Math.sin(d * rad)]),
[A, B, C] = [angleA, angleB, angleC].map(d => [d[0] * size, d[1] * size]),
ABC = [A, B, C],
[dA, dB, dC] = ABC.map(d => [d[0], d[1]]),
dABC = [dA, dB, dC],
solveABx = solveX(-equilateralSlope, A[1]), solveACx = solveX(equilateralSlope, A[1]),
side = solveDist(dA, dB);
const svg = d3.create("svg")
.attr("viewBox", [0, 0, width, height]),
chart = svg.append('g')
.attr('transform', `translate(${w},${h + pad})`),
defs = chart.append('defs')
.append('clipPath')
.attr('id', 'plotarea')
.append('path')
.attr('d', d3.line()(ABC)),
rect = chart.append('path')
.attr('d', d3.line()(ABC))
.attr('fill', 'white'),
gGrid = chart.append('g').attr('clip-path', 'url(#plotarea)'),
fixed = chart.append('g'),
gDot = chart.append('g').attr('clip-path', 'url(#plotarea)'),
dots = gDot.append('g')
.attr('fill', 'none')
.attr('stroke-linecap', 'round');
const points = opt.data.map(d => ({ color: d.color, tooltip: d.tooltip, cartesian: toCartesian(ABC, d.triple) }));
dots.selectAll('path')
.data(points)
.join('path')
.attr('d', d => `M${d.cartesian[0]},${d.cartesian[1]}h0`)
.attr('stroke', d => d.color)
.append('title')
.text(d => d.tooltip);
const a = {}, b = {}, c = {};
a.scale = d3.scaleLinear().domain([100, 0]).range([0, side]);
a.axis = scaledAxis(fixed, A, 30, a.scale, d3.axisLeft);
a.grid = scaledGrid(gGrid, [dA, dB, dC], a.scale);
a.label = rotatedLabel(fixed, [angleB, angleA], angleA, 'middle', -60, -60, 15, 0), a.label(opt.aLabel, opt.aTitle);
b.scale = d3.scaleLinear().domain([100, 0]).range([0, side]);
b.axis = scaledAxis(fixed, B, 0, b.scale, d3.axisBottom);
b.grid = scaledGrid(gGrid, [dB, dC, dA], b.scale);
b.label = rotatedLabel(fixed, [angleC, angleB], angleB, 'end', 180, 0, 30, 20), b.label(opt.bLabel, opt.bTitle);
c.scale = d3.scaleLinear().domain([0, 100]).range([0, side]);
c.axis = scaledAxis(fixed, A, -30, c.scale, d3.axisRight);
c.grid = scaledGrid(gGrid, [dC, dA, dB], c.scale);
c.label = rotatedLabel(fixed, [angleA, angleC], angleC, 'start', 60, 60, 15, 20), c.label(opt.cLabel, opt.cTitle);
const zoom = d3.zoom()
.scaleExtent([1, 100])
.on("zoom", zoomed);

chart.call(zoom).call(zoom.transform, d3.zoomIdentity);
function zoomed({transform}) {
const dt = transform;
// boundary condition for y
const dB_y = B[1] * dt.k + dt.y, dA_y0 = A[1] * dt.k + dt.y;
if (dB_y < B[1]) dt.y += B[1] - dB_y;
else if (dA_y0 > A[1]) dt.y -= dA_y0 - A[1];
// boundary condition for x
const dA_x = A[0] * dt.k + dt.x, dA_y1 = A[1] * dt.k + dt.y,
dAB_xIntercept = solveABx(dA_y1), dAC_xIntercept = solveACx(dA_y1);
if (dA_x > dAB_xIntercept) dt.x -= dA_x - dAB_xIntercept;
else if (dA_x < dAC_xIntercept) dt.x += dAC_xIntercept - dA_x;
// rescale plot
dABC.forEach((d, i) => (d[0] = ABC[i][0] * dt.k + dt.x, d[1] = ABC[i][1] * dt.k + dt.y));
dots.attr('transform', dt).attr('stroke-width', 8 / transform.k);
// rescale axis and grid
const [bA, bB, bC] = ABC.map(d => toBarycentric(dABC, d));
a.scale.domain([rnd100(bA[0]), rnd100(bB[0])]), a.axis(), a.grid();
b.scale.domain([rnd100(bB[1]), rnd100(bC[1])]), b.axis(), b.grid();
c.scale.domain([rnd100(bA[2]), rnd100(bC[2])]), c.axis(), c.grid();
}
return svg.node();
}
Insert cell
solveDist = (p0, p1) => Math.sqrt(Math.pow(p0[0] - p1[0], 2) + Math.pow(p0[1] - p1[1], 2))
Insert cell
solvePoint = (p0, p1, n) => [(p1[0] - p0[0]) * n + p0[0], (p1[1] - p0[1]) * n + p0[1]]
Insert cell
solveX = (m, b) => (y) => (y - b) / m
Insert cell
arrow = (len) => {
const half = len / 2;
return `M ${-half},0 L ${half},0 M ${half - 6},-3 L ${half},0 L ${half - 6},3`;
}
Insert cell
Insert cell
toCartesian = (t, p) => [
[t[0][0] * p[0] + t[1][0] * p[1] + t[2][0] * p[2]],
[t[0][1] * p[0] + t[1][1] * p[1] + t[2][1] * p[2]]
]
Insert cell
toBarycentric = (t, p) => {
const det = (t[1][1] - t[2][1]) * (t[0][0] - t[2][0]) + (t[2][0] - t[1][0]) * (t[0][1] - t[2][1]);
const t1 = ((t[1][1] - t[2][1]) * (p[0] - t[2][0]) + (t[2][0] - t[1][0]) * (p[1] - t[2][1])) / det;
const t2 = ((t[2][1] - t[0][1]) * (p[0] - t[2][0]) + (t[0][0] - t[2][0]) * (p[1] - t[2][1])) / det;
return [t1, t2, 1 - t1 - t2];
}
Insert cell
rotatedLabel = (g, angles, titleAngle, titleAnchor, lineRotation, textRotation, textOffset, titleOffset) => {
const labelSelection = g.append('g'),
lineContainerSelection = labelSelection.append('g'),
textContainerSelection = labelSelection.append('g'),
titleContainerSelection = labelSelection.append('g'),
lineSelection = lineContainerSelection.append('path')
.attr('fill', 'none')
.attr('stroke', 'black')
.attr('stroke-linecap', 'round')
.attr('transform', `rotate(${lineRotation})`),
textSelection = textContainerSelection.append('text')
.attr('text-anchor', 'middle')
.attr('transform', `rotate(${textRotation})`)
.attr('font-family', 'Arial')
.attr('font-size', '12px'),
titleSelection = titleContainerSelection.append('text')
.attr('font-family', 'Arial')
.attr('font-size', '20px')
.attr('text-anchor', titleAnchor);
return (text, title) => {
const titleAnchor = [titleAngle[0] * (size + 25), titleAngle[1] * (size + 25)],
[line0, line1] = angles.map(d => [d[0] * (size + 75), d[1] * (size + 75)]),
[text0, text1] = angles.map(d => [d[0] * (size + 75 + textOffset), d[1] * (size + 75 + textOffset)]),
lineMidpoint = solvePoint(line0, line1, 0.5),
textMidpoint = solvePoint(text0, text1, 0.5);
lineContainerSelection.attr('transform', `translate(${lineMidpoint[0]},${lineMidpoint[1]})`);
textContainerSelection.attr('transform', `translate(${textMidpoint[0]},${textMidpoint[1]})`);
titleContainerSelection.attr('transform', `translate(${titleAnchor[0]},${titleAnchor[1] + titleOffset})`);
lineSelection.attr('d', arrow(100));
textSelection.text(text);
titleSelection.text(title);
}
}
Insert cell
scaledAxis = (g, p, r, scale, axis) => {
const axisSelection = g.append('g');
return () => axisSelection.call(g => g
.attr('transform', `translate(${p[0]},${p[1]}) rotate(${r})`)
.call(axis(scale).tickSizeOuter(0).ticks(5))
.call(g => g.selectAll('line, text').attr('transform', `rotate(-30)`)));
}
Insert cell
scaledGrid = (g, t, scale) => {
const grid = g.append('g');
return () => {
const ticks = scale.ticks(10).map(d => d / 100),
lines = ticks.map(d => d3.line()([
[t[0][0] * d + t[1][0] * (1 - d), t[0][1] * d + t[1][1] * (1 - d)],
[t[0][0] * d + t[2][0] * (1 - d), t[0][1] * d + t[2][1] * (1 - d)]]));
grid.selectAll('path')
.data(lines)
.join(
enter => enter.append('path')
.attr('d', d => d)
.attr('stroke', 'lightgrey'),
update => update
.attr('d', d => d),
exit => exit.remove()
);
}
}
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
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