Published
Edited
Dec 1, 2020
6 forks
Importers
29 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

One platform to build and deploy the best data apps

Experiment and prototype by building visualizations in live JavaScript notebooks. Collaborate with your team and decide which concepts to build out.
Use Observable Framework to build data apps locally. Use data loaders to build in any language or library, including Python, SQL, and R.
Seamlessly deploy to Observable. Test before you ship, use automatic deploy-on-commit, and ensure your projects are always up-to-date.
Learn more