You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

gitGraph.js 12KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426
  1. /* This is a customized version of https://github.com/bluef/gitgraph.js/blob/master/gitgraph.js
  2. Changes include conversion to ES6 and linting fixes */
  3. /*
  4. * @license magnet:?xt=urn:btih:c80d50af7d3db9be66a4d0a86db0286e4fd33292&dn=bsd-3-clause.txt BSD 3-Clause
  5. * Copyright (c) 2011, Terrence Lee <kill889@gmail.com>
  6. * All rights reserved.
  7. *
  8. * Redistribution and use in source and binary forms, with or without
  9. * modification, are permitted provided that the following conditions are met:
  10. * * Redistributions of source code must retain the above copyright
  11. * notice, this list of conditions and the following disclaimer.
  12. * * Redistributions in binary form must reproduce the above copyright
  13. * notice, this list of conditions and the following disclaimer in the
  14. * documentation and/or other materials provided with the distribution.
  15. * * Neither the name of the <organization> nor the
  16. * names of its contributors may be used to endorse or promote products
  17. * derived from this software without specific prior written permission.
  18. *
  19. * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
  20. * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
  21. * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
  22. * DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> BE LIABLE FOR ANY
  23. * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
  24. * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
  25. * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
  26. * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
  27. * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
  28. * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  29. */
  30. export default function gitGraph(canvas, rawGraphList, config) {
  31. if (!canvas.getContext) {
  32. return;
  33. }
  34. if (typeof config === 'undefined') {
  35. config = {
  36. unitSize: 20,
  37. lineWidth: 3,
  38. nodeRadius: 4
  39. };
  40. }
  41. const flows = [];
  42. const graphList = [];
  43. const ctx = canvas.getContext('2d');
  44. const devicePixelRatio = window.devicePixelRatio || 1;
  45. const backingStoreRatio = ctx.webkitBackingStorePixelRatio
  46. || ctx.mozBackingStorePixelRatio
  47. || ctx.msBackingStorePixelRatio
  48. || ctx.oBackingStorePixelRatio
  49. || ctx.backingStorePixelRatio || 1;
  50. const ratio = devicePixelRatio / backingStoreRatio;
  51. const init = function () {
  52. let maxWidth = 0;
  53. let i;
  54. const l = rawGraphList.length;
  55. let row;
  56. let midStr;
  57. for (i = 0; i < l; i++) {
  58. midStr = rawGraphList[i].replace(/\s+/g, ' ').replace(/^\s+|\s+$/g, '');
  59. maxWidth = Math.max(midStr.replace(/(_|\s)/g, '').length, maxWidth);
  60. row = midStr.split('');
  61. graphList.unshift(row);
  62. }
  63. const width = maxWidth * config.unitSize;
  64. const height = graphList.length * config.unitSize;
  65. canvas.width = width * ratio;
  66. canvas.height = height * ratio;
  67. canvas.style.width = `${width}px`;
  68. canvas.style.height = `${height}px`;
  69. ctx.lineWidth = config.lineWidth;
  70. ctx.lineJoin = 'round';
  71. ctx.lineCap = 'round';
  72. ctx.scale(ratio, ratio);
  73. };
  74. const genRandomStr = function () {
  75. const chars = '0123456789ABCDEF';
  76. const stringLength = 6;
  77. let randomString = '', rnum, i;
  78. for (i = 0; i < stringLength; i++) {
  79. rnum = Math.floor(Math.random() * chars.length);
  80. randomString += chars.substring(rnum, rnum + 1);
  81. }
  82. return randomString;
  83. };
  84. const findFlow = function (id) {
  85. let i = flows.length;
  86. while (i-- && flows[i].id !== id);
  87. return i;
  88. };
  89. const findColomn = function (symbol, row) {
  90. let i = row.length;
  91. while (i-- && row[i] !== symbol);
  92. return i;
  93. };
  94. const findBranchOut = function (row) {
  95. if (!row) {
  96. return -1;
  97. }
  98. let i = row.length;
  99. while (i--
  100. && !(row[i - 1] && row[i] === '/' && row[i - 1] === '|')
  101. && !(row[i - 2] && row[i] === '_' && row[i - 2] === '|'));
  102. return i;
  103. };
  104. const findLineBreak = function (row) {
  105. if (!row) {
  106. return -1;
  107. }
  108. let i = row.length;
  109. while (i--
  110. && !(row[i - 1] && row[i - 2] && row[i] === ' ' && row[i - 1] === '|' && row[i - 2] === '_'));
  111. return i;
  112. };
  113. const genNewFlow = function () {
  114. let newId;
  115. do {
  116. newId = genRandomStr();
  117. } while (findFlow(newId) !== -1);
  118. return { id: newId, color: `#${newId}` };
  119. };
  120. // Draw methods
  121. const drawLine = function (moveX, moveY, lineX, lineY, color) {
  122. ctx.strokeStyle = color;
  123. ctx.beginPath();
  124. ctx.moveTo(moveX, moveY);
  125. ctx.lineTo(lineX, lineY);
  126. ctx.stroke();
  127. };
  128. const drawLineRight = function (x, y, color) {
  129. drawLine(x, y + config.unitSize / 2, x + config.unitSize, y + config.unitSize / 2, color);
  130. };
  131. const drawLineUp = function (x, y, color) {
  132. drawLine(x, y + config.unitSize / 2, x, y - config.unitSize / 2, color);
  133. };
  134. const drawNode = function (x, y, color) {
  135. ctx.strokeStyle = color;
  136. drawLineUp(x, y, color);
  137. ctx.beginPath();
  138. ctx.arc(x, y, config.nodeRadius, 0, Math.PI * 2, true);
  139. ctx.fill();
  140. };
  141. const drawLineIn = function (x, y, color) {
  142. drawLine(x + config.unitSize, y + config.unitSize / 2, x, y - config.unitSize / 2, color);
  143. };
  144. const drawLineOut = function (x, y, color) {
  145. drawLine(x, y + config.unitSize / 2, x + config.unitSize, y - config.unitSize / 2, color);
  146. };
  147. const draw = function (graphList) {
  148. let colomn, colomnIndex, prevColomn, condenseIndex, breakIndex = -1;
  149. let x, y;
  150. let color;
  151. let nodePos;
  152. let tempFlow;
  153. let prevRowLength = 0;
  154. let flowSwapPos = -1;
  155. let lastLinePos;
  156. let i, l;
  157. let condenseCurrentLength, condensePrevLength = 0;
  158. let inlineIntersect = false;
  159. // initiate color array for first row
  160. for (i = 0, l = graphList[0].length; i < l; i++) {
  161. if (graphList[0][i] !== '_' && graphList[0][i] !== ' ') {
  162. flows.push(genNewFlow());
  163. }
  164. }
  165. y = (canvas.height / ratio) - config.unitSize * 0.5;
  166. // iterate
  167. for (i = 0, l = graphList.length; i < l; i++) {
  168. x = config.unitSize * 0.5;
  169. const currentRow = graphList[i];
  170. const nextRow = graphList[i + 1];
  171. const prevRow = graphList[i - 1];
  172. flowSwapPos = -1;
  173. condenseCurrentLength = currentRow.filter((val) => {
  174. return (val !== ' ' && val !== '_');
  175. }).length;
  176. // pre process begin
  177. // use last row for analysing
  178. if (prevRow) {
  179. if (!inlineIntersect) {
  180. // intersect might happen
  181. for (colomnIndex = 0; colomnIndex < prevRowLength; colomnIndex++) {
  182. if (prevRow[colomnIndex + 1]
  183. && (prevRow[colomnIndex] === '/' && prevRow[colomnIndex + 1] === '|')
  184. || ((prevRow[colomnIndex] === '_' && prevRow[colomnIndex + 1] === '|')
  185. && (prevRow[colomnIndex + 2] === '/'))) {
  186. flowSwapPos = colomnIndex;
  187. // swap two flow
  188. tempFlow = { id: flows[flowSwapPos].id, color: flows[flowSwapPos].color };
  189. flows[flowSwapPos].id = flows[flowSwapPos + 1].id;
  190. flows[flowSwapPos].color = flows[flowSwapPos + 1].color;
  191. flows[flowSwapPos + 1].id = tempFlow.id;
  192. flows[flowSwapPos + 1].color = tempFlow.color;
  193. }
  194. }
  195. }
  196. /* eslint-disable-next-line */
  197. if (condensePrevLength < condenseCurrentLength
  198. && ((nodePos = findColomn('*', currentRow)) !== -1
  199. && (findColomn('_', currentRow) === -1))) {
  200. flows.splice(nodePos - 1, 0, genNewFlow());
  201. }
  202. /* eslint-disable-next-line */
  203. if (prevRowLength > currentRow.length
  204. && (nodePos = findColomn('*', prevRow)) !== -1) {
  205. if (findColomn('_', currentRow) === -1
  206. && findColomn('/', currentRow) === -1
  207. && findColomn('\\', currentRow) === -1) {
  208. flows.splice(nodePos + 1, 1);
  209. }
  210. }
  211. } // done with the previous row
  212. prevRowLength = currentRow.length; // store for next round
  213. colomnIndex = 0; // reset index
  214. condenseIndex = 0;
  215. condensePrevLength = 0;
  216. breakIndex = -1; // reset break index
  217. while (colomnIndex < currentRow.length) {
  218. colomn = currentRow[colomnIndex];
  219. if (colomn !== ' ' && colomn !== '_') {
  220. ++condensePrevLength;
  221. }
  222. // check and fix line break in next row
  223. if (colomn === '/' && currentRow[colomnIndex - 1] && currentRow[colomnIndex - 1] === '|') {
  224. /* eslint-disable-next-line */
  225. if ((breakIndex = findLineBreak(nextRow)) !== -1) {
  226. nextRow.splice(breakIndex, 1);
  227. }
  228. }
  229. // if line break found replace all '/' with '|' after breakIndex in previous row
  230. if (breakIndex !== -1 && colomn === '/' && colomnIndex > breakIndex) {
  231. currentRow[colomnIndex] = '|';
  232. colomn = '|';
  233. }
  234. if (colomn === ' '
  235. && currentRow[colomnIndex + 1]
  236. && currentRow[colomnIndex + 1] === '_'
  237. && currentRow[colomnIndex - 1]
  238. && currentRow[colomnIndex - 1] === '|') {
  239. currentRow.splice(colomnIndex, 1);
  240. currentRow[colomnIndex] = '/';
  241. colomn = '/';
  242. }
  243. // create new flow only when no intersect happened
  244. if (flowSwapPos === -1
  245. && colomn === '/'
  246. && currentRow[colomnIndex - 1]
  247. && currentRow[colomnIndex - 1] === '|') {
  248. flows.splice(condenseIndex, 0, genNewFlow());
  249. }
  250. // change \ and / to | when it's in the last position of the whole row
  251. if (colomn === '/' || colomn === '\\') {
  252. if (!(colomn === '/' && findBranchOut(nextRow) === -1)) {
  253. /* eslint-disable-next-line */
  254. if ((lastLinePos = Math.max(findColomn('|', currentRow),
  255. findColomn('*', currentRow))) !== -1
  256. && (lastLinePos < colomnIndex - 1)) {
  257. while (currentRow[++lastLinePos] === ' ');
  258. if (lastLinePos === colomnIndex) {
  259. currentRow[colomnIndex] = '|';
  260. }
  261. }
  262. }
  263. }
  264. if (colomn === '*'
  265. && prevRow
  266. && prevRow[condenseIndex + 1] === '\\') {
  267. flows.splice(condenseIndex + 1, 1);
  268. }
  269. if (colomn !== ' ') {
  270. ++condenseIndex;
  271. }
  272. ++colomnIndex;
  273. }
  274. condenseCurrentLength = currentRow.filter((val) => {
  275. return (val !== ' ' && val !== '_');
  276. }).length;
  277. // do some clean up
  278. if (flows.length > condenseCurrentLength) {
  279. flows.splice(condenseCurrentLength, flows.length - condenseCurrentLength);
  280. }
  281. colomnIndex = 0;
  282. // a little inline analysis and draw process
  283. while (colomnIndex < currentRow.length) {
  284. colomn = currentRow[colomnIndex];
  285. prevColomn = currentRow[colomnIndex - 1];
  286. if (currentRow[colomnIndex] === ' ') {
  287. currentRow.splice(colomnIndex, 1);
  288. x += config.unitSize;
  289. continue;
  290. }
  291. // inline interset
  292. if ((colomn === '_' || colomn === '/')
  293. && currentRow[colomnIndex - 1] === '|'
  294. && currentRow[colomnIndex - 2] === '_') {
  295. inlineIntersect = true;
  296. tempFlow = flows.splice(colomnIndex - 2, 1)[0];
  297. flows.splice(colomnIndex - 1, 0, tempFlow);
  298. currentRow.splice(colomnIndex - 2, 1);
  299. colomnIndex -= 1;
  300. } else {
  301. inlineIntersect = false;
  302. }
  303. color = flows[colomnIndex].color;
  304. switch (colomn) {
  305. case '_':
  306. drawLineRight(x, y, color);
  307. x += config.unitSize;
  308. break;
  309. case '*':
  310. drawNode(x, y, color);
  311. break;
  312. case '|':
  313. drawLineUp(x, y, color);
  314. break;
  315. case '/':
  316. if (prevColomn
  317. && (prevColomn === '/'
  318. || prevColomn === ' ')) {
  319. x -= config.unitSize;
  320. }
  321. drawLineOut(x, y, color);
  322. x += config.unitSize;
  323. break;
  324. case '\\':
  325. drawLineIn(x, y, color);
  326. break;
  327. }
  328. ++colomnIndex;
  329. }
  330. y -= config.unitSize;
  331. }
  332. };
  333. init();
  334. draw(graphList);
  335. }
  336. // @end-license