From bb68a14cbc17cd592c05c30cdd0fa22cb72702a7 Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Wed, 7 Oct 2020 23:05:46 -0700 Subject: [PATCH] Improved curves for allocation associations --- ui/app/components/topo-viz.js | 115 ++++++++++++------ ui/app/styles/charts/topo-viz-node.scss | 15 --- ui/app/styles/charts/topo-viz.scss | 16 +++ .../styles/components/dashboard-metric.scss | 4 + ui/app/templates/components/topo-viz.hbs | 13 +- 5 files changed, 107 insertions(+), 56 deletions(-) diff --git a/ui/app/components/topo-viz.js b/ui/app/components/topo-viz.js index a152ac6dc..8eab05e18 100644 --- a/ui/app/components/topo-viz.js +++ b/ui/app/components/topo-viz.js @@ -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]); +} diff --git a/ui/app/styles/charts/topo-viz-node.scss b/ui/app/styles/charts/topo-viz-node.scss index 3f014450a..0baadc7c0 100644 --- a/ui/app/styles/charts/topo-viz-node.scss +++ b/ui/app/styles/charts/topo-viz-node.scss @@ -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; - } -} diff --git a/ui/app/styles/charts/topo-viz.scss b/ui/app/styles/charts/topo-viz.scss index 753d1af82..bfbccb29d 100644 --- a/ui/app/styles/charts/topo-viz.scss +++ b/ui/app/styles/charts/topo-viz.scss @@ -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; + } + } } diff --git a/ui/app/styles/components/dashboard-metric.scss b/ui/app/styles/components/dashboard-metric.scss index f99ae030c..5b04440a1 100644 --- a/ui/app/styles/components/dashboard-metric.scss +++ b/ui/app/styles/components/dashboard-metric.scss @@ -3,6 +3,10 @@ margin-bottom: 1.5em; } + &.column:not(:last-child) { + margin-bottom: 0; + } + .metric { text-align: left; font-weight: $weight-bold; diff --git a/ui/app/templates/components/topo-viz.hbs b/ui/app/templates/components/topo-viz.hbs index 6a604bf30..8648ab396 100644 --- a/ui/app/templates/components/topo-viz.hbs +++ b/ui/app/templates/components/topo-viz.hbs @@ -6,7 +6,10 @@ {{else}} - + - {{#each this.activeEdges as |edge|}} - - {{/each}} + + {{#each this.activeEdges as |edge|}} + + {{/each}} + {{/if}} {{/if}}