Rewrite GitGraph.js (#12137)
The current vendored gitgraph.js is no longer maintained and is difficult to understand, fix and maintain. This PR completely rewrites its logic - hopefully in a clearer fashion and easier to maintain. It also includes @silverwind's improvements of coloring the commit dots and preventing the flash of incorrect content. Further changes to contemplate in future will be abstracting out of the flows to an object, storing the involved commit references on the flows etc. However, this is probably a required step for this. Replaces #12131 Fixes #11981 (part 3) Signed-off-by: Andrew Thornton <art27@cantab.net>
This commit is contained in:
parent
3c4388f668
commit
2ab185d3ab
3 changed files with 567 additions and 436 deletions
|
@ -1,13 +1,568 @@
|
|||
// Although inspired by the https://github.com/bluef/gitgraph.js/blob/master/gitgraph.js
|
||||
// this has been completely rewritten with almost no remaining code
|
||||
|
||||
// GitGraphCanvas is a canvas for drawing gitgraphs on to
|
||||
class GitGraphCanvas {
|
||||
constructor(canvas, widthUnits, heightUnits, config) {
|
||||
this.ctx = canvas.getContext('2d');
|
||||
|
||||
const width = widthUnits * config.unitSize;
|
||||
this.height = heightUnits * config.unitSize;
|
||||
|
||||
const ratio = window.devicePixelRatio || 1;
|
||||
|
||||
canvas.width = width * ratio;
|
||||
canvas.height = this.height * ratio;
|
||||
|
||||
canvas.style.width = `${width}px`;
|
||||
canvas.style.height = `${this.height}px`;
|
||||
|
||||
this.ctx.lineWidth = config.lineWidth;
|
||||
this.ctx.lineJoin = 'round';
|
||||
this.ctx.lineCap = 'round';
|
||||
|
||||
this.ctx.scale(ratio, ratio);
|
||||
this.config = config;
|
||||
}
|
||||
drawLine(moveX, moveY, lineX, lineY, color) {
|
||||
this.ctx.strokeStyle = color;
|
||||
this.ctx.beginPath();
|
||||
this.ctx.moveTo(moveX, moveY);
|
||||
this.ctx.lineTo(lineX, lineY);
|
||||
this.ctx.stroke();
|
||||
}
|
||||
drawLineRight(x, y, color) {
|
||||
this.drawLine(
|
||||
x - 0.5 * this.config.unitSize,
|
||||
y + this.config.unitSize / 2,
|
||||
x + 0.5 * this.config.unitSize,
|
||||
y + this.config.unitSize / 2,
|
||||
color
|
||||
);
|
||||
}
|
||||
drawLineUp(x, y, color) {
|
||||
this.drawLine(
|
||||
x,
|
||||
y + this.config.unitSize / 2,
|
||||
x,
|
||||
y - this.config.unitSize / 2,
|
||||
color
|
||||
);
|
||||
}
|
||||
drawNode(x, y, color) {
|
||||
this.ctx.strokeStyle = color;
|
||||
|
||||
this.drawLineUp(x, y, color);
|
||||
|
||||
this.ctx.beginPath();
|
||||
this.ctx.arc(x, y, this.config.nodeRadius, 0, Math.PI * 2, true);
|
||||
this.ctx.fillStyle = color;
|
||||
this.ctx.fill();
|
||||
}
|
||||
drawLineIn(x, y, color) {
|
||||
this.drawLine(
|
||||
x + 0.5 * this.config.unitSize,
|
||||
y + this.config.unitSize / 2,
|
||||
x - 0.5 * this.config.unitSize,
|
||||
y - this.config.unitSize / 2,
|
||||
color
|
||||
);
|
||||
}
|
||||
drawLineOut(x, y, color) {
|
||||
this.drawLine(
|
||||
x - 0.5 * this.config.unitSize,
|
||||
y + this.config.unitSize / 2,
|
||||
x + 0.5 * this.config.unitSize,
|
||||
y - this.config.unitSize / 2,
|
||||
color
|
||||
);
|
||||
}
|
||||
drawSymbol(symbol, columnNumber, rowNumber, color) {
|
||||
const y = this.height - this.config.unitSize * (rowNumber + 0.5);
|
||||
const x = this.config.unitSize * 0.5 * (columnNumber + 1);
|
||||
switch (symbol) {
|
||||
case '-':
|
||||
if (columnNumber % 2 === 1) {
|
||||
this.drawLineRight(x, y, color);
|
||||
}
|
||||
break;
|
||||
case '_':
|
||||
this.drawLineRight(x, y, color);
|
||||
break;
|
||||
case '*':
|
||||
this.drawNode(x, y, color);
|
||||
break;
|
||||
case '|':
|
||||
this.drawLineUp(x, y, color);
|
||||
break;
|
||||
case '/':
|
||||
this.drawLineOut(x, y, color);
|
||||
break;
|
||||
case '\\':
|
||||
this.drawLineIn(x, y, color);
|
||||
break;
|
||||
case '.':
|
||||
case ' ':
|
||||
break;
|
||||
default:
|
||||
console.error('Unknown symbol', symbol, color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class GitGraph {
|
||||
constructor(canvas, rawRows, config) {
|
||||
this.rows = [];
|
||||
let maxWidth = 0;
|
||||
|
||||
for (let i = 0; i < rawRows.length; i++) {
|
||||
const rowStr = rawRows[i];
|
||||
maxWidth = Math.max(rowStr.replace(/([_\s.-])/g, '').length, maxWidth);
|
||||
|
||||
const rowArray = rowStr.split('');
|
||||
|
||||
this.rows.unshift(rowArray);
|
||||
}
|
||||
|
||||
this.currentFlows = [];
|
||||
this.previousFlows = [];
|
||||
|
||||
this.gitGraphCanvas = new GitGraphCanvas(
|
||||
canvas,
|
||||
maxWidth,
|
||||
this.rows.length,
|
||||
config
|
||||
);
|
||||
}
|
||||
|
||||
generateNewFlow(column) {
|
||||
let newId;
|
||||
|
||||
do {
|
||||
newId = generateRandomColorString();
|
||||
} while (this.hasFlow(newId, column));
|
||||
|
||||
return {id: newId, color: `#${newId}`};
|
||||
}
|
||||
|
||||
hasFlow(id, column) {
|
||||
// We want to find the flow with the current ID
|
||||
// Possible flows are those in the currentFlows
|
||||
// Or flows in previousFlows[column-2:...]
|
||||
for (
|
||||
let idx = column - 2 < 0 ? 0 : column - 2;
|
||||
idx < this.previousFlows.length;
|
||||
idx++
|
||||
) {
|
||||
if (this.previousFlows[idx] && this.previousFlows[idx].id === id) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
for (let idx = 0; idx < this.currentFlows.length; idx++) {
|
||||
if (this.currentFlows[idx] && this.currentFlows[idx].id === id) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
takePreviousFlow(column) {
|
||||
if (column < this.previousFlows.length && this.previousFlows[column]) {
|
||||
const flow = this.previousFlows[column];
|
||||
this.previousFlows[column] = null;
|
||||
return flow;
|
||||
}
|
||||
return this.generateNewFlow(column);
|
||||
}
|
||||
|
||||
draw() {
|
||||
if (this.rows.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.currentFlows = new Array(this.rows[0].length);
|
||||
|
||||
// Generate flows for the first row - I do not believe that this can contain '_', '-', '.'
|
||||
for (let column = 0; column < this.rows[0].length; column++) {
|
||||
if (this.rows[0][column] === ' ') {
|
||||
continue;
|
||||
}
|
||||
this.currentFlows[column] = this.generateNewFlow(column);
|
||||
}
|
||||
|
||||
// Draw the first row
|
||||
for (let column = 0; column < this.rows[0].length; column++) {
|
||||
const symbol = this.rows[0][column];
|
||||
const color = this.currentFlows[column] ? this.currentFlows[column].color : '';
|
||||
this.gitGraphCanvas.drawSymbol(symbol, column, 0, color);
|
||||
}
|
||||
|
||||
for (let row = 1; row < this.rows.length; row++) {
|
||||
// Done previous row - step up the row
|
||||
const currentRow = this.rows[row];
|
||||
const previousRow = this.rows[row - 1];
|
||||
|
||||
this.previousFlows = this.currentFlows;
|
||||
this.currentFlows = new Array(currentRow.length);
|
||||
|
||||
// Set flows for this row
|
||||
for (let column = 0; column < currentRow.length; column++) {
|
||||
column = this.setFlowFor(column, currentRow, previousRow);
|
||||
}
|
||||
|
||||
// Draw this row
|
||||
for (let column = 0; column < currentRow.length; column++) {
|
||||
const symbol = currentRow[column];
|
||||
const color = this.currentFlows[column] ? this.currentFlows[column].color : '';
|
||||
this.gitGraphCanvas.drawSymbol(symbol, column, row, color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setFlowFor(column, currentRow, previousRow) {
|
||||
const symbol = currentRow[column];
|
||||
switch (symbol) {
|
||||
case '|':
|
||||
case '*':
|
||||
return this.setUpFlow(column, currentRow, previousRow);
|
||||
case '/':
|
||||
return this.setOutFlow(column, currentRow, previousRow);
|
||||
case '\\':
|
||||
return this.setInFlow(column, currentRow, previousRow);
|
||||
case '_':
|
||||
return this.setRightFlow(column, currentRow, previousRow);
|
||||
case '-':
|
||||
return this.setLeftFlow(column, currentRow, previousRow);
|
||||
case ' ':
|
||||
// In space no one can hear you flow ... (?)
|
||||
return column;
|
||||
default:
|
||||
// Unexpected so let's generate a new flow and wait for bug-reports
|
||||
this.currentFlows[column] = this.generateNewFlow(column);
|
||||
return column;
|
||||
}
|
||||
}
|
||||
|
||||
// setUpFlow handles '|' or '*' - returns the last column that was set
|
||||
// generally we prefer to take the left most flow from the previous row
|
||||
setUpFlow(column, currentRow, previousRow) {
|
||||
// If ' |/' or ' |_'
|
||||
// '/|' '/|' -> Take the '|' flow directly beneath us
|
||||
if (
|
||||
column + 1 < currentRow.length &&
|
||||
(currentRow[column + 1] === '/' || currentRow[column + 1] === '_') &&
|
||||
column < previousRow.length &&
|
||||
(previousRow[column] === '|' || previousRow[column] === '*') &&
|
||||
previousRow[column - 1] === '/'
|
||||
) {
|
||||
this.currentFlows[column] = this.takePreviousFlow(column);
|
||||
return column;
|
||||
}
|
||||
|
||||
// If ' |/' or ' |_'
|
||||
// '/ ' '/ ' -> Take the '/' flow from the preceding column
|
||||
if (
|
||||
column + 1 < currentRow.length &&
|
||||
(currentRow[column + 1] === '/' || currentRow[column + 1] === '_') &&
|
||||
column - 1 < previousRow.length &&
|
||||
previousRow[column - 1] === '/'
|
||||
) {
|
||||
this.currentFlows[column] = this.takePreviousFlow(column - 1);
|
||||
return column;
|
||||
}
|
||||
|
||||
// If ' |'
|
||||
// '/' -> Take the '/' flow - (we always prefer the left-most flow)
|
||||
if (
|
||||
column > 0 &&
|
||||
column - 1 < previousRow.length &&
|
||||
previousRow[column - 1] === '/'
|
||||
) {
|
||||
this.currentFlows[column] = this.takePreviousFlow(column - 1);
|
||||
return column;
|
||||
}
|
||||
|
||||
// If '|' OR '|' take the '|' flow
|
||||
// '|' '*'
|
||||
if (
|
||||
column < previousRow.length &&
|
||||
(previousRow[column] === '|' || previousRow[column] === '*')
|
||||
) {
|
||||
this.currentFlows[column] = this.takePreviousFlow(column);
|
||||
return column;
|
||||
}
|
||||
|
||||
// If '| ' keep the '\' flow
|
||||
// ' \'
|
||||
if (column + 1 < previousRow.length && previousRow[column + 1] === '\\') {
|
||||
this.currentFlows[column] = this.takePreviousFlow(column + 1);
|
||||
return column;
|
||||
}
|
||||
|
||||
// Otherwise just create a new flow - probably this is an error...
|
||||
this.currentFlows[column] = this.generateNewFlow(column);
|
||||
return column;
|
||||
}
|
||||
|
||||
// setOutFlow handles '/' - returns the last column that was set
|
||||
// generally we prefer to take the left most flow from the previous row
|
||||
setOutFlow(column, currentRow, previousRow) {
|
||||
// If '_/' -> keep the '_' flow
|
||||
if (column > 0 && currentRow[column - 1] === '_') {
|
||||
this.currentFlows[column] = this.currentFlows[column - 1];
|
||||
return column;
|
||||
}
|
||||
|
||||
// If '_|/' -> keep the '_' flow
|
||||
if (
|
||||
column > 1 &&
|
||||
(currentRow[column - 1] === '|' || currentRow[column - 1] === '*') &&
|
||||
currentRow[column - 2] === '_'
|
||||
) {
|
||||
this.currentFlows[column] = this.currentFlows[column - 2];
|
||||
return column;
|
||||
}
|
||||
|
||||
// If '|/'
|
||||
// '/' -> take the '/' flow (if it is still available)
|
||||
if (
|
||||
column > 1 &&
|
||||
currentRow[column - 1] === '|' &&
|
||||
column - 2 < previousRow.length &&
|
||||
previousRow[column - 2] === '/'
|
||||
) {
|
||||
this.currentFlows[column] = this.takePreviousFlow(column - 2);
|
||||
return column;
|
||||
}
|
||||
|
||||
// If ' /'
|
||||
// '/' -> take the '/' flow, but transform the symbol to '|' due to our spacing
|
||||
// This should only happen if there are 3 '/' - in a row so we don't need to be cleverer here
|
||||
if (
|
||||
column > 0 &&
|
||||
currentRow[column - 1] === ' ' &&
|
||||
column - 1 < previousRow.length &&
|
||||
previousRow[column - 1] === '/'
|
||||
) {
|
||||
this.currentFlows[column] = this.takePreviousFlow(column - 1);
|
||||
currentRow[column] = '|';
|
||||
return column;
|
||||
}
|
||||
|
||||
// If ' /'
|
||||
// '|' -> take the '|' flow
|
||||
if (
|
||||
column > 0 &&
|
||||
currentRow[column - 1] === ' ' &&
|
||||
column - 1 < previousRow.length &&
|
||||
(previousRow[column - 1] === '|' || previousRow[column - 1] === '*')
|
||||
) {
|
||||
this.currentFlows[column] = this.takePreviousFlow(column - 1);
|
||||
return column;
|
||||
}
|
||||
|
||||
// If '/' <- Not sure this ever happens... but take the '\' flow
|
||||
// '\'
|
||||
if (column < previousRow.length && previousRow[column] === '\\') {
|
||||
this.currentFlows[column] = this.takePreviousFlow(column);
|
||||
return column;
|
||||
}
|
||||
|
||||
// Otherwise just generate a new flow and wait for bug-reports...
|
||||
this.currentFlows[column] = this.generateNewFlow(column);
|
||||
return column;
|
||||
}
|
||||
|
||||
// setInFlow handles '\' - returns the last column that was set
|
||||
// generally we prefer to take the left most flow from the previous row
|
||||
setInFlow(column, currentRow, previousRow) {
|
||||
// If '\?'
|
||||
// '/?' -> take the '/' flow
|
||||
if (column < previousRow.length && previousRow[column] === '/') {
|
||||
this.currentFlows[column] = this.takePreviousFlow(column);
|
||||
return column;
|
||||
}
|
||||
|
||||
// If '\?'
|
||||
// ' \' -> take the '\' flow and reassign to '|'
|
||||
// This should only happen if there are 3 '\' - in a row so we don't need to be cleverer here
|
||||
if (column + 1 < previousRow.length && previousRow[column + 1] === '\\') {
|
||||
this.currentFlows[column] = this.takePreviousFlow(column + 1);
|
||||
currentRow[column] = '|';
|
||||
return column;
|
||||
}
|
||||
|
||||
// If '\?'
|
||||
// ' |' -> take the '|' flow
|
||||
if (
|
||||
column + 1 < previousRow.length &&
|
||||
(previousRow[column + 1] === '|' || previousRow[column + 1] === '*')
|
||||
) {
|
||||
this.currentFlows[column] = this.takePreviousFlow(column + 1);
|
||||
return column;
|
||||
}
|
||||
|
||||
// Otherwise just generate a new flow and wait for bug-reports if we're wrong...
|
||||
this.currentFlows[column] = this.generateNewFlow(column);
|
||||
return column;
|
||||
}
|
||||
|
||||
// setRightFlow handles '_' - returns the last column that was set
|
||||
// generally we prefer to take the left most flow from the previous row
|
||||
setRightFlow(column, currentRow, previousRow) {
|
||||
// if '__' keep the '_' flow
|
||||
if (column > 0 && currentRow[column - 1] === '_') {
|
||||
this.currentFlows[column] = this.currentFlows[column - 1];
|
||||
return column;
|
||||
}
|
||||
|
||||
// if '_|_' -> keep the '_' flow
|
||||
if (
|
||||
column > 1 &&
|
||||
currentRow[column - 1] === '|' &&
|
||||
currentRow[column - 2] === '_'
|
||||
) {
|
||||
this.currentFlows[column] = this.currentFlows[column - 2];
|
||||
return column;
|
||||
}
|
||||
|
||||
// if ' _' -> take the '/' flow
|
||||
// '/ '
|
||||
if (
|
||||
column > 0 &&
|
||||
column - 1 < previousRow.length &&
|
||||
previousRow[column - 1] === '/'
|
||||
) {
|
||||
this.currentFlows[column] = this.takePreviousFlow(column - 1);
|
||||
return column;
|
||||
}
|
||||
|
||||
// if ' |_'
|
||||
// '/? ' -> take the '/' flow (this may cause generation...)
|
||||
// we can do this because we know that git graph
|
||||
// doesn't create compact graphs like: ' |_'
|
||||
// '//'
|
||||
if (
|
||||
column > 1 &&
|
||||
column - 2 < previousRow.length &&
|
||||
previousRow[column - 2] === '/'
|
||||
) {
|
||||
this.currentFlows[column] = this.takePreviousFlow(column - 2);
|
||||
return column;
|
||||
}
|
||||
|
||||
// There really shouldn't be another way of doing this - generate and wait for bug-reports...
|
||||
|
||||
this.currentFlows[column] = this.generateNewFlow(column);
|
||||
return column;
|
||||
}
|
||||
|
||||
// setLeftFlow handles '----.' - returns the last column that was set
|
||||
// generally we prefer to take the left most flow from the previous row that terminates this left recursion
|
||||
setLeftFlow(column, currentRow, previousRow) {
|
||||
// This is: '----------.' or the like
|
||||
// ' \ \ /|\'
|
||||
|
||||
// Find the end of the '-' or nearest '/|\' in the previousRow :
|
||||
let originalColumn = column;
|
||||
let flow;
|
||||
for (; column < currentRow.length && currentRow[column] === '-'; column++) {
|
||||
if (column > 0 && column - 1 < previousRow.length && previousRow[column - 1] === '/') {
|
||||
flow = this.takePreviousFlow(column - 1);
|
||||
break;
|
||||
} else if (column < previousRow.length && previousRow[column] === '|') {
|
||||
flow = this.takePreviousFlow(column);
|
||||
break;
|
||||
} else if (
|
||||
column + 1 < previousRow.length &&
|
||||
previousRow[column + 1] === '\\'
|
||||
) {
|
||||
flow = this.takePreviousFlow(column + 1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// if we have a flow then we found a '/|\' in the previousRow
|
||||
if (flow) {
|
||||
for (; originalColumn < column + 1; originalColumn++) {
|
||||
this.currentFlows[originalColumn] = flow;
|
||||
}
|
||||
return column;
|
||||
}
|
||||
|
||||
// If the symbol in the column is not a '.' then there's likely an error
|
||||
if (currentRow[column] !== '.') {
|
||||
// It really should end in a '.' but this one doesn't...
|
||||
// 1. Step back - we don't want to eat this column
|
||||
column--;
|
||||
// 2. Generate a new flow and await bug-reports...
|
||||
this.currentFlows[column] = this.generateNewFlow(column);
|
||||
|
||||
// 3. Assign all of the '-' to the same flow.
|
||||
for (; originalColumn < column; originalColumn++) {
|
||||
this.currentFlows[originalColumn] = this.currentFlows[column];
|
||||
}
|
||||
return column;
|
||||
}
|
||||
|
||||
// We have a terminal '.' eg. the current row looks like '----.'
|
||||
// the previous row should look like one of '/|\' eg. ' \'
|
||||
if (column > 0 && column - 1 < previousRow.length && previousRow[column - 1] === '/') {
|
||||
flow = this.takePreviousFlow(column - 1);
|
||||
} else if (column < previousRow.length && previousRow[column] === '|') {
|
||||
flow = this.takePreviousFlow(column);
|
||||
} else if (
|
||||
column + 1 < previousRow.length &&
|
||||
previousRow[column + 1] === '\\'
|
||||
) {
|
||||
flow = this.takePreviousFlow(column + 1);
|
||||
} else {
|
||||
// Again unexpected so let's generate and wait the bug-report
|
||||
flow = this.generateNewFlow(column);
|
||||
}
|
||||
|
||||
// Assign all of the rest of the ----. to this flow.
|
||||
for (; originalColumn < column + 1; originalColumn++) {
|
||||
this.currentFlows[originalColumn] = flow;
|
||||
}
|
||||
|
||||
return column;
|
||||
}
|
||||
}
|
||||
|
||||
function generateRandomColorString() {
|
||||
const chars = '0123456789ABCDEF';
|
||||
const stringLength = 6;
|
||||
let randomString = '',
|
||||
rnum,
|
||||
i;
|
||||
for (i = 0; i < stringLength; i++) {
|
||||
rnum = Math.floor(Math.random() * chars.length);
|
||||
randomString += chars.substring(rnum, rnum + 1);
|
||||
}
|
||||
|
||||
return randomString;
|
||||
}
|
||||
|
||||
export default async function initGitGraph() {
|
||||
const graphCanvas = document.getElementById('graph-canvas');
|
||||
if (!graphCanvas) return;
|
||||
|
||||
const {default: gitGraph} = await import(/* webpackChunkName: "gitgraph" */'../vendor/gitgraph.js');
|
||||
if (!graphCanvas || !graphCanvas.getContext) return;
|
||||
|
||||
// Grab the raw graphList
|
||||
const graphList = [];
|
||||
$('#graph-raw-list li span.node-relation').each(function () {
|
||||
graphList.push($(this).text());
|
||||
});
|
||||
|
||||
gitGraph(graphCanvas, graphList);
|
||||
// Define some drawing parameters
|
||||
const config = {
|
||||
unitSize: 20,
|
||||
lineWidth: 3,
|
||||
nodeRadius: 4
|
||||
};
|
||||
|
||||
|
||||
const gitGraph = new GitGraph(graphCanvas, graphList, config);
|
||||
gitGraph.draw();
|
||||
graphCanvas.closest('#git-graph-container').classList.add('in');
|
||||
}
|
||||
|
|
432
web_src/js/vendor/gitgraph.js
vendored
432
web_src/js/vendor/gitgraph.js
vendored
|
@ -1,432 +0,0 @@
|
|||
/* This is a customized version of https://github.com/bluef/gitgraph.js/blob/master/gitgraph.js
|
||||
Changes include conversion to ES6 and linting fixes */
|
||||
|
||||
/*
|
||||
* @license magnet:?xt=urn:btih:c80d50af7d3db9be66a4d0a86db0286e4fd33292&dn=bsd-3-clause.txt BSD 3-Clause
|
||||
* Copyright (c) 2011, Terrence Lee <kill889@gmail.com>
|
||||
* All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
* * Redistributions of source code must retain the above copyright
|
||||
* notice, this list of conditions and the following disclaimer.
|
||||
* * Redistributions in binary form must reproduce the above copyright
|
||||
* notice, this list of conditions and the following disclaimer in the
|
||||
* documentation and/or other materials provided with the distribution.
|
||||
* * Neither the name of the <organization> nor the
|
||||
* names of its contributors may be used to endorse or promote products
|
||||
* derived from this software without specific prior written permission.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
||||
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
* DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> BE LIABLE FOR ANY
|
||||
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
|
||||
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
export default function gitGraph(canvas, rawGraphList, config) {
|
||||
if (!canvas.getContext) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof config === 'undefined') {
|
||||
config = {
|
||||
unitSize: 20,
|
||||
lineWidth: 3,
|
||||
nodeRadius: 4
|
||||
};
|
||||
}
|
||||
|
||||
const flows = [];
|
||||
const graphList = [];
|
||||
|
||||
const ctx = canvas.getContext('2d');
|
||||
|
||||
const devicePixelRatio = window.devicePixelRatio || 1;
|
||||
const backingStoreRatio = ctx.webkitBackingStorePixelRatio
|
||||
|| ctx.mozBackingStorePixelRatio
|
||||
|| ctx.msBackingStorePixelRatio
|
||||
|| ctx.oBackingStorePixelRatio
|
||||
|| ctx.backingStorePixelRatio || 1;
|
||||
|
||||
const ratio = devicePixelRatio / backingStoreRatio;
|
||||
|
||||
const init = function () {
|
||||
let maxWidth = 0;
|
||||
let i;
|
||||
const l = rawGraphList.length;
|
||||
let row;
|
||||
let midStr;
|
||||
|
||||
for (i = 0; i < l; i++) {
|
||||
midStr = rawGraphList[i].replace(/\s+/g, ' ').replace(/^\s+|\s+$/g, '');
|
||||
midStr = midStr.replace(/(--)|(-\.)/g,'-')
|
||||
maxWidth = Math.max(midStr.replace(/(_|\s)/g, '').length, maxWidth);
|
||||
|
||||
row = midStr.split('');
|
||||
|
||||
graphList.unshift(row);
|
||||
}
|
||||
|
||||
const width = maxWidth * config.unitSize;
|
||||
const height = graphList.length * config.unitSize;
|
||||
|
||||
canvas.width = width * ratio;
|
||||
canvas.height = height * ratio;
|
||||
|
||||
canvas.style.width = `${width}px`;
|
||||
canvas.style.height = `${height}px`;
|
||||
|
||||
ctx.lineWidth = config.lineWidth;
|
||||
ctx.lineJoin = 'round';
|
||||
ctx.lineCap = 'round';
|
||||
|
||||
ctx.scale(ratio, ratio);
|
||||
};
|
||||
|
||||
const genRandomStr = function () {
|
||||
const chars = '0123456789ABCDEF';
|
||||
const stringLength = 6;
|
||||
let randomString = '', rnum, i;
|
||||
for (i = 0; i < stringLength; i++) {
|
||||
rnum = Math.floor(Math.random() * chars.length);
|
||||
randomString += chars.substring(rnum, rnum + 1);
|
||||
}
|
||||
|
||||
return randomString;
|
||||
};
|
||||
|
||||
const findFlow = function (id) {
|
||||
let i = flows.length;
|
||||
|
||||
while (i-- && flows[i].id !== id);
|
||||
|
||||
return i;
|
||||
};
|
||||
|
||||
const findColomn = function (symbol, row) {
|
||||
let i = row.length;
|
||||
|
||||
while (i-- && row[i] !== symbol);
|
||||
|
||||
return i;
|
||||
};
|
||||
|
||||
const findBranchOut = function (row) {
|
||||
if (!row) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
let i = row.length;
|
||||
|
||||
while (i--
|
||||
&& !(row[i - 1] && row[i] === '/' && row[i - 1] === '|')
|
||||
&& !(row[i - 2] && row[i] === '_' && row[i - 2] === '|'));
|
||||
|
||||
return i;
|
||||
};
|
||||
|
||||
const findLineBreak = function (row) {
|
||||
if (!row) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
let i = row.length;
|
||||
|
||||
while (i--
|
||||
&& !(row[i - 1] && row[i - 2] && row[i] === ' ' && row[i - 1] === '|' && row[i - 2] === '_'));
|
||||
|
||||
return i;
|
||||
};
|
||||
|
||||
const genNewFlow = function () {
|
||||
let newId;
|
||||
|
||||
do {
|
||||
newId = genRandomStr();
|
||||
} while (findFlow(newId) !== -1);
|
||||
|
||||
return { id: newId, color: `#${newId}` };
|
||||
};
|
||||
|
||||
// Draw methods
|
||||
const drawLine = function (moveX, moveY, lineX, lineY, color) {
|
||||
ctx.strokeStyle = color;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(moveX, moveY);
|
||||
ctx.lineTo(lineX, lineY);
|
||||
ctx.stroke();
|
||||
};
|
||||
|
||||
const drawLineRight = function (x, y, color) {
|
||||
drawLine(x, y + config.unitSize / 2, x + config.unitSize, y + config.unitSize / 2, color);
|
||||
};
|
||||
|
||||
const drawLineUp = function (x, y, color) {
|
||||
drawLine(x, y + config.unitSize / 2, x, y - config.unitSize / 2, color);
|
||||
};
|
||||
|
||||
const drawNode = function (x, y, color) {
|
||||
ctx.strokeStyle = color;
|
||||
|
||||
drawLineUp(x, y, color);
|
||||
|
||||
ctx.beginPath();
|
||||
ctx.arc(x, y, config.nodeRadius, 0, Math.PI * 2, true);
|
||||
ctx.fill();
|
||||
};
|
||||
|
||||
const drawLineIn = function (x, y, color) {
|
||||
drawLine(x + config.unitSize, y + config.unitSize / 2, x, y - config.unitSize / 2, color);
|
||||
};
|
||||
|
||||
const drawLineOut = function (x, y, color) {
|
||||
drawLine(x, y + config.unitSize / 2, x + config.unitSize, y - config.unitSize / 2, color);
|
||||
};
|
||||
|
||||
const draw = function (graphList) {
|
||||
let colomn, colomnIndex, prevColomn, condenseIndex, breakIndex = -1;
|
||||
let x, y;
|
||||
let color;
|
||||
let nodePos;
|
||||
let tempFlow;
|
||||
let prevRowLength = 0;
|
||||
let flowSwapPos = -1;
|
||||
let lastLinePos;
|
||||
let i, l;
|
||||
let condenseCurrentLength, condensePrevLength = 0;
|
||||
|
||||
let inlineIntersect = false;
|
||||
|
||||
// initiate color array for first row
|
||||
for (i = 0, l = graphList[0].length; i < l; i++) {
|
||||
if (graphList[0][i] !== '_' && graphList[0][i] !== ' ') {
|
||||
flows.push(genNewFlow());
|
||||
}
|
||||
}
|
||||
|
||||
y = (canvas.height / ratio) - config.unitSize * 0.5;
|
||||
|
||||
// iterate
|
||||
for (i = 0, l = graphList.length; i < l; i++) {
|
||||
x = config.unitSize * 0.5;
|
||||
|
||||
const currentRow = graphList[i];
|
||||
const nextRow = graphList[i + 1];
|
||||
const prevRow = graphList[i - 1];
|
||||
|
||||
flowSwapPos = -1;
|
||||
|
||||
condenseCurrentLength = currentRow.filter((val) => {
|
||||
return (val !== ' ' && val !== '_');
|
||||
}).length;
|
||||
|
||||
// pre process begin
|
||||
// use last row for analysing
|
||||
if (prevRow) {
|
||||
if (!inlineIntersect) {
|
||||
// intersect might happen
|
||||
for (colomnIndex = 0; colomnIndex < prevRowLength; colomnIndex++) {
|
||||
if (prevRow[colomnIndex + 1]
|
||||
&& (prevRow[colomnIndex] === '/' && prevRow[colomnIndex + 1] === '|')
|
||||
|| ((prevRow[colomnIndex] === '_' && prevRow[colomnIndex + 1] === '|')
|
||||
&& (prevRow[colomnIndex + 2] === '/'))) {
|
||||
flowSwapPos = colomnIndex;
|
||||
|
||||
// swap two flow
|
||||
tempFlow = { id: flows[flowSwapPos].id, color: flows[flowSwapPos].color };
|
||||
|
||||
flows[flowSwapPos].id = flows[flowSwapPos + 1].id;
|
||||
flows[flowSwapPos].color = flows[flowSwapPos + 1].color;
|
||||
|
||||
flows[flowSwapPos + 1].id = tempFlow.id;
|
||||
flows[flowSwapPos + 1].color = tempFlow.color;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (condensePrevLength < condenseCurrentLength
|
||||
&& ((nodePos = findColomn('*', currentRow)) !== -1 // eslint-disable-line no-cond-assign
|
||||
&& (findColomn('_', currentRow) === -1))) {
|
||||
flows.splice(nodePos - 1, 0, genNewFlow());
|
||||
}
|
||||
|
||||
if (prevRowLength > currentRow.length
|
||||
&& (nodePos = findColomn('*', prevRow)) !== -1) { // eslint-disable-line no-cond-assign
|
||||
if (findColomn('_', currentRow) === -1
|
||||
&& findColomn('/', currentRow) === -1
|
||||
&& findColomn('\\', currentRow) === -1) {
|
||||
flows.splice(nodePos + 1, 1);
|
||||
}
|
||||
}
|
||||
} // done with the previous row
|
||||
|
||||
prevRowLength = currentRow.length; // store for next round
|
||||
colomnIndex = 0; // reset index
|
||||
condenseIndex = 0;
|
||||
condensePrevLength = 0;
|
||||
breakIndex = -1; // reset break index
|
||||
while (colomnIndex < currentRow.length) {
|
||||
colomn = currentRow[colomnIndex];
|
||||
|
||||
if (colomn !== ' ' && colomn !== '_') {
|
||||
++condensePrevLength;
|
||||
}
|
||||
|
||||
// check and fix line break in next row
|
||||
if (colomn === '/' && currentRow[colomnIndex - 1] && currentRow[colomnIndex - 1] === '|') {
|
||||
/* eslint-disable-next-line */
|
||||
if ((breakIndex = findLineBreak(nextRow)) !== -1) {
|
||||
nextRow.splice(breakIndex, 1);
|
||||
}
|
||||
}
|
||||
// if line break found replace all '/' with '|' after breakIndex in previous row
|
||||
if (breakIndex !== -1 && colomn === '/' && colomnIndex > breakIndex) {
|
||||
currentRow[colomnIndex] = '|';
|
||||
colomn = '|';
|
||||
}
|
||||
|
||||
if (colomn === ' '
|
||||
&& currentRow[colomnIndex + 1]
|
||||
&& currentRow[colomnIndex + 1] === '_'
|
||||
&& currentRow[colomnIndex - 1]
|
||||
&& currentRow[colomnIndex - 1] === '|') {
|
||||
currentRow.splice(colomnIndex, 1);
|
||||
|
||||
currentRow[colomnIndex] = '/';
|
||||
colomn = '/';
|
||||
}
|
||||
|
||||
// create new flow only when no intersect happened
|
||||
if (flowSwapPos === -1
|
||||
&& colomn === '/'
|
||||
&& currentRow[colomnIndex - 1]
|
||||
&& currentRow[colomnIndex - 1] === '|') {
|
||||
flows.splice(condenseIndex, 0, genNewFlow());
|
||||
}
|
||||
|
||||
// change \ and / to | when it's in the last position of the whole row
|
||||
if (colomn === '/' || colomn === '\\') {
|
||||
if (!(colomn === '/' && findBranchOut(nextRow) === -1)) {
|
||||
/* eslint-disable-next-line */
|
||||
if ((lastLinePos = Math.max(findColomn('|', currentRow),
|
||||
findColomn('*', currentRow))) !== -1
|
||||
&& (lastLinePos < colomnIndex - 1)) {
|
||||
while (currentRow[++lastLinePos] === ' ');
|
||||
|
||||
if (lastLinePos === colomnIndex) {
|
||||
currentRow[colomnIndex] = '|';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (colomn === '*'
|
||||
&& prevRow
|
||||
&& prevRow[condenseIndex + 1] === '\\') {
|
||||
flows.splice(condenseIndex + 1, 1);
|
||||
}
|
||||
|
||||
if (colomn !== ' ') {
|
||||
++condenseIndex;
|
||||
}
|
||||
|
||||
++colomnIndex;
|
||||
}
|
||||
|
||||
condenseCurrentLength = currentRow.filter((val) => {
|
||||
return (val !== ' ' && val !== '_');
|
||||
}).length;
|
||||
|
||||
colomnIndex = 0;
|
||||
|
||||
// a little inline analysis and draw process
|
||||
while (colomnIndex < currentRow.length) {
|
||||
colomn = currentRow[colomnIndex];
|
||||
prevColomn = currentRow[colomnIndex - 1];
|
||||
|
||||
if (currentRow[colomnIndex] === ' ') {
|
||||
currentRow.splice(colomnIndex, 1);
|
||||
x += config.unitSize;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
// inline intersect
|
||||
if ((colomn === '_' || colomn === '/')
|
||||
&& currentRow[colomnIndex - 1] === '|'
|
||||
&& currentRow[colomnIndex - 2] === '_') {
|
||||
inlineIntersect = true;
|
||||
|
||||
tempFlow = flows.splice(colomnIndex - 2, 1)[0];
|
||||
flows.splice(colomnIndex - 1, 0, tempFlow);
|
||||
currentRow.splice(colomnIndex - 2, 1);
|
||||
|
||||
colomnIndex -= 1;
|
||||
} else {
|
||||
inlineIntersect = false;
|
||||
}
|
||||
|
||||
if (colomn === '|' && currentRow[colomnIndex - 1] && currentRow[colomnIndex - 1] === '\\') {
|
||||
flows.splice(colomnIndex, 0, genNewFlow());
|
||||
}
|
||||
|
||||
color = flows[colomnIndex].color;
|
||||
|
||||
switch (colomn) {
|
||||
case '-':
|
||||
case '_':
|
||||
drawLineRight(x, y, color);
|
||||
|
||||
x += config.unitSize;
|
||||
break;
|
||||
|
||||
case '*':
|
||||
drawNode(x, y, color);
|
||||
break;
|
||||
|
||||
case '|':
|
||||
if (prevColomn && prevColomn === '\\') {
|
||||
x += config.unitSize;
|
||||
}
|
||||
drawLineUp(x, y, color);
|
||||
break;
|
||||
|
||||
case '/':
|
||||
if (prevColomn
|
||||
&& (prevColomn === '/'
|
||||
|| prevColomn === ' ')) {
|
||||
x -= config.unitSize;
|
||||
}
|
||||
|
||||
drawLineOut(x, y, color);
|
||||
|
||||
x += config.unitSize;
|
||||
break;
|
||||
|
||||
case '\\':
|
||||
drawLineIn(x, y, color);
|
||||
break;
|
||||
}
|
||||
|
||||
++colomnIndex;
|
||||
}
|
||||
|
||||
y -= config.unitSize;
|
||||
}
|
||||
|
||||
// do some clean up
|
||||
if (flows.length > condenseCurrentLength) {
|
||||
flows.splice(condenseCurrentLength, flows.length - condenseCurrentLength);
|
||||
}
|
||||
};
|
||||
|
||||
init();
|
||||
draw(graphList);
|
||||
}
|
||||
// @end-license
|
|
@ -3078,3 +3078,11 @@ tbody.commit-list {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
#git-graph-container {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#git-graph-container.in {
|
||||
display: block;
|
||||
}
|
||||
|
|
Reference in a new issue