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

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424
  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. if (condensePrevLength < condenseCurrentLength
  197. && ((nodePos = findColomn('*', currentRow)) !== -1 // eslint-disable-line no-cond-assign
  198. && (findColomn('_', currentRow) === -1))) {
  199. flows.splice(nodePos - 1, 0, genNewFlow());
  200. }
  201. if (prevRowLength > currentRow.length
  202. && (nodePos = findColomn('*', prevRow)) !== -1) { // eslint-disable-line no-cond-assign
  203. if (findColomn('_', currentRow) === -1
  204. && findColomn('/', currentRow) === -1
  205. && findColomn('\\', currentRow) === -1) {
  206. flows.splice(nodePos + 1, 1);
  207. }
  208. }
  209. } // done with the previous row
  210. prevRowLength = currentRow.length; // store for next round
  211. colomnIndex = 0; // reset index
  212. condenseIndex = 0;
  213. condensePrevLength = 0;
  214. breakIndex = -1; // reset break index
  215. while (colomnIndex < currentRow.length) {
  216. colomn = currentRow[colomnIndex];
  217. if (colomn !== ' ' && colomn !== '_') {
  218. ++condensePrevLength;
  219. }
  220. // check and fix line break in next row
  221. if (colomn === '/' && currentRow[colomnIndex - 1] && currentRow[colomnIndex - 1] === '|') {
  222. /* eslint-disable-next-line */
  223. if ((breakIndex = findLineBreak(nextRow)) !== -1) {
  224. nextRow.splice(breakIndex, 1);
  225. }
  226. }
  227. // if line break found replace all '/' with '|' after breakIndex in previous row
  228. if (breakIndex !== -1 && colomn === '/' && colomnIndex > breakIndex) {
  229. currentRow[colomnIndex] = '|';
  230. colomn = '|';
  231. }
  232. if (colomn === ' '
  233. && currentRow[colomnIndex + 1]
  234. && currentRow[colomnIndex + 1] === '_'
  235. && currentRow[colomnIndex - 1]
  236. && currentRow[colomnIndex - 1] === '|') {
  237. currentRow.splice(colomnIndex, 1);
  238. currentRow[colomnIndex] = '/';
  239. colomn = '/';
  240. }
  241. // create new flow only when no intersect happened
  242. if (flowSwapPos === -1
  243. && colomn === '/'
  244. && currentRow[colomnIndex - 1]
  245. && currentRow[colomnIndex - 1] === '|') {
  246. flows.splice(condenseIndex, 0, genNewFlow());
  247. }
  248. // change \ and / to | when it's in the last position of the whole row
  249. if (colomn === '/' || colomn === '\\') {
  250. if (!(colomn === '/' && findBranchOut(nextRow) === -1)) {
  251. /* eslint-disable-next-line */
  252. if ((lastLinePos = Math.max(findColomn('|', currentRow),
  253. findColomn('*', currentRow))) !== -1
  254. && (lastLinePos < colomnIndex - 1)) {
  255. while (currentRow[++lastLinePos] === ' ');
  256. if (lastLinePos === colomnIndex) {
  257. currentRow[colomnIndex] = '|';
  258. }
  259. }
  260. }
  261. }
  262. if (colomn === '*'
  263. && prevRow
  264. && prevRow[condenseIndex + 1] === '\\') {
  265. flows.splice(condenseIndex + 1, 1);
  266. }
  267. if (colomn !== ' ') {
  268. ++condenseIndex;
  269. }
  270. ++colomnIndex;
  271. }
  272. condenseCurrentLength = currentRow.filter((val) => {
  273. return (val !== ' ' && val !== '_');
  274. }).length;
  275. // do some clean up
  276. if (flows.length > condenseCurrentLength) {
  277. flows.splice(condenseCurrentLength, flows.length - condenseCurrentLength);
  278. }
  279. colomnIndex = 0;
  280. // a little inline analysis and draw process
  281. while (colomnIndex < currentRow.length) {
  282. colomn = currentRow[colomnIndex];
  283. prevColomn = currentRow[colomnIndex - 1];
  284. if (currentRow[colomnIndex] === ' ') {
  285. currentRow.splice(colomnIndex, 1);
  286. x += config.unitSize;
  287. continue;
  288. }
  289. // inline interset
  290. if ((colomn === '_' || colomn === '/')
  291. && currentRow[colomnIndex - 1] === '|'
  292. && currentRow[colomnIndex - 2] === '_') {
  293. inlineIntersect = true;
  294. tempFlow = flows.splice(colomnIndex - 2, 1)[0];
  295. flows.splice(colomnIndex - 1, 0, tempFlow);
  296. currentRow.splice(colomnIndex - 2, 1);
  297. colomnIndex -= 1;
  298. } else {
  299. inlineIntersect = false;
  300. }
  301. color = flows[colomnIndex].color;
  302. switch (colomn) {
  303. case '_':
  304. drawLineRight(x, y, color);
  305. x += config.unitSize;
  306. break;
  307. case '*':
  308. drawNode(x, y, color);
  309. break;
  310. case '|':
  311. drawLineUp(x, y, color);
  312. break;
  313. case '/':
  314. if (prevColomn
  315. && (prevColomn === '/'
  316. || prevColomn === ' ')) {
  317. x -= config.unitSize;
  318. }
  319. drawLineOut(x, y, color);
  320. x += config.unitSize;
  321. break;
  322. case '\\':
  323. drawLineIn(x, y, color);
  324. break;
  325. }
  326. ++colomnIndex;
  327. }
  328. y -= config.unitSize;
  329. }
  330. };
  331. init();
  332. draw(graphList);
  333. }
  334. // @end-license