@@ -67,6 +67,7 @@ Ext4.define('LABKEY.targetedms.QCTrendPlotPanel', {
6767 startDate : null ,
6868 endDate : null ,
6969 groupedX : false ,
70+ calendarX : false ,
7071 singlePlot : false ,
7172 showDataPoints : false ,
7273 showExpRunRange : false ,
@@ -85,6 +86,8 @@ Ext4.define('LABKEY.targetedms.QCTrendPlotPanel', {
8586
8687 SHOW_ALL_IN_A_SINGLE_PLOT : 'Show all series in a single plot' ,
8788 LABEL_WIDTH : 115 ,
89+ // Shared column widths (px) so the Excluded-replicates radios line up under the X-axis grouping radios.
90+ XAXIS_COL_WIDTHS : [ 100 , 82 , 80 ] ,
8891
8992 // Max number of plots/series to show per page
9093 maxCount : 50 ,
@@ -227,6 +230,11 @@ Ext4.define('LABKEY.targetedms.QCTrendPlotPanel', {
227230 // apply the initial values to the panel object so they are used in form field initialization
228231 Ext4 . apply ( this , initValues ) ;
229232
233+ // calendar grouping is a variant of date grouping, so it always implies groupedX
234+ if ( this . calendarX ) {
235+ this . groupedX = true ;
236+ }
237+
230238 // if we have a dateRangeOffset, we need to calculate the start and end date
231239 if ( this . dateRangeOffset > - 1 ) {
232240 this . startDate = this . formatDate ( this . calculateStartDateByOffset ( ) ) ;
@@ -1013,28 +1021,40 @@ Ext4.define('LABKEY.targetedms.QCTrendPlotPanel', {
10131021
10141022 getGroupedXRadioGroup : function ( ) {
10151023 if ( ! this . groupedXRadioGroup ) {
1016- this . groupedXRadioGroup = Ext4 . create ( 'Ext.form.RadioGroup' , {
1024+ const me = this ;
1025+ const onChange = function ( radio , checked ) {
1026+ if ( ! checked ) {
1027+ return ;
1028+ }
1029+ const val = radio . inputValue ;
1030+ const newCalendarX = val === 'calendar' ;
1031+ const newGroupedX = val === 'date' || val === 'calendar' ;
1032+ // ignore the change event fired for the initially-checked radio during construction
1033+ if ( newCalendarX === me . calendarX && newGroupedX === me . groupedX ) {
1034+ return ;
1035+ }
1036+ me . calendarX = newCalendarX ;
1037+ me . groupedX = newGroupedX ;
1038+ me . havePlotOptionsChanged = true ;
1039+
1040+ me . setBrushingEnabled ( false ) ;
1041+ me . layoutAnnotationData ( ) ;
1042+ me . setLoadingMsg ( ) ;
1043+ me . processPlotData ( ) ;
1044+ } ;
1045+
1046+ const colWidths = this . XAXIS_COL_WIDTHS ;
1047+ this . groupedXRadioGroup = Ext4 . create ( 'Ext.form.FieldContainer' , {
10171048 id : 'grouped-x-field' ,
10181049 fieldLabel : 'X-axis grouping' ,
10191050 labelWidth : this . LABEL_WIDTH ,
1020- columns : 2 ,
1021- vertical : false ,
1051+ layout : { type : 'hbox' } ,
1052+ defaults : { xtype : 'radio' , name : 'xAxisGrouping' , listeners : { change : onChange } } ,
10221053 items : [
1023- { boxLabel : 'per replicate' , id : 'x-axis-grouping-replicate' , name : 'xAxisGrouping' , inputValue : 'replicate' , checked : this . groupedX === false } ,
1024- { boxLabel : 'per date' , id : 'x-axis-grouping-date' , name : 'xAxisGrouping' , inputValue : 'date' , checked : this . groupedX === true }
1025- ] ,
1026- listeners : {
1027- scope : this ,
1028- change : function ( group , newValue ) {
1029- var val = newValue && ( newValue . xAxisGrouping || newValue [ 'xAxisGrouping' ] ) ;
1030- var groupByDate = val === 'date' || ( val === true ) ; // fallback safety
1031- this . groupedX = groupByDate ;
1032- this . havePlotOptionsChanged = true ;
1033-
1034- this . setBrushingEnabled ( false ) ;
1035- this . getAnnotationData ( ) ;
1036- }
1037- }
1054+ { boxLabel : 'per replicate' , width : colWidths [ 0 ] , id : 'x-axis-grouping-replicate' , inputValue : 'replicate' , checked : this . groupedX === false } ,
1055+ { boxLabel : 'per date' , width : colWidths [ 1 ] , id : 'x-axis-grouping-date' , inputValue : 'date' , checked : this . groupedX === true && this . calendarX !== true } ,
1056+ { boxLabel : 'calendar' , width : colWidths [ 2 ] , id : 'x-axis-grouping-calendar' , inputValue : 'calendar' , checked : this . groupedX === true && this . calendarX === true }
1057+ ]
10381058 } ) ;
10391059 }
10401060
@@ -1074,27 +1094,33 @@ Ext4.define('LABKEY.targetedms.QCTrendPlotPanel', {
10741094
10751095 getExcludedReplicatesRadioGroup : function ( ) {
10761096 if ( ! this . excludedReplicatesRadioGroup ) {
1077- this . excludedReplicatesRadioGroup = Ext4 . create ( 'Ext.form.RadioGroup' , {
1097+ const me = this ;
1098+ const onChange = function ( radio , checked ) {
1099+ if ( ! checked ) {
1100+ return ;
1101+ }
1102+ const newShow = radio . inputValue === 'show' ;
1103+ // ignore the change event fired for the initially-checked radio during construction
1104+ if ( newShow === me . showExcluded ) {
1105+ return ;
1106+ }
1107+ me . showExcluded = newShow ;
1108+ me . havePlotOptionsChanged = true ;
1109+
1110+ me . getAnnotationData ( ) ;
1111+ } ;
1112+
1113+ const colWidths = this . XAXIS_COL_WIDTHS ;
1114+ this . excludedReplicatesRadioGroup = Ext4 . create ( 'Ext.form.FieldContainer' , {
10781115 id : 'show-excluded-points' ,
10791116 fieldLabel : 'Excluded replicates' ,
10801117 labelWidth : this . LABEL_WIDTH ,
1081- columns : 2 ,
1082- vertical : false ,
1118+ layout : { type : 'hbox' } ,
1119+ defaults : { xtype : 'radio' , name : 'excludedSamples' , listeners : { change : onChange } } ,
10831120 items : [
1084- { boxLabel : 'show' , id : 'excluded-replicates-show' , name : 'excludedSamples' , inputValue : 'show' , checked : this . showExcluded === true } ,
1085- { boxLabel : 'hide' , id : 'excluded-replicates-hide' , name : 'excludedSamples' , inputValue : 'hide' , checked : this . showExcluded === false }
1086- ] ,
1087- listeners : {
1088- scope : this ,
1089- change : function ( group , newValue ) {
1090- var val = newValue && ( newValue . excludedSamples || newValue [ 'excludedSamples' ] ) ;
1091- var newShow = val === 'show' || ( val === true ) ; // fallback safety
1092- this . showExcluded = newShow ;
1093- this . havePlotOptionsChanged = true ;
1094-
1095- this . getAnnotationData ( ) ;
1096- }
1097- }
1121+ { boxLabel : 'show' , width : colWidths [ 0 ] , id : 'excluded-replicates-show' , inputValue : 'show' , checked : this . showExcluded === true } ,
1122+ { boxLabel : 'hide' , width : colWidths [ 1 ] , id : 'excluded-replicates-hide' , inputValue : 'hide' , checked : this . showExcluded === false }
1123+ ]
10981124 } ) ;
10991125 }
11001126
@@ -1424,9 +1450,6 @@ Ext4.define('LABKEY.targetedms.QCTrendPlotPanel', {
14241450
14251451 processAnnotationData : function ( data ) {
14261452 if ( data ) {
1427- this . annotationShape = LABKEY . vis . Scale . Shape ( ) [ 4 ] ; // 0: circle, 1: triangle, 2: square, 3: diamond, 4: X
1428- this . legendData = [ ] ;
1429-
14301453 const collapsedData = [ ] ;
14311454 const collapsedMap = { } ;
14321455
@@ -1445,38 +1468,49 @@ Ext4.define('LABKEY.targetedms.QCTrendPlotPanel', {
14451468
14461469 this . annotationData = collapsedData ;
14471470
1448- var dateCount = { } ;
1471+ this . layoutAnnotationData ( ) ;
1472+ this . getPlotsData ( ) ;
1473+ }
1474+ } ,
14491475
1450- // if more than one type of legend present, add a legend header for annotations
1451- if ( this . annotationData . length > 0 && ( this . singlePlot || this . showMeanCUSUMPlot ( ) || this . showVariableCUSUMPlot ( ) ) ) {
1452- this . legendData . push ( {
1453- text : 'Annotations' ,
1454- separator : true
1455- } ) ;
1456- }
1476+ // Compute annotation stacking and the annotation legend entries
1477+ layoutAnnotationData : function ( ) {
1478+ this . annotationShape = LABKEY . vis . Scale . Shape ( ) [ 4 ] ; // 0: circle, 1: triangle, 2: square, 3: diamond, 4: X
1479+ this . legendData = [ ] ;
14571480
1458- for ( let i = 0 ; i < this . annotationData . length ; i ++ ) {
1459- const annotation = this . annotationData [ i ] ;
1460- const annotationDate = this . formatDate ( new Date ( annotation [ 'Date' ] ) , ! this . groupedX ) ;
1481+ if ( ! this . annotationData ) {
1482+ return ;
1483+ }
14611484
1462- // track if we need to stack annotations that fall on the same date
1463- if ( ! dateCount [ annotationDate ] ) {
1464- dateCount [ annotationDate ] = 0 ;
1465- }
1466- annotation . yStepIndex = dateCount [ annotationDate ] ;
1467- dateCount [ annotationDate ] ++ ;
1468-
1469- // get unique annotation names and colors for the legend
1470- if ( Ext4 . Array . pluck ( this . legendData , "text" ) . indexOf ( annotation [ 'Name' ] ) === - 1 ) {
1471- this . legendData . push ( {
1472- text : annotation [ 'Name' ] ,
1473- color : '#' + annotation [ 'Color' ] ,
1474- shape : this . annotationShape
1475- } ) ;
1476- }
1485+ const dateCount = { } ;
1486+
1487+ // if more than one type of legend present, add a legend header for annotations
1488+ if ( this . annotationData . length > 0 && ( this . singlePlot || this . showMeanCUSUMPlot ( ) || this . showVariableCUSUMPlot ( ) ) ) {
1489+ this . legendData . push ( {
1490+ text : 'Annotations' ,
1491+ separator : true
1492+ } ) ;
1493+ }
1494+
1495+ for ( let i = 0 ; i < this . annotationData . length ; i ++ ) {
1496+ const annotation = this . annotationData [ i ] ;
1497+ const annotationDate = this . formatDate ( new Date ( annotation [ 'Date' ] ) , ! this . groupedX ) ;
1498+
1499+ // track if we need to stack annotations that fall on the same date
1500+ if ( ! dateCount [ annotationDate ] ) {
1501+ dateCount [ annotationDate ] = 0 ;
14771502 }
1503+ annotation . yStepIndex = dateCount [ annotationDate ] ;
1504+ dateCount [ annotationDate ] ++ ;
14781505
1479- this . getPlotsData ( ) ;
1506+ // get unique annotation names and colors for the legend
1507+ if ( Ext4 . Array . pluck ( this . legendData , "text" ) . indexOf ( annotation [ 'Name' ] ) === - 1 ) {
1508+ this . legendData . push ( {
1509+ text : annotation [ 'Name' ] ,
1510+ color : '#' + annotation [ 'Color' ] ,
1511+ shape : this . annotationShape
1512+ } ) ;
1513+ }
14801514 }
14811515 } ,
14821516
@@ -2227,6 +2261,23 @@ Ext4.define('LABKEY.targetedms.QCTrendPlotPanel', {
22272261 return d3 . select ( '#' + plot . renderTo + ' svg' ) ;
22282262 } ,
22292263
2264+ // Pixel width of one x-axis slot, used to size guide-set/outlier highlight rectangles. For calendar
2265+ // mode (continuous axis) this is the plot width over the distinct-day count; otherwise inter-tick spacing.
2266+ getXBinWidth : function ( plot ) {
2267+ if ( this . calendarX ) {
2268+ const seen = { } ;
2269+ let count = 0 ;
2270+ Ext4 . each ( plot . data || [ ] , function ( d ) {
2271+ if ( d && d . seqValue !== undefined && ! seen [ d . seqValue ] ) {
2272+ seen [ d . seqValue ] = true ;
2273+ count ++ ;
2274+ }
2275+ } ) ;
2276+ return ( plot . grid . rightEdge - plot . grid . leftEdge ) / Math . max ( count , 10 ) ;
2277+ }
2278+ return ( plot . grid . rightEdge - plot . grid . leftEdge ) / ( plot . scales . x . scale . domain ( ) . length ) ;
2279+ } ,
2280+
22302281 toggleGuideSetMsgDisplay : function ( ) {
22312282 var toolbarMsg = this . down ( '#GuideSetMessageToolBar' ) ;
22322283 if ( toolbarMsg ) {
@@ -2238,7 +2289,7 @@ Ext4.define('LABKEY.targetedms.QCTrendPlotPanel', {
22382289 // for each precursor in precursorInfo
22392290 let me = this ;
22402291
2241- let binWidth = ( plot . grid . rightEdge - plot . grid . leftEdge ) / ( plot . scales . x . scale . domain ( ) . length ) ;
2292+ let binWidth = this . getXBinWidth ( plot ) ;
22422293 let yRange = plot . scales . yLeft . range ;
22432294
22442295 let xAcc = function ( d ) {
@@ -2256,7 +2307,8 @@ Ext4.define('LABKEY.targetedms.QCTrendPlotPanel', {
22562307 let clickedReplicateData = [ ] ;
22572308 clickedReplicateData . push ( {
22582309 'EndIndex' : data . seqValue ,
2259- 'StartIndex' : data . seqValue
2310+ 'StartIndex' : data . seqValue ,
2311+ 'ReplicateName' : data . ReplicateName
22602312 } )
22612313
22622314 let outlierRect = "rect.outlier-" + j ;
@@ -2277,7 +2329,7 @@ Ext4.define('LABKEY.targetedms.QCTrendPlotPanel', {
22772329 . attr ( 'fill' , color ) . attr ( 'fill-opacity' , 0.1 )
22782330 . append ( "title" )
22792331 . text ( function ( d ) {
2280- return "Selected replicate: " + Ext4 . String . htmlEncode ( plot . data [ d . EndIndex ] . ReplicateName ) ;
2332+ return "Selected replicate: " + Ext4 . String . htmlEncode ( d . ReplicateName ) ;
22812333 } ) ;
22822334
22832335 this . sendSvgElementToBack ( plot , outlierRect ) ;
@@ -2325,7 +2377,7 @@ Ext4.define('LABKEY.targetedms.QCTrendPlotPanel', {
23252377 }
23262378 } , this ) ;
23272379
2328- var binWidth = ( plot . grid . rightEdge - plot . grid . leftEdge ) / ( plot . scales . x . scale . domain ( ) . length ) ;
2380+ const binWidth = this . getXBinWidth ( plot ) ;
23292381 var yRange = plot . scales . yLeft . range ;
23302382
23312383 var xAcc = function ( d ) {
@@ -2350,8 +2402,11 @@ Ext4.define('LABKEY.targetedms.QCTrendPlotPanel', {
23502402 var pointsData = precursorInfo . data ;
23512403 var expDataArr = [ ] ;
23522404
2353- for ( var i = startIndex ; i <= endIndex ; i ++ ) {
2354- expDataArr . push ( pointsData [ i ] . value ) ;
2405+ // startIndex/endIndex are seqValues (day offsets in calendar mode), not array indices.
2406+ for ( let i = 0 ; i < pointsData . length ; i ++ ) {
2407+ if ( pointsData [ i ] . seqValue >= startIndex && pointsData [ i ] . seqValue <= endIndex && pointsData [ i ] . value !== undefined ) {
2408+ expDataArr . push ( pointsData [ i ] . value ) ;
2409+ }
23552410 }
23562411
23572412 var expMean = LABKEY . targetedms . PlotSettingsUtil . formatNumeric ( LABKEY . vis . Stat . getMean ( expDataArr ) ) ;
@@ -2483,9 +2538,24 @@ Ext4.define('LABKEY.targetedms.QCTrendPlotPanel', {
24832538 return annotationDates . indexOf ( objDate ) === - 1 ;
24842539 } ) ;
24852540
2541+ // Calendar mode keys the x-axis by day offset, so resolve the earliest plotted day for the conversion.
2542+ let minDayNumber = null ;
2543+ if ( this . calendarX ) {
2544+ Ext4 . each ( precursorInfo . data , function ( row ) {
2545+ const dn = LABKEY . vis . dateToDayNumber ( row [ 'date' ] ) ;
2546+ if ( dn !== null && ( minDayNumber === null || dn < minDayNumber ) ) {
2547+ minDayNumber = dn ;
2548+ }
2549+ } ) ;
2550+ }
2551+
24862552 // use direct D3 code to inject the annotation icons to the rendered SVG
24872553 var xAcc = function ( d ) {
24882554 var annotationDate = me . formatDate ( new Date ( d [ 'Date' ] ) , ! me . groupedX ) ;
2555+ if ( me . calendarX ) {
2556+ const dn = LABKEY . vis . dateToDayNumber ( annotationDate ) ;
2557+ return plot . scales . x . scale ( dn !== null && minDayNumber !== null ? dn - minDayNumber : 0 ) ;
2558+ }
24892559 return plot . scales . x . scale ( xAxisLabels . indexOf ( annotationDate ) ) ;
24902560 } ;
24912561 var yAcc = function ( d ) {
@@ -3155,6 +3225,7 @@ Ext4.define('LABKEY.targetedms.QCTrendPlotPanel', {
31553225 plotTypes : this . plotTypes ,
31563226 yAxisScale : this . yAxisScale ,
31573227 groupedX : this . groupedX ,
3228+ calendarX : this . calendarX ,
31583229 singlePlot : this . singlePlot ,
31593230 showExcluded : this . showExcluded ,
31603231 dateRangeOffset : this . dateRangeOffset ,
0 commit comments