Skip to content
Merged
6 changes: 6 additions & 0 deletions core/webapp/vis/demo/demo_script.js
Original file line number Diff line number Diff line change
Expand Up @@ -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' }
)
}
}
};
Expand Down
53 changes: 53 additions & 0 deletions core/webapp/vis/src/scale.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The no-arguments case for domain, and range, are probably incorrect, since they are not including the values from the valueMap. However, I am not sure how much this matters, I'm not sure if we rely on the domain/range getters for discrete scales at all.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Though it's hard to say if they're definitely incorrect, since I suppose those are the domain and range for the "fallback" scales used by ValueMapDiscrete. One could argue they could be implemented as such:

domain = Array.from(Object.keys()).concat(domain)
range = Array.from(Object.values).concat(range)

However there would then be no way to tell what part of the domain or range are part of the "fallback" scale.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I went back and forth on these implementation for scale.domain and scale.range. It is possible that the valueMap includes keys that aren't in the actual data being plotted, so I went this route.

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.
Expand Down Expand Up @@ -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';
Expand Down
43 changes: 31 additions & 12 deletions visualization/resources/web/vis/genericChart/genericChartHelper.js
Original file line number Diff line number Diff line change
Expand Up @@ -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));

Expand Down Expand Up @@ -1199,14 +1189,32 @@ 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,
scaleType: 'discrete'
}
}

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 = {};
Expand Down Expand Up @@ -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,
Expand Down