diff --git a/src/org/labkey/targetedms/TargetedMSController.java b/src/org/labkey/targetedms/TargetedMSController.java index 24e82282e..9e264e02b 100644 --- a/src/org/labkey/targetedms/TargetedMSController.java +++ b/src/org/labkey/targetedms/TargetedMSController.java @@ -956,6 +956,7 @@ public static class LeveyJenningsPlotOptions private String _metric2; private String _yAxisScale; private Boolean _groupedX; + private Boolean _calendarX; private Boolean _singlePlot; private Boolean _showExcluded; private Boolean _showExcludedPrecursors; @@ -981,6 +982,8 @@ public Map getAsMapOfStrings() valueMap.put("yAxisScale", _yAxisScale); if (_groupedX != null) valueMap.put("groupedX", Boolean.toString(_groupedX)); + if (_calendarX != null) + valueMap.put("calendarX", Boolean.toString(_calendarX)); if (_singlePlot != null) valueMap.put("singlePlot", Boolean.toString(_singlePlot)); if (_showExcluded != null) @@ -1027,6 +1030,11 @@ public void setGroupedX(Boolean groupedX) _groupedX = groupedX; } + public void setCalendarX(Boolean calendarX) + { + _calendarX = calendarX; + } + public void setSinglePlot(Boolean singlePlot) { _singlePlot = singlePlot; diff --git a/test/src/org/labkey/test/components/targetedms/QCPlotsWebPart.java b/test/src/org/labkey/test/components/targetedms/QCPlotsWebPart.java index b91080cb2..2d454faf1 100644 --- a/test/src/org/labkey/test/components/targetedms/QCPlotsWebPart.java +++ b/test/src/org/labkey/test/components/targetedms/QCPlotsWebPart.java @@ -270,6 +270,30 @@ public boolean isGroupXAxisValuesByDateChecked() } } + public void setGroupXAxisValuesByCalendar(boolean check) + { + if (isGroupXAxisValuesByCalendarChecked() != check) + { + if (check) + doAndWaitForUpdate(() -> elementCache().xAxisGroupingCalendarRadio.check()); + else + doAndWaitForUpdate(() -> elementCache().xAxisGroupingReplicateRadio.check()); + } + } + + public boolean isGroupXAxisValuesByCalendarChecked() + { + try + { + return elementCache().xAxisGroupingCalendarRadio.isSelected(); + } + catch (NoSuchElementException | StaleElementReferenceException e) + { + // Fallback: if radios are not present yet, assume unchecked + return false; + } + } + public void setShowAllPeptidesInSinglePlot(boolean check) { // 'check' means show all series combined in a single plot @@ -948,6 +972,7 @@ public class Elements extends BodyWebPart.ElementCache RadioButton xAxisGroupingReplicateRadio = new RadioButton.RadioButtonFinder().withLabel("per replicate").findWhenNeeded(getDriver()); RadioButton xAxisGroupingDateRadio = new RadioButton.RadioButtonFinder().withLabel("per date").findWhenNeeded(getDriver()); + RadioButton xAxisGroupingCalendarRadio = new RadioButton.RadioButtonFinder().withLabel("calendar").findWhenNeeded(getDriver()); RadioButton plotsCombinedRadio = new RadioButton.RadioButtonFinder().withLabel("combined").findWhenNeeded(getDriver()); RadioButton plotsPerPrecursorRadio = new RadioButton.RadioButtonFinder().withLabel("per precursor").findWhenNeeded(getDriver()); diff --git a/test/src/org/labkey/test/tests/targetedms/TargetedMSQCTest.java b/test/src/org/labkey/test/tests/targetedms/TargetedMSQCTest.java index 45a78a539..1669653dd 100644 --- a/test/src/org/labkey/test/tests/targetedms/TargetedMSQCTest.java +++ b/test/src/org/labkey/test/tests/targetedms/TargetedMSQCTest.java @@ -307,6 +307,12 @@ public void testQCPlotInputs() assertNotEquals(initialSVGText, qcPlotsWebPart.getSVGPlotText("precursorPlot0")); qcPlotsWebPart.setGroupXAxisValuesByDate(false); + // test option to group X-Axis values by Calendar (time-scaled date axis) + initialSVGText = qcPlotsWebPart.getSVGPlotText("precursorPlot0"); + qcPlotsWebPart.setGroupXAxisValuesByCalendar(true); + assertNotEquals(initialSVGText, qcPlotsWebPart.getSVGPlotText("precursorPlot0")); + qcPlotsWebPart.setGroupXAxisValuesByCalendar(false); + // test that plot0 changes based on scale for (QCPlotsWebPart.Scale scale : QCPlotsWebPart.Scale.values()) { @@ -410,6 +416,13 @@ public void testQCPlotInputsPersistence() goToProjectHome(); qcPlotsWebPart = qcDashboard.getQcPlotsWebPart(); + // verify the Calendar X-axis grouping option also round-trips on refresh + qcPlotsWebPart.setGroupXAxisValuesByCalendar(true); + refresh(); + qcPlotsWebPart = qcDashboard.getQcPlotsWebPart(); + qcPlotsWebPart.waitForPlots(2); + assertTrue("Calendar X-Axis grouping not round tripped as expected", qcPlotsWebPart.isGroupXAxisValuesByCalendarChecked()); + // reset plot type selection qcPlotsWebPart.resetInitialQCPlotFields(); } diff --git a/webapp/TargetedMS/js/QCPlotHelperBase.js b/webapp/TargetedMS/js/QCPlotHelperBase.js index b701e87fa..7401c44dd 100644 --- a/webapp/TargetedMS/js/QCPlotHelperBase.js +++ b/webapp/TargetedMS/js/QCPlotHelperBase.js @@ -169,8 +169,14 @@ Ext4.define("LABKEY.targetedms.QCPlotHelperBase", { processPlotData: function() { var parsed = this.lastParsedResponse; - if (!parsed) + if (!parsed) { + // nothing to lay out yet (e.g. a plot option changed before the first load); drop any mask we put up + const plotDiv = this.plotDivId ? Ext4.get(this.plotDivId) : null; + if (plotDiv) { + plotDiv.unmask(); + } return; + } var plotDataRows = parsed.plotDataRows; const metricProps = {}; @@ -186,6 +192,8 @@ Ext4.define("LABKEY.targetedms.QCPlotHelperBase", { // process the data to shape it for the JS LeveyJenningsPlot API call this.fragmentPlotData = {}; + // indices below are into the rebuilt fragmentPlotData, so stale ones from a previous layout would splice the wrong rows + this.filterPoints = null; if (this.showMetricValuePlot()) { this.processLJGuideSetData(plotDataRows); @@ -393,6 +401,21 @@ Ext4.define("LABKEY.targetedms.QCPlotHelperBase", { this.renderPlots(); }, + // True when the time-scaled calendar X-axis is in effect (under "always show" the guide-set block becomes an ordinal prefix via calendarPrefixField/Value below, so truncation + separator keep working). + isCalendarAxisActive: function() { + return this.calendarX === true; + }, + + // Under calendar + "always show", tell plot.js the ordinal prefix = guide-set training rows (ReferenceRangeSeries "GuideSet"); harmless otherwise since plot.js only reads these when timeBasedXTick is set. + applyCalendarPrefixProps: function(trendLineProps) { + if (this.calendarX === true && this.filterQCPoints) { + trendLineProps.calendarPrefixField = 'ReferenceRangeSeries'; + trendLineProps.calendarPrefixValue = 'GuideSet'; + // the range-start marker has no data, so name it explicitly as the day the time-scaled window starts + trendLineProps.calendarWindowStartDate = this.startDate ? this.formatDate(this.startDate) : undefined; + } + }, + // filterPoints indices include injected 'missing' entries, but AcquiredTime only exists on raw // plotDataRow.data - translate to raw-space by counting non-missing entries, and guard the lookup. setStartDateFromFilterIndex: function(plotDataRow, fragIndex) { @@ -427,6 +450,7 @@ Ext4.define("LABKEY.targetedms.QCPlotHelperBase", { renderPlots: function() { if (this.filterQCPoints) { this.truncateOutOfRangeQCPoints(); + this.addRangeStartMarkers(); } // do not persist plot options in qc folder if changed after coming through experimental folder link if (!this.showExpRunRange) { @@ -492,6 +516,35 @@ Ext4.define("LABKEY.targetedms.QCPlotHelperBase", { }, this); }, + // Blank entry + x-axis tick at the start of the selected date range, so an out-of-range guide set reads as + // separate from the plotted window instead of running straight into it. No-op if that day already has data. + addRangeStartMarkers: function() { + const rangeStart = this.startDate ? this.formatDate(this.startDate) : null; + if (!rangeStart) { + return; + } + + Ext4.Object.each(this.fragmentPlotData, function(label, fragmentData) { + const data = fragmentData.data; + let insertAt = data.length; + for (let i = 0; i < data.length; i++) { + const rowDate = this.formatDate(data[i].fullDate); + if (rowDate === rangeStart) { + return; // that day is already on the axis + } + if (insertAt === data.length && rowDate > rangeStart) { + insertAt = i; + } + } + data.splice(insertAt, 0, { + type: 'missing', + fullDate: rangeStart, + date: rangeStart, + groupedXTick: rangeStart + }); + }, this); + }, + getBasePlotConfig : function(id, data, legenddata) { return { rendererType : 'd3', @@ -818,6 +871,7 @@ Ext4.define("LABKEY.targetedms.QCPlotHelperBase", { mouseOutFn: this.plotPointMouseOut, mouseOutFnScope: this, position: this.groupedX ? 'sequential' : undefined, + timeBasedXTick: this.isCalendarAxisActive(), legendMouseOverFn: this.legendMouseOver, legendMouseOverFnScope: this, legendMouseOutFn: this.plotPointMouseOut, @@ -832,6 +886,7 @@ Ext4.define("LABKEY.targetedms.QCPlotHelperBase", { hideSDLines: true }; + this.applyCalendarPrefixProps(trendLineProps); if (treeColorMap) { trendLineProps.colorMap = treeColorMap; @@ -975,6 +1030,7 @@ Ext4.define("LABKEY.targetedms.QCPlotHelperBase", { mouseOverFn: this.plotPointMouseOver, mouseOverFnScope: this, position: this.groupedX ? 'sequential' : undefined, + timeBasedXTick: this.isCalendarAxisActive(), disableRangeDisplay: this.isMultiSeries(), hoverTextFn: !showDataPoints ? function() { return 'Narrow the date range to show individual data points.' } : undefined, hideSDLines: true, @@ -987,6 +1043,8 @@ Ext4.define("LABKEY.targetedms.QCPlotHelperBase", { trendLineProps.groupBy = "ReferenceRangeSeries"; } + this.applyCalendarPrefixProps(trendLineProps); + Ext4.apply(trendLineProps, this.getPlotTypeProperties(precursorInfo, plotType, isCUSUMMean, metricProps)); let yZoomDomain = this.getYZoomDomain ? this.getYZoomDomain(id) : null; diff --git a/webapp/TargetedMS/js/QCTrendPlotPanel.js b/webapp/TargetedMS/js/QCTrendPlotPanel.js index d13f0695c..783c19198 100644 --- a/webapp/TargetedMS/js/QCTrendPlotPanel.js +++ b/webapp/TargetedMS/js/QCTrendPlotPanel.js @@ -67,6 +67,7 @@ Ext4.define('LABKEY.targetedms.QCTrendPlotPanel', { startDate: null, endDate: null, groupedX: false, + calendarX: false, singlePlot: false, showDataPoints: false, showExpRunRange: false, @@ -86,6 +87,15 @@ Ext4.define('LABKEY.targetedms.QCTrendPlotPanel', { SHOW_ALL_IN_A_SINGLE_PLOT: 'Show all series in a single plot', LABEL_WIDTH: 115, + // Shared column widths (px) so the Excluded-replicates radios line up under the X-axis grouping radios. + XAXIS_COL_WIDTHS: [100, 82, 80], + + // Radio inputValues for the X-axis grouping option. + XAXIS_GROUPING: { + REPLICATE: 'replicate', + DATE: 'date', + CALENDAR: 'calendar' + }, // Max number of plots/series to show per page maxCount: 50, @@ -228,6 +238,11 @@ Ext4.define('LABKEY.targetedms.QCTrendPlotPanel', { // apply the initial values to the panel object so they are used in form field initialization Ext4.apply(this, initValues); + // calendar grouping is a variant of date grouping, so it always implies groupedX + if (this.calendarX) { + this.groupedX = true; + } + // if we have a dateRangeOffset, we need to calculate the start and end date if (this.dateRangeOffset > -1) { this.startDate = this.formatDate(this.calculateStartDateByOffset()); @@ -1018,28 +1033,76 @@ Ext4.define('LABKEY.targetedms.QCTrendPlotPanel', { getGroupedXRadioGroup : function() { if (!this.groupedXRadioGroup) { - this.groupedXRadioGroup = Ext4.create('Ext.form.RadioGroup', { + const me = this; + const onChange = function(radio, checked) { + if (!checked) { + return; + } + const val = radio.inputValue; + const newCalendarX = val === me.XAXIS_GROUPING.CALENDAR; + const newGroupedX = val === me.XAXIS_GROUPING.DATE || val === me.XAXIS_GROUPING.CALENDAR; + // ignore the change event fired for the initially-checked radio during construction + if (newCalendarX === me.calendarX && newGroupedX === me.groupedX) { + return; + } + me.calendarX = newCalendarX; + me.groupedX = newGroupedX; + me.havePlotOptionsChanged = true; + + me.setBrushingEnabled(false); + me.layoutAnnotationData(); + me.setLoadingMsg(); + me.processPlotData(); + }; + + const colWidths = this.XAXIS_COL_WIDTHS; + this.groupedXRadioGroup = Ext4.create('Ext.form.FieldContainer', { id: 'grouped-x-field', fieldLabel: 'X-axis grouping', labelWidth: this.LABEL_WIDTH, - columns: 2, - vertical: false, + layout: { type: 'hbox' }, + defaults: { xtype: 'radio', name: 'xAxisGrouping' }, items: [ - { boxLabel: 'per replicate', id: 'x-axis-grouping-replicate', name: 'xAxisGrouping', inputValue: 'replicate', checked: this.groupedX === false, listeners: { afterrender: function(r) { r.inputEl.set({'aria-label': 'X-axis grouping: per replicate'}); } } }, - { boxLabel: 'per date', id: 'x-axis-grouping-date', name: 'xAxisGrouping', inputValue: 'date', checked: this.groupedX === true, listeners: { afterrender: function(r) { r.inputEl.set({'aria-label': 'X-axis grouping: per date'}); } } } - ], - listeners: { - scope: this, - change: function(group, newValue) { - var val = newValue && (newValue.xAxisGrouping || newValue['xAxisGrouping']); - var groupByDate = val === 'date' || (val === true); // fallback safety - this.groupedX = groupByDate; - this.havePlotOptionsChanged = true; - - this.setBrushingEnabled(false); - this.getAnnotationData(); + { + boxLabel: 'per replicate', + width: colWidths[0], + id: 'x-axis-grouping-replicate', + inputValue: this.XAXIS_GROUPING.REPLICATE, + checked: this.groupedX === false, + listeners: { + change: onChange, + afterrender: function(r) { + r.inputEl.set({'aria-label': 'X-axis grouping: per replicate'}); + } + } + }, + { + boxLabel: 'per date', + width: colWidths[1], + id: 'x-axis-grouping-date', + inputValue: this.XAXIS_GROUPING.DATE, + checked: this.groupedX === true && this.calendarX !== true, + listeners: { + change: onChange, + afterrender: function(r) { + r.inputEl.set({'aria-label': 'X-axis grouping: per date'}); + } + } + }, + { + boxLabel: 'calendar', + width: colWidths[2], + id: 'x-axis-grouping-calendar', + inputValue: this.XAXIS_GROUPING.CALENDAR, + checked: this.groupedX === true && this.calendarX === true, + listeners: { + change: onChange, + afterrender: function(r) { + r.inputEl.set({'aria-label': 'X-axis grouping: calendar'}); + } + } } - } + ] }); } @@ -1079,27 +1142,57 @@ Ext4.define('LABKEY.targetedms.QCTrendPlotPanel', { getExcludedReplicatesRadioGroup : function() { if (!this.excludedReplicatesRadioGroup) { - this.excludedReplicatesRadioGroup = Ext4.create('Ext.form.RadioGroup', { + const me = this; + const onChange = function(radio, checked) { + if (!checked) { + return; + } + const newShow = radio.inputValue === 'show'; + // ignore the change event fired for the initially-checked radio during construction + if (newShow === me.showExcluded) { + return; + } + me.showExcluded = newShow; + me.havePlotOptionsChanged = true; + + me.getAnnotationData(); + }; + + const colWidths = this.XAXIS_COL_WIDTHS; + this.excludedReplicatesRadioGroup = Ext4.create('Ext.form.FieldContainer', { id: 'show-excluded-points', fieldLabel: 'Excluded replicates', labelWidth: this.LABEL_WIDTH, - columns: 2, - vertical: false, + layout: { type: 'hbox' }, + defaults: { xtype: 'radio', name: 'excludedSamples', listeners: { change: onChange } }, items: [ - { boxLabel: 'show', id: 'excluded-replicates-show', name: 'excludedSamples', inputValue: 'show', checked: this.showExcluded === true, listeners: { afterrender: function(r) { r.inputEl.set({'aria-label': 'Excluded replicates: show'}); } } }, - { boxLabel: 'hide', id: 'excluded-replicates-hide', name: 'excludedSamples', inputValue: 'hide', checked: this.showExcluded === false, listeners: { afterrender: function(r) { r.inputEl.set({'aria-label': 'Excluded replicates: hide'}); } } } - ], - listeners: { - scope: this, - change: function(group, newValue) { - var val = newValue && (newValue.excludedSamples || newValue['excludedSamples']); - var newShow = val === 'show' || (val === true); // fallback safety - this.showExcluded = newShow; - this.havePlotOptionsChanged = true; - - this.getAnnotationData(); + { + boxLabel: 'show', + width: colWidths[0], + id: 'excluded-replicates-show', + inputValue: 'show', + checked: this.showExcluded === true, + listeners: { + change: onChange, + afterrender: function(r) { + r.inputEl.set({'aria-label': 'Excluded replicates: show'}); + } + } + }, + { + boxLabel: 'hide', + width: colWidths[1], + id: 'excluded-replicates-hide', + inputValue: 'hide', + checked: this.showExcluded === false, + listeners: { + change: onChange, + afterrender: function(r) { + r.inputEl.set({'aria-label': 'Excluded replicates: hide'}); + } + } } - } + ] }); } @@ -1431,9 +1524,6 @@ Ext4.define('LABKEY.targetedms.QCTrendPlotPanel', { processAnnotationData: function(data) { if (data) { - this.annotationShape = LABKEY.vis.Scale.Shape()[4]; // 0: circle, 1: triangle, 2: square, 3: diamond, 4: X - this.legendData = []; - const collapsedData = []; const collapsedMap = {}; @@ -1452,38 +1542,49 @@ Ext4.define('LABKEY.targetedms.QCTrendPlotPanel', { this.annotationData = collapsedData; - var dateCount = {}; + this.layoutAnnotationData(); + this.getPlotsData(); + } + }, - // if more than one type of legend present, add a legend header for annotations - if (this.annotationData.length > 0 && (this.singlePlot || this.showMeanCUSUMPlot() || this.showVariableCUSUMPlot())) { - this.legendData.push({ - text: 'Annotations', - separator: true - }); - } + // Compute annotation stacking and the annotation legend entries + layoutAnnotationData: function() { + this.annotationShape = LABKEY.vis.Scale.Shape()[4]; // 0: circle, 1: triangle, 2: square, 3: diamond, 4: X + this.legendData = []; - for (let i = 0; i < this.annotationData.length; i++) { - const annotation = this.annotationData[i]; - const annotationDate = this.formatDate(new Date(annotation['Date']), !this.groupedX); + if (!this.annotationData) { + return; + } - // track if we need to stack annotations that fall on the same date - if (!dateCount[annotationDate]) { - dateCount[annotationDate] = 0; - } - annotation.yStepIndex = dateCount[annotationDate]; - dateCount[annotationDate]++; - - // get unique annotation names and colors for the legend - if (Ext4.Array.pluck(this.legendData, "text").indexOf(annotation['Name']) === -1) { - this.legendData.push({ - text: annotation['Name'], - color: '#' + annotation['Color'], - shape: this.annotationShape - }); - } + const dateCount = {}; + + // if more than one type of legend present, add a legend header for annotations + if (this.annotationData.length > 0 && (this.singlePlot || this.showMeanCUSUMPlot() || this.showVariableCUSUMPlot())) { + this.legendData.push({ + text: 'Annotations', + separator: true + }); + } + + for (let i = 0; i < this.annotationData.length; i++) { + const annotation = this.annotationData[i]; + const annotationDate = this.formatDate(new Date(annotation['Date']), !this.groupedX); + + // track if we need to stack annotations that fall on the same date + if (!dateCount[annotationDate]) { + dateCount[annotationDate] = 0; } + annotation.yStepIndex = dateCount[annotationDate]; + dateCount[annotationDate]++; - this.getPlotsData(); + // get unique annotation names and colors for the legend + if (Ext4.Array.pluck(this.legendData, "text").indexOf(annotation['Name']) === -1) { + this.legendData.push({ + text: annotation['Name'], + color: '#' + annotation['Color'], + shape: this.annotationShape + }); + } } }, @@ -2282,6 +2383,16 @@ Ext4.define('LABKEY.targetedms.QCTrendPlotPanel', { return d3.select('#' + plot.renderTo + ' svg'); }, + // Pixel width of one x-axis slot, used to size guide-set/outlier highlight rectangles. For calendar + // mode (continuous axis) this is the plot width over the distinct-day count; otherwise inter-tick spacing. + getXBinWidth : function(plot) { + if (this.isCalendarAxisActive()) { + // shared with the jitter band so bar/rect widths match it, and neither overruns the closest day spacing + return LABKEY.vis.calendarSlotWidth(plot.scales.x, plot.grid.rightEdge - plot.grid.leftEdge); + } + return (plot.grid.rightEdge - plot.grid.leftEdge) / (plot.scales.x.scale.domain().length); + }, + toggleGuideSetMsgDisplay : function() { var toolbarMsg = this.down('#GuideSetMessageToolBar'); if (toolbarMsg) { @@ -2293,7 +2404,7 @@ Ext4.define('LABKEY.targetedms.QCTrendPlotPanel', { // for each precursor in precursorInfo let me = this; - let binWidth = (plot.grid.rightEdge - plot.grid.leftEdge) / (plot.scales.x.scale.domain().length); + let binWidth = this.getXBinWidth(plot); let yRange = plot.scales.yLeft.range; let xAcc = function (d) { @@ -2311,7 +2422,8 @@ Ext4.define('LABKEY.targetedms.QCTrendPlotPanel', { let clickedReplicateData = []; clickedReplicateData.push({ 'EndIndex': data.seqValue, - 'StartIndex': data.seqValue + 'StartIndex': data.seqValue, + 'ReplicateName': data.ReplicateName }) let outlierRect = "rect.outlier-" + j; @@ -2382,7 +2494,7 @@ Ext4.define('LABKEY.targetedms.QCTrendPlotPanel', { } }, this); - var binWidth = (plot.grid.rightEdge - plot.grid.leftEdge) / (plot.scales.x.scale.domain().length); + const binWidth = this.getXBinWidth(plot); var yRange = plot.scales.yLeft.range; var xAcc = function (d) { @@ -2546,10 +2658,27 @@ Ext4.define('LABKEY.targetedms.QCTrendPlotPanel', { return annotationDates.indexOf(objDate) === -1; }); + // Reuse plot.js's day-number -> offset map (handles the ordinal-prefix split) so annotations don't drift. + const useCalendarAxis = this.isCalendarAxisActive(); + const dayNumberOffsetMap = useCalendarAxis ? plot.scales.x.dayNumberOffsetMap : null; + + // x offset for a date, or null when calendar mode has no slot for it - the offsets are piecewise there, so + // deriving one would drop the glyph somewhere arbitrary + const xOffset = function(d) { + const objDate = me.formatDate(new Date(d['Date']), !me.groupedX); + if (!useCalendarAxis) { + return xAxisLabels.indexOf(objDate); + } + const dn = LABKEY.vis.dateToDayNumber(objDate); + return dn !== null && dayNumberOffsetMap && dayNumberOffsetMap[dn] !== undefined ? dayNumberOffsetMap[dn] : null; + }; + + nonAnnotationsData = nonAnnotationsData.filter(function(d) { return xOffset(d) !== null; }); + const plottableAnnotations = this.annotationData.filter(function(d) { return xOffset(d) !== null; }); + // use direct D3 code to inject the annotation icons to the rendered SVG var xAcc = function(d) { - var annotationDate = me.formatDate(new Date(d['Date']), !me.groupedX); - return plot.scales.x.scale(xAxisLabels.indexOf(annotationDate)); + return plot.scales.x.scale(xOffset(d)); }; var yAcc = function(d) { return plot.scales.yLeft.range[1] - (d['yStepIndex'] * 12) - 12; @@ -2650,7 +2779,7 @@ Ext4.define('LABKEY.targetedms.QCTrendPlotPanel', { // Render the existing annotation glyphs after the add-annotation markers so they paint on // top and take precedence for hover/click when the markers overlap them. - let annotations = this.getSvgElForPlot(plot).selectAll("path.annotation").data(this.annotationData) + let annotations = this.getSvgElForPlot(plot).selectAll("path.annotation").data(plottableAnnotations) .enter().append("path").attr("class", "annotation") .attr("d", this.annotationShape(4)).attr('transform', transformAcc) .style("fill", colorAcc).style("stroke", colorAcc); @@ -3220,6 +3349,7 @@ Ext4.define('LABKEY.targetedms.QCTrendPlotPanel', { plotTypes: this.plotTypes, yAxisScale: this.yAxisScale, groupedX: this.groupedX, + calendarX: this.calendarX, singlePlot: this.singlePlot, showExcluded: this.showExcluded, dateRangeOffset: this.dateRangeOffset,