@@ -6,63 +6,187 @@ export class ControlRenderer extends ScatterplotRenderer {
6
6
timeScale = 'logarithmic' ;
7
7
connectDots = false ;
8
8
9
- constructor ( data , avgMovingRangeFunc , chartName , workTicketsURL ) {
9
+ constructor ( data , chartName , workTicketsURL ) {
10
10
super ( data ) ;
11
11
this . chartName = chartName ;
12
12
this . chartType = 'CONTROL' ;
13
13
this . workTicketsURL = workTicketsURL ;
14
- this . avgMovingRangeFunc = avgMovingRangeFunc ;
15
14
this . dotClass = 'control-dot' ;
16
15
this . yAxisLabel = 'Days' ;
16
+ this . limitData = { } ;
17
+ this . processSignalsData = { } ;
18
+ this . visibleLimits = { } ;
19
+ this . activeProcessSignal = null ;
17
20
}
18
21
19
- renderGraph ( graphElementSelector ) {
20
- this . drawSvg ( graphElementSelector ) ;
21
- this . drawAxes ( ) ;
22
- this . drawArea ( ) ;
23
- this . computeGraphLimits ( ) ;
24
- this . drawGraphLimits ( this . y ) ;
25
- this . setupMouseLeaveHandler ( ) ;
22
+ setLimitData ( limitData ) {
23
+ this . limitData = {
24
+ naturalProcessLimits : limitData ?. naturalProcessLimits || null ,
25
+ twoSigma : limitData ?. twoSigma || null ,
26
+ oneSigma : limitData ?. oneSigma || null ,
27
+ averageCycleTime : limitData ?. averageCycleTime || null ,
28
+ } ;
29
+ this . topLimit = limitData ?. naturalProcessLimits ?. upper ;
30
+ this . drawLimits ( ) ;
31
+ }
32
+
33
+ setProcessSignalsData ( signalsData ) {
34
+ this . processSignalsData = {
35
+ largeChange : signalsData ?. largeChange || null ,
36
+ moderateChange : signalsData ?. moderateChange || null ,
37
+ moderateSustainedShift : signalsData ?. moderateSustainedShift || null ,
38
+ smallSustainedShift : signalsData ?. smallSustainedShift || null ,
39
+ } ;
26
40
}
27
41
28
- drawGraphLimits ( yScale ) {
29
- this . drawHorizontalLine ( yScale , this . topLimit , 'purple' , 'top-pb' , `UPL=${ this . topLimit } ` ) ;
30
- this . drawHorizontalLine ( yScale , this . avgLeadTime , 'orange' , 'mid-pb' , `Avg=${ this . avgLeadTime } ` ) ;
42
+ setVisibleLimits ( limitConfig ) {
43
+ this . visibleLimits = { ...limitConfig } ;
44
+ this . updateLimitVisibility ( ) ;
45
+ }
31
46
32
- if ( this . bottomLimit > 0 ) {
33
- this . drawHorizontalLine ( yScale , this . bottomLimit , 'purple' , 'bottom-pb' , `LPL=${ this . bottomLimit } ` ) ;
34
- } else {
35
- console . warn ( 'The bottom limit is:' , this . bottomLimit ) ;
47
+ setActiveProcessSignal ( signalType ) {
48
+ this . hideSignals ( ) ;
49
+ this . activeProcessSignal = signalType ;
50
+ this . showActiveSignal ( ) ;
51
+ }
52
+
53
+ drawLimits ( ) {
54
+ // Remove existing limits first
55
+ this . svg . selectAll ( '[id^="line-"], [id^="text-"]' ) . remove ( ) ;
56
+
57
+ // Draw new limits
58
+ Object . entries ( this . limitData ) . forEach ( ( [ limitType , limitValue ] ) => {
59
+ if ( limitValue ) {
60
+ this . drawLimit ( limitType , limitValue ) ;
61
+ }
62
+ } ) ;
63
+
64
+ this . updateLimitVisibility ( ) ;
65
+ }
66
+
67
+ drawLimit ( limitType , limitValue ) {
68
+ const limitConfig = {
69
+ naturalProcessLimits : { dash : '3 2' , text : 'NPL' , color : 'orange' } ,
70
+ twoSigma : { dash : '12 8' , text : '2s' , color : 'orange' } ,
71
+ oneSigma : { dash : '20 10' , text : '1s' , color : 'orange' } ,
72
+ averageCycleTime : { dash : '7' , text : 'Avg' , color : 'purple' } ,
73
+ } ;
74
+
75
+ const config = limitConfig [ limitType ] ;
76
+ if ( ! config ) return ;
77
+
78
+ if ( typeof limitValue === 'number' ) {
79
+ this . drawHorizontalLine ( this . currentYScale , limitValue , config . color , limitType , `${ config . text } =${ limitValue } ` , config . dash ) ;
80
+ } else if ( limitValue && typeof limitValue === 'object' ) {
81
+ if ( limitValue . upper !== undefined ) {
82
+ this . drawHorizontalLine (
83
+ this . currentYScale ,
84
+ limitValue . upper ,
85
+ config . color ,
86
+ `${ limitType } -upper` ,
87
+ `${ config . text } U=${ limitValue . upper } ` ,
88
+ config . dash
89
+ ) ;
90
+ }
91
+ if ( limitValue . lower !== undefined && limitValue . lower > 0 ) {
92
+ this . drawHorizontalLine (
93
+ this . currentYScale ,
94
+ limitValue . lower ,
95
+ config . color ,
96
+ `${ limitType } -lower` ,
97
+ `${ config . text } L=${ limitValue . lower } ` ,
98
+ config . dash
99
+ ) ;
100
+ }
101
+ }
102
+ }
103
+
104
+ updateLimitVisibility ( ) {
105
+ Object . entries ( this . visibleLimits ) . forEach ( ( [ limitType , isVisible ] ) => {
106
+ const display = isVisible ? 'block' : 'none' ;
107
+ // Handle both single limits and upper/lower pairs
108
+ this . svg . select ( `#line-${ limitType } ` ) . style ( 'display' , display ) ;
109
+ this . svg . select ( `#text-${ limitType } ` ) . style ( 'display' , display ) ;
110
+ this . svg . select ( `#line-${ limitType } -upper` ) . style ( 'display' , display ) ;
111
+ this . svg . select ( `#text-${ limitType } -upper` ) . style ( 'display' , display ) ;
112
+ this . svg . select ( `#line-${ limitType } -lower` ) . style ( 'display' , display ) ;
113
+ this . svg . select ( `#text-${ limitType } -lower` ) . style ( 'display' , display ) ;
114
+ } ) ;
115
+ }
116
+
117
+ showActiveSignal ( ) {
118
+ if ( ! this . activeProcessSignal || ! this . processSignalsData [ this . activeProcessSignal ] ) {
119
+ return ;
36
120
}
121
+
122
+ const signals = this . processSignalsData [ this . activeProcessSignal ] ;
123
+ this . drawSignals ( signals ) ;
37
124
}
38
125
39
- computeGraphLimits ( ) {
40
- this . avgLeadTime = this . getAvgLeadTime ( ) ;
41
- const avgMovingRange = this . avgMovingRangeFunc ( this . baselineStartDate , this . baselineEndDate ) ;
42
- this . topLimit = Math . ceil ( this . avgLeadTime + avgMovingRange * 2.66 ) ;
126
+ hideSignals ( ) {
127
+ this . svg . selectAll ( '.signal-point' ) . classed ( 'signal-point' , false ) . attr ( 'fill' , this . color ) ;
128
+ }
43
129
44
- this . bottomLimit = Math . ceil ( this . avgLeadTime - avgMovingRange * 2.66 ) ;
45
- const maxY = this . y . domain ( ) [ 1 ] > this . topLimit ? this . y . domain ( ) [ 1 ] : this . topLimit + 5 ;
46
- let minY = this . y . domain ( ) [ 0 ] ;
47
- if ( this . bottomLimit > 5 ) {
48
- minY = this . y . domain ( ) [ 0 ] < this . bottomLimit ? this . y . domain ( ) [ 0 ] : this . bottomLimit - 5 ;
130
+ drawSignals ( signals ) {
131
+ if ( signals . upper && signals . lower ) {
132
+ [ ... signals . upper , ... signals . lower ] . forEach ( ( id ) => {
133
+ this . svg . select ( `#control- ${ id } ` ) . classed ( 'signal-point' , true ) . transition ( ) . duration ( 200 ) . attr ( 'fill' , 'orange' ) ;
134
+ } ) ;
49
135
}
50
- this . y . domain ( [ minY , maxY ] ) ;
136
+ }
137
+
138
+ renderGraph ( graphElementSelector ) {
139
+ this . drawSvg ( graphElementSelector ) ;
140
+ this . drawAxes ( ) ;
141
+ this . drawArea ( ) ;
142
+ this . drawLimits ( ) ;
143
+ this . showActiveSignal ( ) ;
51
144
}
52
145
53
146
populateTooltip ( event ) {
54
- this . tooltip
55
- . style ( 'pointer-events' , 'auto' )
56
- . style ( 'opacity' , 0.9 )
57
- . append ( 'div' )
58
- . append ( 'a' )
59
- . style ( 'text-decoration' , 'underline' )
60
- . attr ( 'href' , `${ this . workTicketsURL } /${ event . ticketId } ` )
61
- . text ( event . ticketId )
62
- . attr ( 'target' , '_blank' )
63
- . on ( 'click' , ( ) => {
64
- this . hideTooltip ( ) ;
147
+ this . tooltip . style ( 'pointer-events' , 'auto' ) . style ( 'opacity' , 0.9 ) ;
148
+
149
+ if ( event . overlappingTickets && event . overlappingTickets . length > 1 ) {
150
+ // Add header for multiple tickets
151
+ this . tooltip
152
+ . append ( 'div' )
153
+ . style ( 'font-weight' , 'bold' )
154
+ . style ( 'margin-bottom' , '8px' )
155
+ . text ( `${ event . overlappingTickets . length } tickets at this point:` ) ;
156
+
157
+ event . overlappingTickets . forEach ( ( ticket ) => {
158
+ const ticketDiv = this . tooltip . append ( 'div' ) . style ( 'margin-bottom' , '4px' ) ;
159
+
160
+ ticketDiv
161
+ . append ( 'a' )
162
+ . style ( 'text-decoration' , 'underline' )
163
+ . attr ( 'href' , `${ this . workTicketsURL } /${ ticket . ticketId } ` )
164
+ . text ( ticket . ticketId )
165
+ . attr ( 'target' , '_blank' )
166
+ . on ( 'click' , ( ) => {
167
+ this . hideTooltip ( ) ;
168
+ } ) ;
65
169
} ) ;
170
+
171
+ // Optionally add shared information (date, lead time)
172
+ if ( event . date && event . metrics ) {
173
+ this . tooltip . append ( 'div' ) . style ( 'margin-top' , '8px' ) . style ( 'font-size' , '12px' ) . style ( 'color' , '#666' ) . html ( `
174
+ <div><strong>Date:</strong> ${ event . date } </div>
175
+ <div><strong>Lead Time:</strong> ${ event . metrics . leadTime } days</div>
176
+ ` ) ;
177
+ }
178
+ } else {
179
+ this . tooltip
180
+ . append ( 'div' )
181
+ . append ( 'a' )
182
+ . style ( 'text-decoration' , 'underline' )
183
+ . attr ( 'href' , `${ this . workTicketsURL } /${ event . ticketId } ` )
184
+ . text ( event . ticketId )
185
+ . attr ( 'target' , '_blank' )
186
+ . on ( 'click' , ( ) => {
187
+ this . hideTooltip ( ) ;
188
+ } ) ;
189
+ }
66
190
}
67
191
68
192
drawScatterplot ( chartArea , data , x , y ) {
@@ -74,7 +198,12 @@ export class ControlRenderer extends ScatterplotRenderer {
74
198
. attr ( 'class' , this . dotClass )
75
199
. attr ( 'id' , ( d ) => `control-${ d . ticketId } ` )
76
200
. attr ( 'data-date' , ( d ) => d . deliveredDate )
77
- . attr ( 'r' , 5 )
201
+ . attr ( 'r' , ( d ) => {
202
+ const overlapping = data . filter (
203
+ ( item ) => item . deliveredDate . getTime ( ) === d . deliveredDate . getTime ( ) && item . leadTime === d . leadTime
204
+ ) ;
205
+ return overlapping . length > 1 ? 7 : 5 ;
206
+ } )
78
207
. attr ( 'cx' , ( d ) => x ( d . deliveredDate ) )
79
208
. attr ( 'cy' , ( d ) => this . applyYScale ( y , d . leadTime ) )
80
209
. style ( 'cursor' , 'pointer' )
@@ -83,11 +212,6 @@ export class ControlRenderer extends ScatterplotRenderer {
83
212
this . connectDots && this . generateLines ( chartArea , data , x , y ) ;
84
213
}
85
214
86
- getAvgLeadTime ( ) {
87
- const filteredData = this . data . filter ( ( d ) => d . deliveredDate >= this . baselineStartDate && d . deliveredDate <= this . baselineEndDate ) ;
88
- return Math . ceil ( filteredData . reduce ( ( acc , curr ) => acc + curr . leadTime , 0 ) / filteredData . length ) ;
89
- }
90
-
91
215
generateLines ( chartArea , data , x , y ) {
92
216
// Define the line generator
93
217
const line = d3
@@ -108,7 +232,6 @@ export class ControlRenderer extends ScatterplotRenderer {
108
232
}
109
233
110
234
updateGraph ( domain ) {
111
- this . computeGraphLimits ( ) ;
112
235
this . updateChartArea ( domain ) ;
113
236
if ( this . connectDots ) {
114
237
const line = d3
@@ -117,7 +240,15 @@ export class ControlRenderer extends ScatterplotRenderer {
117
240
. y ( ( d ) => this . applyYScale ( this . currentYScale , d . leadTime ) ) ;
118
241
this . chartArea . selectAll ( '.dot-line' ) . attr ( 'd' , line ) ;
119
242
}
120
- this . drawGraphLimits ( this . currentYScale ) ;
243
+ this . drawLimits ( ) ;
244
+ this . showActiveSignal ( ) ;
121
245
this . displayObservationMarkers ( this . observations ) ;
122
246
}
247
+
248
+ cleanup ( ) {
249
+ this . limitData = { } ;
250
+ this . processSignalsData = { } ;
251
+ this . visibleLimits = { } ;
252
+ this . activeProcessSignal = null ;
253
+ }
123
254
}
0 commit comments