From 601de536e3478c4e4ad5d88c92d8ceb29c080da6 Mon Sep 17 00:00:00 2001 From: Marcus Williams Date: Wed, 9 Aug 2023 14:25:04 +0100 Subject: [PATCH 1/3] Added a cycletime histogram * Added a cycle time histogram tracking frequency of cycle times * Cycle times rounded to nearest 10th of "day" (flooring hundredths) bin size --- index.html | 3 ++ src/histogram.js | 87 ++++++++++++++++++++++++++++++++++++++++++++++++ src/setup.js | 5 +++ 3 files changed, 95 insertions(+) create mode 100644 src/histogram.js diff --git a/index.html b/index.html index 0d31981..b97e4bd 100644 --- a/index.html +++ b/index.html @@ -80,6 +80,9 @@
+
+ +
diff --git a/src/histogram.js b/src/histogram.js new file mode 100644 index 0000000..c101d27 --- /dev/null +++ b/src/histogram.js @@ -0,0 +1,87 @@ +const TimeAdjustments = require('./timeAdjustments'); +const Chart = require('chart.js'); +const PubSub = require("pubsub-js"); + +function createChart(ctx,speed) { + const histogram = {} + const labels = []; + const startTime = new Date(); + + const data = { + datasets: [ + { + label: 'Cycle time frequency', + data: histogram, + backgroundColor: 'rgba(54, 162, 235, 0.1)', + borderColor: 'rgba(54, 162, 235, 1)', + borderWidth: 1, + borderRadius: 5, + minBarThickness: 3, + }, + ] + }; + + const config = { + type: 'bar', + data: data, + options: { + animation: false, + scales: { + x: { + type: 'linear', + title: { + display: true, + text: 'Cycle time (days)' + } + }, + y: { + type: 'linear', + position: 'left', + ticks: { + stepSize: 1 + }, + title: { + display: true, + text: 'Frequency (# of work items)' + } + }, + }, + plugins: { + legend: {display: true, position: 'bottom', align: 'start'}, + title: { + display: true, + text: 'Cycle time histogram' + } + } + } + }; + const chart = new Chart(ctx, config); + return {histogram, chart, labels}; +} + +function HistogramChart($chart, updateInterval, speed) { + const ctx = $chart.getContext('2d'); + + let state = createChart(ctx, speed); + PubSub.subscribe('board.ready', () => { + const timerId = setInterval(() => state.chart.update(), updateInterval); + PubSub.subscribe('board.done', () => { + clearInterval(timerId); + state.chart.update(); + }); + + PubSub.subscribe('workitem.finished', (topic, item) => { + duration = item.duration / (TimeAdjustments.multiplicator() * 1000); + duration = Math.floor(duration * 10)/10; /* bucket into 10ths of day */ + if (state.histogram[duration] !== undefined) { + state.histogram[duration] = state.histogram[duration] + 1; + } else { + state.histogram[duration] = 1; + } + }); + }); + + return state.chart; +} + +module.exports = HistogramChart diff --git a/src/setup.js b/src/setup.js index 1a7f81a..1940da5 100644 --- a/src/setup.js +++ b/src/setup.js @@ -7,6 +7,7 @@ const Stats = require('./stats'); const WorkerStats = require('./worker-stats'); const Scenario = require("./scenario"); const LineChart = require("./charts"); +const HistogramChart = require("./histogram"); const Cfd = require("./CumulativeFlowDiagram"); const {parseInput} = require("./parsing"); @@ -64,6 +65,7 @@ const wipLimiter = LimitBoardWip(); let lineChart = undefined; let cfd = undefined; +let histogram = undefined; function run(scenario) { PubSub.clearAllSubscriptions(); @@ -85,6 +87,9 @@ function run(scenario) { if(cfd) cfd.destroy() cfd = Cfd(document.getElementById('cfd'), 2000, scenario.speed) + if(histogram) histogram.destroy() + histogram = HistogramChart(document.getElementById('histogram'), 1000, scenario.speed) + const board = scenario.run(); } From d8b4f3f4a0c7593235af158620c932c186805a5f Mon Sep 17 00:00:00 2001 From: Marcus Williams Date: Tue, 15 Aug 2023 11:30:44 +0100 Subject: [PATCH 2/3] Added better bin sizes for histogram and percentile marker * Bin sizes automatically calculated (chart split into 25 bins) * 50th percentile marker on histogram --- package.json | 1 + src/histogram.js | 81 +++++++++++++++++++++++++++++++++++++++++++----- 2 files changed, 75 insertions(+), 7 deletions(-) diff --git a/package.json b/package.json index 80de38f..8532f84 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ }, "dependencies": { "chart.js": "^3.8.0", + "chartjs-plugin-annotation": "^2.2.1", "pubsub-js": "^1.9.4", "seedrandom": "^3.0.5" } diff --git a/src/histogram.js b/src/histogram.js index c101d27..aeebffe 100644 --- a/src/histogram.js +++ b/src/histogram.js @@ -1,11 +1,16 @@ const TimeAdjustments = require('./timeAdjustments'); const Chart = require('chart.js'); +const annotationPlugin = require('chartjs-plugin-annotation'); const PubSub = require("pubsub-js"); +const BINSIZE = 25; + +Chart.register(annotationPlugin); function createChart(ctx,speed) { const histogram = {} const labels = []; const startTime = new Date(); + const percentile = { cycletimes: [], binned: false, average: 0, sle: 0 }; const data = { datasets: [ @@ -16,7 +21,7 @@ function createChart(ctx,speed) { borderColor: 'rgba(54, 162, 235, 1)', borderWidth: 1, borderRadius: 5, - minBarThickness: 3, + minBarThickness: 5, }, ] }; @@ -51,12 +56,37 @@ function createChart(ctx,speed) { title: { display: true, text: 'Cycle time histogram' - } + }, + annotation: { + annotations: { + line1: { + display: () => { return percentile.average > 0; }, + type: 'line', + xMax: () => { return percentile.average; }, + xMin: () => { return percentile.average; }, + borderColor: 'rgb(255, 99, 132)', + borderWidth: 2, + borderDash: [5, 5] + }, + label1: { + display: () => { return percentile.average > 0; }, + type: 'label', + rotation: 270, + xValue: () => { return percentile.average; }, + yValue: () => { return Math.max(...(Object.values(histogram))); }, + yAdjust: 15, + xAdjust: -10, + color: 'rgb(255, 99, 132)', + fontSize: 8, + content: '50%', + } + } + }, } } }; const chart = new Chart(ctx, config); - return {histogram, chart, labels}; + return {histogram, percentile, chart, labels}; } function HistogramChart($chart, updateInterval, speed) { @@ -71,12 +101,49 @@ function HistogramChart($chart, updateInterval, speed) { }); PubSub.subscribe('workitem.finished', (topic, item) => { - duration = item.duration / (TimeAdjustments.multiplicator() * 1000); + let duration = item.duration / (TimeAdjustments.multiplicator() * 1000); + state.percentile.cycletimes.push(duration); + state.percentile.cycletimes.sort((a,b) => { return a-b; }); + duration = Math.floor(duration * 10)/10; /* bucket into 10ths of day */ - if (state.histogram[duration] !== undefined) { - state.histogram[duration] = state.histogram[duration] + 1; + + if (state.percentile.binned || Object.keys(state.histogram).length>BINSIZE) { + let bins = {}; + let cycletimes_rev = JSON.parse(JSON.stringify(state.percentile.cycletimes)).reverse(); + let chunk = (cycletimes_rev[0] - cycletimes_rev[cycletimes_rev.length-1])/BINSIZE; + let chunk_top = cycletimes_rev[cycletimes_rev.length-1]+chunk + let chunk_mid = Math.floor((chunk_top-(chunk/2))*10)/10; + while (cycletimes_rev.length>0) { + let c = cycletimes_rev.pop(); + if (c < chunk_top) { + if (bins[chunk_mid] === undefined) { + bins[chunk_mid] = 0; + } + bins[chunk_mid] = bins[chunk_mid] + 1; + } else { + cycletimes_rev.push(c); + chunk_top += chunk; + chunk_mid = Math.floor((chunk_top-(chunk/2))*10)/10; + } + } + state.percentile.binned = true; + for (var prop in state.histogram) { + if (state.histogram.hasOwnProperty(prop)) { + delete state.histogram[prop]; + } + } + Object.assign(state.histogram, bins); } else { - state.histogram[duration] = 1; + if (state.histogram[duration] === undefined) { + state.histogram[duration] = 0; + } + state.histogram[duration] = state.histogram[duration] + 1; + } + + + if (state.percentile.cycletimes.length>1) { + let pos = Math.floor(state.percentile.cycletimes.length/2); + state.percentile.average = state.percentile.cycletimes[pos]; } }); }); From a7a80f07378d58040824f5d989600c2357712e60 Mon Sep 17 00:00:00 2001 From: Marcus Williams Date: Wed, 16 Aug 2023 15:07:46 +0100 Subject: [PATCH 3/3] Added 85th percentile marker (SLE) * 85th percentile marker as an example for service level expectation for team --- src/histogram.js | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/src/histogram.js b/src/histogram.js index aeebffe..04cd238 100644 --- a/src/histogram.js +++ b/src/histogram.js @@ -68,6 +68,15 @@ function createChart(ctx,speed) { borderWidth: 2, borderDash: [5, 5] }, + line2: { + display: () => { return percentile.sle > 0; }, + type: 'line', + xMax: () => { return percentile.sle; }, + xMin: () => { return percentile.sle; }, + borderColor: 'rgb(0, 204, 34)', + borderWidth: 2, + borderDash: [5, 5] + }, label1: { display: () => { return percentile.average > 0; }, type: 'label', @@ -79,6 +88,18 @@ function createChart(ctx,speed) { color: 'rgb(255, 99, 132)', fontSize: 8, content: '50%', + }, + label2: { + display: () => { return percentile.sle > 0; }, + type: 'label', + rotation: 270, + xValue: () => { return percentile.sle; }, + yValue: () => { return Math.max(...(Object.values(histogram))); }, + yAdjust: 15, + xAdjust: -10, + color: 'rgb(0, 204, 34)', + fontSize: 8, + content: '85%', } } }, @@ -144,6 +165,8 @@ function HistogramChart($chart, updateInterval, speed) { if (state.percentile.cycletimes.length>1) { let pos = Math.floor(state.percentile.cycletimes.length/2); state.percentile.average = state.percentile.cycletimes[pos]; + pos = Math.floor(state.percentile.cycletimes.length*(85/100)); + state.percentile.sle = state.percentile.cycletimes[pos]; } }); });