Skip to content

Commit 2ea88b8

Browse files
authored
Merge pull request #39 from pfizer-opensource/moving-range-dataset-recalculation
Change the data set calculation for the Moving Range Graph
2 parents 1f27c42 + 095b0dd commit 2ea88b8

File tree

9 files changed

+184
-130
lines changed

9 files changed

+184
-130
lines changed

src/graphs/UIControlsRenderer.js

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@ import * as d3 from 'd3';
77
*/
88
export default class UIControlsRenderer extends Renderer {
99
selectedTimeRange;
10+
preventEventLoop;
11+
chartName;
12+
chartType;
1013
datePropertyName;
1114
defaultTimeRange;
1215
#defaultReportingRangeDays = 90;
@@ -17,7 +20,6 @@ export default class UIControlsRenderer extends Renderer {
1720
brush;
1821
isManualBrushUpdate = true;
1922
saveConfigsToBrowserStorage = false;
20-
timeIntervalChangeEventName;
2123

2224
constructor(data) {
2325
super(data);
@@ -45,11 +47,19 @@ export default class UIControlsRenderer extends Renderer {
4547
updateBrushSelection(newTimeRange) {
4648
if (newTimeRange) {
4749
this.isManualBrushUpdate = false;
48-
this.selectedTimeRange = newTimeRange;
50+
const maxX = newTimeRange[1] > this.x.domain()[1] ? this.x.domain()[1] : newTimeRange[1];
51+
const minX = newTimeRange[0] < this.x.domain()[0] ? this.x.domain()[0] : newTimeRange[0];
52+
this.selectedTimeRange = [minX, maxX];
53+
// Set the flag before emitting an event
54+
this.preventEventLoop = true;
55+
4956
this.brushGroup?.call(this.brush)?.call(
5057
this.brush.move,
51-
newTimeRange?.map((d) => this.x(d))
58+
this.selectedTimeRange?.map((d) => this.x(d))
5259
);
60+
61+
// Reset the flag after the event is handled
62+
this.preventEventLoop = false;
5363
}
5464
}
5565

@@ -100,13 +110,13 @@ export default class UIControlsRenderer extends Renderer {
100110
if (this.selectedTimeRange) {
101111
endDate = new Date(this.selectedTimeRange[1]);
102112
startDate = new Date(this.selectedTimeRange[0]);
103-
const diffDays = Number(noOfDays) - calculateDaysBetweenDates(startDate, endDate);
113+
const diffDays = Number(noOfDays) - calculateDaysBetweenDates(startDate, endDate).roundedDays;
104114
if (diffDays < 0) {
105115
startDate = addDaysToDate(startDate, -Number(diffDays));
106116
} else {
107117
endDate = addDaysToDate(endDate, Number(diffDays));
108118
if (endDate > finalDate) {
109-
const diffEndDays = calculateDaysBetweenDates(finalDate, endDate);
119+
const diffEndDays = calculateDaysBetweenDates(finalDate, endDate).roundedDays;
110120
endDate = finalDate;
111121
startDate = addDaysToDate(startDate, -Number(diffEndDays));
112122
}
@@ -115,6 +125,9 @@ export default class UIControlsRenderer extends Renderer {
115125
if (startDate < this.data[0][this.datePropertyName]) {
116126
startDate = this.data[0][this.datePropertyName];
117127
}
128+
if (endDate < this.x.domain()[1]) {
129+
endDate = this.x.domain()[1];
130+
}
118131
return [startDate, endDate];
119132
}
120133

@@ -153,7 +166,7 @@ export default class UIControlsRenderer extends Renderer {
153166
* This function changes the time interval state between days, weeks, and months,
154167
* and then redraws the x-axis based on the selected time range.
155168
*/
156-
changeTimeInterval(isManualUpdate, chart) {
169+
changeTimeInterval(isManualUpdate) {
157170
if (isManualUpdate) {
158171
switch (this.timeInterval) {
159172
case 'weeks':
@@ -172,7 +185,7 @@ export default class UIControlsRenderer extends Renderer {
172185
this.timeInterval = this.determineTheAppropriateAxisLabels();
173186
}
174187

175-
this.eventBus?.emitEvents(`change-time-interval-${chart}`, this.timeInterval);
188+
this.eventBus?.emitEvents(`change-time-interval-${this.chartName}`, this.timeInterval);
176189
}
177190

178191
determineTheAppropriateAxisLabels() {

src/graphs/cfd/CFDRenderer.js

Lines changed: 47 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -49,25 +49,35 @@ class CFDRenderer extends UIControlsRenderer {
4949
* }
5050
* ];
5151
*/
52-
constructor(data, states) {
52+
constructor(data, states, chartName) {
5353
super(data);
5454
this.states = states;
55+
this.chartType = 'CFD';
56+
this.chartName = chartName;
5557
this.#statesColors = d3.scaleOrdinal().domain(this.states).range(this.#colorPalette);
5658
}
5759

5860
/**
5961
* Sets up an event bus for the renderer to listen to events.
6062
* @param {Object} eventBus - The event bus for communication.
6163
*/
62-
setupEventBus(eventBus) {
64+
setupEventBus(eventBus, mouseChartsEvents, timeRangeChartsEvents) {
6365
this.eventBus = eventBus;
64-
this.eventBus?.addEventListener('change-time-range-scatterplot', this.updateBrushSelection.bind(this));
65-
this.eventBus?.addEventListener('scatterplot-mousemove', (event) => this.#handleMouseEvent(event, 'scatterplot-mousemove'));
66-
this.eventBus?.addEventListener('scatterplot-mouseleave', () => this.hideTooltipAndMovingLine());
67-
this.eventBus?.addEventListener('change-time-interval-scatterplot', (timeInterval) => {
68-
this.timeInterval = timeInterval;
69-
this.drawXAxis(this.gx, this.x?.copy().domain(this.selectedTimeRange), this.height, true);
70-
});
66+
if (this.eventBus && Array.isArray(timeRangeChartsEvents)) {
67+
timeRangeChartsEvents.forEach((chart) => {
68+
this.eventBus.addEventListener(`change-time-range-${chart}`, (newTimeRange) => {
69+
if (!this.preventEventLoop) {
70+
this.updateBrushSelection(newTimeRange);
71+
}
72+
});
73+
});
74+
}
75+
if (this.eventBus && Array.isArray(mouseChartsEvents)) {
76+
mouseChartsEvents.forEach((chart) => {
77+
this.eventBus?.addEventListener(`${chart}-mousemove`, (event) => this.#handleMouseEvent(event, `${chart}-mousemove`));
78+
this.eventBus?.addEventListener(`${chart}-mouseleave`, () => this.hideTooltipAndMovingLine());
79+
});
80+
}
7181
}
7282

7383
//region Graph and brush rendering
@@ -101,7 +111,7 @@ class CFDRenderer extends UIControlsRenderer {
101111
this.selectedTimeRange = selection.map(this.x.invert, this.x);
102112
this.updateGraph(this.selectedTimeRange);
103113
if (this.isManualBrushUpdate && this.eventBus) {
104-
this.eventBus?.emitEvents('change-time-range-cfd', this.selectedTimeRange);
114+
this.eventBus?.emitEvents(`change-time-range-${this.chartName}`, this.selectedTimeRange);
105115
}
106116
this.isManualBrushUpdate = true;
107117
})
@@ -113,7 +123,7 @@ class CFDRenderer extends UIControlsRenderer {
113123

114124
const brushArea = this.#createAreaGenerator(this.x, this.y.copy().range([this.focusHeight - this.margin.top, 4]));
115125
this.#drawStackedAreaChart(svgBrush, this.#stackedData, brushArea);
116-
this.changeTimeInterval(false, 'cfd');
126+
this.changeTimeInterval(false);
117127
this.drawXAxis(svgBrush.append('g'), this.x, this.focusHeight - this.margin.top);
118128
this.brushGroup = svgBrush.append('g');
119129
this.brushGroup.call(this.brush).call(
@@ -142,10 +152,10 @@ class CFDRenderer extends UIControlsRenderer {
142152
*/
143153
updateGraph(domain) {
144154
const maxY = d3.max(this.#stackedData[this.#stackedData.length - 1], (d) => (d.data.date <= domain[1] ? d[1] : -1));
145-
this.reportingRangeDays = calculateDaysBetweenDates(domain[0], domain[1]);
155+
this.reportingRangeDays = calculateDaysBetweenDates(domain[0], domain[1]).roundedDays;
146156
this.currentXScale = this.x.copy().domain(domain);
147157
this.currentYScale = this.y.copy().domain([0, maxY]).nice();
148-
this.changeTimeInterval(false, 'cfd');
158+
this.changeTimeInterval(false);
149159
this.drawXAxis(this.gx, this.currentXScale, this.height, true);
150160
this.drawYAxis(this.gy, this.currentYScale);
151161

@@ -181,8 +191,13 @@ class CFDRenderer extends UIControlsRenderer {
181191
* @private
182192
*/
183193
#drawArea() {
184-
this.chartArea = this.addClipPath(this.svg, 'cfd-clip');
185-
this.chartArea.append('rect').attr('width', '100%').attr('height', '100%').attr('id', 'cfd-area').attr('fill', 'transparent');
194+
this.chartArea = this.addClipPath(this.svg, `${this.chartName}-clip`);
195+
this.chartArea
196+
.append('rect')
197+
.attr('width', '100%')
198+
.attr('height', '100%')
199+
.attr('id', `${this.chartName}-area`)
200+
.attr('fill', 'transparent');
186201
const areaGenerator = this.#createAreaGenerator(this.x, this.y);
187202
this.#drawStackedAreaChart(this.chartArea, this.#stackedData, areaGenerator);
188203
this.#drawLegend();
@@ -289,7 +304,7 @@ class CFDRenderer extends UIControlsRenderer {
289304
*/
290305
setupXAxisControl() {
291306
this.gx.on('click', () => {
292-
this.changeTimeInterval(true, 'cfd');
307+
this.changeTimeInterval(true);
293308
this.drawXAxis(this.gx, this.x.copy().domain(this.selectedTimeRange), this.height, true);
294309
});
295310
}
@@ -327,7 +342,7 @@ class CFDRenderer extends UIControlsRenderer {
327342
*/
328343
drawXAxis(g, x, height = this.height, isGraph = false) {
329344
let axis;
330-
const clipId = 'cfd-x-axis-clip';
345+
const clipId = `${this.chartName}-x-axis-clip`;
331346
this.svg
332347
.append('clipPath')
333348
.attr('id', clipId)
@@ -371,7 +386,7 @@ class CFDRenderer extends UIControlsRenderer {
371386
* @param {Object} observations - Observations data for the renderer.
372387
*/
373388
setupObservationLogging(observations) {
374-
if (observations.length > 0) {
389+
if (observations.data.length > 0) {
375390
this.displayObservationMarkers(observations);
376391
this.enableMetrics();
377392
}
@@ -398,7 +413,7 @@ class CFDRenderer extends UIControlsRenderer {
398413
const trianglePath = `M${-triangleBase / 2},0 L${triangleBase / 2},0 L0,-${triangleHeight} Z`;
399414
this.chartArea
400415
.selectAll('observations')
401-
.data(observations?.data?.filter((d) => d.chart_type === 'CFD'))
416+
.data(observations?.data?.filter((d) => d.chart_type === this.chartType))
402417
.join('path')
403418
.attr('class', 'observation-marker')
404419
.attr('d', trianglePath)
@@ -448,7 +463,7 @@ class CFDRenderer extends UIControlsRenderer {
448463
this.tooltip = d3.select('body').append('div').attr('class', styles.tooltip).attr('id', 'c-tooltip').style('opacity', 0);
449464
this.cfdLine = this.chartArea
450465
.append('line')
451-
.attr('id', 'cfd-line')
466+
.attr('id', `${this.chartName}-line`)
452467
.attr('stroke', 'black')
453468
.attr('y1', 0)
454469
.attr('y2', y)
@@ -551,8 +566,8 @@ class CFDRenderer extends UIControlsRenderer {
551566
return; // Exit the function if metrics are already enabled
552567
}
553568
this.#areMetricsEnabled = true;
554-
this.chartArea.on('mousemove', (event) => this.#handleMouseEvent(event, 'cfd-mousemove'));
555-
this.chartArea.on('click', (event) => this.#handleMouseEvent(event, 'cfd-click'));
569+
this.chartArea.on('mousemove', (event) => this.#handleMouseEvent(event, `${this.chartName}-mousemove`));
570+
this.chartArea.on('click', (event) => this.#handleMouseEvent(event, `${this.chartName}-click`));
556571
this.#setupMouseLeaveHandler();
557572
}
558573

@@ -566,7 +581,7 @@ class CFDRenderer extends UIControlsRenderer {
566581
#handleMouseEvent(event, eventName) {
567582
if (this.#areMetricsEnabled) {
568583
this.#removeMetricsLines();
569-
const coords = d3.pointer(event, d3.select('#cfd-area').node());
584+
const coords = d3.pointer(event, d3.select(`#${this.chartName}-area`).node());
570585
const xPosition = coords[0];
571586
const yPosition = coords[1];
572587

@@ -577,12 +592,13 @@ class CFDRenderer extends UIControlsRenderer {
577592

578593
const date = this.currentXScale.invert(xPosition);
579594
const cumulativeCountOfWorkItems = this.currentYScale.invert(yPosition);
580-
const excludeCycleTime = eventName === 'scatterplot-mousemove';
595+
const excludeCycleTime = eventName.includes('mousemove') && !eventName.includes(this.chartName);
581596

582597
const metrics = this.computeMetrics(date, Math.floor(cumulativeCountOfWorkItems), excludeCycleTime);
598+
583599
this.#drawMetricLines(metrics.metricLinesData);
584600
delete metrics.metricLinesData;
585-
const observation = this.observations?.data?.find((o) => o.chart_type === 'CFD' && areDatesEqual(o.date_from, date));
601+
const observation = this.observations?.data?.find((o) => o.chart_type === this.chartType && areDatesEqual(o.date_from, date));
586602
const data = {
587603
date: date,
588604
lineX: xPosition,
@@ -624,13 +640,14 @@ class CFDRenderer extends UIControlsRenderer {
624640
const leadTimeDateBefore = this.#computeLeadTimeDate(currentDeliveredItems, filteredData);
625641
let { cycleTimeDateBefore, averageCycleTime, biggestCycleTime, currentStateCumulativeCount, cycleTimesByState } =
626642
this.computeCycleTimeAndLeadTimeMetrics(currentDataEntry, filteredData, currentDate, currentStateIndex);
627-
const averageLeadTime = leadTimeDateBefore ? Math.floor(calculateDaysBetweenDates(leadTimeDateBefore, currentDate)) : null;
643+
const averageLeadTime = leadTimeDateBefore
644+
? Math.floor(calculateDaysBetweenDates(leadTimeDateBefore, currentDate).roundedDays)
645+
: null;
628646
const noOfItemsBefore = this.#getNoOfItems(currentDataEntry, this.states[this.states.indexOf('delivered')]);
629647
const noOfItemsAfter = this.#getNoOfItems(currentDataEntry, this.states[this.states.indexOf('analysis_active')]);
630648

631649
const wip = noOfItemsAfter - noOfItemsBefore;
632650
const throughput = averageLeadTime ? parseFloat((wip / averageLeadTime).toFixed(1)) : undefined;
633-
634651
excludeCycleTime && (averageCycleTime = null);
635652
return {
636653
currentState: this.states[currentStateIndex],
@@ -664,7 +681,9 @@ class CFDRenderer extends UIControlsRenderer {
664681
for (let i = 0; i < this.states.length - 1; i++) {
665682
let stateCumulativeCount = this.#getNoOfItems(currentDataEntry, this.states[i]);
666683
let cycleTimeDate = this.#computeCycleTimeDate(stateCumulativeCount, i, filteredData);
667-
cycleTimesByState[this.states[i + 1]] = cycleTimeDate ? Math.floor(calculateDaysBetweenDates(cycleTimeDate, currentDate)) : null;
684+
cycleTimesByState[this.states[i + 1]] = cycleTimeDate
685+
? Math.floor(calculateDaysBetweenDates(cycleTimeDate, currentDate).roundedDays)
686+
: null;
668687
if (cycleTimesByState[this.states[i + 1]] > biggestCycleTime) {
669688
biggestCycleTime = cycleTimesByState[this.states[i + 1]];
670689
}

src/graphs/control-chart/ControlRenderer.js

Lines changed: 22 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -6,43 +6,45 @@ class ControlRenderer extends ScatterplotRenderer {
66
timeScale = 'linear';
77
connectDots = false;
88

9-
constructor(data, avgMovingRange) {
9+
constructor(data, avgMovingRange, chartName) {
1010
super(data);
11-
this.chartName = 'control';
11+
this.chartName = chartName;
1212
this.chartType = 'CONTROL';
1313
this.avgMovingRange = avgMovingRange;
1414
this.dotClass = 'control-dot';
1515
this.yAxisLabel = 'Days';
1616
}
1717

18-
setupEventBus(eventBus) {
19-
this.eventBus = eventBus;
20-
this.eventBus?.addEventListener('change-time-range-moving-range', this.updateBrushSelection.bind(this));
21-
}
22-
2318
renderGraph(graphElementSelector) {
2419
this.drawSvg(graphElementSelector);
2520
this.drawAxes();
21+
this.drawArea();
22+
this.computeGraphLimits();
23+
this.drawGraphLimits(this.y);
24+
this.setupMouseLeaveHandler();
25+
}
26+
27+
drawGraphLimits(yScale) {
28+
this.drawHorizontalLine(yScale, this.topLimit, 'purple', 'top-pb', `UPL=${this.topLimit}`);
29+
this.drawHorizontalLine(yScale, this.avgLeadTime, 'orange', 'mid-pb', `Avg=${this.avgLeadTime}`);
30+
if (this.bottomLimit > 0) {
31+
this.drawHorizontalLine(yScale, this.bottomLimit, 'purple', 'bottom-pb', `LPL=${this.bottomLimit}`);
32+
} else {
33+
console.warn('The bottom limit is:', this.bottomLimit);
34+
}
35+
}
2636

37+
computeGraphLimits() {
2738
this.avgLeadTime = this.getAvgLeadTime();
2839
this.topLimit = Math.ceil(this.avgLeadTime + this.avgMovingRange * 2.66);
2940

3041
this.bottomLimit = Math.ceil(this.avgLeadTime - this.avgMovingRange * 2.66);
31-
const maxY = this.y.domain()[1] > this.topLimit ? this.y.domain()[1] : this.topLimit + 2;
42+
const maxY = this.y.domain()[1] > this.topLimit ? this.y.domain()[1] : this.topLimit + 5;
3243
let minY = this.y.domain()[0];
33-
if (this.bottomLimit > 0) {
34-
minY = this.y.domain()[0] < this.bottomLimit ? this.y.domain()[0] : this.bottomLimit - 2;
44+
if (this.bottomLimit > 5) {
45+
minY = this.y.domain()[0] < this.bottomLimit ? this.y.domain()[0] : this.bottomLimit - 5;
3546
}
3647
this.y.domain([minY, maxY]);
37-
this.drawArea();
38-
this.drawHorizontalLine(this.y, this.topLimit, 'purple', 'top');
39-
this.drawHorizontalLine(this.y, this.avgLeadTime, 'orange', 'center');
40-
if (this.bottomLimit > 0) {
41-
this.drawHorizontalLine(this.y, this.bottomLimit, 'purple', 'bottom');
42-
} else {
43-
console.warn('The bottom limit is:', this.bottomLimit);
44-
}
45-
this.setupMouseLeaveHandler();
4648
}
4749

4850
drawScatterplot(chartArea, data, x, y) {
@@ -95,9 +97,7 @@ class ControlRenderer extends ScatterplotRenderer {
9597
.y((d) => this.applyYScale(this.currentYScale, d.leadTime));
9698
this.chartArea.selectAll('.dot-line').attr('d', line);
9799
}
98-
this.drawHorizontalLine(this.currentYScale, this.topLimit, 'purple', 'top');
99-
this.drawHorizontalLine(this.currentYScale, this.avgLeadTime, 'orange', 'center');
100-
this.bottomLimit > 0 && this.drawHorizontalLine(this.currentYScale, this.bottomLimit, 'purple', 'bottom');
100+
this.drawGraphLimits(this.currentYScale);
101101
this.displayObservationMarkers(this.observations);
102102
}
103103
}

0 commit comments

Comments
 (0)