Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,9 @@
<div class="chart col col-4">
<canvas id="cfd" class="cfd"></canvas>
</div>
<div class="chart col col-4">
<canvas id="histogram" class="histogram"></canvas>
</div>
</div>

<div class="row">
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
Expand Down
177 changes: 177 additions & 0 deletions src/histogram.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
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: [
{
label: 'Cycle time frequency',
data: histogram,
backgroundColor: 'rgba(54, 162, 235, 0.1)',
borderColor: 'rgba(54, 162, 235, 1)',
borderWidth: 1,
borderRadius: 5,
minBarThickness: 5,
},
]
};

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'
},
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]
},
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',
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%',
},
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%',
}
}
},
}
}
};
const chart = new Chart(ctx, config);
return {histogram, percentile, 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) => {
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.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 {
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];
pos = Math.floor(state.percentile.cycletimes.length*(85/100));
state.percentile.sle = state.percentile.cycletimes[pos];
}
});
});

return state.chart;
}

module.exports = HistogramChart
5 changes: 5 additions & 0 deletions src/setup.js
Original file line number Diff line number Diff line change
Expand Up @@ -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");

Expand Down Expand Up @@ -64,6 +65,7 @@ const wipLimiter = LimitBoardWip();

let lineChart = undefined;
let cfd = undefined;
let histogram = undefined;

function run(scenario) {
PubSub.clearAllSubscriptions();
Expand All @@ -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();
}