Improved curves for allocation associations

This commit is contained in:
Michael Lange
2020-10-07 23:05:46 -07:00
parent a4c8ce4ee0
commit bb68a14cbc
5 changed files with 107 additions and 56 deletions

View File

@@ -5,6 +5,7 @@ import { run } from '@ember/runloop';
import { task } from 'ember-concurrency';
import { scaleLinear } from 'd3-scale';
import { extent, deviation, mean } from 'd3-array';
import { line, curveBasis } from 'd3-shape';
import RSVP from 'rsvp';
export default class TopoViz extends Component {
@@ -16,6 +17,7 @@ export default class TopoViz extends Component {
@tracked activeNode = null;
@tracked activeAllocation = null;
@tracked activeEdges = [];
@tracked edgeOffset = { x: 0, y: 0 };
get isSingleColumn() {
if (this.topology.datacenters.length <= 1) return true;
@@ -33,7 +35,7 @@ export default class TopoViz extends Component {
get datacenterIsSingleColumn() {
// If there are enough nodes, use two columns of nodes within
// a single column layout of datacenteres to increase density.
// a single column layout of datacenters to increase density.
return !this.isSingleColumn || (this.isSingleColumn && this.args.nodes.length <= 20);
}
@@ -204,46 +206,85 @@ export default class TopoViz extends Component {
computedActiveEdges() {
// Wait a render cycle
run.next(() => {
const activeEl = this.element.querySelector(
`[data-allocation-id="${this.activeAllocation.allocation.id}"]`
);
const selectedAllocations = this.element.querySelectorAll('.memory .bar.is-selected');
const activeBBox = activeEl.getBoundingClientRect();
// const path = line().curve(curveCardinal.tension(0.5));
const path = line().curve(curveBasis);
// 1. Get the active element
const allocation = this.activeAllocation.allocation;
const activeEl = this.element.querySelector(`[data-allocation-id="${allocation.id}"]`);
const activePoint = centerOfBBox(activeEl.getBoundingClientRect());
const vLeft = window.visualViewport.pageLeft;
const vTop = window.visualViewport.pageTop;
// Lines to the memory rect of each selected allocation
const edges = [];
for (let allocation of selectedAllocations) {
if (allocation !== activeEl) {
const bbox = allocation.getBoundingClientRect();
edges.push({
x1: activeBBox.x + activeBBox.width / 2 + vLeft,
y1: activeBBox.y + activeBBox.height / 2 + vTop,
x2: bbox.x + bbox.width / 2 + vLeft,
y2: bbox.y + bbox.height / 2 + vTop,
});
}
}
// Lines from the memory rect to the cpu rect
for (let allocation of selectedAllocations) {
const id = allocation.closest('[data-allocation-id]').dataset.allocationId;
const cpu = allocation
// 2. Collect the mem and cpu pairs for all selected allocs
const selectedMem = Array.from(this.element.querySelectorAll('.memory .bar.is-selected'));
const selectedPairs = selectedMem.map(mem => {
const id = mem.closest('[data-allocation-id]').dataset.allocationId;
const cpu = mem
.closest('.topo-viz-node')
.querySelector(`.cpu .bar[data-allocation-id="${id}"]`);
const bboxMem = allocation.getBoundingClientRect();
const bboxCpu = cpu.getBoundingClientRect();
edges.push({
x1: bboxMem.x + bboxMem.width / 2 + vLeft,
y1: bboxMem.y + bboxMem.height / 2 + vTop,
x2: bboxCpu.x + bboxCpu.width / 2 + vLeft,
y2: bboxCpu.y + bboxCpu.height / 2 + vTop,
});
}
return [mem, cpu];
});
const selectedPoints = selectedPairs.map(pair => {
return pair.map(el => centerOfBBox(el.getBoundingClientRect()));
});
this.activeEdges = edges;
// 3. For each pair, compute the midpoint of the truncated triangle of points [Mem, Cpu, Active]
selectedPoints.forEach(points => {
const d1 = pointBetween(points[0], activePoint, 100, 0.5);
const d2 = pointBetween(points[1], activePoint, 100, 0.5);
points.push(midpoint(d1, d2));
});
// 4. Generate curves for each active->mem and active->cpu pair going through the bisector
const curves = [];
// Steps are used to restrict the range of curves. The closer control points are placed, the less
// curvature the curve generator will generate.
const stepsMain = [0, 0.8, 1.0];
// The second prong the fork does not need to retrace the entire path from the activePoint
const stepsSecondary = [0.8, 1.0];
selectedPoints.forEach(points => {
curves.push(
curveFromPoints(...pointsAlongPath(activePoint, points[2], stepsMain), points[0]),
curveFromPoints(...pointsAlongPath(activePoint, points[2], stepsSecondary), points[1])
);
});
this.activeEdges = curves.map(curve => path(curve));
this.edgeOffset = { x: window.visualViewport.pageLeft, y: window.visualViewport.pageTop };
});
}
}
function centerOfBBox(bbox) {
return {
x: bbox.x + bbox.width / 2,
y: bbox.y + bbox.height / 2,
};
}
function dist(p1, p2) {
return Math.sqrt(Math.pow(p2.x - p1.x, 2) + Math.pow(p2.y - p1.y, 2));
}
// Return the point between p1 and p2 at len (or pct if len > dist(p1, p2))
function pointBetween(p1, p2, len, pct) {
const d = dist(p1, p2);
const ratio = d < len ? pct : len / d;
return pointBetweenPct(p1, p2, ratio);
}
function pointBetweenPct(p1, p2, pct) {
const dx = p2.x - p1.x;
const dy = p2.y - p1.y;
return { x: p1.x + dx * pct, y: p1.y + dy * pct };
}
function pointsAlongPath(p1, p2, pcts) {
return pcts.map(pct => pointBetweenPct(p1, p2, pct));
}
function midpoint(p1, p2) {
return pointBetweenPct(p1, p2, 0.5);
}
function curveFromPoints(...points) {
return points.map(p => [p.x, p.y]);
}

View File

@@ -77,18 +77,3 @@
.flex-masonry-columns-2 > .flex-masonry-item > .chart.topo-viz-node svg {
width: calc(100% - 0.75em);
}
.chart.topo-viz-edges {
width: 100%;
height: 100%;
position: absolute;
left: 0;
top: 0;
pointer-events: none;
overflow: visible;
.edge {
stroke-width: 2;
stroke: $blue;
}
}

View File

@@ -16,4 +16,20 @@
&.is-single-column .topo-viz-datacenter {
width: 100%;
}
.topo-viz-edges {
width: 100%;
height: 100%;
position: absolute;
left: 0;
top: 0;
pointer-events: none;
overflow: visible;
.edge {
stroke-width: 2;
stroke: $blue;
fill: none;
}
}
}

View File

@@ -3,6 +3,10 @@
margin-bottom: 1.5em;
}
&.column:not(:last-child) {
margin-bottom: 0;
}
.metric {
text-align: left;
font-weight: $weight-bold;

View File

@@ -6,7 +6,10 @@
<LoadingSpinner />
</div>
{{else}}
<FlexMasonry @columns={{if this.isSingleColumn 1 2}} @items={{this.topology.datacenters}} @withSpacing={{true}} as |dc reflow|>
<FlexMasonry
@columns={{if this.isSingleColumn 1 2}}
@items={{this.topology.datacenters}}
@withSpacing={{true}} as |dc reflow|>
<TopoViz::Datacenter
@datacenter={{dc}}
@isSingleColumn={{this.datacenterIsSingleColumn}}
@@ -19,9 +22,11 @@
{{#if this.activeAllocation}}
<svg class="chart topo-viz-edges" {{window-resize this.computedActiveEdges}}>
{{#each this.activeEdges as |edge|}}
<line class="edge" x1={{edge.x1}} y1={{edge.y1}} x2={{edge.x2}} y2={{edge.y2}} />
{{/each}}
<g transform="translate({{this.edgeOffset.x}},{{this.edgeOffset.y}})">
{{#each this.activeEdges as |edge|}}
<path class="edge" d={{edge}} />
{{/each}}
</g>
</svg>
{{/if}}
{{/if}}