From ac8a3f71594aac4039dbd578a9b81c41656b5f50 Mon Sep 17 00:00:00 2001 From: cnathe Date: Thu, 11 Dec 2025 13:26:04 -0600 Subject: [PATCH 1/7] Chart support for lineType aes and scale on path geom --- core/webapp/vis/demo/demo_script.js | 11 +++++++++-- core/webapp/vis/src/geom.js | 6 ++++++ core/webapp/vis/src/internal/D3Renderer.js | 13 +++++++++++-- core/webapp/vis/src/scale.js | 2 +- 4 files changed, 27 insertions(+), 5 deletions(-) diff --git a/core/webapp/vis/demo/demo_script.js b/core/webapp/vis/demo/demo_script.js index eb13bf45e82..a9fcab09713 100644 --- a/core/webapp/vis/demo/demo_script.js +++ b/core/webapp/vis/demo/demo_script.js @@ -14,7 +14,7 @@ var CD4PointLayer = new LABKEY.vis.Layer({ }); var CD4PathLayer = new LABKEY.vis.Layer({ - geom: new LABKEY.vis.Geom.Path({size: 3, opacity: .4}), + geom: new LABKEY.vis.Geom.Path({size: 3, opacity: 0.8}), name: 'CD4+ (cells/mm3)', aes: { y: function(row){return row.study_LabResults_CD4.value} @@ -31,7 +31,7 @@ var hemoglobinPointLayer = new LABKEY.vis.Layer({ }); var hemoglobinPathLayer = new LABKEY.vis.Layer({ - geom: new LABKEY.vis.Geom.Path({opacity: .4}), + geom: new LABKEY.vis.Geom.Path({opacity: 0.8}), name: 'Hemoglobin', aes: { yRight: function(row){return row.study_LabResults_Hemoglobin.value} @@ -104,6 +104,7 @@ var coffeePathLayer = new LABKEY.vis.Layer({ geom: new LABKEY.vis.Geom.Path({}), aes: { pathColor: 'person', + lineType: 'person', group: 'person' } }); @@ -138,6 +139,12 @@ var coffeePlot = new LABKEY.vis.Plot({ return group == 'LabKey Dev 1 Efficiency' ? '#0000A0' : '#ADD8E6'; } }, + lineType: { + scaleType: 'discrete', + scale: function(group) { + return group == 'LabKey Dev 1 Efficiency' ? 'dashed' : 'solid'; + } + }, xTop: { scaleType: 'discrete' }, diff --git a/core/webapp/vis/src/geom.js b/core/webapp/vis/src/geom.js index 39ab71d86e8..aa333eb26fe 100644 --- a/core/webapp/vis/src/geom.js +++ b/core/webapp/vis/src/geom.js @@ -74,6 +74,9 @@ LABKEY.vis.Geom.XY.prototype.initAesthetics = function(scales, layerAes, parentA this.colorAes = layerAes.color ? layerAes.color : parentAes.color; this.colorScale = scales.color ? scales.color : null; + this.lineTypeAes = layerAes.lineType ? layerAes.lineType : parentAes.lineType; + this.lineTypeScale = scales.lineType ? scales.lineType : null; + if(!this.yAes){ console.error('y aesthetic is required for ' + this.type + ' geom to render.'); return false; @@ -238,6 +241,7 @@ LABKEY.vis.Geom.Bin.prototype.render = function(renderer, grid, scales, data, la * @param {Number} [config.opacity] (Optional) Number between 0 and 1, used to determine the opacity of all paths. * Useful if there are many overlapping paths. Defaults to 1. * @param {boolean} [config.dashed] (Optional) True for dashed path, false for solid path. Defaults to false. + * @param {boolean} [config.dotted] (Optional) True for dotted path, false for solid path. Defaults to false. */ LABKEY.vis.Geom.Path = function(config){ this.type = "Path"; @@ -249,6 +253,7 @@ LABKEY.vis.Geom.Path = function(config){ this.size = ('size' in config && config.size != null && config.size != undefined) ? config.size : 3; this.opacity = ('opacity' in config && config.opacity != null && config.opacity != undefined) ? config.opacity : 1; this.dashed = ('dashed' in config && config.dashed != null && config.dashed != undefined) ? config.dashed : false; + this.dotted = ('dotted' in config && config.dotted != null && config.dotted != undefined) ? config.dotted : false; this._dataspaceBoxPlot = ('dataspaceBoxPlot' in config && config.dataspaceBoxPlot != null && config.dataspaceBoxPlot != undefined) ? config.dataspaceBoxPlot : false; @@ -264,6 +269,7 @@ LABKEY.vis.Geom.Path.prototype.render = function(renderer, grid, scales, data, l this.sortFnAes = layerAes.sortFn ? layerAes.sortFn : parentAes.sortFn; this.sizeAes = layerAes.size ? layerAes.size : parentAes.size; this.pathColorAes = layerAes.pathColor ? layerAes.pathColor : parentAes.pathColor; + this.lineTypeAes = layerAes.lineType ? layerAes.lineType : parentAes.lineType; this.sizeScale = scales.size; this.hoverTextAes = layerAes.hoverText ? layerAes.hoverText : parentAes.hoverText; diff --git a/core/webapp/vis/src/internal/D3Renderer.js b/core/webapp/vis/src/internal/D3Renderer.js index 3a6ae5a06e3..ba960ff05f9 100644 --- a/core/webapp/vis/src/internal/D3Renderer.js +++ b/core/webapp/vis/src/internal/D3Renderer.js @@ -2562,8 +2562,17 @@ LABKEY.vis.internal.D3Renderer = function(plot) { pathSel.append('title').text(geom.hoverTextAes.getValue); } - if (geom.dashed) { - layer.selectAll('path').style("stroke-dasharray", ("3, 3")); + const dashedVal = "12, 3"; + const dottedVal = "2, 2"; + if (geom.lineTypeAes && geom.lineTypeScale) { + pathSel.style("stroke-dasharray", function(d) { + const lineType = geom.lineTypeScale.scale(geom.lineTypeAes.getValue(d.data) + geom.layerName); + return lineType === 'dashed' ? dashedVal : (lineType === 'dotted' ? dottedVal : undefined); + }); + } else if (geom.dashed) { + layer.selectAll('path').style("stroke-dasharray", dashedVal); + } else if (geom.dotted) { + layer.selectAll('path').style("stroke-dasharray", dottedVal); } bindMouseEvents(pathSel, geom, layer); diff --git a/core/webapp/vis/src/scale.js b/core/webapp/vis/src/scale.js index 41fd1142af9..56061640c46 100644 --- a/core/webapp/vis/src/scale.js +++ b/core/webapp/vis/src/scale.js @@ -65,7 +65,7 @@ LABKEY.vis.Scale.ValueMapDiscrete = function(valueMap) { // copied from d3.scale.ordinal scale.range = function(_) { if (!arguments.length) return range.slice(); - range = Array.from(_); + range = _ ? Array.from(_) : []; return scale; }; From 6f02d03036b4618bc4b5decfb3e29706db031433 Mon Sep 17 00:00:00 2001 From: cnathe Date: Thu, 11 Dec 2025 13:26:33 -0600 Subject: [PATCH 2/7] GenericChartHelper generatePlotConfig to support lineType value map scale for series --- .../resources/web/vis/genericChart/genericChartHelper.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/visualization/resources/web/vis/genericChart/genericChartHelper.js b/visualization/resources/web/vis/genericChart/genericChartHelper.js index ec6d1600dc2..5d5786407a0 100644 --- a/visualization/resources/web/vis/genericChart/genericChartHelper.js +++ b/visualization/resources/web/vis/genericChart/genericChartHelper.js @@ -1132,6 +1132,7 @@ LABKEY.vis.GenericChartHelper = new function(){ if (hasSeries) { pathAes.pathColor = generateGroupingAcc(chartConfig.measures.series.name); pathAes.group = generateGroupingAcc(chartConfig.measures.series.name); + pathAes.lineType = generateGroupingAcc(chartConfig.measures.series.name); pathAes.hoverText = function (row) { return chartConfig.measures.series.label + ': ' + row.group }; } // if no series measures but we have multiple y-measures, force the color and grouping to be distinct for each measure @@ -1212,9 +1213,11 @@ LABKEY.vis.GenericChartHelper = new function(){ if (!LABKEY.Utils.isEmptyObj(chartConfig.measuresOptions?.series)) { const colorValueMap = {}; const shapeValueMap = {}; + const lineTypeValueMap = {}; Object.entries(chartConfig.measuresOptions.series).forEach(([key, val]) => { if (val.color) colorValueMap[key] = val.color; if (val.shape) shapeValueMap[key] = LABKEY.vis.Scale.ShapeMap[val.shape]; + if (val.lineType) lineTypeValueMap[key] = val.lineType; }); if (!LABKEY.Utils.isEmptyObj(colorValueMap)) { @@ -1225,6 +1228,10 @@ LABKEY.vis.GenericChartHelper = new function(){ if (!scales.shape) scales.shape = { scaleType: 'discrete' }; scales.shape.scale = LABKEY.vis.Scale.ValueMapDiscrete(shapeValueMap); } + if (!LABKEY.Utils.isEmptyObj(lineTypeValueMap)) { + if (!scales.lineType) scales.lineType = { scaleType: 'discrete' }; + scales.lineType.scale = LABKEY.vis.Scale.ValueMapDiscrete(lineTypeValueMap); + } } if ((renderType === 'line_plot' || renderType === 'scatter_plot') && yMeasures.length > 0) { From 495bc32c4ec5a78c22c09b80ee5377af025b4fa1 Mon Sep 17 00:00:00 2001 From: cnathe Date: Mon, 15 Dec 2025 16:10:18 -0600 Subject: [PATCH 3/7] LKS chart rendering to apply chartConfig measuresOptions --- .../resources/web/vis/chartWizard/genericChartPanel.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/visualization/resources/web/vis/chartWizard/genericChartPanel.js b/visualization/resources/web/vis/chartWizard/genericChartPanel.js index 9e035d70f98..566300991b5 100644 --- a/visualization/resources/web/vis/chartWizard/genericChartPanel.js +++ b/visualization/resources/web/vis/chartWizard/genericChartPanel.js @@ -1033,6 +1033,10 @@ Ext4.define('LABKEY.ext4.GenericChartPanel', { if (this.savedReportInfo?.visualizationConfig?.chartConfig?.legendPos) config.legendPos = this.savedReportInfo.visualizationConfig.chartConfig.legendPos; + // Apps can set series specific measuresOptions, so we use the measuresOptions if it's set on the original config + if (this.savedReportInfo?.visualizationConfig?.chartConfig?.measuresOptions) + config.measuresOptions = this.savedReportInfo.visualizationConfig.chartConfig.measuresOptions; + return config; }, From ac50ce5ae4aeb59330d5c238f344ecf96770e6c5 Mon Sep 17 00:00:00 2001 From: cnathe Date: Tue, 16 Dec 2025 10:41:01 -0600 Subject: [PATCH 4/7] Apply per series line type to Trendline path layer --- .../resources/web/vis/genericChart/genericChartHelper.js | 1 + 1 file changed, 1 insertion(+) diff --git a/visualization/resources/web/vis/genericChart/genericChartHelper.js b/visualization/resources/web/vis/genericChart/genericChartHelper.js index 5d5786407a0..eeaf19d7dfd 100644 --- a/visualization/resources/web/vis/genericChart/genericChartHelper.js +++ b/visualization/resources/web/vis/genericChart/genericChartHelper.js @@ -1147,6 +1147,7 @@ LABKEY.vis.GenericChartHelper = new function(){ const layerAes = { x: 'x', y: 'y' }; if (hasSeries) { layerAes.pathColor = function () { return trendline.name }; + layerAes.lineType = function () { return trendline.name }; } layerAes.hoverText = generateTrendlinePathHover(trendline); From eba362542f5b90dc652d2c3552e2d5f1376142fc Mon Sep 17 00:00:00 2001 From: cnathe Date: Tue, 16 Dec 2025 11:34:06 -0600 Subject: [PATCH 5/7] LKS chart rendering to include trendlineParameters in measureStore data --- .../resources/web/vis/chartWizard/genericChartPanel.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/visualization/resources/web/vis/chartWizard/genericChartPanel.js b/visualization/resources/web/vis/chartWizard/genericChartPanel.js index 566300991b5..af65a1e65cd 100644 --- a/visualization/resources/web/vis/chartWizard/genericChartPanel.js +++ b/visualization/resources/web/vis/chartWizard/genericChartPanel.js @@ -899,7 +899,8 @@ Ext4.define('LABKEY.ext4.GenericChartPanel', { { // If we're not in edit mode or if this is the first load we need to only load the minimum amount of data. columns = []; - var measures = this.getChartConfig().measures; + var config = this.getChartConfig(); + var measures = config.measures; if (measures.x) { @@ -931,6 +932,10 @@ Ext4.define('LABKEY.ext4.GenericChartPanel', { columns.push(this.autoColumnName.toString()); } + if (config.geomOptions.trendlineParameters) { + columns.push(config.geomOptions.trendlineParameters); + } + Ext4.each(['ySub', 'xSub', 'color', 'shape', 'series'], function(name) { if (measures[name]) { this.addMeasureForColumnQuery(columns, measures[name]); From f79230cbc9b37b05693a58aa2953af524f175ca0 Mon Sep 17 00:00:00 2001 From: cnathe Date: Tue, 16 Dec 2025 14:09:25 -0600 Subject: [PATCH 6/7] change to strokeValue instead of lineType name --- core/webapp/vis/src/internal/D3Renderer.js | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/core/webapp/vis/src/internal/D3Renderer.js b/core/webapp/vis/src/internal/D3Renderer.js index ba960ff05f9..2f9f2c4a790 100644 --- a/core/webapp/vis/src/internal/D3Renderer.js +++ b/core/webapp/vis/src/internal/D3Renderer.js @@ -2562,17 +2562,14 @@ LABKEY.vis.internal.D3Renderer = function(plot) { pathSel.append('title').text(geom.hoverTextAes.getValue); } - const dashedVal = "12, 3"; - const dottedVal = "2, 2"; if (geom.lineTypeAes && geom.lineTypeScale) { pathSel.style("stroke-dasharray", function(d) { - const lineType = geom.lineTypeScale.scale(geom.lineTypeAes.getValue(d.data) + geom.layerName); - return lineType === 'dashed' ? dashedVal : (lineType === 'dotted' ? dottedVal : undefined); + return geom.lineTypeScale.scale(geom.lineTypeAes.getValue(d.data) + geom.layerName); }); } else if (geom.dashed) { - layer.selectAll('path').style("stroke-dasharray", dashedVal); + layer.selectAll('path').style("stroke-dasharray", "12, 3"); } else if (geom.dotted) { - layer.selectAll('path').style("stroke-dasharray", dottedVal); + layer.selectAll('path').style("stroke-dasharray", "2, 2"); } bindMouseEvents(pathSel, geom, layer); From 7d1bbfc143dfcf84a9eb007cf5cb449476797502 Mon Sep 17 00:00:00 2001 From: cnathe Date: Thu, 18 Dec 2025 14:55:12 -0600 Subject: [PATCH 7/7] Change dashed to 6,6 and dotted to 0.1,6 with stroke-linecap --- core/webapp/vis/demo/demo_script.js | 4 ++-- core/webapp/vis/src/internal/D3Renderer.js | 17 ++++++++++++++--- 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/core/webapp/vis/demo/demo_script.js b/core/webapp/vis/demo/demo_script.js index a9fcab09713..d24746199b4 100644 --- a/core/webapp/vis/demo/demo_script.js +++ b/core/webapp/vis/demo/demo_script.js @@ -127,7 +127,7 @@ var coffeePlot = new LABKEY.vis.Plot({ yLeft: 'efficiency' }, legendData: [ - {text: 'LabKey Dev 1', color: '#0000A0'}, + {text: 'LabKey Dev 1', color: '#0000A0', lineType: 'dotted'}, {text: 'LabKey Dev 2', color: '#ADD8E6'}, {text: 'No Coffee Consumed', shape: LABKEY.vis.Scale.Shape()[0]}, {text: 'Coffee Consumed', shape: LABKEY.vis.Scale.Shape()[1]} @@ -142,7 +142,7 @@ var coffeePlot = new LABKEY.vis.Plot({ lineType: { scaleType: 'discrete', scale: function(group) { - return group == 'LabKey Dev 1 Efficiency' ? 'dashed' : 'solid'; + return group == 'LabKey Dev 1 Efficiency' ? 'dotted' : undefined; } }, xTop: { diff --git a/core/webapp/vis/src/internal/D3Renderer.js b/core/webapp/vis/src/internal/D3Renderer.js index 2f9f2c4a790..d751cf0f245 100644 --- a/core/webapp/vis/src/internal/D3Renderer.js +++ b/core/webapp/vis/src/internal/D3Renderer.js @@ -2562,14 +2562,25 @@ LABKEY.vis.internal.D3Renderer = function(plot) { pathSel.append('title').text(geom.hoverTextAes.getValue); } + // these are our default values that can be referenced by name (dashed, dotted) but we allow for custom + // dash arrays as well via lineTypeScale + const dashed = "6,6"; + const dotted = "0.1,6"; + if (geom.lineTypeAes && geom.lineTypeScale) { pathSel.style("stroke-dasharray", function(d) { - return geom.lineTypeScale.scale(geom.lineTypeAes.getValue(d.data) + geom.layerName); + const val = geom.lineTypeScale.scale(geom.lineTypeAes.getValue(d.data) + geom.layerName); + return val === 'dashed' ? dashed : val === 'dotted' ? dotted : val; + }); + pathSel.style("stroke-linecap", function(d) { + const val = geom.lineTypeScale.scale(geom.lineTypeAes.getValue(d.data) + geom.layerName); + return val === 'dotted' ? "round" : null; }); } else if (geom.dashed) { - layer.selectAll('path').style("stroke-dasharray", "12, 3"); + layer.selectAll('path').style("stroke-dasharray", dashed); } else if (geom.dotted) { - layer.selectAll('path').style("stroke-dasharray", "2, 2"); + layer.selectAll('path').style("stroke-dasharray", dotted); + layer.selectAll('path').style("stroke-linecap", "round"); } bindMouseEvents(pathSel, geom, layer);