Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
80 changes: 65 additions & 15 deletions webapp/TargetedMS/js/QCPlotHelperBase.js
Original file line number Diff line number Diff line change
Expand Up @@ -330,17 +330,23 @@ Ext4.define("LABKEY.targetedms.QCPlotHelperBase", {
}


// ReferenceRangeSeries is used to separate series. Default to "InRange" and only
// promote to "GuideSet" on a match, so that a later non-matching guide set in the
// map cannot clobber the label back to "InRange" when multiple guide sets exist.
plotData['ReferenceRangeSeries'] = "InRange";
Ext4.Object.each(this.guideSetDataMap, function(guideSetId, guideSetData) {
// guideSetDataMap is keyed by guide set ID, so the iteration key guideSetId is a
// String (JS object keys are always strings), whereas plotData.guideSetId is a Number
// (set from the numeric server value in QCPlotHelperWrapper.processPlotDataRow).
// Parse the String key to an int so this is a type-safe === comparison and we don't
// rely on == coercion (mirrors the parseInt pattern in QCTrendPlotPanel.js).
const guideSetIdInt = parseInt(guideSetId, 10);
// for truncating out of range guideset data find first index of plotDate ending at guideset.trainingEnd
if (plotData.guideSetId === guideSetId && plotData.inGuideSetTrainingRange && guideSetData.TrainingEnd <= this.startDate) {
if (plotData.guideSetId === guideSetIdInt && plotData.inGuideSetTrainingRange && guideSetData.TrainingEnd <= this.startDate) {
this.filterPoints[frag][plotData.MetricId]['filterPointsFirstIndex'] = j + 1;
// ReferenceRangeSeries is used to separate series
plotData['ReferenceRangeSeries'] = "GuideSet";
return false; // stop once the matching guide set is found
}
else {
plotData['ReferenceRangeSeries'] = "InRange";
}

}, this);

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

for (let i = 0; i < plotDataRows.length; i++) {
Ext4.Object.each(this.filterPoints[plotDataRows[i].SeriesLabel], function (metricId, filterPointsData) {
const seriesPoints = this.filterPoints && this.filterPoints[plotDataRows[i].SeriesLabel];
if (!seriesPoints) {
continue;
}
Ext4.Object.each(seriesPoints, function (metricId, filterPointsData) {
// no need to filter if less than 6 data points are present between reference end of guideset and startdate
if (filterPointsData['filterPointsFirstIndex'] && filterPointsData['filterPointsLastIndex']) {
if (filterPointsData['filterPointsLastIndex'] - filterPointsData['filterPointsFirstIndex'] < 6) {
this.filterQCPoints = false;
// set the startDate field = acquired time of the 1st point of 5 points before the experiment run range

this.getStartDateField().setValue(this.formatDate(plotDataRows[i].data[filterPointsData['filterPointsFirstIndex']].AcquiredTime));
// Fewer than 6 out-of-range points for this series/metric, so there is nothing to truncate
// for it. Flag only this entry rather than clearing the global this.filterQCPoints, so that
// other series still truncate and the separator / guide-set line break still render.
filterPointsData['skipTruncation'] = true;
// set the startDate field = acquired time of the point right before the experiment run range
this.setStartDateFromFilterIndex(plotDataRows[i], filterPointsData['filterPointsFirstIndex']);
}
else { // skip 5 points
filterPointsData['filterPointsLastIndex'] = filterPointsData['filterPointsLastIndex'] - 6;
// set the startDate field = acquired time of the 1st point of 5 points before the experiment run range
// adding 1 as the point is right after filter last index
this.getStartDateField().setValue(this.formatDate(plotDataRows[i].data[filterPointsData['filterPointsLastIndex'] + 1].AcquiredTime));
// set the startDate field = acquired time of the point right after the new filter last index
this.setStartDateFromFilterIndex(plotDataRows[i], filterPointsData['filterPointsLastIndex'] + 1);
}
}
}, this);
Expand All @@ -389,6 +400,42 @@ Ext4.define("LABKEY.targetedms.QCPlotHelperBase", {
this.renderPlots();
},

// Sets the start-date form field from the AcquiredTime of a point identified by a filterPoints index.
// The filterPoints indices (filterPointsFirstIndex / filterPointsLastIndex) are computed against
// this.fragmentPlotData[label].data, which has injected type:'missing' placeholder entries spliced in
// (see the "add any missing dates" block in processPlotData). plotDataRow.data is the raw server array:
// it has no missing entries and is the only place AcquiredTime is available, so a fragmentPlotData-space
// index cannot be used against it directly. Translate the index to raw-space by counting the non-missing
// entries before it, and guard the lookup so a stale/out-of-range index can never throw.
setStartDateFromFilterIndex: function(plotDataRow, fragIndex) {
if (!plotDataRow || fragIndex == null) {
return;
}
const fragData = this.fragmentPlotData[plotDataRow.SeriesLabel] && this.fragmentPlotData[plotDataRow.SeriesLabel].data;
if (!fragData || fragData.length === 0) {
return;
}
// Walk back to the nearest real (non-missing) entry at or before the requested index
let idx = Math.min(fragIndex, fragData.length - 1);
while (idx >= 0 && fragData[idx] && fragData[idx].type === 'missing') {
idx--;
}
if (idx < 0) {
return;
}
// raw index = number of non-missing entries strictly before idx (missing entries exist only in fragData)
let rawIndex = 0;
for (let k = 0; k < idx; k++) {
if (!fragData[k] || fragData[k].type !== 'missing') {
rawIndex++;
}
}
const rawPoint = plotDataRow.data[rawIndex];
if (rawPoint && rawPoint.AcquiredTime) {
this.getStartDateField().setValue(this.formatDate(rawPoint.AcquiredTime));
}
},

renderPlots: function() {
if (this.filterQCPoints) {
this.truncateOutOfRangeQCPoints();
Expand Down Expand Up @@ -434,7 +481,7 @@ Ext4.define("LABKEY.targetedms.QCPlotHelperBase", {
Ext4.Object.each(this.fragmentPlotData, function(label, fragmentData) {
// traverse plotData backwards from firstIndex to lastIndex and
// remove them from the array
if (this.filterQCPoints && this.filterPoints) {
if (this.filterQCPoints && this.filterPoints && this.filterPoints[label]) {

// when we're plotting two different metrics at the same time, then we
// have repeated dates (from oldest to newest for metric 1, and then oldest to newest for metric 2, all in the same array).
Expand All @@ -443,6 +490,9 @@ Ext4.define("LABKEY.targetedms.QCPlotHelperBase", {
const lab = label;

filterPointsReversed.forEach(metricId => {
if (this.filterPoints[lab][metricId]['skipTruncation']) {
return; // too few out-of-range points for this series/metric to truncate
}
let firstIndex = this.filterPoints[lab][metricId]['filterPointsFirstIndex'];
let lastIndex = this.filterPoints[lab][metricId]['filterPointsLastIndex'];

Expand Down
20 changes: 16 additions & 4 deletions webapp/TargetedMS/js/QCTrendPlotPanel.js
Original file line number Diff line number Diff line change
Expand Up @@ -2325,8 +2325,10 @@ Ext4.define('LABKEY.targetedms.QCTrendPlotPanel', {
.attr('stroke', color).attr('stroke-opacity', 0.1)
.attr('fill', color).attr('fill-opacity', 0.1)
.append("title")
.text(function (d) {
return "Selected replicate: " + Ext4.String.htmlEncode(plot.data[d.EndIndex].ReplicateName);
.text(function () {
// 'data' is the already-matched point for this replicate, don't index plot.data by
// seqValue (EndIndex), which breaks once out-of-range points have been truncated.
return "Selected replicate: " + Ext4.String.htmlEncode(data.ReplicateName);
});

this.sendSvgElementToBack(plot, outlierRect);
Expand Down Expand Up @@ -2399,8 +2401,18 @@ Ext4.define('LABKEY.targetedms.QCTrendPlotPanel', {
var pointsData = precursorInfo.data;
var expDataArr = [];

for (var i = startIndex; i <= endIndex; i++) {
expDataArr.push(pointsData[i].value);
// startIndex/endIndex are seqValues (x-axis positions), not array indices. Once out-of-range
// points are truncated, the array positions no longer line up with seqValue, so collect the
// experiment-range values by matching seqValue rather than indexing pointsData directly.
// In multi-series mode pointsData holds both metrics' points (the same seqValues repeated), so
// restrict to the primary metric - otherwise the experiment-range mean/std-dev/%CV would blend
// values from two different metrics into a single, meaningless statistic.
for (var i = 0; i < pointsData.length; i++) {
if (pointsData[i].seqValue >= startIndex && pointsData[i].seqValue <= endIndex
&& pointsData[i].MetricId === this.metric
&& pointsData[i].value !== undefined && pointsData[i].value !== null) {
expDataArr.push(pointsData[i].value);
}
}

var expMean = LABKEY.targetedms.PlotSettingsUtil.formatNumeric(LABKEY.vis.Stat.getMean(expDataArr));
Expand Down
Loading