diff --git a/core/webapp/vis/demo/demo_script.js b/core/webapp/vis/demo/demo_script.js index 2dd62926b00..eb13bf45e82 100644 --- a/core/webapp/vis/demo/demo_script.js +++ b/core/webapp/vis/demo/demo_script.js @@ -73,6 +73,12 @@ var labResultsPlotConfig = { }, shape: { scaleType: 'discrete' + }, + color: { + scaleType: 'discrete', + scale: LABKEY.vis.Scale.ValueMapDiscrete( + { '249318596 CD4+ (cells/mm3)': 'gray', '249318596 Hemoglobin': 'gray', '249320107 CD4+ (cells/mm3)': 'black', '249320107 Hemoglobin': 'black' } + ) } } }; diff --git a/core/webapp/vis/src/scale.js b/core/webapp/vis/src/scale.js index 49135f2ef0e..41fd1142af9 100644 --- a/core/webapp/vis/src/scale.js +++ b/core/webapp/vis/src/scale.js @@ -27,6 +27,51 @@ LABKEY.vis.Scale.DarkColorDiscrete = function(){ return ["#378a70", "#f34704", "#4b67a6", "#d53597", "#72a124", "#c8a300", "#d19641", "#808080"]; }; +/* + * Alternative to d3.scale.ordinal that allows for a value map to be provided for specific values. + * If a value is not found in the value map, it falls back to the standard behavior of d3.scale.ordinal. + */ +LABKEY.vis.Scale.ValueMapDiscrete = function(valueMap) { + let index = {}; + let domain = []; + let range = []; + + function scale(d) { + let i = index[d]; + if (i === undefined) { + i = domain.push(d) - 1; + index[d] = i; + } + + if (valueMap && valueMap.hasOwnProperty(d)) { + return valueMap[d]; + } + + return range[i % range.length]; + } + + // copied from d3.scale.ordinal + scale.domain = function(_) { + if (!arguments.length) return domain.slice(); + domain = []; + index = {}; + for (const value of _) { + if (index[value] !== undefined) continue; + index[value] = domain.push(value) - 1; + } + return scale; + }; + + // copied from d3.scale.ordinal + scale.range = function(_) { + if (!arguments.length) return range.slice(); + range = Array.from(_); + return scale; + }; + + return scale; +} + /** * Function that returns a discrete scale used to determine the shape of points in {@link LABKEY.vis.Geom.BoxPlot} and * {@link LABKEY.vis.Geom.Point} geoms. @@ -58,6 +103,14 @@ LABKEY.vis.Scale.Shape = function(){ return [circle, triangle, square, diamond, x]; }; +LABKEY.vis.Scale.ShapeMap = { + 'circle': LABKEY.vis.Scale.Shape()[0], + 'triangle': LABKEY.vis.Scale.Shape()[1], + 'square': LABKEY.vis.Scale.Shape()[2], + 'diamond': LABKEY.vis.Scale.Shape()[3], + 'x': LABKEY.vis.Scale.Shape()[4] +}; + LABKEY.vis.Scale.DataspaceShape = function(){ var shape01 = function(s){ return 'M0-2.6c-1.5,0-2.6,1.1-2.6,2.6S-1.4,2.6,0,2.6 c1.5,0,2.6-1.2,2.6-2.6C2.6-1.5,1.5-2.6,0-2.6z M0,1.9c-1.1,0-1.9-0.8-1.9-1.9S-1-1.9,0-1.9C1.1-1.9,1.9-1,1.9,0 C1.9,1.1,1.1,1.9,0,1.9z'; diff --git a/visualization/resources/web/vis/genericChart/genericChartHelper.js b/visualization/resources/web/vis/genericChart/genericChartHelper.js index 5fd704b0604..cb2a4911b81 100644 --- a/visualization/resources/web/vis/genericChart/genericChartHelper.js +++ b/visualization/resources/web/vis/genericChart/genericChartHelper.js @@ -1097,17 +1097,7 @@ LABKEY.vis.GenericChartHelper = new function(){ // if user has set a max but not a min, default to 0 for bar chart scales.y.domain[0] = min; } - } - else if (renderType === 'box_plot' && chartConfig.pointType === 'all') - { - layers.push( - new LABKEY.vis.Layer({ - geom: LABKEY.vis.GenericChartHelper.generatePointGeom(chartConfig.geomOptions), - aes: {hoverText: LABKEY.vis.GenericChartHelper.generatePointHover(chartConfig.measures)} - }) - ); - } - else if (renderType === 'line_plot') { + } else if (renderType === 'line_plot') { var xName = chartConfig.measures.x.name, isDate = isDateType(getMeasureType(chartConfig.measures.x)); @@ -1199,7 +1189,7 @@ LABKEY.vis.GenericChartHelper = new function(){ plotConfig.margins = margins; } - if (chartConfig.measures.color) + if (chartConfig.measures.color || chartConfig.geomOptions.colorPaletteScale) { scales.color = { colorType: chartConfig.geomOptions.colorPaletteScale, @@ -1207,6 +1197,24 @@ LABKEY.vis.GenericChartHelper = new function(){ } } + if (!LABKEY.Utils.isEmptyObj(chartConfig.measuresOptions?.series)) { + const colorValueMap = {}; + const shapeValueMap = {}; + 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 (!LABKEY.Utils.isEmptyObj(colorValueMap)) { + if (!scales.color) scales.color = { scaleType: 'discrete' }; + scales.color.scale = LABKEY.vis.Scale.ValueMapDiscrete(colorValueMap); + } + if (!LABKEY.Utils.isEmptyObj(shapeValueMap)) { + if (!scales.shape) scales.shape = { scaleType: 'discrete' }; + scales.shape.scale = LABKEY.vis.Scale.ValueMapDiscrete(shapeValueMap); + } + } + if ((renderType === 'line_plot' || renderType === 'scatter_plot') && yMeasures.length > 0) { $.each(yMeasures, function (idx, yMeasure) { var layerAes = {}; @@ -1243,6 +1251,17 @@ LABKEY.vis.GenericChartHelper = new function(){ ); } + // render box plot points after box layer so they are not obscured + if (renderType === 'box_plot' && chartConfig.pointType === 'all') + { + layers.push( + new LABKEY.vis.Layer({ + geom: LABKEY.vis.GenericChartHelper.generatePointGeom(chartConfig.geomOptions), + aes: {hoverText: LABKEY.vis.GenericChartHelper.generatePointHover(chartConfig.measures)} + }) + ); + } + plotConfig = $.extend(plotConfig, { clipRect: clipRect, data: data,