summaryrefslogtreecommitdiff
path: root/app/assets/javascripts/pipelines/components/dag/drawing_utils.js
diff options
context:
space:
mode:
Diffstat (limited to 'app/assets/javascripts/pipelines/components/dag/drawing_utils.js')
-rw-r--r--app/assets/javascripts/pipelines/components/dag/drawing_utils.js134
1 files changed, 134 insertions, 0 deletions
diff --git a/app/assets/javascripts/pipelines/components/dag/drawing_utils.js b/app/assets/javascripts/pipelines/components/dag/drawing_utils.js
new file mode 100644
index 00000000000..d56addc473f
--- /dev/null
+++ b/app/assets/javascripts/pipelines/components/dag/drawing_utils.js
@@ -0,0 +1,134 @@
+import * as d3 from 'd3';
+import { sankey, sankeyLeft } from 'd3-sankey';
+
+export const calculateClip = ({ y0, y1, source, target, width }) => {
+ /*
+ Because large link values can overrun their box, we create a clip path
+ to trim off the excess in charts that have few nodes per column and are
+ therefore tall.
+
+ The box is created by
+ M: moving to outside midpoint of the source node
+ V: drawing a vertical line to maximum of the bottom link edge or
+ the lowest edge of the node (can be d.y0 or d.y1 depending on the link's path)
+ H: drawing a horizontal line to the outside edge of the destination node
+ V: drawing a vertical line back up to the minimum of the top link edge or
+ the highest edge of the node (can be d.y0 or d.y1 depending on the link's path)
+ H: drawing a horizontal line back to the outside edge of the source node
+ Z: closing the path, back to the start point
+ */
+
+ const bottomLinkEdge = Math.max(y1, y0) + width / 2;
+ const topLinkEdge = Math.min(y0, y1) - width / 2;
+
+ /* eslint-disable @gitlab/require-i18n-strings */
+ return `
+ M${source.x0}, ${y1}
+ V${Math.max(bottomLinkEdge, y0, y1)}
+ H${target.x1}
+ V${Math.min(topLinkEdge, y0, y1)}
+ H${source.x0}
+ Z
+ `;
+ /* eslint-enable @gitlab/require-i18n-strings */
+};
+
+export const createLinkPath = ({ y0, y1, source, target, width }, idx, nodeWidth) => {
+ /*
+ Creates a series of staggered midpoints for the link paths, so they
+ don't run along one channel and can be distinguished.
+
+ First, get a point staggered by index and link width, modulated by the link box
+ to find a point roughly between the nodes.
+
+ Then offset it by nodeWidth, so it doesn't run under any nodes at the left.
+
+ Determine where it would overlap at the right.
+
+ Finally, select the leftmost of these options:
+ - offset from the source node based on index + fudge;
+ - a fuzzy offset from the right node, using Math.random adds a little blur
+ - a hard offset from the end node, if random pushes it over
+
+ Then draw a line from the start node to the bottom-most point of the midline
+ up to the topmost point in that line and then to the middle of the end node
+ */
+
+ const xValRaw = source.x1 + (((idx + 1) * width) % (target.x1 - source.x0));
+ const xValMin = xValRaw + nodeWidth;
+ const overlapPoint = source.x1 + (target.x0 - source.x1);
+ const xValMax = overlapPoint - nodeWidth * 1.4;
+
+ const midPointX = Math.min(xValMin, target.x0 - nodeWidth * 4 * Math.random(), xValMax);
+
+ return d3.line()([
+ [(source.x0 + source.x1) / 2, y0],
+ [midPointX, y0],
+ [midPointX, y1],
+ [(target.x0 + target.x1) / 2, y1],
+ ]);
+};
+
+/*
+ createSankey calls the d3 layout to generate the relationships and positioning
+ values for the nodes and links in the graph.
+ */
+
+export const createSankey = ({
+ width = 10,
+ height = 10,
+ nodeWidth = 10,
+ nodePadding = 10,
+ paddingForLabels = 1,
+} = {}) => {
+ const sankeyGenerator = sankey()
+ .nodeId(({ name }) => name)
+ .nodeAlign(sankeyLeft)
+ .nodeWidth(nodeWidth)
+ .nodePadding(nodePadding)
+ .extent([
+ [paddingForLabels, paddingForLabels],
+ [width - paddingForLabels, height - paddingForLabels],
+ ]);
+ return ({ nodes, links }) =>
+ sankeyGenerator({
+ nodes: nodes.map(d => ({ ...d })),
+ links: links.map(d => ({ ...d })),
+ });
+};
+
+export const labelPosition = ({ x0, x1, y0, y1 }, viewOptions) => {
+ const { paddingForLabels, labelMargin, nodePadding, width } = viewOptions;
+
+ const firstCol = x0 <= paddingForLabels;
+ const lastCol = x1 >= width - paddingForLabels;
+
+ if (firstCol) {
+ return {
+ x: 0 + labelMargin,
+ y: y0,
+ height: `${y1 - y0}px`,
+ width: paddingForLabels - 2 * labelMargin,
+ textAlign: 'right',
+ };
+ }
+
+ if (lastCol) {
+ return {
+ x: width - paddingForLabels + labelMargin,
+ y: y0,
+ height: `${y1 - y0}px`,
+ width: paddingForLabels - 2 * labelMargin,
+ textAlign: 'left',
+ };
+ }
+
+ return {
+ x: (x1 + x0) / 2,
+ y: y0 - nodePadding,
+ height: `${nodePadding}px`,
+ width: 'max-content',
+ wrapperWidth: paddingForLabels - 2 * labelMargin,
+ textAlign: x0 < width / 2 ? 'left' : 'right',
+ };
+};