From 3809a8ca242b9c127ccdaea44395b61de5a8aaa5 Mon Sep 17 00:00:00 2001 From: Adam Rauch Date: Tue, 27 Oct 2026 19:14:24 -0700 Subject: [PATCH] Move single-module classes out of api into their owning modules --- .../targetedms/data/LazyForeignKey.java | 138 ++++++++++ .../targetedms/model/GuideSetStats.java | 3 +- .../targetedms/model/RawMetricDataSet.java | 2 +- .../query/QCAnnotationTypeTable.java | 2 +- .../targetedms/query/SampleFileTable.java | 2 +- .../view/ColorPickerDisplayColumn.java | 82 ++++++ .../targetedms/visualization/Stats.java | 243 ++++++++++++++++++ 7 files changed, 468 insertions(+), 4 deletions(-) create mode 100644 src/org/labkey/targetedms/data/LazyForeignKey.java create mode 100644 src/org/labkey/targetedms/view/ColorPickerDisplayColumn.java create mode 100644 src/org/labkey/targetedms/visualization/Stats.java diff --git a/src/org/labkey/targetedms/data/LazyForeignKey.java b/src/org/labkey/targetedms/data/LazyForeignKey.java new file mode 100644 index 000000000..cbdeece5e --- /dev/null +++ b/src/org/labkey/targetedms/data/LazyForeignKey.java @@ -0,0 +1,138 @@ +/* + * Copyright (c) 2021-2026 LabKey Corporation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.labkey.targetedms.data; + +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.labkey.api.collections.NamedObjectList; +import org.labkey.api.data.ColumnInfo; +import org.labkey.api.data.Container; +import org.labkey.api.data.ForeignKey; +import org.labkey.api.data.RenderContext; +import org.labkey.api.data.TableInfo; +import org.labkey.api.query.FieldKey; +import org.labkey.api.query.SchemaKey; +import org.labkey.api.util.StringExpression; + +import java.util.Map; +import java.util.Set; +import java.util.function.Supplier; + +/** + * A foreign key implementation that waits until it's asked for something and then uses the Supplier to create the + * real foreign key. This lets us postpone potentially expensive initialization of the lookup, and in many cases + * avoid needing to do it at all if the lookup isn't being actively used in a given codepath. + */ +public class LazyForeignKey implements ForeignKey +{ + private final Supplier _supplier; + private final boolean _suggestColumns; + + private boolean _delegateCreationAttempted = false; + private ForeignKey _delegate; + + public LazyForeignKey(Supplier supplier, boolean suggestColumns) + { + _supplier = supplier; + _suggestColumns = suggestColumns; + } + + private ForeignKey getDelegate() + { + if (!_delegateCreationAttempted) + { + _delegate = _supplier.get(); + _delegateCreationAttempted = true; + } + return _delegate; + } + + @Override + public @Nullable ColumnInfo createLookupColumn(ColumnInfo parent, String displayField) + { + return getDelegate() == null ? null : getDelegate().createLookupColumn(parent, displayField); + } + + @Override + public @Nullable TableInfo getLookupTableInfo() + { + return getDelegate() == null ? null : getDelegate().getLookupTableInfo(); + } + + @Override + public StringExpression getURL(ColumnInfo parent) + { + return getDelegate() == null ? null : getDelegate().getURL(parent); + } + + @Override + public @NotNull NamedObjectList getSelectList(RenderContext ctx) + { + return getDelegate() == null ? new NamedObjectList() : getDelegate().getSelectList(ctx); + } + + @Override + public Container getLookupContainer() + { + return getDelegate() == null ? null : getDelegate().getLookupContainer(); + } + + @Override + public String getLookupTableName() + { + return getDelegate() == null ? null : getDelegate().getLookupTableName(); + } + + @Override + public String getLookupSchemaName() + { + return getDelegate() == null ? null : getDelegate().getLookupSchemaName(); + } + + @Override + public SchemaKey getLookupSchemaKey() + { + return getDelegate() == null ? null : getDelegate().getLookupSchemaKey(); + } + + @Override + public String getLookupColumnName() + { + return getDelegate() == null ? null : getDelegate().getLookupColumnName(); + } + + @Override + public String getLookupDisplayName() + { + return getDelegate() == null ? null : getDelegate().getLookupDisplayName(); + } + + @Override + public @Nullable ForeignKey remapFieldKeys(@Nullable FieldKey parent, @Nullable Map mapping) + { + return getDelegate() == null ? null : getDelegate().remapFieldKeys(parent, mapping); + } + + @Override + public @Nullable Set getSuggestedColumns() + { + if (!_suggestColumns) + { + return null; + } + return getDelegate() == null ? null : getDelegate().getSuggestedColumns(); + } +} diff --git a/src/org/labkey/targetedms/model/GuideSetStats.java b/src/org/labkey/targetedms/model/GuideSetStats.java index d58b37125..a0f43fb97 100644 --- a/src/org/labkey/targetedms/model/GuideSetStats.java +++ b/src/org/labkey/targetedms/model/GuideSetStats.java @@ -15,11 +15,12 @@ */ package org.labkey.targetedms.model; -import org.labkey.api.visualization.Stats; + import java.util.ArrayList; import java.util.List; import java.util.stream.Collectors; +import org.labkey.targetedms.visualization.Stats; public class GuideSetStats { diff --git a/src/org/labkey/targetedms/model/RawMetricDataSet.java b/src/org/labkey/targetedms/model/RawMetricDataSet.java index c35e37d02..f7d0914d5 100644 --- a/src/org/labkey/targetedms/model/RawMetricDataSet.java +++ b/src/org/labkey/targetedms/model/RawMetricDataSet.java @@ -20,8 +20,8 @@ import org.labkey.api.targetedms.model.OutlierCounts; import org.labkey.api.targetedms.model.QCMetricConfiguration; import org.labkey.api.targetedms.model.QCMetricStatus; -import org.labkey.api.visualization.Stats; import org.labkey.targetedms.chart.LabelFactory; +import org.labkey.targetedms.visualization.Stats; import java.text.DecimalFormat; import java.util.Date; diff --git a/src/org/labkey/targetedms/query/QCAnnotationTypeTable.java b/src/org/labkey/targetedms/query/QCAnnotationTypeTable.java index cee9391d7..6fbb56d28 100644 --- a/src/org/labkey/targetedms/query/QCAnnotationTypeTable.java +++ b/src/org/labkey/targetedms/query/QCAnnotationTypeTable.java @@ -32,9 +32,9 @@ import org.labkey.api.security.User; import org.labkey.api.security.UserPrincipal; import org.labkey.api.security.permissions.Permission; -import org.labkey.api.view.ColorPickerDisplayColumn; import org.labkey.targetedms.TargetedMSManager; import org.labkey.targetedms.TargetedMSSchema; +import org.labkey.targetedms.view.ColorPickerDisplayColumn; import java.sql.SQLException; import java.util.Map; diff --git a/src/org/labkey/targetedms/query/SampleFileTable.java b/src/org/labkey/targetedms/query/SampleFileTable.java index 62ef9a9b8..30be4f5e5 100644 --- a/src/org/labkey/targetedms/query/SampleFileTable.java +++ b/src/org/labkey/targetedms/query/SampleFileTable.java @@ -25,7 +25,6 @@ import org.labkey.api.data.ContainerManager; import org.labkey.api.data.DataColumn; import org.labkey.api.data.JdbcType; -import org.labkey.api.data.LazyForeignKey; import org.labkey.api.data.RenderContext; import org.labkey.api.data.SQLFragment; import org.labkey.api.data.SqlSelector; @@ -64,6 +63,7 @@ import org.labkey.targetedms.TargetedMSManager; import org.labkey.targetedms.TargetedMSRun; import org.labkey.targetedms.TargetedMSSchema; +import org.labkey.targetedms.data.LazyForeignKey; import org.labkey.targetedms.datasource.MsDataSourceUtil; import org.labkey.targetedms.parser.DataSettings; import org.labkey.targetedms.parser.SampleFile; diff --git a/src/org/labkey/targetedms/view/ColorPickerDisplayColumn.java b/src/org/labkey/targetedms/view/ColorPickerDisplayColumn.java new file mode 100644 index 000000000..9f4383c05 --- /dev/null +++ b/src/org/labkey/targetedms/view/ColorPickerDisplayColumn.java @@ -0,0 +1,82 @@ +/* + * Copyright (c) 2014-2026 LabKey Corporation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.labkey.targetedms.view; + +import org.labkey.api.data.ColumnInfo; +import org.labkey.api.data.DataColumn; +import org.labkey.api.data.RenderContext; +import org.labkey.api.util.JavaScriptFragment; +import org.labkey.api.util.PageFlowUtil; +import org.labkey.api.util.UniqueID; +import org.labkey.api.writer.HtmlWriter; + +import static org.labkey.api.util.DOM.Attribute.style; +import static org.labkey.api.util.DOM.DIV; +import static org.labkey.api.util.DOM.SCRIPT; +import static org.labkey.api.util.DOM.at; +import static org.labkey.api.util.DOM.id; + +/** + * {@link org.labkey.api.data.DisplayColumn} that shows an ExtJS-based color picker component for insert/update forms + * and a small square of the color in grid views. + */ +public class ColorPickerDisplayColumn extends DataColumn +{ + public ColorPickerDisplayColumn(ColumnInfo col) + { + super(col); + } + + @Override + public void renderGridCellContents(RenderContext ctx, HtmlWriter out) + { + Object value = getValue(ctx); + if (value != null) + { + DIV( + at(style, "height: 20px; width: 20px; background: #" + value) + ).appendTo(out); + } + } + + @Override + public void renderInputHtml(RenderContext ctx, HtmlWriter out, Object value) + { + String name = getFormFieldName(ctx); + renderHiddenFormInput(out, name, value); + + String renderId = "color-picker-div-" + UniqueID.getRequestScopedUID(ctx.getRequest()); + + SCRIPT( + JavaScriptFragment.unsafe( + " LABKEY.requiresExt4Sandbox(function(){\n" + + " Ext4.onReady(function(){\n" + + " Ext4.create('Ext.picker.Color', {\n" + + " renderTo : " + PageFlowUtil.jsString(renderId) + ",\n" + + " value : " + PageFlowUtil.jsString(value == null ? null : value.toString()) + ",\n" + + " listeners: { \n" + + " select: function(picker, selColor) {\n" + + " document.getElementsByName(" + PageFlowUtil.jsString(name) + ")[0].value = selColor;\n" + + " }\n" + + " }\n" + + " });\n" + + " });\n" + + " });\n") + ).appendTo(out); + + DIV(id(renderId)).appendTo(out); + } +} diff --git a/src/org/labkey/targetedms/visualization/Stats.java b/src/org/labkey/targetedms/visualization/Stats.java new file mode 100644 index 000000000..9f22618c4 --- /dev/null +++ b/src/org/labkey/targetedms/visualization/Stats.java @@ -0,0 +1,243 @@ +/* + * Copyright (c) 2019-2026 LabKey Corporation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.labkey.targetedms.visualization; + +import java.util.Arrays; +import java.util.Objects; + +public class Stats +{ + + public static final double MOVING_RANG_EPSILON = 0.0000001; + public static final int CUSUM_CONTROL_LIMIT = 5; + public static final double MOVING_RANGE_UPPER_LIMIT_WEIGHT = 3.268; + + + /** + * CUSUM_WEIGHT_FACTOR of 0.5 and CUSUM_CONTROL_LIMIT of 5 to achieve a 3*stdDev boundary + */ + public static final double CUSUM_WEIGHT_FACTOR = 0.5; + public static final double CUSUM_EPSILON = 0.0000001; + + /** + * Returns the average value. + * + * @param values An array of double. + * @returns {double} + */ + public static double getMean(Double[] values) + { + if (values == null || values.length == 0) + return 0; + + double sum = Arrays.stream(values).filter(Objects::nonNull).mapToDouble(i -> i).sum(); + return sum / values.length; + } + + /** + * Returns the standard deviation. + * + * @param values An array of values. Nulls are ignored. + */ + public static double getStdDev(Double[] values, boolean isBiasCorrected) + { + if (values == null || values.length == 0) + return 0; + + double mean = getMean(values); + double sumSquareDiffs = 0.0; + int count = 0; + + for (Double value : values) + { + if (value != null) + { + double diff = value - mean; + sumSquareDiffs += diff * diff; + count++; + } + } + + return Math.sqrt(sumSquareDiffs / (double)(count - (isBiasCorrected ? 1 : 0))); + } + + /** + * Calculates a variety of cumulative sums for a data array. + * + * @param values Array of data values to calculate from + * @param negative True to calculate CUSUM-, false to calculate CUSUM+. (default to false) + * @param transform True to calculate CUSUMv (Variability CUSUM), false to calculate CUSUMm (Mean CUSUM). (default to false) + * @param forcePositiveResult True to force all result values to be no less than a specified positive value, usually used for log scale. (default to false) + * @param epsilon The smallest value that all returned value can be, only used if forcePositiveResult is true. (default to LABKEY.vis.Stat.CUSUM_EPSILON) + * @returns {number[]} + */ + public static double[] getCUSUMS(Double[] values, boolean negative, boolean transform, boolean forcePositiveResult, Double epsilon) + { + if (values == null || values.length < 2) + return new double[0]; + + double mean = getMean(values); + double stdDEV = getStdDev(values, false); + if (stdDEV == 0) // in the case when all values are equal, calculation has to abort, special case CUSUM to all be 0 + { + return new double[values.length]; + } + + double[] cusums = new double[values.length+1]; + cusums[0] = 0; + for (int i = 1; i <= values.length; i++) + { + if(values[i-1] != null) + { + double standardized = (values[i-1] - mean) / stdDEV; //standard value (z-score) + if (transform) + standardized = (Math.sqrt(Math.abs(standardized)) - 0.822) / 0.349; //the transformed standardize normal quantity value so that it is sensitive to variability changes + if (negative) + standardized = standardized * -1; + double cusum = Math.max(0, standardized - CUSUM_WEIGHT_FACTOR + cusums[i - 1]); + cusums[i] = cusum; + } + } + int n = cusums.length-1; + double[] shiftCusums = new double[n]; + System.arraycopy(cusums, 1, shiftCusums, 0, n); + if (forcePositiveResult) + { + double lowerBound = epsilon != null ? epsilon : CUSUM_EPSILON; + for (int j = 0; j < cusums.length; j++) + { + cusums[j] = Math.max(cusums[j], lowerBound); + } + } + return shiftCusums; + } + + /** + * Calculate the moving range values for a data array, which are sequential differences between two successive values. + * + * @param values Array of data values to calculate from + * @param forcePositiveResult True to force all result values to be no less than a specified positive value, usually used for log scale. (default to false) + * @param epsilon The smallest value that all returned value can be, only used if forcePositiveResult is true. (default to LABKEY.vis.Stat.MOVING_RANGE_EPSILON) + * @returns {double[]} + */ + public static Double[] getMovingRanges(Double[] values, boolean forcePositiveResult, Double epsilon) + { + if (values == null || values.length <= 1) + return new Double[0]; + + Double[] mR = new Double[values.length]; + mR[0] = Double.valueOf(0); // mR[0] is always 0 + for (int i = 1; i < values.length; i++) + { + if(values[i] != null && values[i-1] != null) + mR[i] = Math.abs(values[i] - values[i - 1]); + } + + if (forcePositiveResult) + { + double lowerBound = epsilon != null ? epsilon : MOVING_RANG_EPSILON; + for (int j = 0; j < mR.length; j++) + { + mR[j] = Math.max(lowerBound, mR[j]); + } + } + return mR; + } + + /** + * Calculate the trailing mean values for a data array, which is a series of averages of different subsets of the full data set. + * + * @param values Array of data values to calculate from + * @param N The subset size for the averages + * @returns {double[]} + */ + public static Double[] getTrailingMeans(Double[] values, int N) + { + if (values == null || values.length <= 1) + return new Double[0]; + + Double[] trailingMeans = new Double[values.length]; + int start = 0; + int end = 1; + trailingMeans[0] = values[0]; + for (int i = 1; i < values.length; i++) + { + // Issue 49131: At the very beginning of the folder's date range, always calculate the values for the first N-1 samples, + // based on their sequence. And so on until you hit N, when we start using the moving window. + if (i >= N) + { + start++; + } + trailingMeans[i] = getMean(getValuesFromRange(values, start, end)); + end++; + } + + return trailingMeans; + } + + /** + * Calculate the trailing CV values for a data array, which is calculated by taking the standard deviation of the values over a certain period, + * + * @param values Array of data values to calculate from + * @param N The subset size for the cv + * @returns {double[]} + */ + public static Double[] getTrailingCVs(Double[] values, int N) + { + if (values == null || values.length <= 1) + return new Double[0]; + + Double[] trailingCVs = new Double[values.length]; + int start = 0; + int end = 1; + trailingCVs[0] = 0.0; + for (int i = 1; i < values.length; i++) + { + // Issue 49131: At the very beginning of the folder's date range, always calculate the values for the first N-1 samples, + // based on their sequence. And so on until you hit N, when we start using the moving window. + if (i >= N) + { + start++; + } + calcCV(values, trailingCVs, start, end, i); + end++; + } + + return trailingCVs; + } + + private static void calcCV(Double[] values, Double[] trailingCVs, int start, int end, int i) + { + Double[] vals = getValuesFromRange(values, start, end); + double sd = getStdDev(vals, false); + double mean = getMean(vals); + if (mean == 0) + { + trailingCVs[i] = null; + } + else + { + trailingCVs[i] = sd / mean * 100; + } + } + + private static Double[] getValuesFromRange(Double[] values, int start, int end) + { + Double[] valuesFromRange = new Double[end-start+1]; + if (end - start >= 0) System.arraycopy(values, start, valuesFromRange, 0, end - start + 1); + return valuesFromRange; + } +}