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 13KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432
  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. midStr = midStr.replace(/(--)|(-\.)/g,'-')
  60. maxWidth = Math.max(midStr.replace(/(_|\s)/g, '').length, maxWidth);
  61. row = midStr.split('');
  62. graphList.unshift(row);
  63. }
  64. const width = maxWidth * config.unitSize;
  65. const height = graphList.length * config.unitSize;
  66. canvas.width = width * ratio;
  67. canvas.height = height * ratio;
  68. canvas.style.width = `${width}px`;
  69. canvas.style.height = `${height}px`;
  70. ctx.lineWidth = config.lineWidth;
  71. ctx.lineJoin = 'round';
  72. ctx.lineCap = 'round';
  73. ctx.scale(ratio, ratio);
  74. };
  75. const genRandomStr = function () {
  76. const chars = '0123456789ABCDEF';
  77. const stringLength = 6;
  78. let randomString = '', rnum, i;
  79. for (i = 0; i < stringLength; i++) {
  80. rnum = Math.floor(Math.random() * chars.length);
  81. randomString += chars.substring(rnum, rnum + 1);
  82. }
  83. return randomString;
  84. };
  85. const findFlow = function (id) {
  86. let i = flows.length;
  87. while (i-- && flows[i].id !== id);
  88. return i;
  89. };
  90. const findColomn = function (symbol, row) {
  91. let i = row.length;
  92. while (i-- && row[i] !== symbol);
  93. return i;
  94. };
  95. const findBranchOut = function (row) {
  96. if (!row) {
  97. return -1;
  98. }
  99. let i = row.length;
  100. while (i--
  101. && !(row[i - 1] && row[i] === '/' && row[i - 1] === '|')
  102. && !(row[i - 2] && row[i] === '_' && row[i - 2] === '|'));
  103. return i;
  104. };
  105. const findLineBreak = function (row) {
  106. if (!row) {
  107. return -1;
  108. }
  109. let i = row.length;
  110. while (i--
  111. && !(row[i - 1] && row[i - 2] && row[i] === ' ' && row[i - 1] === '|' && row[i - 2] === '_'));
  112. return i;
  113. };
  114. const genNewFlow = function () {
  115. let newId;
  116. do {
  117. newId = genRandomStr();
  118. } while (findFlow(newId) !== -1);
  119. return { id: newId, color: `#${newId}` };
  120. };
  121. // Draw methods
  122. const drawLine = function (moveX, moveY, lineX, lineY, color) {
  123. ctx.strokeStyle = color;
  124. ctx.beginPath();
  125. ctx.moveTo(moveX, moveY);
  126. ctx.lineTo(lineX, lineY);
  127. ctx.stroke();
  128. };
  129. const drawLineRight = function (x, y, color) {
  130. drawLine(x, y + config.unitSize / 2, x + config.unitSize, y + config.unitSize / 2, color);
  131. };
  132. const drawLineUp = function (x, y, color) {
  133. drawLine(x, y + config.unitSize / 2, x, y - config.unitSize / 2, color);
  134. };
  135. const drawNode = function (x, y, color) {
  136. ctx.strokeStyle = color;
  137. drawLineUp(x, y, color);
  138. ctx.beginPath();
  139. ctx.arc(x, y, config.nodeRadius, 0, Math.PI * 2, true);
  140. ctx.fill();
  141. };
  142. const drawLineIn = function (x, y, color) {
  143. drawLine(x + config.unitSize, y + config.unitSize / 2, x, y - config.unitSize / 2, color);
  144. };
  145. const drawLineOut = function (x, y, color) {
  146. drawLine(x, y + config.unitSize / 2, x + config.unitSize, y - config.unitSize / 2, color);
  147. };
  148. const draw = function (graphList) {
  149. let colomn, colomnIndex, prevColomn, condenseIndex, breakIndex = -1;
  150. let x, y;
  151. let color;
  152. let nodePos;
  153. let tempFlow;
  154. let prevRowLength = 0;
  155. let flowSwapPos = -1;
  156. let lastLinePos;
  157. let i, l;
  158. let condenseCurrentLength, condensePrevLength = 0;
  159. let inlineIntersect = false;
  160. // initiate color array for first row
  161. for (i = 0, l = graphList[0].length; i < l; i++) {
  162. if (graphList[0][i] !== '_' && graphList[0][i] !== ' ') {
  163. flows.push(genNewFlow());
  164. }
  165. }
  166. y = (canvas.height / ratio) - config.unitSize * 0.5;
  167. // iterate
  168. for (i = 0, l = graphList.length; i < l; i++) {
  169. x = config.unitSize * 0.5;
  170. const currentRow = graphList[i];
  171. const nextRow = graphList[i + 1];
  172. const prevRow = graphList[i - 1];
  173. flowSwapPos = -1;
  174. condenseCurrentLength = currentRow.filter((val) => {
  175. return (val !== ' ' && val !== '_');
  176. }).length;
  177. // pre process begin
  178. // use last row for analysing
  179. if (prevRow) {
  180. if (!inlineIntersect) {
  181. // intersect might happen
  182. for (colomnIndex = 0; colomnIndex < prevRowLength; colomnIndex++) {
  183. if (prevRow[colomnIndex + 1]
  184. && (prevRow[colomnIndex] === '/' && prevRow[colomnIndex + 1] === '|')
  185. || ((prevRow[colomnIndex] === '_' && prevRow[colomnIndex + 1] === '|')
  186. && (prevRow[colomnIndex + 2] === '/'))) {
  187. flowSwapPos = colomnIndex;
  188. // swap two flow
  189. tempFlow = { id: flows[flowSwapPos].id, color: flows[flowSwapPos].color };
  190. flows[flowSwapPos].id = flows[flowSwapPos + 1].id;
  191. flows[flowSwapPos].color = flows[flowSwapPos + 1].color;
  192. flows[flowSwapPos + 1].id = tempFlow.id;
  193. flows[flowSwapPos + 1].color = tempFlow.color;
  194. }
  195. }
  196. }
  197. if (condensePrevLength < condenseCurrentLength
  198. && ((nodePos = findColomn('*', currentRow)) !== -1 // eslint-disable-line no-cond-assign
  199. && (findColomn('_', currentRow) === -1))) {
  200. flows.splice(nodePos - 1, 0, genNewFlow());
  201. }
  202. if (prevRowLength > currentRow.length
  203. && (nodePos = findColomn('*', prevRow)) !== -1) { // eslint-disable-line no-cond-assign
  204. if (findColomn('_', currentRow) === -1
  205. && findColomn('/', currentRow) === -1
  206. && findColomn('\\', currentRow) === -1) {
  207. flows.splice(nodePos + 1, 1);
  208. }
  209. }
  210. } // done with the previous row
  211. prevRowLength = currentRow.length; // store for next round
  212. colomnIndex = 0; // reset index
  213. condenseIndex = 0;
  214. condensePrevLength = 0;
  215. breakIndex = -1; // reset break index
  216. while (colomnIndex < currentRow.length) {
  217. colomn = currentRow[colomnIndex];
  218. if (colomn !== ' ' && colomn !== '_') {
  219. ++condensePrevLength;
  220. }
  221. // check and fix line break in next row
  222. if (colomn === '/' && currentRow[colomnIndex - 1] && currentRow[colomnIndex - 1] === '|') {
  223. /* eslint-disable-next-line */
  224. if ((breakIndex = findLineBreak(nextRow)) !== -1) {
  225. nextRow.splice(breakIndex, 1);
  226. }
  227. }
  228. // if line break found replace all '/' with '|' after breakIndex in previous row
  229. if (breakIndex !== -1 && colomn === '/' && colomnIndex > breakIndex) {
  230. currentRow[colomnIndex] = '|';
  231. colomn = '|';
  232. }
  233. if (colomn === ' '
  234. && currentRow[colomnIndex + 1]
  235. && currentRow[colomnIndex + 1] === '_'
  236. && currentRow[colomnIndex - 1]
  237. && currentRow[colomnIndex - 1] === '|') {
  238. currentRow.splice(colomnIndex, 1);
  239. currentRow[colomnIndex] = '/';
  240. colomn = '/';
  241. }
  242. // create new flow only when no intersect happened
  243. if (flowSwapPos === -1
  244. && colomn === '/'
  245. && currentRow[colomnIndex - 1]
  246. && currentRow[colomnIndex - 1] === '|') {
  247. flows.splice(condenseIndex, 0, genNewFlow());
  248. }
  249. // change \ and / to | when it's in the last position of the whole row
  250. if (colomn === '/' || colomn === '\\') {
  251. if (!(colomn === '/' && findBranchOut(nextRow) === -1)) {
  252. /* eslint-disable-next-line */
  253. if ((lastLinePos = Math.max(findColomn('|', currentRow),
  254. findColomn('*', currentRow))) !== -1
  255. && (lastLinePos < colomnIndex - 1)) {
  256. while (currentRow[++lastLinePos] === ' ');
  257. if (lastLinePos === colomnIndex) {
  258. currentRow[colomnIndex] = '|';
  259. }
  260. }
  261. }
  262. }
  263. if (colomn === '*'
  264. && prevRow
  265. && prevRow[condenseIndex + 1] === '\\') {
  266. flows.splice(condenseIndex + 1, 1);
  267. }
  268. if (colomn !== ' ') {
  269. ++condenseIndex;
  270. }
  271. ++colomnIndex;
  272. }
  273. condenseCurrentLength = currentRow.filter((val) => {
  274. return (val !== ' ' && val !== '_');
  275. }).length;
  276. colomnIndex = 0;
  277. // a little inline analysis and draw process
  278. while (colomnIndex < currentRow.length) {
  279. colomn = currentRow[colomnIndex];
  280. prevColomn = currentRow[colomnIndex - 1];
  281. if (currentRow[colomnIndex] === ' ') {
  282. currentRow.splice(colomnIndex, 1);
  283. x += config.unitSize;
  284. continue;
  285. }
  286. // inline intersect
  287. if ((colomn === '_' || colomn === '/')
  288. && currentRow[colomnIndex - 1] === '|'
  289. && currentRow[colomnIndex - 2] === '_') {
  290. inlineIntersect = true;
  291. tempFlow = flows.splice(colomnIndex - 2, 1)[0];
  292. flows.splice(colomnIndex - 1, 0, tempFlow);
  293. currentRow.splice(colomnIndex - 2, 1);
  294. colomnIndex -= 1;
  295. } else {
  296. inlineIntersect = false;
  297. }
  298. if (colomn === '|' && currentRow[colomnIndex - 1] && currentRow[colomnIndex - 1] === '\\') {
  299. flows.splice(colomnIndex, 0, genNewFlow());
  300. }
  301. color = flows[colomnIndex].color;
  302. switch (colomn) {
  303. case '-':
  304. case '_':
  305. drawLineRight(x, y, color);
  306. x += config.unitSize;
  307. break;
  308. case '*':
  309. drawNode(x, y, color);
  310. break;
  311. case '|':
  312. if (prevColomn && prevColomn === '\\') {
  313. x += config.unitSize;
  314. }
  315. drawLineUp(x, y, color);
  316. break;
  317. case '/':
  318. if (prevColomn
  319. && (prevColomn === '/'
  320. || prevColomn === ' ')) {
  321. x -= config.unitSize;
  322. }
  323. drawLineOut(x, y, color);
  324. x += config.unitSize;
  325. break;
  326. case '\\':
  327. drawLineIn(x, y, color);
  328. break;
  329. }
  330. ++colomnIndex;
  331. }
  332. y -= config.unitSize;
  333. }
  334. // do some clean up
  335. if (flows.length > condenseCurrentLength) {
  336. flows.splice(condenseCurrentLength, flows.length - condenseCurrentLength);
  337. }
  338. };
  339. init();
  340. draw(graphList);
  341. }
  342. // @end-license