Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
c6bf9a1
add LABKEY.vis.Stat.getStdErr and jest test coverage
cnathe Oct 7, 2025
69514a7
Fix issue with showing vertical lines in error bars on study time chart
cnathe Oct 7, 2025
f638dd6
LABKEY.vis.getAggregateData updates to support errorBarType calculati…
cnathe Oct 8, 2025
57e678d
Merge branch 'develop' into fb_chartErrorBars
cnathe Oct 8, 2025
a345537
vis/demo fix to make sure LABKEY object is declared
cnathe Oct 8, 2025
5a9eacb
Plot x-axis tick rotate vs wrap text behavior (default to 35deg rotat…
cnathe Oct 8, 2025
924a34d
Render error bars for line chart points and bar charts if errorAes is…
cnathe Oct 8, 2025
777054a
Include error bar value in the point and bar hover text
cnathe Oct 8, 2025
ef04f43
LABKEY.vis.getValue optional param for preferredProp to be used in ge…
cnathe Oct 8, 2025
f1f3377
LABKEY.vis.GenericChartHelper.generateDataForChartType() to account f…
cnathe Oct 8, 2025
1337be0
null checks (found by LinePlotTest)
cnathe Oct 8, 2025
9a0761c
comment out testing code
cnathe Oct 8, 2025
ef97612
getAggregateData() fix for stat calc with no values (return null inst…
cnathe Oct 9, 2025
b5a5be1
jest test updates
cnathe Oct 9, 2025
6293d42
Merge branch 'develop' into fb_chartErrorBars
cnathe Oct 9, 2025
320409c
Fix for bar chart error bars for grouped bar chart to use xAcc
cnathe Oct 9, 2025
4cb3a6e
get aggErrorType from chartConfig.measures.y.errorBars
cnathe Oct 9, 2025
d804d0e
Merge branch 'develop' into fb_chartErrorBars
cnathe Oct 10, 2025
787f405
LKS Chart wizard support for bar and line chart aggregate method and …
cnathe Oct 10, 2025
2264e9a
D3Renderer.js to use tickOverlapRotation when hasTickAction (time cha…
cnathe Oct 10, 2025
ebeb025
Fix for JS error found in selenium tests
cnathe Oct 13, 2025
8c19c44
renderErrorBar fix for bar chart use of geom.topOnly
cnathe Oct 13, 2025
d2b3a34
Merge branch 'develop' into fb_chartErrorBars
cnathe Oct 13, 2025
f10d9b7
restore CRLF
cnathe Oct 13, 2025
e31dd4b
github CR feedback
cnathe Oct 13, 2025
7852b93
Merge branch 'develop' into fb_chartErrorBars
cnathe Oct 14, 2025
df97e29
add metric for genericChartWithErrorBarsCount
cnathe Oct 14, 2025
78a2de4
CR feedback - var to const/let, misc linting, use ?? and arrow functi…
cnathe Oct 15, 2025
bfcbdec
LKS Chart wizard update to remove duplicate aggregate method combo in…
cnathe Oct 15, 2025
4e2db1e
Merge branch 'develop' into fb_chartErrorBars
cnathe Oct 16, 2025
9951d14
for bar chart, set min range to 0 when not set by user (even when use…
cnathe Oct 16, 2025
c414d89
better handling for LKS multiple y-axis measures scenario (only apply…
cnathe Oct 16, 2025
b26e889
better handling for LKS multiple y-axis measures scenario (only apply…
cnathe Oct 16, 2025
bbccc56
add try/catch to getChartTypeBasedWidth to account for missing/rename…
cnathe Oct 16, 2025
eb25aa1
Merge branch 'develop' into fb_chartErrorBars
cnathe Oct 17, 2025
84cb325
LKS chart wizard fix to not show aggregate/error bars options for mul…
cnathe Oct 17, 2025
2b09077
LKS chart wizard fix to not show aggregate/error bars options for mul…
cnathe Oct 17, 2025
ee3f169
Bar chart default min fix for when max is set manually
cnathe Oct 17, 2025
ec4a092
misc cleanup
cnathe Oct 17, 2025
ad6ddc8
wrapAxisTickLabel() revert for loop (we need to iterate through the w…
cnathe Oct 17, 2025
8c24be0
Issue 54125: Line chart x-axis to use row value instead of formatted …
cnathe Oct 17, 2025
a92426c
Issue 54125: use continuous domain and date format for x-axis date fi…
cnathe Oct 20, 2025
9ebdde7
Merge branch 'develop' into fb_chartErrorBars
cnathe Oct 20, 2025
5373a1c
revert line endings
cnathe Oct 21, 2025
1421ca0
fix for getAggregateData function to account for date vs non-date acc…
cnathe Oct 21, 2025
0c966d1
fix for getAggregateData function to account for date vs non-date acc…
cnathe Oct 21, 2025
8292979
Merge branch 'develop' into fb_chartErrorBars
cnathe Oct 22, 2025
92d9893
back to generateDiscreteAcc() for date fields instead of generateCont…
cnathe Oct 22, 2025
679ef87
handle blank/null values in sorting aggregate data
cnathe Oct 22, 2025
ba862da
Merge branch 'develop' into fb_chartErrorBars
cnathe Oct 22, 2025
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
32 changes: 32 additions & 0 deletions core/src/client/vis/statistics.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
LABKEY.vis = {};
require('../../../webapp/vis/src/statistics.js');

describe('LABKEY.vis.Stats', () => {
test('getMean', () => {
expect(LABKEY.vis.Stat.getMean([1, 2, 3, 4, 5])).toBe(3);
expect(LABKEY.vis.Stat.getMean([-1, 0, 1])).toBe(0);
expect(LABKEY.vis.Stat.getMean([1.123])).toBe(1.123);
});

test('getStdDev', () => {
expect(LABKEY.vis.Stat.getStdDev([1, 2, 3, 4, 5])).toBeCloseTo(1.4142, 4);
expect(LABKEY.vis.Stat.getStdDev([1, 2, 3, 4, 5], true)).toBeCloseTo(1.5811, 4);
expect(LABKEY.vis.Stat.getStdDev([-1, 0, 1])).toBeCloseTo(0.8165, 4);
expect(LABKEY.vis.Stat.getStdDev([-1, 0, 1], true)).toBeCloseTo(1, 4);
expect(LABKEY.vis.Stat.getStdDev([1.123])).toBe(0);

expect(LABKEY.vis.Stat.getStdDev([])).toBe(undefined);
expect(LABKEY.vis.Stat.getStdDev([1.123], true)).toBe(undefined);
});

test('getStdErr', () => {
expect(LABKEY.vis.Stat.getStdErr([1, 2, 3, 4, 5])).toBeCloseTo(0.6325, 4);
expect(LABKEY.vis.Stat.getStdErr([1, 2, 3, 4, 5], true)).toBeCloseTo(0.7071, 4);
expect(LABKEY.vis.Stat.getStdErr([-1, 0, 1])).toBeCloseTo(0.4714, 4);
expect(LABKEY.vis.Stat.getStdErr([-1, 0, 1], true)).toBeCloseTo(0.5774, 4);
expect(LABKEY.vis.Stat.getStdErr([1.123])).toBe(0);

expect(LABKEY.vis.Stat.getStdErr([])).toBe(undefined);
expect(LABKEY.vis.Stat.getStdErr([1.123], true)).toBe(undefined);
});
});
279 changes: 279 additions & 0 deletions core/src/client/vis/utils.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,279 @@
LABKEY.vis = {};
require('../../../webapp/vis/src/statistics.js');
require('../../../webapp/vis/src/utils.js');

describe('LABKEY.vis.getValue', () => {
test('value not object', () => {
expect(LABKEY.vis.getValue()).toBeUndefined();
expect(LABKEY.vis.getValue(undefined)).toBeUndefined();
expect(LABKEY.vis.getValue(null)).toBeNull();
expect(LABKEY.vis.getValue(5)).toBe(5);
expect(LABKEY.vis.getValue('test')).toBe('test');
});

test('value is object', () => {
expect(LABKEY.vis.getValue({})).toBeUndefined();
expect(LABKEY.vis.getValue({ value: undefined })).toBeUndefined();
expect(LABKEY.vis.getValue({ value: null })).toBeNull();
expect(LABKEY.vis.getValue({ value: 5 })).toBe(5);
expect(LABKEY.vis.getValue({ value: 'test' })).toBe('test');
expect(LABKEY.vis.getValue({ value: 'test', other: 1 })).toBe('test');
});

test('formattedValue, displayValue, preferredProp', () => {
expect(LABKEY.vis.getValue({ formattedValue: 'formatted', displayValue: 'display', value: 'value' })).toBe('formatted');
expect(LABKEY.vis.getValue({ formattedValue: null, displayValue: 'display', value: 'value' })).toBe(null);
expect(LABKEY.vis.getValue({ formattedValue: undefined, displayValue: 'display', value: 'value' })).toBe(undefined);
expect(LABKEY.vis.getValue({ displayValue: 'display', value: 'value' })).toBe('display');
expect(LABKEY.vis.getValue({ displayValue: null, value: 'value' })).toBe(null);
expect(LABKEY.vis.getValue({ displayValue: undefined, value: 'value' })).toBe(undefined);
expect(LABKEY.vis.getValue({ value: 'value' })).toBe('value');
expect(LABKEY.vis.getValue({ value: null })).toBeNull();
expect(LABKEY.vis.getValue({ value: undefined })).toBeUndefined();

expect(LABKEY.vis.getValue({ formattedValue: 'formatted', displayValue: 'display', value: 'value' }, 'bogus')).toBe('formatted');
expect(LABKEY.vis.getValue({ formattedValue: 'formatted', displayValue: 'display', value: 'value' }, 'formattedValue')).toBe('formatted');
expect(LABKEY.vis.getValue({ formattedValue: 'formatted', displayValue: 'display', value: 'value' }, 'displayValue')).toBe('display');
expect(LABKEY.vis.getValue({ formattedValue: 'formatted', displayValue: 'display', value: 'value' }, 'value')).toBe('value');
});
});

describe('LABKEY.vis.getAggregateData', () => {
const data = [
{ main: 'A', sub: 'a', value: 10 },
{ main: 'A', sub: 'b', value: 20 },
{ main: 'A', sub: 'b', value: 15 },
{ main: 'B', sub: 'a', value: 30 },
];

test('without subgroup', () => {
let results = LABKEY.vis.getAggregateData(data, 'main', undefined, 'value');
expect(results).toStrictEqual([
{ label: 'A', value: 3 },
{ label: 'B', value: 1 },
]);
results = LABKEY.vis.getAggregateData(data, 'main', undefined, 'value', 'COUNT');
expect(results).toStrictEqual([
{ label: 'A', value: 3 },
{ label: 'B', value: 1 },
]);
results = LABKEY.vis.getAggregateData(data, 'main', undefined, 'value', 'MEAN');
expect(results).toStrictEqual([
{ aggType: 'MEAN', label: 'A', value: 15 },
{ aggType: 'MEAN', label: 'B', value: 30 },
]);
});

test('with subgroup', () => {
let results = LABKEY.vis.getAggregateData(data, 'main', 'sub', 'value');
expect(results).toStrictEqual([
{ label: 'A', subLabel: 'a', value: 1 },
{ label: 'A', subLabel: 'b', value: 2 },
{ label: 'B', subLabel: 'a', value: 1 },
]);
results = LABKEY.vis.getAggregateData(data, 'main', 'sub', 'value', 'MEAN');
expect(results).toStrictEqual([
{ aggType: 'MEAN', label: 'A', subLabel: 'a', value: 10 },
{ aggType: 'MEAN', label: 'A', subLabel: 'b', value: 17.5 },
{ aggType: 'MEAN', label: 'B', subLabel: 'a', value: 30 },
]);
});

test('errorBarType', () => {
const data2 = [
{ main: 'A', sub: 'a', value: 10 },
{ main: 'A', sub: 'b', value: 1 },
{ main: 'A', sub: 'b', value: 2 },
{ main: 'A', sub: 'b', value: 3 },
{ main: 'B', sub: 'a', value: 30 },
];
let results = LABKEY.vis.getAggregateData(data2, 'main', 'sub', 'value', 'COUNT', undefined, false, 'SD');
expect(results).toStrictEqual([
{ label: 'A', subLabel: 'a', value: 1 },
{ label: 'A', subLabel: 'b', value: 3 },
{ label: 'B', subLabel: 'a', value: 1 },
]);
results = LABKEY.vis.getAggregateData(data2, 'main', 'sub', 'value', 'MEAN', undefined, false, 'SD');
expect(results).toStrictEqual([
{ aggType: 'MEAN', label: 'A', subLabel: 'a', value: 10, errorType: 'SD', error: undefined },
{ aggType: 'MEAN', label: 'A', subLabel: 'b', value: 2, errorType: 'SD', error: 1 },
{ aggType: 'MEAN', label: 'B', subLabel: 'a', value: 30, errorType: 'SD', error: undefined },
]);
results = LABKEY.vis.getAggregateData(data2, 'main', 'sub', 'value', 'MEAN', undefined, false, 'SEM');
expect(results[1].errorType).toBe('SEM');
expect(results[1].error).toBeCloseTo(0.5774, 4);
});

test('includeTotal', () => {
let results = LABKEY.vis.getAggregateData(data, 'main', 'sub', 'value', 'COUNT', undefined, true);
expect(results).toStrictEqual([
{ label: 'A', subLabel: 'a', value: 1, total: 1 },
{ label: 'A', subLabel: 'b', value: 2, total: 3 },
{ label: 'B', subLabel: 'a', value: 1, total: 4 },
]);
results = LABKEY.vis.getAggregateData(data, 'main', 'sub', 'value', 'MEAN', undefined, true);
expect(results).toStrictEqual([
{ aggType: 'MEAN', label: 'A', subLabel: 'a', value: 10, total: 1 },
{ aggType: 'MEAN', label: 'A', subLabel: 'b', value: 17.5, total: 3 },
{ aggType: 'MEAN', label: 'B', subLabel: 'a', value: 30, total: 4 },
]);
});

test('keepNames', () => {
let results = LABKEY.vis.getAggregateData(data, 'main', 'sub', 'value', 'COUNT', undefined, false, undefined, true);
expect(results).toStrictEqual([
{ label: 'A', main: { value: 'A' }, subLabel: 'a', sub: { value: 'a' }, value: { aggType: 'COUNT', value: 1 } },
{ label: 'A', main: { value: 'A' }, subLabel: 'b', sub: { value: 'b' }, value: { aggType: 'COUNT', value: 2 } },
{ label: 'B', main: { value: 'B' }, subLabel: 'a', sub: { value: 'a' }, value: { aggType: 'COUNT', value: 1 } },
]);
results = LABKEY.vis.getAggregateData(data, 'main', 'sub', 'value', 'MEAN', undefined, false, undefined, true);
expect(results).toStrictEqual([
{ label: 'A', main: { value: 'A' }, subLabel: 'a', sub: { value: 'a' }, value: { aggType: 'MEAN', value: 10 }, aggType: 'MEAN' },
{ label: 'A', main: { value: 'A' }, subLabel: 'b', sub: { value: 'b' }, value: { aggType: 'MEAN', value: 17.5 }, aggType: 'MEAN' },
{ label: 'B', main: { value: 'B' }, subLabel: 'a', sub: { value: 'a' }, value: { aggType: 'MEAN', value: 30 }, aggType: 'MEAN' },
]);
});

test('nullDisplayValue', () => {
const dataWithNulls = [
{ main: 'A', sub: 'a', value: 10 },
{ main: null, sub: 'b', value: 20 },
{ main: 'A', sub: null, value: 15 },
{ main: 'B', sub: 'a', value: 30 },
{ main: null, sub: null, value: 5 },
];
let results = LABKEY.vis.getAggregateData(dataWithNulls, 'main', 'sub', 'value', 'COUNT', '(empty)');
expect(results).toStrictEqual([
{ label: 'A', subLabel: 'a', value: 1 },
{ label: 'A', subLabel: '(empty)', value: 1 },
{ label: '(empty)', subLabel: 'b', value: 1 },
{ label: '(empty)', subLabel: '(empty)', value: 1 },
{ label: 'B', subLabel: 'a', value: 1 },
]);
});

test('no values', () => {
const dataWithNulls = [
{ main: 'A', sub: 'a', value: undefined },
];
expect(LABKEY.vis.getAggregateData(dataWithNulls, 'main', 'sub', 'value', 'COUNT')).toStrictEqual([{ label: 'A', subLabel: 'a', value: 0 }]);
expect(LABKEY.vis.getAggregateData(dataWithNulls, 'main', 'sub', 'value', 'SUM')).toStrictEqual([{ aggType: 'SUM', label: 'A', subLabel: 'a', value: null }]);
expect(LABKEY.vis.getAggregateData(dataWithNulls, 'main', 'sub', 'value', 'MIN')).toStrictEqual([{ aggType: 'MIN', label: 'A', subLabel: 'a', value: null }]);
expect(LABKEY.vis.getAggregateData(dataWithNulls, 'main', 'sub', 'value', 'MAX')).toStrictEqual([{ aggType: 'MAX', label: 'A', subLabel: 'a', value: null }]);
expect(LABKEY.vis.getAggregateData(dataWithNulls, 'main', 'sub', 'value', 'MEAN')).toStrictEqual([{ aggType: 'MEAN', label: 'A', subLabel: 'a', value: null }]);
expect(LABKEY.vis.getAggregateData(dataWithNulls, 'main', 'sub', 'value', 'MEDIAN')).toStrictEqual([{ aggType: 'MEDIAN', label: 'A', subLabel: 'a', value: null }]);
});
});

describe('LABKEY.vis.formatDate', () => {
// see supported date and time formats https://www.labkey.org/Documentation/wiki-page.view?name=dateformats#date
const dateFormats = ["yyyy-MM-dd", "yyyy-MMM-dd", "yyyy-MM", "dd-MM-yyyy", "dd-MMM-yyyy", "dd-MMM-yy", "ddMMMyyyy", "ddMMMyy", "MM/dd/yyyy", "MM-dd-yyyy", "MMMM dd yyyy"];
const timeFormats = ["", "HH:mm:ss", "HH:mm", "HH:mm:ss.SSS", "hh:mm a"];

test('dateFormat only', () => {
const testDate = new Date(Date.UTC(2024, 0, 15, 13, 45, 30, 123)); // Jan 15, 2024
const expectedResults = [
"2024-01-15",
"2024-Jan-15",
"2024-01",
"15-01-2024",
"15-Jan-2024",
"15-Jan-24",
"15Jan2024",
"15Jan24",
"01/15/2024",
"01-15-2024",
"January 15 2024"
];

dateFormats.forEach((format, index) => {
const formattedDate = LABKEY.vis.formatDate(testDate, format);
expect(formattedDate).toBe(expectedResults[index]);
});
});

test('dateFormat and timeFormat', () => {
const testDate = new Date("2024-01-15 13:45:30.123");
const expectedResults = [
"2024-01-15",
"2024-01-15 13:45:30",
"2024-01-15 13:45",
"2024-01-15 13:45:30.123",
"2024-01-15 01:45 PM",
"2024-Jan-15",
"2024-Jan-15 13:45:30",
"2024-Jan-15 13:45",
"2024-Jan-15 13:45:30.123",
"2024-Jan-15 01:45 PM",
"2024-01",
"2024-01 13:45:30",
"2024-01 13:45",
"2024-01 13:45:30.123",
"2024-01 01:45 PM",
"15-01-2024",
"15-01-2024 13:45:30",
"15-01-2024 13:45",
"15-01-2024 13:45:30.123",
"15-01-2024 01:45 PM",
"15-Jan-2024",
"15-Jan-2024 13:45:30",
"15-Jan-2024 13:45",
"15-Jan-2024 13:45:30.123",
"15-Jan-2024 01:45 PM",
"15-Jan-24",
"15-Jan-24 13:45:30",
"15-Jan-24 13:45",
"15-Jan-24 13:45:30.123",
"15-Jan-24 01:45 PM",
"15Jan2024",
"15Jan2024 13:45:30",
"15Jan2024 13:45",
"15Jan2024 13:45:30.123",
"15Jan2024 01:45 PM",
"15Jan24",
"15Jan24 13:45:30",
"15Jan24 13:45",
"15Jan24 13:45:30.123",
"15Jan24 01:45 PM",
"01/15/2024",
"01/15/2024 13:45:30",
"01/15/2024 13:45",
"01/15/2024 13:45:30.123",
"01/15/2024 01:45 PM",
"01-15-2024",
"01-15-2024 13:45:30",
"01-15-2024 13:45",
"01-15-2024 13:45:30.123",
"01-15-2024 01:45 PM",
"January 15 2024",
"January 15 2024 13:45:30",
"January 15 2024 13:45",
"January 15 2024 13:45:30.123",
"January 15 2024 01:45 PM"
];

dateFormats.forEach((dateFormat, di) => {
timeFormats.forEach((timeFormat, ti) => {
const formattedDate = LABKEY.vis.formatDate(testDate, dateFormat + (timeFormat !== '' ? ' ' + timeFormat : ''));
expect(formattedDate).toBe(expectedResults[di * timeFormats.length + ti]);
});
});
});
});

describe('LABKEY.vis.isValidDate', () => {
test('valid dates', () => {
expect(LABKEY.vis.isValidDate(new Date())).toBe(true);
expect(LABKEY.vis.isValidDate(new Date('2024-01-15'))).toBe(true);
expect(LABKEY.vis.isValidDate(new Date('January 15, 2024'))).toBe(true);
expect(LABKEY.vis.isValidDate(new Date('2024-01-15T13:45:30Z'))).toBe(true);
});

test('invalid dates', () => {
expect(LABKEY.vis.isValidDate(new Date('invalid date string'))).toBe(false);
expect(LABKEY.vis.isValidDate(NaN)).toBe(false);
expect(LABKEY.vis.isValidDate(undefined)).toBe(false);
expect(LABKEY.vis.isValidDate(null)).toBe(false);
expect(LABKEY.vis.isValidDate('2024-01-15')).toBe(false); // string,
});
});
5 changes: 5 additions & 0 deletions core/webapp/vis/demo/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,11 @@
<div class="example" id="stats">
</div>

<script type="text/javascript">
if (!LABKEY) {
var LABKEY = {};
}
</script>
<script type="text/javascript" src="../src/utils.js"></script>
<script type="text/javascript" src="../src/geom.js"></script>
<script type="text/javascript" src="../src/statistics.js"></script>
Expand Down
4 changes: 3 additions & 1 deletion core/webapp/vis/src/geom.js
Original file line number Diff line number Diff line change
Expand Up @@ -328,6 +328,7 @@ LABKEY.vis.Geom.ControlRange.prototype.render = function(renderer, grid, scales,
* @param {Number} [config.size] (Optional) Number used to determine the size of all paths. Defaults to 2.
* @param {Boolean} [config.dashed] (Optional) Whether or not to use dashed lines for top and bottom bars. Defaults to false.
* @param {Boolean} [config.topOnly] (Optional) Whether or not to only render the top line of the error bar. Defaults to false. Allows optimizing bars that will have an error of zero to not double-render.
* @param {Boolean} [config.errorShowVertical] (Optional) Whether or not to render the vertical line of the error bar. Defaults to false.
*/
LABKEY.vis.Geom.ErrorBar = function(config){
this.type = "ErrorBar";
Expand All @@ -339,7 +340,8 @@ LABKEY.vis.Geom.ErrorBar = function(config){
this.size = ('size' in config && config.size != null && config.size != undefined) ? config.size : 2;
this.dashed = ('dashed' in config && config.dashed != null && config.dashed != undefined) ? config.dashed : false;
this.width = ('width' in config && config.width != null && config.width != undefined) ? config.width : 6;
this.topOnly = ('topOnly' in config && config.topOnly != null && config.topOnly != undefined) ? config.topOnly : false;
this.topOnly = config.topOnly ?? false;
this.errorShowVertical = config.showVertical ?? false;

return this;
};
Expand Down
Loading