Gisle Aune
2 years ago
9 changed files with 572 additions and 2 deletions
-
177frontend/package-lock.json
-
1frontend/package.json
-
131frontend/src/lib/components/common/BurndownChart.svelte
-
114frontend/src/lib/components/common/layercake/AxisX.svelte
-
101frontend/src/lib/components/common/layercake/AxisY.svelte
-
27frontend/src/lib/components/common/layercake/Line.svelte
-
7frontend/src/lib/components/scope/SprintBody.svelte
-
5frontend/src/lib/models/sprint.ts
-
11usecases/sprints/burndown_test.go
@ -0,0 +1,131 @@ |
|||
<script lang="ts" context="module"> |
|||
const MILESTONE_COLORS = [ |
|||
"#f4b083", |
|||
"#d8dce4", |
|||
"#ffd966", |
|||
"#84f5ff", |
|||
"#8ef88e", |
|||
]; |
|||
|
|||
</script> |
|||
|
|||
<script lang="ts"> |
|||
import { LayerCake, Svg } from 'layercake'; |
|||
import Line from './layercake/Line.svelte'; |
|||
import AxisY from './layercake/AxisY.svelte'; |
|||
|
|||
import type { SprintBurndownDataPoint } from '$lib/models/sprint'; |
|||
import { getTimeContext } from '../contexts/TimeContext.svelte'; |
|||
import AxisX from './layercake/AxisX.svelte'; |
|||
|
|||
const {now} = getTimeContext(); |
|||
|
|||
export let data: SprintBurndownDataPoint[] |
|||
export let from: Date | string | number |
|||
export let to: Date | string | number |
|||
export let milestone: number = 10000; |
|||
export let green: boolean = false; |
|||
|
|||
let after = false; |
|||
let today = 0; |
|||
let lineColor = "#777777"; |
|||
let points: {x: number, y: number}[] = []; |
|||
let milestones: number[] = []; |
|||
let milestoneIndex = 0; |
|||
let fromTime: number |
|||
let toTime: number |
|||
let timeSplits: number[] = []; |
|||
|
|||
$: fromTime = new Date(from).getTime(); |
|||
$: toTime = new Date(to).getTime(); |
|||
$: after = toTime < $now.getTime(); |
|||
$: today = Math.floor($now.getTime() / 86400000) * 86400000; |
|||
|
|||
$: { |
|||
const dataMap = {} |
|||
dataMap[fromTime] = 0; |
|||
if (after) { |
|||
dataMap[toTime] = data?.[data?.length - 1]?.value || 0; |
|||
} else { |
|||
dataMap[today] = data?.[data?.length - 1]?.value || 0; |
|||
} |
|||
|
|||
timeSplits = []; |
|||
if (toTime - fromTime > (86400000 * 350)) { |
|||
let current = new Date(fromTime); |
|||
current.setUTCDate(1); |
|||
while (current.getTime() < toTime) { |
|||
timeSplits.push(current.getTime()); |
|||
current.setUTCMonth(current.getUTCMonth() + 1); |
|||
} |
|||
} else if (toTime - fromTime > (86400000 * 7)) { |
|||
for (let x = fromTime; x < toTime; x += 86400000 * 7) { |
|||
timeSplits.push(x) |
|||
} |
|||
} else { |
|||
for (let x = fromTime; x < toTime; x += 86400000) { |
|||
timeSplits.push(x) |
|||
} |
|||
} |
|||
|
|||
let last = 0; |
|||
for (const {date, value} of data) { |
|||
const currentX = new Date(date+"T00:00:00Z").getTime(); |
|||
const prevX = currentX - 86400000; |
|||
|
|||
if (dataMap[prevX] == null) { |
|||
dataMap[prevX] = last; |
|||
} |
|||
dataMap[currentX] = value; |
|||
last = value; |
|||
} |
|||
|
|||
milestones = []; |
|||
milestoneIndex = 0; |
|||
for (let n = milestone; n < last; n += milestone) { |
|||
milestones.push(n); |
|||
milestoneIndex += 1; |
|||
if (milestoneIndex == MILESTONE_COLORS.length - 1) { |
|||
break |
|||
} |
|||
} |
|||
if (green) { |
|||
lineColor = "#78ff78"; |
|||
} else { |
|||
lineColor = MILESTONE_COLORS[milestoneIndex]; |
|||
} |
|||
|
|||
points = Object.keys(dataMap).map(k => ({x: parseInt(k), y: dataMap[k]})).sort((a,b) => a.x - b.x); |
|||
}; |
|||
</script> |
|||
|
|||
<div class="chart-container"> |
|||
<LayerCake |
|||
padding={{ top: 0, right: 5, bottom: 0, left: 5 }} |
|||
x="x" |
|||
y="y" |
|||
yNice={8} |
|||
xDomain={[fromTime, toTime]} |
|||
yDomain={[0, milestones.length == 0 ? milestone : null]} |
|||
data={points} |
|||
> |
|||
<Svg> |
|||
<AxisX |
|||
ticks={timeSplits} |
|||
formatTick={_t => ""} |
|||
/> |
|||
<AxisY |
|||
ticks={milestones} |
|||
formatTick={_tick => ""} |
|||
gridlines={true} |
|||
/> |
|||
<Line stroke={lineColor} /> |
|||
</Svg> |
|||
</LayerCake> |
|||
</div> |
|||
|
|||
<style lang="sass"> |
|||
.chart-container |
|||
width: 100% |
|||
height: 30px |
|||
</style> |
@ -0,0 +1,114 @@ |
|||
<!-- |
|||
@component |
|||
Generates an SVG x-axis. This component is also configured to detect if your x-scale is an ordinal scale. If so, it will place the markers in the middle of the bandwidth. |
|||
--> |
|||
<script> |
|||
import { getContext } from 'svelte'; |
|||
const { width, height, xScale, yRange } = getContext('LayerCake'); |
|||
|
|||
/** @type {Boolean} [gridlines=true] - Extend lines from the ticks into the chart space */ |
|||
export let gridlines = true; |
|||
|
|||
/** @type {Boolean} [tickMarks=false] - Show a vertical mark for each tick. */ |
|||
export let tickMarks = false; |
|||
|
|||
/** @type {Boolean} [baseline=false] – Show a solid line at the bottom. */ |
|||
export let baseline = false; |
|||
|
|||
/** @type {Boolean} [snapTicks=false] - Instead of centering the text on the first and the last items, align them to the edges of the chart. */ |
|||
export let snapTicks = false; |
|||
|
|||
/** @type {Function} [formatTick=d => d] - A function that passes the current tick value and expects a nicely formatted value in return. */ |
|||
export let formatTick = d => d; |
|||
|
|||
/** @type {Number|Array|Function} [ticks] - If this is a number, it passes that along to the [d3Scale.ticks](https://github.com/d3/d3-scale) function. If this is an array, hardcodes the ticks to those values. If it's a function, passes along the default tick values and expects an array of tick values in return. If nothing, it uses the default ticks supplied by the D3 function. */ |
|||
export let ticks = undefined; |
|||
|
|||
/** @type {Number} [xTick=0] - TK */ |
|||
export let xTick = 0; |
|||
|
|||
/** @type {Number} [yTick=16] - The distance from the baseline to place each tick value. */ |
|||
export let yTick = 16; |
|||
|
|||
$: isBandwidth = typeof $xScale.bandwidth === 'function'; |
|||
|
|||
$: tickVals = Array.isArray(ticks) ? ticks : |
|||
isBandwidth ? |
|||
$xScale.domain() : |
|||
typeof ticks === 'function' ? |
|||
ticks($xScale.ticks()) : |
|||
$xScale.ticks(ticks); |
|||
|
|||
function textAnchor(i) { |
|||
if (snapTicks === true) { |
|||
if (i === 0) { |
|||
return 'start'; |
|||
} |
|||
if (i === tickVals.length - 1) { |
|||
return 'end'; |
|||
} |
|||
} |
|||
return 'middle'; |
|||
} |
|||
</script> |
|||
|
|||
<g class="axis x-axis" class:snapTicks> |
|||
{#each tickVals as tick, i (tick)} |
|||
<g class="tick tick-{i}" transform="translate({$xScale(tick)},{Math.max(...$yRange)})"> |
|||
{#if gridlines !== false} |
|||
<line class="gridline" y1={$height * -1} y2="0" x1="0" x2="0" /> |
|||
{/if} |
|||
{#if tickMarks === true} |
|||
<line |
|||
class="tick-mark" |
|||
y1={0} |
|||
y2={6} |
|||
x1={xTick || isBandwidth ? $xScale.bandwidth() / 2 : 0} |
|||
x2={xTick || isBandwidth ? $xScale.bandwidth() / 2 : 0} |
|||
/> |
|||
{/if} |
|||
<text |
|||
x={xTick || isBandwidth ? $xScale.bandwidth() / 2 : 0} |
|||
y={8} |
|||
dx="" |
|||
dy="" |
|||
font-size="0.66em" |
|||
text-anchor={textAnchor(i)}>{formatTick(tick)}</text |
|||
> |
|||
</g> |
|||
{/each} |
|||
{#if baseline === true} |
|||
<line class="baseline" y1={$height + 0.5} y2={$height + 0.5} x1="0" x2={$width} /> |
|||
{/if} |
|||
</g> |
|||
|
|||
<style lang="scss"> |
|||
@import "../../../css/colors.sass"; |
|||
|
|||
.tick { |
|||
font-size: 0.725em; |
|||
font-weight: 200; |
|||
} |
|||
|
|||
line, |
|||
.tick line { |
|||
stroke: $color-entry2; |
|||
stroke-dasharray: 1; |
|||
} |
|||
|
|||
.tick text { |
|||
fill: $color-entry5; |
|||
} |
|||
|
|||
.tick .tick-mark, |
|||
.baseline { |
|||
stroke-dasharray: 0; |
|||
} |
|||
/* This looks slightly better */ |
|||
.axis.snapTicks .tick:last-child text { |
|||
transform: translateX(3px); |
|||
} |
|||
.axis.snapTicks .tick.tick-0 text { |
|||
transform: translateX(-3px); |
|||
} |
|||
</style> |
@ -0,0 +1,101 @@ |
|||
<!-- |
|||
@component |
|||
Generates an HTML y-axis. |
|||
--> |
|||
<script> |
|||
import { getContext } from 'svelte'; |
|||
|
|||
const { padding, xRange, yScale } = getContext('LayerCake'); |
|||
|
|||
/** @type {Boolean} [gridlines=true] - Extend lines from the ticks into the chart space */ |
|||
export let gridlines = true; |
|||
|
|||
/** @type {Boolean} [tickMarks=false] - Show a vertical mark for each tick. */ |
|||
export let tickMarks = false; |
|||
|
|||
/** @type {Function} [formatTick=d => d] - A function that passes the current tick value and expects a nicely formatted value in return. */ |
|||
export let formatTick = d => d; |
|||
|
|||
/** @type {Number|Array|Function} [ticks=4] - If this is a number, it passes that along to the [d3Scale.ticks](https://github.com/d3/d3-scale) function. If this is an array, hardcodes the ticks to those values. If it's a function, passes along the default tick values and expects an array of tick values in return. */ |
|||
export let ticks = 4; |
|||
|
|||
/** @type {Number} [xTick=0] - How far over to position the text marker. */ |
|||
export let xTick = 0; |
|||
|
|||
/** @type {Number} [yTick=0] - How far up and down to position the text marker. */ |
|||
export let yTick = 0; |
|||
|
|||
/** @type {Number} [dxTick=0] - Any optional value passed to the `dx` attribute on the text marker and tick mark (if visible). This is ignored on the text marker if your scale is ordinal. */ |
|||
export let dxTick = 0; |
|||
|
|||
/** @type {Number} [dyTick=-4] - Any optional value passed to the `dy` attribute on the text marker and tick mark (if visible). This is ignored on the text marker if your scale is ordinal. */ |
|||
export let dyTick = -4; |
|||
|
|||
/** @type {String} [textAnchor='start'] The CSS `text-anchor` passed to the label. This is automatically set to "end" if the scale has a bandwidth method, like in ordinal scales. */ |
|||
export let textAnchor = 'start'; |
|||
|
|||
$: isBandwidth = typeof $yScale.bandwidth === 'function'; |
|||
|
|||
$: tickVals = Array.isArray(ticks) ? ticks : |
|||
isBandwidth ? |
|||
$yScale.domain() : |
|||
typeof ticks === 'function' ? |
|||
ticks($yScale.ticks()) : |
|||
$yScale.ticks(ticks); |
|||
</script> |
|||
|
|||
<g class='axis y-axis' transform='translate({-$padding.left}, 0)'> |
|||
{#each tickVals as tick (tick)} |
|||
<g class='tick tick-{tick}' transform='translate({$xRange[0] + (isBandwidth ? $padding.left : 0)}, {$yScale(tick)})'> |
|||
{#if gridlines !== false} |
|||
<line |
|||
class="gridline" |
|||
x1={5} |
|||
x2='98.75%' |
|||
y1={yTick + (isBandwidth ? ($yScale.bandwidth() / 2) : 0)} |
|||
y2={yTick + (isBandwidth ? ($yScale.bandwidth() / 2) : 0)} |
|||
></line> |
|||
{/if} |
|||
{#if tickMarks === true} |
|||
<line |
|||
class='tick-mark' |
|||
x1='0' |
|||
x2='{isBandwidth ? -6 : 6}' |
|||
y1={yTick + (isBandwidth ? ($yScale.bandwidth() / 2) : 0)} |
|||
y2={yTick + (isBandwidth ? ($yScale.bandwidth() / 2) : 0)} |
|||
></line> |
|||
{/if} |
|||
<text |
|||
x='{xTick}' |
|||
y='{yTick + (isBandwidth ? $yScale.bandwidth() / 2 : 0)}' |
|||
dx='{isBandwidth ? -9 : dxTick}' |
|||
dy='{isBandwidth ? 4 : dyTick}' |
|||
style="text-anchor:{isBandwidth ? 'end' : textAnchor};" |
|||
>{formatTick(tick)}</text> |
|||
</g> |
|||
{/each} |
|||
</g> |
|||
|
|||
<style lang="scss"> |
|||
@import "../../../css/colors.sass"; |
|||
|
|||
.tick { |
|||
font-size: .725em; |
|||
font-weight: 200; |
|||
} |
|||
|
|||
.tick line { |
|||
stroke: $color-entry2; |
|||
} |
|||
.tick .gridline { |
|||
stroke-dasharray: 2; |
|||
} |
|||
|
|||
.tick text { |
|||
fill: #666; |
|||
} |
|||
|
|||
.tick.tick-0 line { |
|||
stroke-dasharray: 0; |
|||
} |
|||
</style> |
@ -0,0 +1,27 @@ |
|||
<!-- |
|||
@component |
|||
Generates an SVG area shape using the `area` function from [d3-shape](https://github.com/d3/d3-shape). |
|||
--> |
|||
<script> |
|||
import { getContext } from 'svelte'; |
|||
|
|||
const { data, xGet, yGet } = getContext('LayerCake'); |
|||
|
|||
/** @type {String} [stroke='#ab00d6'] - The shape's fill color. This is technically optional because it comes with a default value but you'll likely want to replace it with your own color. */ |
|||
export let stroke = '#ab00d6'; |
|||
|
|||
$: path = 'M' + $data |
|||
.map(d => $xGet(d) + ',' + $yGet(d)) |
|||
.join('L'); |
|||
</script> |
|||
|
|||
<path class='path-line' d='{path}' {stroke}></path> |
|||
|
|||
<style> |
|||
.path-line { |
|||
fill: none; |
|||
stroke-linejoin: round; |
|||
stroke-linecap: round; |
|||
stroke-width: 1; |
|||
} |
|||
</style> |
Write
Preview
Loading…
Cancel
Save
Reference in new issue