ActivityDiagram.svelte
1 <script lang="ts"> 2 import type { WeeklyActivity } from "@app/lib/commit"; 3 4 import { onMount } from "svelte"; 5 6 export let id: string; 7 export let activity: WeeklyActivity[]; 8 export let viewBoxHeight: number; 9 export let styleColor: string; 10 11 const strokeWidth = 3; 12 const viewBoxWidth = 600; 13 14 // The path strings to be inserted into the svg <path>. 15 let path = ""; 16 let areaPath = ""; 17 18 const heightWithPadding = viewBoxHeight + 16; 19 20 // The latest point on the x axis, starting at 0 until `viewBoxWidth`. 21 let lastWidthPoint = viewBoxWidth; 22 23 // The amount of points on the x axis. 24 const widthIteration = viewBoxWidth / 52; 25 26 // The highest value on the y axis. 27 const commitCountArray: number[] = []; 28 29 // The minimal amplitude shown e.g. commitCount = 1 => `minimalHeight` 30 // points of height in the SVG. 31 const minimalHeight = 5; 32 33 let week = 0; 34 35 for (const point of activity) { 36 if (point.week - week > 1) { 37 commitCountArray.push(...new Array(point.week - week).fill(0)); 38 } 39 commitCountArray.push(point.commits.length); 40 week = point.week; 41 } 42 43 // Formats the points passed in, into a svg path string, without closing 44 // the area. 45 function createPath() { 46 let i = 1; 47 48 if (commitCountArray.length < 52) { 49 commitCountArray.push(...new Array(52 - commitCountArray.length).fill(0)); 50 } 51 52 const maxValue = Math.max(...commitCountArray); 53 const minValue = Math.min(...commitCountArray); 54 55 // Normalizes the values to the viewBox dimensions. 56 const normalizedArray = commitCountArray.map(c => { 57 // If we are not crossing the `viewBoxHeight` we want to return the 58 // actual value, and don't want to normalize <`minimalHeight` commit 59 // counts as huge spikes. 60 if (maxValue < viewBoxHeight && c >= minimalHeight) { 61 return c; 62 } 63 // If the value is 0..minimalHeight though we don't want to set it to 64 // the minimalHeight. 65 else if (c > 0 && c < minimalHeight) { 66 return minimalHeight; 67 } 68 // If the count is 0 we have to make sure the normalization is not being 69 // run since it would return NaN. 70 else { 71 return c === 0 72 ? 0 73 : ((viewBoxHeight - 0) * (c - minValue)) / (maxValue - minValue); 74 } 75 }); 76 77 const path = normalizedArray.slice(1).reduce( 78 (acc, curr) => { 79 const s = `${viewBoxWidth - widthIteration * i},${ 80 viewBoxHeight - curr 81 }`; 82 lastWidthPoint = viewBoxWidth - widthIteration * i; 83 i += 1; 84 return acc.concat(s); 85 }, 86 [`M${viewBoxWidth},${viewBoxHeight - normalizedArray[0]}`], 87 ); 88 return path.join(); 89 } 90 91 onMount(() => { 92 // Creates the stroke path with the array of points. 93 path = createPath(); 94 // Concats a path closing for it to be the area under the stroke. 95 areaPath = path.concat( 96 `L${lastWidthPoint},${viewBoxHeight}L${viewBoxWidth},${viewBoxHeight}Z`, 97 ); 98 }); 99 </script> 100 101 <svg 102 style:color={styleColor} 103 viewBox="0 0 {viewBoxWidth} {heightWithPadding}" 104 xmlns="http://www.w3.org/2000/svg"> 105 <linearGradient id={`${id}:fillGradient`} x1="0" y1="1" x2="0" y2="0"> 106 <stop offset="0%" stop-color="currentColor" stop-opacity="0" /> 107 <stop offset="100%" stop-color="currentColor" stop-opacity="0.2" /> 108 </linearGradient> 109 <linearGradient id={`${id}:gradient`} x1="0" y1="1" x2="0" y2="0"> 110 <stop offset="0%" stop-color="currentColor" stop-opacity="0.2" /> 111 <stop offset="50%" stop-color="currentColor" stop-opacity="0.8" /> 112 <stop offset="100%" stop-color="currentColor" stop-opacity="1" /> 113 </linearGradient> 114 {#if activity.length > 0} 115 <g> 116 <path 117 fill="transparent" 118 stroke={`url(#${id}:gradient)`} 119 stroke-width={strokeWidth} 120 stroke-linejoin="round" 121 d={path} /> 122 <path 123 fill={`url(#${id}:fillGradient)`} 124 stroke="transparent" 125 d={areaPath} /> 126 </g> 127 {:else} 128 <!-- If no commits have been made in a year, we show a straight line --> 129 <line 130 x1="0" 131 y1={viewBoxHeight} 132 x2="600" 133 y2={viewBoxHeight} 134 stroke="currentColor" 135 stroke-width={1} /> 136 {/if} 137 </svg>