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();
}