Skip to content

Commit 9c3c8f3

Browse files
authored
QC plots ignore date range when showing guide set with many replicates (#1224)
1 parent 354e074 commit 9c3c8f3

2 files changed

Lines changed: 83 additions & 34 deletions

File tree

webapp/TargetedMS/js/QCPlotHelperBase.js

Lines changed: 67 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -330,17 +330,16 @@ Ext4.define("LABKEY.targetedms.QCPlotHelperBase", {
330330
}
331331

332332

333+
// default "InRange"; promote to "GuideSet" on match so a later guide set can't clobber it
334+
plotData['ReferenceRangeSeries'] = "InRange";
333335
Ext4.Object.each(this.guideSetDataMap, function(guideSetId, guideSetData) {
334-
// for truncating out of range guideset data find first index of plotDate ending at guideset.trainingEnd
335-
if (plotData.guideSetId === guideSetId && plotData.inGuideSetTrainingRange && guideSetData.TrainingEnd <= this.startDate) {
336+
// guideSetId (map key) is a String; plotData.guideSetId a Number - parse for ===
337+
const guideSetIdInt = parseInt(guideSetId, 10);
338+
if (plotData.guideSetId === guideSetIdInt && plotData.inGuideSetTrainingRange && guideSetData.TrainingEnd <= this.startDate) {
336339
this.filterPoints[frag][plotData.MetricId]['filterPointsFirstIndex'] = j + 1;
337-
// ReferenceRangeSeries is used to separate series
338340
plotData['ReferenceRangeSeries'] = "GuideSet";
341+
return false; // stop once the matching guide set is found
339342
}
340-
else {
341-
plotData['ReferenceRangeSeries'] = "InRange";
342-
}
343-
344343
}, this);
345344

346345
// for truncating out of range guideset data find last index of plotData starting from this.startDate
@@ -365,20 +364,25 @@ Ext4.define("LABKEY.targetedms.QCPlotHelperBase", {
365364
if (this.showExpRunRange && this.filterPoints) {
366365

367366
for (let i = 0; i < plotDataRows.length; i++) {
368-
Ext4.Object.each(this.filterPoints[plotDataRows[i].SeriesLabel], function (metricId, filterPointsData) {
367+
const seriesPoints = this.filterPoints && this.filterPoints[plotDataRows[i].SeriesLabel];
368+
if (!seriesPoints) {
369+
continue;
370+
}
371+
Ext4.Object.each(seriesPoints, function (metricId, filterPointsData) {
369372
// no need to filter if less than 6 data points are present between reference end of guideset and startdate
370373
if (filterPointsData['filterPointsFirstIndex'] && filterPointsData['filterPointsLastIndex']) {
371374
if (filterPointsData['filterPointsLastIndex'] - filterPointsData['filterPointsFirstIndex'] < 6) {
372-
this.filterQCPoints = false;
373-
// set the startDate field = acquired time of the 1st point of 5 points before the experiment run range
374-
375-
this.getStartDateField().setValue(this.formatDate(plotDataRows[i].data[filterPointsData['filterPointsFirstIndex']].AcquiredTime));
375+
// Fewer than 6 out-of-range points for this series/metric, so there is nothing to truncate
376+
// for it. Flag only this entry rather than clearing the global this.filterQCPoints, so that
377+
// other series still truncate and the separator / guide-set line break still render.
378+
filterPointsData['skipTruncation'] = true;
379+
// set the startDate field = acquired time of the point right before the experiment run range
380+
this.setStartDateFromFilterIndex(plotDataRows[i], filterPointsData['filterPointsFirstIndex']);
376381
}
377382
else { // skip 5 points
378383
filterPointsData['filterPointsLastIndex'] = filterPointsData['filterPointsLastIndex'] - 6;
379-
// set the startDate field = acquired time of the 1st point of 5 points before the experiment run range
380-
// adding 1 as the point is right after filter last index
381-
this.getStartDateField().setValue(this.formatDate(plotDataRows[i].data[filterPointsData['filterPointsLastIndex'] + 1].AcquiredTime));
384+
// set the startDate field = acquired time of the point right after the new filter last index
385+
this.setStartDateFromFilterIndex(plotDataRows[i], filterPointsData['filterPointsLastIndex'] + 1);
382386
}
383387
}
384388
}, this);
@@ -389,6 +393,37 @@ Ext4.define("LABKEY.targetedms.QCPlotHelperBase", {
389393
this.renderPlots();
390394
},
391395

396+
// filterPoints indices include injected 'missing' entries, but AcquiredTime only exists on raw
397+
// plotDataRow.data - translate to raw-space by counting non-missing entries, and guard the lookup.
398+
setStartDateFromFilterIndex: function(plotDataRow, fragIndex) {
399+
if (!plotDataRow || fragIndex == null) {
400+
return;
401+
}
402+
const fragData = this.fragmentPlotData[plotDataRow.SeriesLabel] && this.fragmentPlotData[plotDataRow.SeriesLabel].data;
403+
if (!fragData || fragData.length === 0) {
404+
return;
405+
}
406+
// back up to the nearest non-missing entry at or before the index
407+
let idx = Math.min(fragIndex, fragData.length - 1);
408+
while (idx >= 0 && fragData[idx] && fragData[idx].type === 'missing') {
409+
idx--;
410+
}
411+
if (idx < 0) {
412+
return;
413+
}
414+
let rawIndex = 0; // count of non-missing entries before idx
415+
416+
for (let k = 0; k < idx; k++) {
417+
if (!fragData[k] || fragData[k].type !== 'missing') {
418+
rawIndex++;
419+
}
420+
}
421+
const rawPoint = plotDataRow.data[rawIndex];
422+
if (rawPoint && rawPoint.AcquiredTime) {
423+
this.getStartDateField().setValue(this.formatDate(rawPoint.AcquiredTime));
424+
}
425+
},
426+
392427
renderPlots: function() {
393428
if (this.filterQCPoints) {
394429
this.truncateOutOfRangeQCPoints();
@@ -432,25 +467,27 @@ Ext4.define("LABKEY.targetedms.QCPlotHelperBase", {
432467

433468
truncateOutOfRangeQCPoints: function() {
434469
Ext4.Object.each(this.fragmentPlotData, function(label, fragmentData) {
435-
// traverse plotData backwards from firstIndex to lastIndex and
436-
// remove them from the array
437-
if (this.filterQCPoints && this.filterPoints) {
438-
439-
// when we're plotting two different metrics at the same time, then we
440-
// have repeated dates (from oldest to newest for metric 1, and then oldest to newest for metric 2, all in the same array).
441-
// so, removing the array elements from the back
442-
const filterPointsReversed = Object.keys(this.filterPoints[label]).reverse();
443-
const lab = label;
444-
445-
filterPointsReversed.forEach(metricId => {
446-
let firstIndex = this.filterPoints[lab][metricId]['filterPointsFirstIndex'];
447-
let lastIndex = this.filterPoints[lab][metricId]['filterPointsLastIndex'];
470+
if (this.filterQCPoints && this.filterPoints && this.filterPoints[label]) {
471+
472+
// Points are date-sorted with both metrics interleaved, so the out-of-range block (guide set
473+
// training end -> start date) is one contiguous range spanning both metrics. Splicing the
474+
// per-metric ranges separately would overlap and corrupt indices, so combine them: start after
475+
// the last training point of any metric, end at the last "first in-range" point of any metric.
476+
let firstIndex, lastIndex;
477+
Ext4.Object.each(this.filterPoints[label], function(metricId, range) {
478+
if (range['skipTruncation'] || range['filterPointsFirstIndex'] === undefined
479+
|| range['filterPointsLastIndex'] === undefined) {
480+
return;
481+
}
482+
firstIndex = firstIndex === undefined ? range['filterPointsFirstIndex'] : Math.max(firstIndex, range['filterPointsFirstIndex']);
483+
lastIndex = lastIndex === undefined ? range['filterPointsLastIndex'] : Math.max(lastIndex, range['filterPointsLastIndex']);
484+
}, this);
448485

486+
if (firstIndex !== undefined && lastIndex !== undefined) {
449487
for (let i = lastIndex; i >= firstIndex; i--) {
450488
fragmentData.data.splice(i, 1);
451489
}
452-
});
453-
490+
}
454491
}
455492
}, this);
456493
},

webapp/TargetedMS/js/QCTrendPlotPanel.js

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -786,6 +786,10 @@ Ext4.define('LABKEY.targetedms.QCTrendPlotPanel', {
786786
this.startDate = this.formatDate(this.calculateStartDateByOffset());
787787
this.endDate = this.formatDate(this.calculateEndDateByOffset());
788788

789+
if (this.filterQCPoints) {
790+
this.resetFilterPointsIndices();
791+
}
792+
789793
this.displayTrendPlot();
790794
}
791795
else {
@@ -2327,8 +2331,10 @@ Ext4.define('LABKEY.targetedms.QCTrendPlotPanel', {
23272331
.attr('stroke', color).attr('stroke-opacity', 0.1)
23282332
.attr('fill', color).attr('fill-opacity', 0.1)
23292333
.append("title")
2330-
.text(function (d) {
2331-
return "Selected replicate: " + Ext4.String.htmlEncode(plot.data[d.EndIndex].ReplicateName);
2334+
.text(function () {
2335+
// 'data' is the already-matched point for this replicate, don't index plot.data by
2336+
// seqValue (EndIndex), which breaks once out-of-range points have been truncated.
2337+
return "Selected replicate: " + Ext4.String.htmlEncode(data.ReplicateName);
23322338
});
23332339

23342340
this.sendSvgElementToBack(plot, outlierRect);
@@ -2401,8 +2407,14 @@ Ext4.define('LABKEY.targetedms.QCTrendPlotPanel', {
24012407
var pointsData = precursorInfo.data;
24022408
var expDataArr = [];
24032409

2404-
for (var i = startIndex; i <= endIndex; i++) {
2405-
expDataArr.push(pointsData[i].value);
2410+
// match on seqValue (not array index); restrict to primary metric so multi-series doesn't
2411+
// blend both metrics into one mean/std-dev/%CV
2412+
for (var i = 0; i < pointsData.length; i++) {
2413+
if (pointsData[i].seqValue >= startIndex && pointsData[i].seqValue <= endIndex
2414+
&& pointsData[i].MetricId === this.metric
2415+
&& pointsData[i].value !== undefined && pointsData[i].value !== null) {
2416+
expDataArr.push(pointsData[i].value);
2417+
}
24062418
}
24072419

24082420
var expMean = LABKEY.targetedms.PlotSettingsUtil.formatNumeric(LABKEY.vis.Stat.getMean(expDataArr));

0 commit comments

Comments
 (0)