Skip to content

Commit fc29d1e

Browse files
authored
Merge pull request #65 from pfizer-opensource/percentile-lines-for-work-item-age-chart
Add percentile lines for the Work Item Age chart
2 parents 2b2bec0 + 2041792 commit fc29d1e

File tree

1 file changed

+75
-7
lines changed

1 file changed

+75
-7
lines changed

src/graphs/work-item-age/WorkItemAgeRenderer.js

Lines changed: 75 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ class WorkItemAgeRenderer extends Renderer {
1818
super(filteredData);
1919
this.states = states.filter((d) => d !== 'delivered');
2020
this.data = this.groupData(filteredData);
21+
this.groupedData = this.groupData(filteredData);
2122
this.workTicketsURL = workTicketsURL;
2223
this.chartType = 'WORK_ITEM_AGE';
2324
}
@@ -44,6 +45,7 @@ class WorkItemAgeRenderer extends Renderer {
4445
this.drawSvg(graphElementSelector);
4546
this.drawAxes();
4647
this.drawArea();
48+
this.drawPercentileLines(this.data, this.y);
4749
}
4850

4951
drawSvg(graphElementSelector) {
@@ -70,7 +72,7 @@ class WorkItemAgeRenderer extends Renderer {
7072
// Draw dots
7173
this.svg
7274
.selectAll('.dot')
73-
.data(this.data)
75+
.data(this.groupedData)
7476
.enter()
7577
.append('circle')
7678
.attr('class', 'dot')
@@ -85,7 +87,7 @@ class WorkItemAgeRenderer extends Renderer {
8587
// Add numbers inside the dots
8688
this.svg
8789
.selectAll('.dot-label')
88-
.data(this.data)
90+
.data(this.groupedData)
8991
.enter()
9092
.append('text')
9193
.attr('class', 'dot-label')
@@ -101,7 +103,7 @@ class WorkItemAgeRenderer extends Renderer {
101103
}
102104

103105
computeDotPositions() {
104-
const groupedData = d3.group(this.data, (d) => d.currentState);
106+
const groupedData = d3.group(this.groupedData, (d) => d.currentState);
105107

106108
// Generate x positions for dots within each state
107109
const stateWidth = this.x.bandwidth();
@@ -139,7 +141,7 @@ class WorkItemAgeRenderer extends Renderer {
139141

140142
updateChartArea() {
141143
this.drawYAxis(this.gy, this.y);
142-
this.computeDotPositions();
144+
// this.computeDotPositions();
143145
this.svg
144146
.selectAll(`.dot`)
145147
.attr('cx', (d) => d.xJitter)
@@ -148,6 +150,8 @@ class WorkItemAgeRenderer extends Renderer {
148150
.selectAll(`.dot-label`)
149151
.attr('x', (d) => d.xJitter)
150152
.attr('y', (d) => this.y(d.age));
153+
this.displayObservationMarkers(this.observations);
154+
this.drawPercentileLines(this.data, this.y);
151155
}
152156

153157
drawAxes() {
@@ -164,12 +168,12 @@ class WorkItemAgeRenderer extends Renderer {
164168
if (this.timeScale === 'logarithmic') {
165169
this.y = d3
166170
.scaleLog()
167-
.domain([1, d3.max(this.data, (d) => d.age)])
171+
.domain([1, d3.max(this.groupedData, (d) => d.age)])
168172
.range([this.height, 0]);
169173
} else if (this.timeScale === 'linear') {
170174
this.y = d3
171175
.scaleLinear()
172-
.domain([0, d3.max(this.data, (d) => d.age)])
176+
.domain([0, d3.max(this.groupedData, (d) => d.age)])
173177
.range([this.height, 0]);
174178
}
175179
}
@@ -282,6 +286,7 @@ class WorkItemAgeRenderer extends Renderer {
282286

283287
setupObservationLogging(observations) {
284288
if (observations?.data?.length > 0) {
289+
this.observations = observations;
285290
this.displayObservationMarkers(observations);
286291
}
287292
}
@@ -302,7 +307,7 @@ class WorkItemAgeRenderer extends Renderer {
302307
this.svg
303308
.selectAll('ring')
304309
.data(
305-
this.data.filter((d) =>
310+
this.groupedData.filter((d) =>
306311
this.observations?.data?.some(
307312
(o) => d.items.find((i) => i.ticketId === o.work_item.toString()) && o.chart_type === this.chartType
308313
)
@@ -318,6 +323,69 @@ class WorkItemAgeRenderer extends Renderer {
318323
.attr('stroke', 'black')
319324
.attr('stroke-width', '2px');
320325
}
326+
327+
//region Percentile lines rendering
328+
329+
computePercentileLine(data, percent) {
330+
const percentileIndex = Math.floor(data.length * percent);
331+
return data[percentileIndex]?.age;
332+
}
333+
334+
drawPercentileLines(data, y) {
335+
console.log('drawPercentileLines');
336+
const dataSortedByAge = [...data].sort((a, b) => a.age - b.age);
337+
console.log('dataSortedByAge');
338+
console.table(dataSortedByAge);
339+
const percentile1 = this.computePercentileLine(dataSortedByAge, 0.5);
340+
const percentile2 = this.computePercentileLine(dataSortedByAge, 0.75);
341+
const percentile3 = this.computePercentileLine(dataSortedByAge, 0.85);
342+
343+
percentile1 && this.drawHorizontalLine(y, percentile1, 'green', 'p1', '50%');
344+
percentile1 && this.drawHorizontalLine(y, percentile2, 'orange', 'p2', '75%');
345+
percentile1 && this.drawHorizontalLine(y, percentile3, 'red', 'p3', '85%');
346+
}
347+
348+
drawHorizontalLine(yScale, yValue, color, id, text = '') {
349+
let lineEl = this.svg.select('#line-' + id);
350+
let textEl = this.svg.select('#text-' + id);
351+
352+
if (lineEl.empty()) {
353+
lineEl = this.svg
354+
.append('line')
355+
.attr('x1', 0)
356+
.attr('x2', this.width)
357+
.attr('id', 'line-' + id)
358+
.attr('class', 'average-line')
359+
.attr('stroke-width', 3)
360+
.attr('stroke-dasharray', '7');
361+
textEl = this.svg
362+
.append('text')
363+
.attr('text-anchor', 'start')
364+
.attr('id', 'text-' + id)
365+
.style('font-size', '12px');
366+
}
367+
lineEl.attr('y1', yScale(yValue)).attr('y2', yScale(yValue)).attr('stroke', color);
368+
if (text) {
369+
textEl
370+
.text(text)
371+
.attr('fill', color)
372+
.attr('y', yScale(yValue) - 4);
373+
// Measure text width
374+
const textWidth = this.#getTextWidth(text, '12px');
375+
const adjustedX = this.width - textWidth;
376+
textEl.attr('x', adjustedX);
377+
}
378+
}
379+
380+
#getTextWidth(text, fontSize = '12px', fontFamily = 'Arial') {
381+
const canvas = document.createElement('canvas');
382+
const context = canvas.getContext('2d');
383+
context.font = `${fontSize} ${fontFamily}`;
384+
const width = context.measureText(text).width;
385+
return width;
386+
}
387+
388+
//endregion
321389
}
322390

323391
export default WorkItemAgeRenderer;

0 commit comments

Comments
 (0)