Files
Redmine/app/assets/javascripts/revision_graph.js
Go MAEDA df2257eff0 Remove the Raphael.js dependency by switching to SVG API (#43845).
Patch by Mizuki ISHIKAWA (user:ishikawa999).


git-svn-id: https://svn.redmine.org/redmine/trunk@24469 e93f8b46-1217-0410-a6f0-8f06a7374b81
2026-03-01 06:58:28 +00:00

226 lines
7.7 KiB
JavaScript

/**
* Redmine - project management software
* Copyright (C) 2006- Jean-Philippe Lang
* This code is released under the GNU General Public License.
*/
var revisionGraph = null;
const SVG_NS = 'http://www.w3.org/2000/svg';
const XLINK_NS = 'http://www.w3.org/1999/xlink';
function createSvgElement(name) {
return document.createElementNS(SVG_NS, name);
}
function buildRevisionGraph(holder) {
const svg = createSvgElement('svg');
const pathsLayer = createSvgElement('g');
const nodesLayer = createSvgElement('g');
const overlaysLayer = createSvgElement('g');
svg.setAttribute('aria-hidden', 'true');
svg.style.display = 'block';
svg.style.overflow = 'visible';
svg.appendChild(pathsLayer);
svg.appendChild(nodesLayer);
svg.appendChild(overlaysLayer);
while (holder.firstChild) {
holder.removeChild(holder.firstChild);
}
holder.appendChild(svg);
return {
holder: holder,
svg: svg,
pathsLayer: pathsLayer,
nodesLayer: nodesLayer,
overlaysLayer: overlaysLayer
};
}
function clearRevisionGraph(graph) {
[graph.pathsLayer, graph.nodesLayer, graph.overlaysLayer].forEach(layer => layer.replaceChildren());
}
function setRevisionGraphSize(graph, width, height) {
const graphWidth = Math.max(1, Math.ceil(width));
const graphHeight = Math.max(1, Math.ceil(height));
graph.svg.setAttribute('width', graphWidth);
graph.svg.setAttribute('height', graphHeight);
graph.svg.setAttribute('viewBox', '0 0 ' + graphWidth + ' ' + graphHeight);
}
function drawPath(graph, pathData, attrs) {
const path = createSvgElement('path');
const d = pathData.map(function(item) { return String(item); }).join(' ');
path.setAttribute('d', d);
Object.keys(attrs).forEach(function(name) {
path.setAttribute(name, String(attrs[name]));
});
graph.pathsLayer.appendChild(path);
}
function drawCircle(layer, x, y, r, attrs) {
const circle = createSvgElement('circle');
circle.setAttribute('cx', String(x));
circle.setAttribute('cy', String(y));
circle.setAttribute('r', String(r));
Object.keys(attrs).forEach(function(name) {
circle.setAttribute(name, String(attrs[name]));
});
layer.appendChild(circle);
return circle;
}
// Generates a distinct, consistent HSL color for each index.
function colorBySpace(index) {
const hue = (index * 27) % 360;
const band = Math.floor(index / 14);
const saturation = Math.max(40, 72 - (band % 3) * 12);
const lightness = Math.min(56, 42 + (band % 2) * 6);
return 'hsl(' + hue + ', ' + saturation + '%, ' + lightness + '%)';
}
function drawRevisionGraph(holder, commits_hash, graph_space) {
var XSTEP = 20,
CIRCLE_INROW_OFFSET = 10;
var commits_by_scmid = commits_hash,
commits = $.map(commits_by_scmid, function(val,i){return val;});
var max_rdmid = commits.length - 1;
var commit_table_rows = $('table.changesets tr.changeset');
if (!revisionGraph || revisionGraph.holder !== holder) {
revisionGraph = buildRevisionGraph(holder);
}
const graph = revisionGraph;
clearRevisionGraph(graph);
if (commit_table_rows.length === 0) {
setRevisionGraphSize(graph, 1, 1);
return;
}
// init dimensions
var graph_x_offset = commit_table_rows.first().find('td').first().position().left - $(holder).position().left,
graph_y_offset = $(holder).position().top,
graph_right_side = graph_x_offset + (graph_space + 1) * XSTEP,
graph_bottom = commit_table_rows.last().position().top + commit_table_rows.last().height() - graph_y_offset;
var yForRow = function (index, commit) {
var row = commit_table_rows.eq(index);
switch (row.find("td:first").css("vertical-align")) {
case "middle":
return row.position().top + (row.height() / 2) - graph_y_offset;
default:
return row.position().top - graph_y_offset + CIRCLE_INROW_OFFSET;
}
};
setRevisionGraphSize(graph, graph_right_side, graph_bottom);
// init colors
var colors = [];
for (let k = 0; k <= graph_space; k++) {
colors.push(colorBySpace(k));
}
var parent_commit;
var x, y, parent_x, parent_y;
var path, title;
var revision_dot_overlay;
$.each(commits, function(index, commit) {
if (!commit.hasOwnProperty("space"))
commit.space = 0;
y = yForRow(max_rdmid - commit.rdmid);
x = graph_x_offset + XSTEP / 2 + XSTEP * commit.space;
drawCircle(graph.nodesLayer, x, y, 3, {
fill: colors[commit.space],
stroke: 'none'
});
// check for parents in the same column
let noVerticalParents = true;
$.each(commit.parent_scmids, function (index, parentScmid) {
parent_commit = commits_by_scmid[parentScmid];
if (parent_commit) {
if (!parent_commit.hasOwnProperty("space"))
parent_commit.space = 0;
// has parent in the same column on this page
if (parent_commit.space === commit.space)
noVerticalParents = false;
} else {
// has parent in the same column on the other page
noVerticalParents = false;
}
});
// paths to parents
$.each(commit.parent_scmids, function(index, parent_scmid) {
parent_commit = commits_by_scmid[parent_scmid];
if (parent_commit) {
parent_y = yForRow(max_rdmid - parent_commit.rdmid);
parent_x = graph_x_offset + XSTEP / 2 + XSTEP * parent_commit.space;
const controlPointDelta = (parent_y - y) / 8;
if (parent_commit.space === commit.space) {
// vertical path
path = [
'M', x, y,
'V', parent_y];
} else if (noVerticalParents) {
// branch start (Bezier curve)
path = [
'M', x, y,
'C', x, y + controlPointDelta, x, parent_y - controlPointDelta, parent_x, parent_y];
} else if (!parent_commit.hasOwnProperty('vertical_children')) {
// branch end (Bezier curve)
path = [
'M', x, y,
'C', parent_x, y + controlPointDelta, parent_x, parent_y, parent_x, parent_y];
} else {
// path to a commit in a different branch (Bezier curve)
path = [
'M', x, y,
'C', parent_x, y, x, parent_y, parent_x, parent_y];
}
} else {
// vertical path ending at the bottom of the revisionGraph
path = [
'M', x, y,
'V', graph_bottom];
}
drawPath(graph, path, {stroke: colors[commit.space], 'stroke-width': 1.5, fill: 'none'});
});
let overlayLayer = graph.overlaysLayer;
if (commit.href) {
overlayLayer = createSvgElement('a');
overlayLayer.setAttribute('href', commit.href);
overlayLayer.setAttributeNS(XLINK_NS, 'href', commit.href);
graph.overlaysLayer.appendChild(overlayLayer);
}
revision_dot_overlay = drawCircle(overlayLayer, x, y, 10, {
fill: '#000',
opacity: 0,
cursor: commit.href ? 'pointer' : 'default'
});
if(commit.refs != null && commit.refs.length > 0) {
title = createSvgElement('title');
title.appendChild(document.createTextNode(commit.refs));
revision_dot_overlay.appendChild(title);
}
});
};