Skip to content

Commit ee942fd

Browse files
committed
Add process behaviour charts
1 parent bda2e04 commit ee942fd

File tree

14 files changed

+568
-261
lines changed

14 files changed

+568
-261
lines changed

examples/example.html

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,6 @@
22
<html lang="en">
33
<head>
44
<meta charset="UTF-8">
5-
<meta
6-
content="connect-src 'self' https://4njxsfgzvh.execute-api.us-east-1.amazonaws.com"
7-
http-equiv="Content-Security-Policy" />
85
<title>Graphs Example</title>
96
<script src="https://cdn.tailwindcss.com" integrity="sha384-76mJWTQIdZ/g5T0cSoBptGTnvK4ZolzknwUlxOwBUSP9kr82qsUh1aavwwx31NAa" crossorigin="anonymous"></script>
107
</head>
@@ -71,7 +68,7 @@ <h2 class="text-center text-xl text-white">Observation</h2>
7168
id="work-item-input"
7269
readonly
7370
type="text" />
74-
<label class="block text-black font-medium mt-4" for="average-cycle-time-input">Lead time</label>
71+
<label class="block text-black font-medium mt-4" for="lead-time-input">Lead time</label>
7572
<input
7673
class="shadow appearance-none bg-gray-300 px-4 w-50 cursor-not-allowed border py-1 rounded text-blue-500 leading-tight focus:outline-none focus:shadow-outline"
7774
id="lead-time-input"
@@ -157,6 +154,13 @@ <h1 class="my-2 text-center font-bold">Scatterplot</h1>
157154
<div id="scatterplot-brush-div"></div>
158155
<h1 class="my-2 text-center font-bold">Histogram</h1>
159156
<div id="histogram-area-div"></div>
157+
158+
<h1 class="my-2 text-center font-bold">Moving range</h1>
159+
<div id="moving-range-area-div"></div>
160+
<div id="moving-range-brush-div"></div>
161+
<h1 class="my-2 text-center font-bold">Control area</h1>
162+
<div id="control-area-div"></div>
163+
<div id="control-brush-div"></div>
160164
</div>
161165
</div>
162166
</div>

src/graphs/UIControlsRenderer.js

Lines changed: 26 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import * as d3 from 'd3';
77
*/
88
export default class UIControlsRenderer extends Renderer {
99
selectedTimeRange;
10+
datePropertyName;
1011
defaultTimeRange;
1112
#defaultReportingRangeDays = 90;
1213
#defaultTimeInterval = 'weeks';
@@ -87,7 +88,6 @@ export default class UIControlsRenderer extends Renderer {
8788
});
8889
}
8990

90-
9191
/**
9292
* Computes the reporting range for the chart based on the number of days.
9393
* @param {number} noOfDays - The number of days for the reporting range.
@@ -96,6 +96,7 @@ export default class UIControlsRenderer extends Renderer {
9696
computeReportingRange(noOfDays) {
9797
const finalDate = this.data[this.data.length - 1][this.datePropertyName];
9898
let endDate = new Date(finalDate);
99+
console.log(this.data[this.data.length - 1], finalDate, noOfDays);
99100
let startDate = addDaysToDate(finalDate, -Number(noOfDays));
100101
if (this.selectedTimeRange) {
101102
endDate = new Date(this.selectedTimeRange[1]);
@@ -118,7 +119,6 @@ export default class UIControlsRenderer extends Renderer {
118119
return [startDate, endDate];
119120
}
120121

121-
122122
/**
123123
* Creates and configures an x-axis based on the specified time interval.
124124
* The axis is created using D3.js and configured for different time intervals: days, weeks, or months.
@@ -127,21 +127,20 @@ export default class UIControlsRenderer extends Renderer {
127127
* @returns {d3.Axis} - The configured D3 axis for the x-axis.
128128
*/
129129
createXAxis(x, timeInterval = this.timeInterval) {
130-
console.log("time interval", this.timeInterval)
131130
let axis;
132131
switch (timeInterval) {
133-
case "days":
132+
case 'days':
134133
axis = d3
135-
.axisBottom(x)
136-
.ticks(d3.timeDay.every(1)) // label every 2 days
137-
.tickFormat((d, i) => {
138-
return i % 2 === 0 ? d3.timeFormat("%b %d")(d) : "";
139-
});
134+
.axisBottom(x)
135+
.ticks(d3.timeDay.every(1)) // label every 2 days
136+
.tickFormat((d, i) => {
137+
return i % 2 === 0 ? d3.timeFormat('%b %d')(d) : '';
138+
});
140139
break;
141-
case "weeks":
140+
case 'weeks':
142141
axis = d3.axisBottom(x).ticks(d3.timeWeek);
143142
break;
144-
case "months":
143+
case 'months':
145144
axis = d3.axisBottom(x).ticks(d3.timeMonth);
146145
break;
147146
default:
@@ -159,19 +158,19 @@ export default class UIControlsRenderer extends Renderer {
159158
// console.log("this.timeInterval", this.timeInterval)
160159

161160
if (isManualUpdate) {
162-
switch (this.timeInterval) {
163-
case 'weeks':
164-
this.timeInterval = "months";
165-
break;
166-
case "months":
167-
this.timeInterval = "days";
168-
break;
169-
case "days":
170-
this.timeInterval = "weeks";
171-
break;
172-
default:
173-
this.timeInterval = "weeks";
174-
}
161+
switch (this.timeInterval) {
162+
case 'weeks':
163+
this.timeInterval = 'months';
164+
break;
165+
case 'months':
166+
this.timeInterval = 'days';
167+
break;
168+
case 'days':
169+
this.timeInterval = 'weeks';
170+
break;
171+
default:
172+
this.timeInterval = 'weeks';
173+
}
175174
} else {
176175
this.timeInterval = this.determineTheAppropriateAxisLabels();
177176
}
@@ -181,15 +180,14 @@ export default class UIControlsRenderer extends Renderer {
181180
determineTheAppropriateAxisLabels() {
182181
// console.log("this.reportingRangeDays", this.reportingRangeDays)
183182
if (this.reportingRangeDays <= 31) {
184-
return "days";
183+
return 'days';
185184
}
186185
if (this.reportingRangeDays > 31 && this.reportingRangeDays <= 124) {
187-
return "weeks";
186+
return 'weeks';
188187
}
189-
return "months";
188+
return 'months';
190189
}
191190

192-
193191
/**
194192
* Abstract method to render the brush. Must be implemented in subclasses.
195193
*/

src/graphs/cfd/CFDRenderer.js

Lines changed: 54 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -65,8 +65,9 @@ class CFDRenderer extends UIControlsRenderer {
6565
this.eventBus?.addEventListener('change-time-range-scatterplot', this.updateBrushSelection.bind(this));
6666
this.eventBus?.addEventListener('scatterplot-mousemove', (event) => this.#handleMouseEvent(event, 'scatterplot-mousemove'));
6767
this.eventBus?.addEventListener('scatterplot-mouseleave', () => this.hideTooltipAndMovingLine());
68-
this.eventBus?.addEventListener('change-time-interval-scatterplot', () => {
69-
this.handleXAxisClick();
68+
this.eventBus?.addEventListener('change-time-interval-scatterplot', (timeInterval) => {
69+
this.timeInterval = timeInterval;
70+
this.drawXAxis(this.gx, this.x.copy().domain(this.selectedTimeRange), this.height, true);
7071
});
7172
}
7273

@@ -113,7 +114,7 @@ class CFDRenderer extends UIControlsRenderer {
113114

114115
const brushArea = this.#createAreaGenerator(this.x, this.y.copy().range([this.focusHeight - this.margin.top, 4]));
115116
this.#drawStackedAreaChart(svgBrush, this.#stackedData, brushArea);
116-
this.changeTimeInterval(false, "cfd");
117+
this.changeTimeInterval(false, 'cfd');
117118
this.drawXAxis(svgBrush.append('g'), this.x, this.focusHeight - this.margin.top);
118119
this.brushGroup = svgBrush.append('g');
119120
this.brushGroup.call(this.brush).call(
@@ -139,11 +140,11 @@ class CFDRenderer extends UIControlsRenderer {
139140
*/
140141
updateGraph(domain) {
141142
const maxY = d3.max(this.#stackedData[this.#stackedData.length - 1], (d) => (d.data.date <= domain[1] ? d[1] : -1));
142-
this.reportingRangeDays = calculateDaysBetweenDates(domain[0], domain[1])
143+
this.reportingRangeDays = calculateDaysBetweenDates(domain[0], domain[1]);
143144
this.currentXScale = this.x.copy().domain(domain);
144145
this.currentYScale = this.y.copy().domain([0, maxY]).nice();
145-
this.changeTimeInterval(false, "cfd");
146-
this.drawXAxis(this.gx, this.currentXScale, this.height,true);
146+
this.changeTimeInterval(false, 'cfd');
147+
this.drawXAxis(this.gx, this.currentXScale, this.height, true);
147148
this.drawYAxis(this.gy, this.currentYScale);
148149

149150
this.chartArea
@@ -286,9 +287,8 @@ class CFDRenderer extends UIControlsRenderer {
286287
*/
287288
setupXAxisControl() {
288289
this.gx.on('click', () => {
289-
this.changeTimeInterval(true, "cfd");
290+
this.changeTimeInterval(true, 'cfd');
290291
this.drawXAxis(this.gx, this.x.copy().domain(this.selectedTimeRange), this.height, true);
291-
292292
});
293293
}
294294

@@ -355,7 +355,7 @@ class CFDRenderer extends UIControlsRenderer {
355355
g.selectAll('text').attr('y', 30).style('fill', 'black');
356356
g.attr('clip-path', `url(#${clipId})`);
357357
} else {
358-
axis = this.createXAxis(x, "months");
358+
axis = this.createXAxis(x, 'months');
359359
g.call(axis).attr('transform', `translate(0, ${height})`);
360360
}
361361
}
@@ -442,9 +442,17 @@ class CFDRenderer extends UIControlsRenderer {
442442
* Creates a tooltip and a moving line for the chart used for the metrics and observation logging.
443443
* @private
444444
*/
445-
#createTooltipAndMovingLine(x,y) {
445+
#createTooltipAndMovingLine(x, y) {
446446
this.tooltip = d3.select('body').append('div').attr('class', styles.tooltip).attr('id', 'c-tooltip').style('opacity', 0);
447-
this.cfdLine = this.chartArea.append('line').attr('id', 'cfd-line').attr('stroke', 'black').attr('y1', 0).attr('y2', y).attr('x1', x).attr('x2', x).style('display', 'none');
447+
this.cfdLine = this.chartArea
448+
.append('line')
449+
.attr('id', 'cfd-line')
450+
.attr('stroke', 'black')
451+
.attr('y1', 0)
452+
.attr('y2', y)
453+
.attr('x1', x)
454+
.attr('x2', x)
455+
.style('display', 'none');
448456
}
449457

450458
/**
@@ -469,54 +477,53 @@ class CFDRenderer extends UIControlsRenderer {
469477
const gridContainer = this.tooltip?.append('div').attr('class', 'grid grid-cols-2');
470478
if (event.metrics.averageCycleTime > 0) {
471479
gridContainer
472-
.append("span")
473-
.text("Cycle time:")
474-
.attr("class", "pr-1")
475-
.style("text-align", "start")
476-
.style("color", this.#cycleTimeColor);
480+
.append('span')
481+
.text('Cycle time:')
482+
.attr('class', 'pr-1')
483+
.style('text-align', 'start')
484+
.style('color', this.#cycleTimeColor);
477485
gridContainer
478-
.append("span")
479-
.text(`${event.metrics.averageCycleTime} days`)
480-
.attr("class", "pl-1")
481-
.style("text-align", "start")
482-
.style("color", this.#cycleTimeColor);
486+
.append('span')
487+
.text(`${event.metrics.averageCycleTime} days`)
488+
.attr('class', 'pl-1')
489+
.style('text-align', 'start')
490+
.style('color', this.#cycleTimeColor);
483491
}
484492
if (event.metrics.averageLeadTime > 0) {
485493
gridContainer
486-
.append("span")
487-
.text("Lead time:")
488-
.attr("class", "pr-1")
489-
.style("text-align", "start")
490-
.style("color", this.#leadTimeColor);
494+
.append('span')
495+
.text('Lead time:')
496+
.attr('class', 'pr-1')
497+
.style('text-align', 'start')
498+
.style('color', this.#leadTimeColor);
491499
gridContainer
492-
.append("span")
493-
.text(`${event.metrics.averageLeadTime} days`)
494-
.attr("class", "pl-1")
495-
.style("text-align", "start")
496-
.style("color", this.#leadTimeColor);
500+
.append('span')
501+
.text(`${event.metrics.averageLeadTime} days`)
502+
.attr('class', 'pl-1')
503+
.style('text-align', 'start')
504+
.style('color', this.#leadTimeColor);
497505
}
498506
if (event.metrics.wip > 0) {
499-
gridContainer.append("span").text("WIP:").attr("class", "pr-1").style("text-align", "start").style("color", this.#wipColor);
507+
gridContainer.append('span').text('WIP:').attr('class', 'pr-1').style('text-align', 'start').style('color', this.#wipColor);
500508
gridContainer
501-
.append("span")
502-
.text(`${event.metrics.wip} items`)
503-
.attr("class", "pl-1")
504-
.style("text-align", "start")
505-
.style("color", this.#wipColor);
509+
.append('span')
510+
.text(`${event.metrics.wip} items`)
511+
.attr('class', 'pl-1')
512+
.style('text-align', 'start')
513+
.style('color', this.#wipColor);
506514
}
507515
if (event.metrics.throughput > 0) {
508-
gridContainer.append("span").text("Throughput:").attr("class", "pr-1").style("text-align", "start");
509-
gridContainer.append("span").text(`${event.metrics.throughput} items`).attr("class", "pl-1").style("text-align", "start");
516+
gridContainer.append('span').text('Throughput:').attr('class', 'pr-1').style('text-align', 'start');
517+
gridContainer.append('span').text(`${event.metrics.throughput} items`).attr('class', 'pl-1').style('text-align', 'start');
510518
}
511519
if (event.observationBody) {
512-
gridContainer.append("span").text("Observation:").attr("class", "pr-1").style("text-align", "start");
520+
gridContainer.append('span').text('Observation:').attr('class', 'pr-1').style('text-align', 'start');
513521
gridContainer
514-
.append("span")
515-
.text(`${event.observationBody.substring(0, 15)}...`)
516-
.attr("class", "pl-1")
517-
.style("text-align", "start");
522+
.append('span')
523+
.text(`${event.observationBody.substring(0, 15)}...`)
524+
.attr('class', 'pl-1')
525+
.style('text-align', 'start');
518526
}
519-
520527
}
521528

522529
/**
@@ -569,7 +576,7 @@ class CFDRenderer extends UIControlsRenderer {
569576

570577
// Ensure xPosition is within the chart's range
571578
if (xPosition < 0 || xPosition > this.width) {
572-
console.log("xPosition out of bounds:", xPosition);
579+
console.log('xPosition out of bounds:', xPosition);
573580
return;
574581
}
575582

@@ -629,7 +636,7 @@ class CFDRenderer extends UIControlsRenderer {
629636
const noOfItemsAfter = this.#getNoOfItems(currentDataEntry, this.states[this.states.indexOf('analysis_active')]);
630637

631638
const wip = noOfItemsAfter - noOfItemsBefore;
632-
const throughput = averageLeadTime ? parseFloat((wip/averageLeadTime).toFixed(1)) : undefined;
639+
const throughput = averageLeadTime ? parseFloat((wip / averageLeadTime).toFixed(1)) : undefined;
633640

634641
excludeCycleTime && (averageCycleTime = null);
635642
return {
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import ScatterplotRenderer from '../scatterplot/ScatterplotRenderer.js';
2+
3+
class ControlRenderer extends ScatterplotRenderer {
4+
color = '#0ea5e9';
5+
timeScale = 'linear';
6+
7+
constructor(data, avgMovingRange) {
8+
super(data);
9+
this.chartName = 'control';
10+
this.chartType = 'CONTROL';
11+
this.avgMovingRange = avgMovingRange;
12+
this.dotClass = 'control-dot';
13+
}
14+
15+
setupEventBus(eventBus) {
16+
this.eventBus = eventBus;
17+
this.eventBus?.addEventListener('change-time-range-moving-range', this.updateBrushSelection.bind(this));
18+
}
19+
20+
renderGraph(graphElementSelector, timeScaleSelector) {
21+
console.log(timeScaleSelector);
22+
this.drawSvg(graphElementSelector);
23+
this.drawAxes();
24+
this.drawArea();
25+
this.avgLeadTime = this.getAvgLeadTime();
26+
this.topLimit = Math.ceil(this.avgLeadTime + this.avgMovingRange * 2.66);
27+
this.bottomLimit = Math.ceil(this.avgLeadTime - this.avgMovingRange * 2.66);
28+
this.drawHorizontalLine(this.y, this.topLimit, 'purple', 'top');
29+
this.drawHorizontalLine(this.y, this.avgLeadTime, 'orange', 'center');
30+
this.bottomLimit > 0 && this.drawHorizontalLine(this.y, this.bottomLimit, 'purple', 'bottom');
31+
console.log('avgLeadTime', this.avgLeadTime);
32+
console.log('avgMovingRange', this.avgMovingRange);
33+
console.log('top', this.topLimit);
34+
console.log('bottom', this.bottomLimit);
35+
this.drawAxesLabels(this.svg, 'Time', 'Days');
36+
}
37+
38+
drawScatterplot(chartArea, data, x, y) {
39+
chartArea
40+
.selectAll(`.${this.dotClass}`)
41+
.data(data)
42+
.enter()
43+
.append('circle')
44+
.attr('class', this.dotClass)
45+
.attr('id', (d) => `control-${d.ticketId}`)
46+
.attr('data-date', (d) => d.deliveredDate)
47+
.attr('r', 5)
48+
.attr('cx', (d) => x(d.deliveredDate))
49+
.attr('cy', (d) => y(d.leadTime))
50+
.style('cursor', 'pointer')
51+
.attr('fill', this.color)
52+
.on('click', (event, d) => this.handleMouseClickEvent(event, d));
53+
}
54+
55+
getAvgLeadTime() {
56+
return Math.ceil(this.data.reduce((acc, curr) => acc + curr.leadTime, 0) / this.data.length);
57+
}
58+
59+
updateGraph(domain) {
60+
this.updateChartArea(domain);
61+
this.drawHorizontalLine(this.currentYScale, this.topLimit, 'purple', 'top');
62+
this.drawHorizontalLine(this.currentYScale, this.avgLeadTime, 'orange', 'center');
63+
this.bottomLimit > 0 && this.drawHorizontalLine(this.currentYScale, this.bottomLimit, 'purple', 'bottom');
64+
this.displayObservationMarkers(this.observations);
65+
}
66+
}
67+
68+
export default ControlRenderer;

0 commit comments

Comments
 (0)