Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
3 changes: 3 additions & 0 deletions core/webapp/vis/src/geom.js
Original file line number Diff line number Diff line change
Expand Up @@ -348,6 +348,9 @@ LABKEY.vis.Geom.ErrorBar = function(config){
this.width = ('width' in config && config.width != null && config.width != undefined) ? config.width : 6;
this.topOnly = config.topOnly ?? false;
this.errorShowVertical = config.showVertical ?? false;
// when true, each segment spans to its neighbors' midpoints so the line reads as one dashed line
// across no-data gaps (calendar axis) instead of disjoint per-point dashes
this.connectAdjacent = config.connectAdjacent ?? false;

return this;
};
Expand Down
83 changes: 80 additions & 3 deletions core/webapp/vis/src/internal/D3Renderer.js
Original file line number Diff line number Diff line change
Expand Up @@ -2021,8 +2021,22 @@ LABKEY.vis.internal.D3Renderer = function(plot) {
// For sequential jitters, keep track of the current count for a given x value
var jitters = {};

if (geom.xScale.scaleType === scaleType.discrete && (geom.position === position.jitter || geom.position === position.sequential)) {
xBinWidth = ((plot.grid.rightEdge - plot.grid.leftEdge) / (geom.xScale.scale.domain().length)) / 2;
const jitterPosition = geom.position === position.jitter || geom.position === position.sequential;
// Only the opt-in time-based continuous axis (calendar mode) gets day-jittered; other
// continuous-x callers (e.g. CDS scatter) keep their prior no-horizontal-jitter behavior.
const timeBasedContinuous = geom.xScale.scaleType === scaleType.continuous && geom.xScale.timeBasedXTick === true;
if (jitterPosition && (geom.xScale.scaleType === scaleType.discrete || timeBasedContinuous)) {
if (geom.xScale.scaleType === scaleType.discrete) {
xBinWidth = ((plot.grid.rightEdge - plot.grid.leftEdge) / (geom.xScale.scale.domain().length)) / 2;
}
else {
// Continuous (time-based) x-axis: size the same-day jitter band by the distinct-day count
// (mirroring the per-date slot half-width) so replicates fan out the same as per-date and
// aren't hidden when real-time spacing squeezes a day into a few pixels. Day centers stay
// at their true time position; only the same-day spread is normalized. The shared helper caps
// that width at the closest actual day spacing so the fan can't reach the neighbouring day.
xBinWidth = LABKEY.vis.calendarSlotWidth(geom.xScale, plot.grid.rightEdge - plot.grid.leftEdge) / 2;
}
xAcc = function(row) {
var x = geom.xAes.getValue(row);
var value = geom.getX(row);
Expand Down Expand Up @@ -2265,6 +2279,66 @@ LABKEY.vis.internal.D3Renderer = function(plot) {
return (isNaN(x) || x == null || isNaN(y) || y == null || isNaN(error) || error == null);
});

// connectAdjacent: render each level (top/bottom) as ONE continuous polyline through the points so a
// dash pattern runs evenly across the whole line, including no-data gaps ("---- ---- ----"). Per-point
// tiled segments restart the dash each segment and read as solid where points are dense, so avoid that.
if (geom.connectAdjacent) {
const strokeColor = typeof colorAcc === 'function' ? (data.length ? colorAcc(data[0]) : '#000000') : colorAcc;
// pixel position of the calendar axis break, so no run spans the guide-set/window discontinuity
const breakOffset = geom.xScale.calendarBreakOffset;
const breakX = breakOffset === null || breakOffset === undefined ? null : geom.xScale.scale(breakOffset);
// Build the line as flat horizontal runs at each constant level: connect consecutive same-y points
// (so the dash spans no-data gaps within a level), but START A NEW SUBPATH whenever the level changes
// (guide-set boundary) or y is undefined (e.g. log scale where mean +/- error <= 0). This avoids a
// diagonal connector between levels and avoids bridging over undefined points, matching the per-point
// bars. Each run extends +/- errorLineWidth at its ends, like the per-date segments.
const buildPath = function(sign) {
let d = '', runStartX = null, runEndX = null, runY = null;
const flush = function() {
if (runY !== null) {
d += 'M' + (runStartX - errorLineWidth) + ',' + runY + ' L' + (runEndX + errorLineWidth) + ',' + runY + ' ';
}
};
for (let k = 0; k < data.length; k++) {
const row = data[k];
const x = xAcc_(row), value = geom.yAes.getValue(row), error = geom.errorAes.getValue(row);
if (value == null || isNaN(x)) {
continue; // no data point that day (e.g. missing-fill row): bridge over it, don't break the level
}
if (breakX !== null && runY !== null && runEndX < breakX && x > breakX) { // crossing the axis break: end the run here
flush();
runStartX = runEndX = runY = null;
}
const y = geom.yScale.scale(value + sign * error);
if (y == null || isNaN(y)) { // defined value but unplottable (log scale, value +/- error <= 0): break here
flush();
runStartX = runEndX = runY = null;
} else if (runY === null) { // start a new run
runStartX = runEndX = x; runY = y;
} else if (y === runY) { // same level: extend the run across the gap
runEndX = x;
} else { // level changed (guide-set boundary): close run, start a new one (no diagonal)
flush();
runStartX = runEndX = x; runY = y;
}
}
flush();
return d;
};
const drawLine = function(cls, sign) {
const lineSel = layer.selectAll('path.' + cls).data([data]);
lineSel.enter().append('path').attr('class', cls);
lineSel.attr('d', buildPath(sign)).attr('stroke', strokeColor).attr('fill', 'none')
.attr('stroke-width', 1).style('stroke-dasharray', '6, 3');
lineSel.exit().remove();
};
drawLine('error-bar-top', 1);
if (!geom.topOnly) {
drawLine('error-bar-bottom', -1);
}
return;
}

const selection = layer.selectAll('.error-bar').data(data);
selection.exit().remove();

Expand Down Expand Up @@ -2532,11 +2606,14 @@ LABKEY.vis.internal.D3Renderer = function(plot) {
};

var _renderPath = function(layer, data, geom, xAcc) {
// don't connect points across the calendar axis break (guide-set block -> date window)
var breakOffset = geom.xScale ? geom.xScale.calendarBreakOffset : null,
breakX = breakOffset === null || breakOffset === undefined ? null : geom.xScale.scale(breakOffset);
var yAcc = function(d) {var val = geom.getY(d); return val == null ? null : val;},
size = geom.sizeAes && geom.sizeScale ? geom.sizeScale.scale(geom.sizeAes.getValue(data)) : function() {return geom.size},
color = geom.color,
line = function(d) {
var path = LABKEY.vis.makePath(d.data, xAcc, yAcc);
var path = LABKEY.vis.makePath(d.data, xAcc, yAcc, breakX);
return path.length == 0 ? null : path;
};
if (geom.pathColorAes && geom.colorScale) {
Expand Down
Loading