fee658e200a700b6ff336bcb5d449892021e5708
max
Fri May 2 10:08:38 2025 -0700
adding one more drawing stage so when plotting gene expression zero values will not interfere with non zero values
diff --git src/cbPyLib/cellbrowser/cbWeb/js/maxPlot.js src/cbPyLib/cellbrowser/cbWeb/js/maxPlot.js
index 942da9a..94ffd6a 100644
--- src/cbPyLib/cellbrowser/cbWeb/js/maxPlot.js
+++ src/cbPyLib/cellbrowser/cbWeb/js/maxPlot.js
@@ -1,3205 +1,3263 @@
'use strict';
// maxPlot: a fast scatter plot class
/*jshint globalstrict: true*/
/*jshint -W069 */
/*jshint -W104 */
/*jshint -W117 */
// TODO:
// fix mouseout into body -> marquee stays
function getAttr(obj, attrName, def) {
var val = obj[attrName];
if (val===undefined)
return def;
else
return val;
}
function cloneObj(d) {
/* returns a deep copy of an object, wasteful and destroys old references */
// see http://stackoverflow.com/questions/122102/what-is-the-most-efficient-way-to-deep-clone-an-object-in-javascript
return JSON.parse(JSON.stringify(d));
}
function isValid(x) {
/* x is not null nor undefined */
return (x!==null && x!==undefined)
}
function cloneArray(a) {
/* returns a copy of an array */
return a.slice();
}
function copyObj(src, trg) {
/* object copying: copies all values from src to trg */
var key;
for (key in src) {
trg[key] = src[key]; // copies each property to the objCopy object
}
}
function debug(msg) {
if (window.doDebug)
console.log(msg);
}
function MaxPlot(div, top, left, width, height, args) {
// a class that draws circles onto a canvas, like a scatter plot
// div is a div DOM element under which the canvas will be created
// top, left: position in pixels, integers
// width and height: integers, in pixels, includes the status line
const HIDCOORD = 12345; // magic value for missing coordinates
// In rare instances, coordinates are saved but should not be shown. This way of implementing hiding
// may look hacky, but it simplifies the logic and improves performance.
// export this special value so other part of the code can use it
this.hiddenCoord = HIDCOORD;
var self = this; // 'this' has two conflicting meanings in javascript.
// I use 'self' to refer to object variables, so I can use 'this' to refer to the caller context
const gTextSize = 16; // size of cluster labels
const gTitleSize = 18; // size of title text
const gStatusHeight = 14; // height of status bar
const gSliderFromBottom = 45; // distance from buttom to top of slider div
const gZoomButtonSize = 30; // size of zoom buttons
const gZoomFromLeft = 10; // position of zoom buttons from left
const gZoomFromBottom = 140; // position of zoom buttons from bottom
const gButtonBackground = "rgb(230, 230, 230, 0.85)" // grey level of buttons
const gButtonBackgroundClicked = "rgb(180, 180, 180, 0.6)"; // grey of buttons when clicked
const gCloseButtonFromRight = 60; // distance of "close" button from right edge
const nonFatColor = "F9F9F9"; // color used in fattening mode for all non-fat cells
const nonFatColorRect = "DDDDDD"; // rectangle mode: color used in fattening mode for all non-fat cells
const nonFatColorCircles = "BBBBBB"; // color used in fattening mode for all non-fat cell circles
// the rest of the initialization is done at the end of this file,
// because the init involves many functions that are not defined yet here
this.initCanvas = function (div, top, left, width, height, args) {
/* initialize a new Canvas */
div.style.top = top+"px";
div.style.left = left+"px";
div.style.position = "absolute";
div.style.display = "block";
self.div = div;
self.gSampleDescription = "cell";
self.ctx = null; // the canvas context
self.canvas = addCanvasToDiv(div, top, left, width, height-gStatusHeight );
self.interact = false;
if (args && args.showClose===true) {
self.closeButton = addChildControls(10, width-gCloseButtonFromRight);
}
if (args===undefined || (args["interact"]!==false)) {
self.interact = true;
addZoomButtons(height-gZoomFromBottom, gZoomFromLeft, self);
addModeButtons(10, 10, self);
addStatusLine(height-gStatusHeight, left, width, gStatusHeight);
addTitleDiv(height-gTitleSize-gStatusHeight-4, 8);
/* add the div used for the mouse selection/zoom rectangle to the DOM */
var selectDiv = document.createElement('div');
selectDiv.id = "mpSelectBox";
selectDiv.style.border = "1px dotted black";
selectDiv.style.position = "absolute";
selectDiv.style.display = "none";
selectDiv.style.pointerEvents = "none";
self.div.appendChild(selectDiv);
// callbacks when user clicks or hovers over label or cell
self.onLabelClick = null; // called on label click, args: text of label and event
self.onCellClick = null; // called on cell click, args: array of cellIds and event
self.onCellHover = null; // called on cell hover, arg: array of cellIds
self.onNoCellHover = null; // called on hover over empty background
self.onSelChange = null; // called when the selection has been changed, arg: array of cell Ids
self.onLabelHover = null; // called when mouse hovers over a label
self.onNoLabelHover = null; // called when mouse does not hover over a label
self.onLineHover = null; // called when mouse over a trajectory line
self.onRadiusAlphaChange = null; // called when user changes radius or alpha
// self.onZoom100Click: called when user clicks the zoom100 button. Implemented below.
self.selectBox = selectDiv; // we need this later
self.setupMouse();
// connected plots
self.childPlot = null; // plot that is syncing from us, see split()
self.parentPlot = null; // plot that syncs to us, see split()
}
addProgressBars(top+Math.round(height*0.3), left+30);
if (!args || args.showSliders===undefined || args.showSliders===true)
addSliders();
// timer that is reset on every mouse move
self.timer = null;
}
function isHidden(x, y) {
/* special coords are used for circles that are off-screen or otherwise not visible */
return ((x===HIDCOORD && y===HIDCOORD)) // not shown (e.g. no coordinate or off-screen)
}
function hexToGrey(hexColors) {
let greyArray = []
for (let i = 0; i < hexColors.length; i++) {
// Extract red, green, and blue components
let hexColor = hexColors[i];
const maxCol = 200;
const addCol = 20;
const red = Math.min(maxCol, addCol+parseInt(hexColor.slice(1, 3), 16));
const green = Math.min(maxCol, addCol+parseInt(hexColor.slice(3, 5), 16));
const blue = Math.min(maxCol, addCol+parseInt(hexColor.slice(5, 7), 16));
// Calculate the grayscale value using the luminosity method
const gray = Math.round(0.2126 * red + 0.7152 * green + 0.0722 * blue);
// Convert the grayscale value to a two-character hex string
const grayHex = gray.toString(16).padStart(2, '0');
// Return the grayscale hex color
const greySixHex = `${grayHex}${grayHex}${grayHex}`;
greyArray.push(greySixHex);
}
return greyArray;
}
this.initPort = function(args) {
/* init all viewport related state (zoom, radius, alpha) */
self.port = {};
self.port.zoomRange = {}; // object with keys minX, , maxX, minY, maxY, in data units
self.port.radius = getAttr(args, "radius", null); // current radius of the circles, 0=one pixel dots
// we keep a copy of the 'initial' arguments at 100% zoom
self.port.initZoom = {};
self.port.initRadius = self.port.radius; // circle radius at full zoom
self.port.initAlpha = getAttr(args, "alpha", 0.3);
};
this.initPlot = function(args) {
/* create a new scatter plot on the canvas */
if (args===undefined)
args = {};
self.scalingDone = false;
self.globalOpts = args;
self.mode = 1; // drawing mode
// everything related to circle coordinates
self.coords = {};
self.coords.orig = null; // coordinates of cells in original coordinates
self.coords.labels = null; // cluster label positions in pixels, array of [x,y,text]
self.coords.px = null; // coordinates of cells and labels as screen pixels or (HIDCOORD,HIDCOORD) if not shown
self.coords.labelBbox = null; // cluster label bounding boxes, array of [x1,x2,x2,y2]
self.col = {};
self.col.pal = null; // list of six-digit hex codes
self.col.arr = null; // length is coords.px/2, one byte per cell = index into self.col.pal
self.selCells = new Set(); // IDs of cells that are selected (drawn in black)
self.fatIdx = null; // Index of value that is in "fat mode" (=cells bigger, all other cells in light-grey)
self.doDrawLabels = true; // should cluster labels be drawn?
self.initPort(args);
// mouse drag mode: can be "select", "move" or "zoom"
self.dragMode = "select";
// for zooming and panning
self.mouseDownX = null;
self.mouseDownY = null;
self.panCopy = null;
// to detect if user just clicked on a dot
self.dotClickX = null;
self.dotClickY = null;
// the background image for spatial mode
self.background = null;
self.activateMode(getAttr(args, "mode", "move"));
};
this.clear = function() {
clearCanvas(self.ctx, self.canvas.width, self.canvas.height);
};
this.setTitle = function (text) {
self.title = text;
self.titleDiv.innerHTML = text;
};
this.activateSliders = function () {
$(self.alphaSlider).slider({
"value": 4,
"min" : 1,
"max" : 7,
"step" : 1,
"slide": onChangeAlpha
});
$(self.radiusSlider).slider({
"value": 4,
"min" : 1,
"max" : 7,
"step" : 1,
"slide": onChangeRadius
});
}
this.setWatermark = function (text) {
if (text==="" && self.watermark) {
self.watermark.parentNode.removeChild(self.watermark);
self.watermark = undefined;
return;
}
if (self.watermark)
self.watermark.parentNode.removeChild(self.watermark);
var elem = document.createElement('div');
elem.id = "tpWatermark";
elem.style.cssText = 'pointer-events: none;position: absolute; width: 1000px; opacity: 0.5; top: 10px; left: 45px; text-align: left; vertical-align: top; color: black; font-size: 20px; font-weight:bold; font-style:oblique';
elem.textContent = text;
self.div.appendChild(elem);
self.watermark = elem;
}
// -- (private) helper functions
// -- these are normal functions, not methods, they do not access "self"
function gebi(idStr) {
return document.getElementById(idStr);
}
function removeElById(idStr) {
var el = gebi(idStr);
if (el!==null) {
el.parentNode.removeChild(el);
}
}
function activateTooltip(selector) {
/* uses bootstrap tooltip. Use noconflict in html, I had to rename BS's tooltip to avoid overwrite by jquery
*/
if (window.jQuery && $.fn.bsTooltip!==undefined) {
var ttOpt = {"html": true, "animation": false, "delay":{"show":400, "hide":100}, container:"body"};
$(selector).bsTooltip(ttOpt);
}
}
function guessRadius(coordCount) {
/* a few rules to find a good initial radius, depending on the number of dots */
if (coordCount > 50000)
return 0;
else if (coordCount > 10000)
return 2;
else if (coordCount > 4000)
return 4;
else
return 5;
}
function createSliderSpan(id, width, height, left) {
/* create div with given width and height */
var div = document.createElement('span');
div.id = id;
div.style.position = "relative";
div.style.width = width+"px";
div.style.height = height+"px";
div.style.left = left+"px";
return div;
}
function createButton(width, height, id, title, text, imgFname, paddingTop, paddingBottom, addSep, addThickSep, fontSize) {
/* make a light-grey div that behaves like a button, with text and/or an image on it
* Images are hard to vertically center, so padding top can be specified.
* */
var div = document.createElement('div');
div.id = id;
div.className = "mpButton";
div.style.backgroundColor = gButtonBackground;
div.style.width = width+"px";
div.style.height = height+"px";
div.style["z-index"]="10";
div.style["text-align"]="center";
div.style["vertical-align"]="middle";
div.style["line-height"]=height+"px";
if (fontSize === undefined || fontSize=== null) {
if (text!==null)
if (text.length>3)
div.style["font-size"]="11px";
else
div.style["font-size"]="14px";
} else {
div.style["font-size"]=fontSize+"px";
}
div.style["font-weight"]="bold";
div.style["font-family"]="sans-serif";
if (title!==null)
div.title = title;
if (text!==null)
div.innerHTML = text;
if (imgFname!==null && imgFname!==undefined) {
var img = document.createElement('img');
img.src = imgFname;
if (paddingTop!==null && paddingTop!==undefined) {
img.style.paddingTop = paddingTop+"px";
if (paddingBottom)
img.style.paddingBottom = paddingBottom+"px";
}
div.appendChild(img);
}
if (addSep===true)
div.style["border-bottom"] = "1px solid #D7D7D7";
if (addThickSep===true)
div.style["border-bottom"] = "2px solid #C7C7C7";
// make color dark grey when mouse is pressed
div.addEventListener("mousedown", function() {
this.style.backgroundColor = gButtonBackgroundClicked;
});
div.addEventListener("mouseup", function() {
this.style.backgroundColor = gButtonBackground;
});
return div;
}
function makeCtrlContainer(top, left) {
/* make a container for half-transprent ctrl buttons over the canvas */
var ctrlDiv = document.createElement('div');
ctrlDiv.id = "mpCtrls";
ctrlDiv.style.position = "absolute";
ctrlDiv.style.left = left+"px";
ctrlDiv.style.top = top+"px";
ctrlDiv.style["border-radius"]="2px";
ctrlDiv.style["cursor"]="pointer";
ctrlDiv.style["box-shadow"]="0px 2px 4px rgba(0,0,0,0.3)";
ctrlDiv.style["border-top-left-radius"]="2px";
ctrlDiv.style["border-top-right-radius"]="2px";
ctrlDiv.style["user-select"]="none";
return ctrlDiv;
}
function addZoomButtons(top, left, self) {
/* add the plus/minus buttons to the DOM and place at position x,y on the screen */
var width = gZoomButtonSize;
var height = gZoomButtonSize;
var plusDiv = createButton(width, height, "mpCtrlZoomPlus", "Zoom in. Keyboard: +", "+", null, null, null, true);
//plusDiv.style["border-bottom"] = "1px solid #D7D7D7";
var fullDiv = createButton(width, height, "mpCtrlZoom100", "Zoom in. Keyboard: space", "100%", null, null, null, true);
//full.style["border-bottom"] = "1px solid #D7D7D7";
var minusDiv = createButton(width, height, "mpCtrlZoomMinus", "Zoom out. Keyboard: -", "-");
var ctrlDiv = makeCtrlContainer(top, left);
ctrlDiv.appendChild(plusDiv);
ctrlDiv.appendChild(fullDiv);
ctrlDiv.appendChild(minusDiv);
self.zoomDiv = ctrlDiv;
self.div.appendChild(ctrlDiv);
minusDiv.addEventListener('click', function() { self.zoomBy(0.75); self.drawDots(); });
fullDiv.addEventListener('click', function() { self.zoom100(); self.drawDots()});
plusDiv.addEventListener('click', function() { self.zoomBy(1.333); self.drawDots(); });
}
function addTitleDiv(top, left) {
var div = document.createElement('div');
div.className = "tpTitle";
div.style.top = top+"px";
div.style.left = left+"px";
div.style.fontSize = gTitleSize;
div.id = 'mpTitle';
self.div.appendChild(div);
self.titleDiv = div;
}
function onChangeAlpha(ev, ui) {
console.log("alpha: "+ui.value);
var sliderVal = ui.value; // 1-7
var multMap = {
7 : 0.15,
6 : 0.4,
5 : 0.8,
4 : 1.0,
3 : 1.2,
2 : 1.5,
1 : 1.8
}
self.port.alphaMult = multMap[sliderVal];
console.log("alphaMult: "+self.port.alphaMult);
self.calcRadius();
self.drawDots();
}
function onChangeRadius(ev, ui) {
//console.log("radius: "+ui.value);
var sliderVal = ui.value; // 1-7
var multMap = {
1 : 1/3,
2 : 1/2,
3 : 1/1.5,
4 : 1.0,
5 : 1.5,
6 : 2.0,
7 : 3.0
}
self.port.radiusMult = multMap[sliderVal];
self.calcRadius();
self.drawDots();
}
function addSliders() {
/* add sliders for transparency and radius */
// alpha reset slider: a label, a slider + a reset button
var sliderWidth = 90;
//var fromLeft = canvWidth - sliderWidth - 2*45 - 50;
var alphaSlider = createSliderSpan("mpAlphaSlider", sliderWidth, 10, 35);
self.alphaSlider = alphaSlider; // see activateSliders() for the jquery UI part of the code, executed later
alphaSlider.style.float = "left";
//alphaSlider.style.top = "3px";
// container for label + control elements
var alphaCont = document.createElement('div');
alphaCont.id = "mpAlphaCont";
//alphaCont.style.left = "150px"; // cellbrowser.css defines grid widths: 45
alphaCont.className = "sliderContainer";
alphaCont.style.top = "15px";
alphaCont.style.left = "0px";
var alphaLabel = document.createElement('div'); // contains the slider and the reset button, floats right
alphaLabel.id = "alphaSliderLabel";
alphaLabel.textContent = "Transparency";
alphaLabel.className = "sliderLabel";
// reset button
var undoSvg = '';
//var alphaReset = createButton(15, 15, "mpAlphaReset", "Reset transparency", undoSvg, null, null, null, false, false, 10);
//alphaReset.style.float = "right";
//alphaReset.style.marginLeft = "2px";
//alphaReset.addEventListener ('click', function() { self.resetAlpha(); self.drawDots();}, false);
var sliderReset = createButton(15, 15, "mpSliderReset", "Reset transparency and circle size", undoSvg, null, null, null, false, false, 10);
sliderReset.style.backgroundColor = "transparent";
sliderReset.style.float = "right";
//sliderReset.style.lineHeight = "16px";
sliderReset.style.marginLeft = "10px";
sliderReset.style.top = "0px";
sliderReset.style.top = "0px";
sliderReset.style.position = "relative";
sliderReset["z-index"] = "10"; // ? why ?
sliderReset.addEventListener ('click', function() { self.resetAlpha(); self.resetRadius(); self.drawDots()}, false);
alphaCont.appendChild(alphaLabel);
alphaCont.appendChild(alphaSlider);
//alphaCont.appendChild(alphaReset);
alphaCont.appendChild(sliderReset);
// Radius reset slider: label, slider and reset button
var radiusSlider = createSliderSpan("mpRadiusSlider", sliderWidth, 10, 35);
radiusSlider.style.float = "left";
self.radiusSlider = radiusSlider; // see activateSliders() for the jquery UI part of the code, executed later
// container for label + slider and reset button
var radiusCont = document.createElement('span');
radiusCont.className = "sliderContainer";
radiusCont.id = "mpRadiusDiv";
radiusCont.style.left = "0px";
radiusCont.style.top = "0px";
radiusCont.appendChild(radiusSlider)
var radiusLabel = document.createElement('span'); // contains the slider and the reset button, floats right
radiusLabel.id = "radiusSliderLabel";
radiusLabel.textContent = "Circle Size";
radiusLabel.style.width = "110px";
radiusLabel.className = "sliderLabel";
radiusCont.appendChild(radiusLabel);
radiusCont.appendChild(radiusSlider);
//radiusCont.appendChild(sliderReset);
var brEl = document.createElement('br');
radiusCont.appendChild(brEl);
// add both to the big container div that holds all three slider elements
var sliderDiv = document.createElement('span');
//sliderDiv.style.top = fromTop+"px";
//sliderDiv.style.left = fromLeft+"px";
sliderDiv.style.bottom = "28px";
sliderDiv.style.right = "200px";
sliderDiv.style.position = "absolute";
sliderDiv.style.zIndex = "10";
sliderDiv.id = "mpSliderDiv";
sliderDiv.appendChild(radiusCont);
sliderDiv.appendChild(alphaCont);
self.div.appendChild(sliderDiv);
//self.canvasDiv.appendChild(sliderDiv);
self.sliderDiv = sliderDiv; // for quickResize()
}
function addCloseButton(top, left) {
/* add close button and sync checkbox */
var div = document.createElement('div');
div.style.cursor = "default";
div.style.left = left+"px";
div.style.top = top+"px";
div.style.display = "block";
div.style.position = "absolute";
div.style.fontSize = gTitleSize;
div.style.padding = "3px";
div.style.borderRadius = "3px";
div.style.border = "1px solid #c5c5c5";
div.style.backgroundColor = "#f6f6f6";
div.style.color = "#454545";
div.id = 'mpCloseButton';
div.textContent = "Close";
self.div.appendChild(div);
return div;
}
function addChildControls(top, left) {
addCloseButton(top, left);
}
function appendButton(parentDiv, id, title, imgName) {
/* add a div styled like a button under div */
var div = document.createElement('div');
div.title = title;
div.id = id;
}
function addModeButtons(top, left, self) {
/* add the zoom/move/select control buttons to the DOM */
var ctrlDiv = makeCtrlContainer(top, left);
var bSize = gZoomButtonSize;
var selectButton = createButton(bSize, bSize, "mpIconModeSelect", "Select mode. Keyboard: shift or s", null, "img/select.png", 0, 4, true, true);
selectButton.addEventListener ('click', function() { self.activateMode("select")}, false);
var zoomButton = createButton(bSize, bSize, "mpIconModeZoom", "Zoom-to-rectangle mode. Keyboard: Windows/Command or z", null, "img/zoom.png", 4, 4, true);
zoomButton.addEventListener ('click', function() { self.activateMode("zoom")}, false);
var moveButton = createButton(bSize, bSize, "mpIconModeMove", "Move mode. Keyboard: Alt or m", null, "img/move.png", 4, 4);
moveButton.addEventListener('click', function() { self.activateMode("move");}, false);
self.icons = {};
self.icons["move"] = moveButton;
self.icons["select"] = selectButton;
self.icons["zoom"] = zoomButton;
//ctrlDiv.innerHTML = htmls.join("");
ctrlDiv.appendChild(moveButton);
ctrlDiv.appendChild(selectButton);
ctrlDiv.appendChild(zoomButton);
self.div.appendChild(ctrlDiv);
self.toolDiv = ctrlDiv;
activateTooltip('.mpIconButton');
}
function setStatus(text) {
self.statusLine.innerHTML = text;
}
function addStatusLine(top, left, width, height) {
/* add a status line div */
var div = document.createElement('div');
div.id = "mpStatus";
div.style.backgroundColor = "rgb(240, 240, 240)";
div.style.position = "absolute";
div.style.top = top+"px";
//div.style.left = left+"px";
div.style.width = width+"px";
div.style.height = height+"px";
div.style["border-left"]="1px solid #DDD";
div.style["border-right"]="1px solid #DDD";
div.style["border-top"]="1px solid #DDD";
div.style["font-size"]=(gStatusHeight-1)+"px";
div.style["cursor"]="pointer";
div.style["font-family"] = "sans-serif";
self.div.appendChild(div);
self.statusLine = div;
}
function addProgressBars(top, left) {
/* add the progress bar DIVs to the DOM */
var div = document.createElement('div');
div.id = "mpProgressBars";
div.style.top = top+"px";
div.style.left = left+"px";
div.style.position = "absolute";
var htmls = [];
for (var i=0; i<3; i++) {
htmls.push('
');
htmls.push('');
htmls.push('
Loading...
');
htmls.push('
');
}
div.innerHTML = htmls.join("");
self.div.appendChild(div);
}
function addCanvasToDiv(div, top, left, width, height) {
/* add a canvas element to the body element of the current page and keep left/top/width/eight in self */
var canv = document.createElement('canvas');
self.canvasDiv = canv;
canv.id = 'mpCanvas';
//canv.style.border = "1px solid #AAAAAA";
canv.style.backgroundColor = "white";
canv.style.position = "relative";
canv.style.display = "block";
canv.style.width = width+"px";
canv.style.height = height+"px";
//canv.style.top = top+"px";
//canv.style.left = left+"px";
// No scaling = one unit on screen is one pixel. Essential for speed.
canv.width = width;
canv.height = height;
// need to keep these as ints, need them all the time
self.width = width;
self.height = height;
self.top = top; // location of the canvas in pixels
self.left = left;
div.appendChild(canv); // adds the canvas to the div element
self.canvas = canv;
// alpha:false recommended by https://developer.mozilla.org/en-US/docs/Web/API/Canvas_API/Tutorial/Optimizing_canvas
self.ctx = self.canvas.getContext("2d", { alpha: false });
// by default, the canvas background is transparent+black
// we use alpha=false, so we need to initialize the canvas with white pixels
clearCanvas(self.ctx, width, height);
return canv;
}
function scaleLabels(labels, zoomRange, borderSize, winWidth, winHeight) {
/* scale cluster label position to pixel coordinates */
if (labels===undefined)
return undefined;
winWidth = winWidth-(2*borderSize);
winHeight = winHeight-(2*borderSize);
var minX = zoomRange.minX;
var maxX = zoomRange.maxX;
var minY = zoomRange.minY;
var maxY = zoomRange.maxY;
var spanX = maxX - minX;
var spanY = maxY - minY;
var xMult = winWidth / spanX;
var yMult = winHeight / spanY;
// scale the label coords
var pxLabels = [];
for (var i = 0; i < labels.length; i++) {
var annot = labels[i];
var x = annot[0];
var y = annot[1];
var text = annot[2];
// XX ignore anything outside of current zoom range. Performance?
if (isHidden(x,y) || (x < minX) || (x > maxX) || (y < minY) || (y > maxY)) {
pxLabels.push(null);
}
else {
var xPx = Math.round((x-minX)*xMult)+borderSize;
var yPx = winHeight - Math.round((y-minY)*yMult)+borderSize;
pxLabels.push([xPx, yPx, text]);
}
}
return pxLabels;
}
function constrainVal(x, min, max) {
/* if x is not in range min, max, limit to min or max */
if (x < min)
return min;
if (x > max)
return max;
return x;
}
function scaleLines(lines, zoomRange, winWidth, winHeight) {
/* scale an array of (x1, y1, x2, y2), cutting lines at the screen edges */
var minX = zoomRange.minX;
var maxX = zoomRange.maxX;
var minY = zoomRange.minY;
var maxY = zoomRange.maxY;
var spanX = maxX - minX;
var spanY = maxY - minY;
var xMult = winWidth / spanX;
var yMult = winHeight / spanY;
// transform from data floats to screen pixel coordinates
var pxLines = [];
for (var lineIdx = 0; lineIdx < lines.length; lineIdx++) {
var line = lines[lineIdx];
var x1 = line[0];
var y1 = line[1];
var x2 = line[2];
var y2 = line[3];
var startInvis = ((x1 < minX) || (x1 > maxX) || (y1 < minY) || (y1 > maxY));
var endInvis = ((x2 < minX) || (x2 > maxX) || (y2 < minY) || (y2 > maxY));
// line is entirely hidden
if (startInvis && endInvis)
continue;
if (startInvis) {
x1 = constrainVal(x1, minX, maxX);
y1 = constrainVal(y1, minY, maxY);
}
if (endInvis) {
x2 = constrainVal(x2, minX, maxX);
y2 = constrainVal(y2, minY, maxY);
}
var x1Px = Math.round((x1-minX)*xMult);
var y1Px = winHeight - Math.round((y1-minY)*yMult);
var x2Px = Math.round((x2-minX)*xMult);
var y2Px = winHeight - Math.round((y2-minY)*yMult);
pxLines.push( [x1Px, y1Px, x2Px, y2Px] );
}
return pxLines;
}
function scaleCoords(coords, borderSize, zoomRange, winWidth, winHeight, annots, aspectRatio) {
/* scale list of [x (float),y (float)] to integer pixels on screen and
* annots is an array with on-screen annotations in the format (x, y,
* otherInfo) that is also scaled. return [array of (x (int), y (int)),
* scaled annots array]. Take into account the current zoom range. *
* Canvas origin is top-left, but usually plotting origin is bottom-left,
* so also flip the Y axis. sets invisible coords to HIDCOORD
* */
if (coords===null)
return;
console.time("scale");
var minX = zoomRange.minX;
var maxX = zoomRange.maxX;
var minY = zoomRange.minY;
var maxY = zoomRange.maxY;
var spanX = maxX - minX;
var spanY = maxY - minY;
//if (aspectRatio) {
//xMult = Math.min(xMult, yMult);
//yMult = Math.min(xMult, yMult);
//let ratio = spanX / spanY;
//spanY = spanY*0.5;
//}
winWidth = winWidth-(2*borderSize);
winHeight = winHeight-(2*borderSize);
var xMult = winWidth / spanX;
var yMult = winHeight / spanY;
// transform from data floats to screen pixel coordinates
var pixelCoords = new Uint16Array(coords.length);
for (var i = 0; i < coords.length/2; i++) {
var x = coords[i*2];
var y = coords[i*2+1];
// set everything outside of current zoom range to hidden
if ((x < minX) || (x > maxX) || (y < minY) || (y > maxY)) {
pixelCoords[2*i] = HIDCOORD; // see isHidden()
pixelCoords[2*i+1] = HIDCOORD;
}
else {
var xPx = Math.round((x-minX)*xMult)+borderSize;
// our y-axis is flipped compared to matplotlib/R, so we do winHeight - pixel value
// to make sure that our plot looks like the figures in the papers
var yPx = winHeight - Math.round((y-minY)*yMult)+borderSize;
pixelCoords[2*i] = xPx;
pixelCoords[2*i+1] = yPx;
}
}
console.timeEnd("scale");
return pixelCoords;
}
function drawRect(ctx, pxCoords, coordColors, colors, radius, alpha, selCells, fatIdx) {
/* draw not circles but tiny rectangles. Maybe good enough for 2pixels sizes */
debug("Drawing "+coordColors.length+" rectangles, with fillRect");
ctx.save();
ctx.globalAlpha = alpha;
var dblSize = 2*radius;
var count = 0;
if (selCells.size!==0 || fatIdx!==null)
//colors = makeAllGreyHex(colors.length);
colors = hexToGrey(colors);
var fatCells = [];
+ // first draw all the cells with value 0. Poor mans approximation of a z index.
+ // (Should be faster than sorting by z and drawing afterwards.)
for (var i = 0; i < pxCoords.length/2; i++) {
var pxX = pxCoords[2*i];
var pxY = pxCoords[2*i+1];
if (isHidden(pxX, pxY))
continue;
var valIdx = coordColors[i];
+ // only plot color 0
+ if (valIdx!==0)
+ continue;
+
+ var col = colors[valIdx];
+ if (fatIdx!==null) {
+ if (valIdx===fatIdx) {
+ // fattened cells must be overdrawn later, so just save their coords now
+ fatCells.push(pxX);
+ fatCells.push(pxY);
+ continue
+ }
+ }
+ ctx.fillStyle="#"+col;
+ ctx.fillRect(pxX-radius, pxY-radius, dblSize, dblSize);
+ count++;
+ }
+
+ // then draw all the cells with value <> 0.
+ for (var i = 0; i < pxCoords.length/2; i++) {
+ var pxX = pxCoords[2*i];
+ var pxY = pxCoords[2*i+1];
+ if (isHidden(pxX, pxY))
+ continue;
+
+ var valIdx = coordColors[i];
+ // only plot color != 0
+ if (valIdx===0)
+ continue;
+
var col = colors[valIdx];
if (fatIdx!==null) {
if (valIdx===fatIdx) {
// fattened cells must be overdrawn later, so just save their coords now
fatCells.push(pxX);
fatCells.push(pxY);
continue
}
//else
//col = "DDDDDD";
//col = nonFatColorRect;
}
ctx.fillStyle="#"+col;
ctx.fillRect(pxX-radius, pxY-radius, dblSize, dblSize);
count++;
}
// overdraw the selection as black rectangles on top
//if (fatIdx===null) {
ctx.globalAlpha = 0.7;
ctx.fillStyle="black";
selCells.forEach(function(cellId) {
let pxX = pxCoords[2*cellId];
let pxY = pxCoords[2*cellId+1];
ctx.fillRect(pxX-radius, pxY-radius, dblSize, dblSize);
count += 1;
})
//}
// overdraw the fattened cells as blue rectangles on top
if (fatCells.length!==0) {
ctx.globalAlpha = 0.7;
ctx.fillStyle="blue";
for (var i = 0; i < fatCells.length/2; i++) {
var pxX = fatCells[2*i];
var pxY = fatCells[2*i+1];
ctx.fillRect(pxX-radius, pxY-radius, dblSize, dblSize);
count++;
}
}
debug(count+" rectangles drawn (including selection+fattening)");
ctx.restore();
return count;
}
function drawCirclesStupid(ctx, pxCoords, coordColors, colors, radius, alpha, selCells, fatIdx) {
/* TOO SLOW - Only used in testing/for demos. Not used anywhere else. Draw circles onto canvas with very slow functions.. */
debug("Drawing "+coordColors.length+" circles with stupid renderer");
ctx.globalAlpha = alpha;
var dblSize = 2*radius;
var count = 0;
for (var i = 0; i < pxCoords.length/2; i++) {
var pxX = pxCoords[2*i];
var pxY = pxCoords[2*i+1];
if (isHidden(pxX, pxY))
continue;
var valIdx = coordColors[i];
var col = colors[valIdx];
if (fatIdx!==null && valIdx!==fatIdx)
col = nonFatColor;
ctx.fillStyle="#"+col;
ctx.beginPath();
ctx.arc(pxX, pxY, radius, 0, 2 * Math.PI);
ctx.closePath();
ctx.fill();
count++;
}
return count;
}
function intersectRect(r1left, r1right, r1top, r1bottom, r2left, r2right, r2top, r2bottom) {
/* return true if two rectangles overlap,
https://stackoverflow.com/questions/2752349/fast-rectangle-to-rectangle-intersection
*/
return !(r2left > r1right || r2right < r1left || r2top > r1bottom || r2bottom < r1top);
}
function drawLines(ctx, pxLines, width, height, attrs) {
/* draw lines defined by array with (x1, y1, x2, y2) arrays.
* color is a CSS name, so usually prefixed by # if a hexcode
* width is the width in pixels.
* */
ctx.save();
//ctx.globalAlpha = 1.0;
ctx.strokeStyle = attrs.lineColor || "#888888";
ctx.lineWidth = attrs.lineWidth || 3;
ctx.globalAlpha = attrs.lineAlpha || 0.5;
//ctx.miterLimit =2;
//ctx.strokeStyle = "rgba(200, 200, 200, 0.3)";
for (var i=0; i < pxLines.length; i++) {
var line = pxLines[i];
var x1 = line[0];
var y1 = line[1];
var x2 = line[2];
var y2 = line[3];
ctx.beginPath();
ctx.moveTo(x1, y1);
ctx.lineTo(x2, y2);
ctx.stroke();
}
ctx.restore();
}
function drawLabelsSvg(svgLines, labelCoords, winWidth, winHeight, zoomFact) {
/* given an array of [x, y, text], draw the text. returns bounding
* boxes as array of [x1, y1, x2, y2] */
if (labelCoords===undefined)
return undefined;
for (var i=0; i < labelCoords.length; i++) {
var coord = labelCoords[i];
if (coord===null) { // outside of view range, push a null to avoid messing up the order of bboxArr
continue;
}
var x = coord[0];
var y = coord[1];
var text = coord[2];
// don't draw labels where the midpoint is off-screen
if (x<0 || y<0 || x>winWidth || y>winHeight) {
continue;
}
svgLines.push(""+text+"");
}
}
self.drawLegendSvg = function (legend) {
/* draw a legend onto the SVG given a legend object. */
var legWidth = self.svgLabelWidth;
var svgLines = self.svgLines;
var rows = legend.rows;
var legTitle = legend.title;
var subTitle = legend.subTitle;
var left = self.canvas.width; // x position where legend starts
var lineHeight = gTextSize;
var x = left + 11;
var y = lineHeight;
svgLines.push(""+legTitle+"");
y += lineHeight;
if (subTitle) {
svgLines.push(""+subTitle+"");
y += lineHeight;
}
y += lineHeight;
// get the sum of all rows, to calculate frequency
// this code was copied from buildLegend -> refactor one day
var sum = 0;
for (var i = 0; i < rows.length; i++) {
let count = rows[i].count;
sum += count;
}
for (i = 0; i < rows.length; i++) {
// a lot was copied from cellBrowser:buildLegend(), could use some refactoring to reduce duplication
var row = rows[i];
var colorHex = row.color; // manual color
if (colorHex===null)
colorHex = row.defColor; // default color
var label = row.label;
var longLabel = row.longLabel;
let count = row.count;
var valueIndex = row.intKey;
var freq = 100*count/sum;
if (count===0) // never output categories with 0 count.
continue;
// this was copied from cellbrowser:buildLegend - refactor soon
label = label.replace(/_/g, " ").replace(/'/g, "'").trim();
if (label==="") {
label = "(empty)";
}
label = label.replace("–", "-");
// draw colored rectangle first
var textSize = gTextSize-3;
svgLines.push("");
var prec = 1;
if (freq<1)
prec = 2;
//var lineCount = 0;
//svgLines.push(""+label+"");
svgLines.push(""+label+"");
//}
svgLines.push(""+freq.toFixed(prec)+"%");
y+= textSize;
}
// cannot draw violin plots in SVG - no library for it
};
function drawLabels(ctx, labelCoords, winWidth, winHeight, zoomFact, doGrey) {
/* given an array of [x, y, text], draw the text. returns bounding
* boxes as array of [x1, y1, x2, y2] */
if (labelCoords===undefined)
return undefined;
console.time("labels");
ctx.save();
ctx.font = "bold "+gTextSize+"px Sans-serif"
ctx.globalAlpha = 1.0;
//ctx.strokeStyle = '#EEEEEE';
if (doGrey===undefined) {
ctx.strokeStyle = "rgba(200, 200, 200, 0.3)";
ctx.lineWidth = 5;
ctx.miterLimit =2;
}
//else
//ctx.strokeStyle = "rgba(20, 20, 20, 0.3)";
ctx.textBaseline = "top";
if (doGrey===undefined) {
ctx.fillStyle = "rgba(0,0,0,0.8)";
ctx.shadowBlur=6;
ctx.shadowColor="white";
}
else
ctx.fillStyle = "rgba(0,0,0,1.0)";
ctx.textAlign = "left";
var addMargin = 1; // how many pixels to extend the bbox around the text, make clicking easier
var bboxArr = []; // array of click hit boxes
for (var i=0; i < labelCoords.length; i++) {
var coord = labelCoords[i];
if (coord===null) { // outside of view range, push a null to avoid messing up the order of bboxArr
bboxArr.push( null );
continue;
}
var x = coord[0];
var y = coord[1];
var text = coord[2];
var textWidth = Math.round(ctx.measureText(text).width);
// move x to the left, so text is centered on x
x = x - Math.round(textWidth*0.5);
var textX1 = x;
var textY1 = y;
var textX2 = Math.round(x+textWidth);
var textY2 = y+gTextSize;
// don't draw labels where the midpoint is off-screen
if (x<0 || y<0 || x>winWidth || y>winHeight) {
bboxArr.push( null );
continue;
}
ctx.strokeText(text,x,y);
ctx.fillText(text,x,y);
bboxArr.push( [textX1-addMargin, textY1-addMargin, textX2+addMargin, textY2+addMargin] );
}
ctx.restore();
console.timeEnd("labels");
return bboxArr;
}
// function drawLabels_dom(ctx, labelCoords, isFull) {
// /* given an array of [x, y, text], draw the text. returns bounding boxes as array of [x1, y1, x2, y2] */
// for (var i=0; i < labelCoords.length; i++) {
// var coord = labelCoords[i];
// var x = coord[0];
// var y = coord[1];
// var text = coord[2];
// var div = document.createElement('div');
// div.id = id;
// //div.style.border = "1px solid #DDDDDD";
// div.style.backgroundColor = "rgb(230, 230, 230, 0.6)";
// div.style.width = width+"px";
// div.style.height = height+"px";
// div.style["text-align"]="center";
// div.style["vertical-align"]="middle";
// div.style["line-height"]=height+"px";
// }
// ctx.restore();
// return bboxArr;
// }
// https://stackoverflow.com/questions/5560248/programmatically-lighten-or-darken-a-hex-color-or-rgb-and-blend-colors
function shadeColor(color, percent) {
var f=parseInt(color,16),t=percent<0?0:255,p=percent<0?percent*-1:percent,R=f>>16,G=f>>8&0x00FF,B=f&0x0000FF;
return (0x1000000+(Math.round((t-R)*p)+R)*0x10000+(Math.round((t-G)*p)+G)*0x100+(Math.round((t-B)*p)+B)).toString(16).slice(1);
}
function makeCircleTemplates(radius, tileWidth, tileHeight, colors, fatIdx) {
/* create an off-screen-canvas with the circle-templates that will be stamped later onto the bigger canvas
* Returns the canvas. This feels very much like sprites on the AMIGA in the 1980s.
*
* returns an object with these attributes:
* .off = off-screen canvas
* .selImgIdx = index on off with circle with a black outline only, for the selection
* .greyImgIdx= index on off with grey circle for the non-selected cells
* .nonFatImgIdx (only when fatIdx is != null) = index on off with grey circle(?) for the non-fattened cells
*/
var off = document.createElement('canvas'); // not added to DOM, will be gc'ed at some point
var nonFatImgIdx = 0;
if (fatIdx!==null) {
//colCount = 2;
//colors = [colors[fatIdx], nonFatColorCircles];
nonFatImgIdx = colors.length;
colors.push(nonFatColorCircles);
}
var colCount = colors.length;
off.width = (colCount+2) * tileWidth; // "+2" because we have three additional circles at the end
off.height = tileHeight;
var ctxOff = off.getContext('2d');
//pre-render circles into the off-screen canvas.
for (var i = 0; i < colors.length; ++i) {
ctxOff.fillStyle = "#"+colors[i];
ctxOff.beginPath();
// parameters are: arc(x, y, r, 0, 2*pi)
ctxOff.arc(i * tileWidth + radius + 1, radius + 1, radius, 0, 2 * Math.PI);
ctxOff.closePath();
ctxOff.fill();
// only draw a pretty shaded outline for very big circles, at extremely high zoom levels
if (radius > 6) {
ctxOff.lineWidth=1.0;
var strokeCol = "#"+shadeColor(colors[i], 0.9);
//if (fatIdx!==null)
//strokeCol = "#000000";
//else
ctxOff.strokeStyle=strokeCol;
ctxOff.beginPath();
ctxOff.arc(i * tileWidth + radius + 1, radius + 1, radius, 0, 2 * Math.PI);
ctxOff.closePath();
ctxOff.stroke();
}
}
// pre-render a black circle outline for the selection, quality of anti-aliasing?
var selImgId = colors.length;
ctxOff.lineWidth=2;
ctxOff.strokeStyle="black";
ctxOff.beginPath();
// args: arc(x, y, r, 0, 2*pi)
ctxOff.arc((selImgId * tileWidth) + radius + 1, radius + 1, radius - 1, 0, 2 * Math.PI);
ctxOff.closePath();
ctxOff.stroke();
// pre-render a grey circle for the non-selection, when something is selected, all the rest is drawn in grey
let greyImgId = colors.length + 1;
ctxOff.lineWidth = 1;
ctxOff.fillStyle = "#b2b2b2";
ctxOff.beginPath();
ctxOff.arc((greyImgId * tileWidth) + radius + 1, radius + 1, radius, 0, 2 * Math.PI);
ctxOff.closePath();
ctxOff.fill();
let ret = {};
ret.off = off;
ret.selImgIdx = selImgId;
ret.greyImgIdx = greyImgId;
ret.nonFatImgIdx = nonFatImgIdx;
return ret;
}
//function blitTwo(ctx, off, pxCoords, coordColors, tileWidth, tileHeight, radius, fatIdx, selImgId) {
/* blit only the fatIdx circle in color, and all the rest in grey. Also draw selection circles. */
//var count = 0;
//for (let i = 0; i < pxCoords.length/2; i++) {
//var pxX = pxCoords[2*i];
//var pxY = pxCoords[2*i+1];
//if (isHidden(pxX, pxY))
//continue;
//var col = coordColors[i];
//if (col===fatIdx)
//col = 0;
//else
//col = 1;
//count++;
//ctx.drawImage(off, col * tileWidth, 0, tileWidth, tileHeight, pxX - radius - 1, pxY - radius - 1, tileWidth, tileHeight);
//
//if (radius>=5)
//ctx.drawImage(off, selImgId * tileWidth, 0, tileWidth, tileHeight, pxX - radius -1, pxY - radius-1, tileWidth, tileHeight);
//}
//return count;
//}
function blitAll(ctx, off, pxCoords, coordColors, tileWidth, tileHeight, radius, selCells, greyIdx, fatIdx, colors) {
/* blit the circles onto the main canvas, using all colors */
var count = 0;
var hasSelection = false;
if (selCells.size!==0)
hasSelection = true;
var col = 0;
+ // first draw all the cells of the color 0
for (let i = 0; i < pxCoords.length/2; i++) {
var pxX = pxCoords[2*i];
var pxY = pxCoords[2*i+1];
if (isHidden(pxX, pxY))
continue;
// when a selection is active, draw everything in grey. This only works because the selection is overdrawn afterwards
// (The selection must be overdrawn later, because otherwise circles shine through the selection)
col = coordColors[i];
+ count++;
+ // only draw the cells of entry 0 of the palette first.
+ // This is a poor approximation of a z-index, but a real z-index would take way too long.
+ // So we're just drawing twice.
+ if (col!==0)
+ continue
+ if (fatIdx===null) {
+ if (hasSelection)
+ col = greyIdx;
+ }
+ else if (!hasSelection && !(fatIdx!=null && fatIdx===col))
+ col = greyIdx;
+
+ ctx.drawImage(off, col * tileWidth, 0, tileWidth, tileHeight, pxX - radius - 1, pxY - radius - 1, tileWidth, tileHeight);
+ }
+
+ // then draw all the cells with the colors != 0
+ for (let i = 0; i < pxCoords.length/2; i++) {
+ var pxX = pxCoords[2*i];
+ var pxY = pxCoords[2*i+1];
+ if (isHidden(pxX, pxY))
+ continue;
+
+ col = coordColors[i];
+ if (col===0)
+ continue
if (fatIdx===null) {
if (hasSelection)
col = greyIdx;
}
else if (!hasSelection && !(fatIdx!=null && fatIdx===col))
col = greyIdx;
- count++;
ctx.drawImage(off, col * tileWidth, 0, tileWidth, tileHeight, pxX - radius - 1, pxY - radius - 1, tileWidth, tileHeight);
}
if (fatIdx!==null) {
// do not fatten
//radius = radius * 2;
//let templates = makeCircleTemplates(radius, tileWidth, tileHeight, colors, fatIdx);
//let off = templates.off;
radius *= 1.5;
for (let i = 0; i < pxCoords.length/2; i++) {
col = coordColors[i];
if (fatIdx!==col)
continue;
var pxX = pxCoords[2*i];
var pxY = pxCoords[2*i+1];
if (isHidden(pxX, pxY))
continue;
count++;
ctx.drawImage(off, col * tileWidth, 0, tileWidth, tileHeight, pxX - radius - 1, pxY - radius - 1, tileWidth, tileHeight);
}
}
return count;
}
function copyColorsOnly(coordColors, fatIdx, nonFatImgIdx)
/* copy numbers in coordColors array to a new array of the same size and keep only fatIdx, set, all others to nonFatImgIdx */
{
let newArr = new Array(coordColors.length);
for (let i=0; i>> 16) & 0xff;
var newG = (newRgb >>> 8) & 0xff;
var newB = (newRgb) & 0xff;
var mixR = ~~(oldR * invAlpha + newR * alpha);
var mixG = ~~(oldG * invAlpha + newG * alpha);
var mixB = ~~(oldB * invAlpha + newB * alpha);
cData[p] = mixR;
cData[p+1] = mixG;
cData[p+2] = mixB;
cData[p+3] = 255; // no transparency... ever?
}
}
function drawBackground(ctx, back) {
/* draw the background image onto the canvas ctx */
if (!back)
return;
console.time("image");
//var ctxWidth = ctx.canvas.width; // size of the canvas on the screen in pixels
//var ctxHeight = ctx.canvas.height;
//var clipWidth = backuwidth;
//var clipHeight = back.height;
// arguments are: (imgObject, x/y coord on image for clipping, width / height of clipped image, where to place the image, width/height of image)
//var a = getSafeRect(back.image.width, back.image.height, back.clipX, back.clipY, back.image.width, back.image.height, 0, 0, ctxWidth, ctxHeight);
//console.log("drawing fixed coords", a.sx, a.sy, a.sw, a.sh, a.dx, a.dy, a.dw, a.dh);
//self.ctx.drawImage(back.image, a.sx, a.sy, a.sw, a.sh, a.dx, a.dy, a.dw, a.dh);
//self.ctx.drawImage(back.image, a.sx, a.sy, back.width, back.height, a.dx, a.dy, a.dw, a.dh);
console.log("drawImage sx, sy, sw, sh, dx, dy, dw, dh", back.sx, back.sy, back.sw, back.sh, back.dx,back.dy, back.dw, back.dh);
//self.ctx.drawImage(back.image, back.sx, back.sy, back.width, back.height, 0, 0, ctxWidth, ctxHeight);
self.ctx.drawImage(back.image, back.sx, back.sy, back.sw, back.sh, back.dx, back.dy, back.dw, back.dh);
console.timeEnd("image");
}
function drawPixels(ctx, width, height, pxCoords, coordColors, colors, alpha, selCells, fatIdx) {
/* draw single pixels into a pixel buffer and copy the buffer into a canvas */
// by default the canvas has black pixels
// so not doing: var canvasData = ctx.createImageData(width, height);
// XX is this really faster than manually zero'ing the array?
var canvasData = ctx.getImageData(0, 0, width, height);
var cData = canvasData.data;
var rgbColors = null;
if (selCells.size===0)
rgbColors = hexToInt(colors);
else
rgbColors = hexToInt(makeAllGreyHex(colors.length)); // selection is active, all cells are grey, except the selection
var invAlpha = 1.0 - alpha;
var count = 0;
// alpha-blend pixels into array
for (var i = 0; i < pxCoords.length/2; i++) {
var pxX = pxCoords[2*i];
var pxY = pxCoords[2*i+1];
if (isHidden(pxX, pxY))
continue;
var p = 4 * (pxY*width+pxX); // pointer to red value of pixel at x,y
var valIdx = coordColors[i];
if (fatIdx!==null) {
// fattening mode: fat cluster is black, all the rest is blue
let grey;
if (valIdx!==fatIdx) {
grey = 0xDD;
cData[p] = grey;
cData[p+1] = grey;
cData[p+2] = grey;
}
else {
// stop all transparency, just overdraw fat blue here
cData[p] = 0;
cData[p+1] = 0;
cData[p+2] = 255;
}
cData[p+3] = 255; // no transparency... ever?
} else {
// normal colors
var oldR = cData[p];
var oldG = cData[p+1];
var oldB = cData[p+2];
var newRgb = rgbColors[valIdx];
var newR = (newRgb >>> 16) & 0xff;
var newG = (newRgb >>> 8) & 0xff;
var newB = (newRgb) & 0xff;
var mixR = ~~(oldR * invAlpha + newR * alpha);
var mixG = ~~(oldG * invAlpha + newG * alpha);
var mixB = ~~(oldB * invAlpha + newB * alpha);
cData[p] = mixR;
cData[p+1] = mixG;
cData[p+2] = mixB;
cData[p+3] = 255; // no transparency... ever?
}
count++;
}
// overdraw the selection as black pixels
if (fatIdx===null) {
selCells.forEach(function(cellId) {
let pxX = pxCoords[2*cellId];
let pxY = pxCoords[2*cellId+1];
if (isHidden(pxX, pxY))
return;
let p = 4 * (pxY*width+pxX); // pointer to red value of pixel at x,y
cData[p] = 0;
cData[p+1] = 0;
cData[p+2] = 0;
})
}
self.ctx.putImageData(canvasData, 0, 0);
return count;
}
function findRange(coords) {
/* find range of pairs-array and return obj with attributes minX/maxX/minY/maxY */
var minX = 9999999;
var maxX = -9999999;
var minY = 9999999;
var maxY = -9999999;
for (var i = 0; i < coords.length/2; i++) {
var x = coords[i*2];
var y = coords[i*2+1];
minX = Math.min(minX, x);
maxX = Math.max(maxX, x);
minY = Math.min(minY, y);
maxY = Math.max(maxY, y);
}
var obj = {};
obj.minX = minX;
obj.maxX = maxX;
obj.minY = minY;
obj.maxY = maxY;
return obj; // not needed, but more explicit
}
function clearCanvas(ctx, width, height) {
/* clear with a white background */
// jsperf says this is fastest on Chrome, and still OK-ish in FF
//console.time("clear");
ctx.save();
ctx.globalAlpha = 1.0;
ctx.fillStyle = "rgb(255,255,255)";
ctx.fillRect(0, 0, width, height);
ctx.restore();
//console.timeEnd("clear");
}
// -- object methods (=access the self object)
this.onZoom100Click = function(ev) {
self.zoom100();
self.drawDots();
};
this.setBackground = function(img) {
/* */
if (self.background===undefined || self.background===null)
self.background = {};
self.background.image = img;
self.scaleBackground(self.background, self.port.initZoom, self.port.zoomRange);
if (self.childPlot)
self.childPlot.setBackground(img);
};
function getSafeRect(width, height, sx, sy, sw, sh, dx, dy, dw, dh) {
if( sw < 0 ) {
sx += sw;
sw = Math.abs( sw );
}
if( sh < 0 ) {
sy += sh;
sh = Math.abs( sh );
}
if( dw < 0 ) {
dx += dw;
dw = Math.abs( dw );
}
if( dh < 0 ) {
dy += dh;
dh = Math.abs( dh );
}
const x1 = Math.max( sx, 0 );
const x2 = Math.min( sx + sw, width );
const y1 = Math.max( sy, 0 );
const y2 = Math.min( sy + sh, height );
const w_ratio = dw / sw;
const h_ratio = dh / sh;
}
this.scaleBackground = function(background, dataRange, zoomRange) {
/* determine the (x,y,width,height) in pixels of the current rectangle of the background bitmap on the canvas */
if (!background)
return;
var width = background.image.width; // source image width
var height = background.image.height; // source image height
var ctxWidth = self.canvas.width;
var ctxHeight = self.canvas.height;
var coordHeight = dataRange.maxY-dataRange.minY;
var coordWidth = dataRange.maxX-dataRange.minX;
var scaleX = width / coordWidth; // this is px/dataUnit to convert background image pixels to canvas pixels
var scaleY = height / coordHeight;
var sx1 = zoomRange.minX * scaleX; // sx = x position on source image
var sx2 = zoomRange.maxX * scaleX;
var sy1 = (coordHeight - zoomRange.maxY) * scaleY; // Y-positions must be subtracted from coordHeight - y axis is flipped!
var sy2 = (coordHeight - zoomRange.minY) * scaleY; // Our y-axis is always flipped!
var sw = sx2 - sx1; // size of slice of background image that is currently shown, in source pixels
var sh = sy2 - sy1;
// lame: since I wasn't able to figure out how to transform negative sx, sy to corrected dx, dy, coords - safari doesn't
// understand negative sx/sy - I simply use the scaleData function
// somehow https://gist.github.com/Kaiido/ca9c837382d89b9d0061e96181d1d862 didn't work for me
//var coords = [0.0, 0.0, dataRange.maxX, dataRange.maxY];
//var newCoords = scaleCoords(coords, 0, zoomRange, ctxWidth, ctxHeight, [])
var dx = 0;
var dy = 0;
var dw = ctxWidth;
var dh = ctxHeight;
//if (sx1 < 0) {
//sx1 = 0;
//sw = width;
//dx = newCoords[0];
//dw = newCoords[2] - dx;
//}
//if (sy1 < 0) {
//sy1 = 0;
//sh = height;
//dy = newCoords[1];
//dh = newCoords[3] - dy;
//}
background.sx = sx1;
background.sy = sy1;
background.sw = sw;
background.sh = sh;
background.dx = dx;
background.dy = dy;
background.dw = dw;
background.dh = dh;
}
this.scaleData = function() {
/* scale coords and labels to current zoom range, write results to pxCoords and pxLabels */
if (!self.coords) // window resize can call this before coordinates are loaded.
return;
var borderMargin = self.port.radius;
self.calcRadius();
let w = self.canvas.width;
let h = self.canvas.height;
self.coords.px = scaleCoords(self.coords.orig, borderMargin, self.port.zoomRange, w, h, self.coords.aspectRatio);
if (self.coords.lines)
self.coords.pxLines = scaleLines(self.coords.lines, self.port.zoomRange, self.canvas.width, self.canvas.height);
self.scaleBackground(self.background, self.port.initZoom, self.port.zoomRange);
self.scalingDone = true;
}
this.readyToDraw = function() {
return (self.scalingDone)
}
this.setTopLeft = function(top, left) {
/* set top and left position in pixels of the canvas */
self.top = top;
self.left = left; // keep an integer version of these numbers
self.div.style.top = top+"px";
self.div.style.left = left+"px";
self.setSize(self.width, self.height, false); // resize the various buttons
}
this.quickResize = function(width, height) {
/* resize the canvas and move the status line, don't rescale or draw */
self.div.style.width = width+"px";
self.div.style.height = height+"px";
// if in split screen mode, pass on the message to the second window
if (self.childPlot) {
width = width/2;
//self.childPlot.left = self.left+width;
//self.childPlot.canvas.style.left = self.childPlot.left+"px";
self.childPlot.setPos(null, self.left+width);
self.childPlot.setSize(width, height, true);
}
if (self.closeButton) {
self.closeButton.style.left = width - gCloseButtonFromRight;
}
// css and actual canvas sizes: these must be identical, otherwise canvas gets super slow
self.canvas.style.width = width+"px";
self.width = width;
self.height = height;
//let canvHeight = height - gStatusHeight;
let canvHeight = height - gStatusHeight;
self.canvas.height = canvHeight;
self.canvas.width = width;
self.canvas.style.height = canvHeight+"px";
self.zoomDiv.style.top = (height-gZoomFromBottom)+"px";
self.zoomDiv.style.left = (gZoomFromLeft)+"px";
// move status line, dataset name and radius/transparency sliders
var statusDiv = self.statusLine;
statusDiv.style.top = (height-gStatusHeight)+"px";
statusDiv.style.width = width+"px";
self.titleDiv.style.top = (height-gStatusHeight-gTitleSize)+"px";
if (self.sliderDiv)
self.sliderDiv.style.top = (height-gStatusHeight-gSliderFromBottom)+"px";
}
this.setPos = function(top, left) {
/* position canvas. Does not affect child */
if (top) {
self.top = top;
self.div.style.top = top+"px";
}
if (left) {
self.left = left;
self.div.style.left = left+"px";
}
}
this.setSize = function(width, height, doRedraw) {
/* resize canvas on the page re-scale the data and re-draw, unless doRedraw is false */
if (width===null)
width = self.div.getBoundingClientRect().width;
self.quickResize(width, height);
if (self.coords)
self.scaleData();
if (doRedraw===undefined || doRedraw===true)
self.drawDots();
};
this.setCoords = function(coords, clusterLabels, coordInfo, opts) {
/* specify new coordinates of circles to draw, an array of (x,y) coordinates */
/* Scale data to current screen dimensions */
/* clusterLabels is optional: array of [x, y, labelString]*/
/* minX, maxX, etc are optional
* opts are optional arguments like radius, alpha etc, see initPlot/args */
self.scalingDone = false;
if (coords.length === 0)
alert("cbDraw-setCoords called with no coordinates");
var minX = coordInfo.minX;
var maxX = coordInfo.maxX;
var minY = coordInfo.minY;
var maxY = coordInfo.maxY;
var useRaw = coordInfo.useRaw;
var coordOpts = cloneObj(self.globalOpts);
copyObj(opts, coordOpts);
var oldRadius = self.port.initRadius;
var oldAlpha = self.port.initAlpha;
var oldLabels = self.coords.pxLabels;
self.port = {};
self.initPort(coordOpts);
if (oldRadius)
self.port.initRadius = oldRadius;
if (oldAlpha)
self.port.initAlpha = oldAlpha;
self.coords = {};
self.coords.origAll = undefined;
var newZr = {};
if (minX===undefined || maxX===undefined || minY===undefined || maxY===undefined)
newZr = findRange(coords);
else {
newZr = {minX:minX, maxX:maxX, minY:minY, maxY:maxY};
}
// switch off any moving of the spots to the minimum position
if (useRaw) {
newZr.minX = 0.0;
newZr.minY = 0.0;
}
// we need the maximal min-max ranges of the original coordinates for later
copyObj(newZr, self.port.initZoom);
// the current min-max ranges of the values are the maximal values, so copy them = zoom 100%
copyObj(newZr, self.port.zoomRange);
self.coords.orig = coords;
self.coords.coordInfo = coordInfo; // we need to find out the label of the coords
self.coords.labels = clusterLabels;
if (coordInfo.aspectRatio)
self.coords.aspectRatio = coordInfo.aspectRatio;
var count = 0;
for (var i = 0; i < coords.length/2; i++) {
var cellX = coords[i*2];
var cellY = coords[i*2+1];
if (!(isHidden(cellX, cellY)))
count++;
}
setStatus(count+ " visible " + self.gSampleDescription+"s loaded");
if (opts.lines)
self._setLines(opts["lines"], opts);
self.scaleData();
};
this.setLabelCoords = function(labelCoords) {
/* set the label coords and return true if there were any labels before */
var hadLabelsBefore = (self.coords.labels && self.coords.labels.length > 0);
self.coords.labels = labelCoords;
return hadLabelsBefore;
};
this.setColorArr = function(colorArr) {
/* set the color array, one array with one index per coordinate */
self.col.arr = colorArr;
};
this.setColors = function(colors) {
/* set the colors, one for each value of a in setColorArr(a). colors is an
* array of six-digit hex strings. Not #-prefixed! */
self.col.pal = colors;
};
this.calcRadius = function() {
/* calculate the radius from current zoom factor and set radius, alpha and zoomFact in self.port */
// make the circles a bit smaller than expected
var zr = self.port.zoomRange;
var iz = self.port.initZoom;
var initAlpha = self.port.initAlpha;
var initSpan = iz.maxX-iz.minX;
var currentSpan = zr.maxX-zr.minX;
var zoomFact = initSpan/currentSpan;
// both radius and alpha can be change by a 'multiplier'
var alphaMult = self.port.alphaMult || 1.0;
var radiusMult = self.port.radiusMult || 1.0;
var baseRadius = self.port.initRadius;
if (baseRadius===0)
baseRadius = 0.7;
var radius = Math.floor(baseRadius * Math.sqrt(zoomFact) * radiusMult);
// the higher the zoom factor, the higher the alpha value
var zoomFrac = Math.min(1.0, zoomFact/100.0); // zoom as fraction, max is 1.0
var alpha = initAlpha + 3.0*zoomFrac*(1.0 - initAlpha);
alpha = Math.min(0.8, alpha)*alphaMult;
debug("Zoom factor: ", zoomFact, ", Radius: "+radius+", alpha: "+alpha);
if (self.onRadiusAlphaChange)
self.onRadiusAlphaChange(radiusMult, alphaMult);
self.port.zoomFact = zoomFact;
self.port.alpha = alpha;
self.port.alphaMult = alphaMult;
self.port.radius = radius;
self.port.radiusMult = radiusMult;
}
this.drawSvg = function(alpha, radius, coords, colArr, pal) {
self.svgLines = [];
var plotHeight = self.canvas.height;
var plotWidth = self.canvas.width;
var width = plotWidth+self.svgLabelWidth;
var height = 1500; // enough space for 100 lines in the legend
self.svgLines.push("\n");
self.svgLines = null;
return svgLines;
}
this.drawLabels = function() {
/* draw only the labels */
self.coords.pxLabels = scaleLabels(
self.coords.labels,
self.port.zoomRange,
self.port.radius,
self.canvas.width,
self.canvas.height
);
self.coords.labelBbox = drawLabels(
self.ctx,
self.coords.pxLabels,
self.canvas.width,
self.canvas.height,
self.port.zoomFact
);
// draw annotations - look like labels, but cannot be clicked
self.coords.pxAnnots = scaleLabels(
self.coords.coordInfo.annots,
self.port.zoomRange,
self.port.radius,
self.canvas.width,
self.canvas.height
);
drawLabels(self.ctx, self.coords.pxAnnots, self.canvas.width, self.canvas.height, self.port.zoomFact, true);
};
this.cellsAtPixel = function(x, y) {
/* return the Ids of all cells at a particular pixel */
var res = [];
var pxCoords = self.coords.px;
for (var i = 0; i < pxCoords.length/2; i++) {
var cellX = pxCoords[i*2];
var cellY = pxCoords[i*2+1];
if (cellX===x || cellY===y)
res.push(i);
}
return res;
};
this.cellsInRect = function(x1, y1, x2, y2) {
/* return the Ids of all cells within certain pixel boundaries */
var res = [];
var pxCoords = self.coords.px;
for (var i = 0; i < pxCoords.length/2; i++) {
var cellX = pxCoords[i*2];
var cellY = pxCoords[i*2+1];
if ((cellX >= x1) && (cellX <= x2) && (cellY >= y1) && (cellY <= y2))
res.push(i);
}
return res;
};
this.resetAlpha = function() {
self.port.alphaMult = 1.0;
$('#mpAlphaSlider').slider({value:4});
self.calcRadius();
};
this.resetRadius = function() {
self.port.radiusMult = 1.0;
$('#mpRadiusSlider').slider({value:4});
self.calcRadius();
};
this.setRadiusAlpha = function(radius, alpha) {
self.port.radiusMult = radius;
self.port.alphaMult = alpha;
self.calcRadius();
};
this.zoom100 = function() {
/* zoom to 100% and redraw */
copyObj(self.port.initZoom, self.port.zoomRange);
self.resetAlpha();
self.resetRadius();
self.scaleData();
};
this.zoomToTest = function(x1, y1, x2, y2) {
self.port.zoomRange = {"minX":x1, "minY":y1, "maxX":x2, "maxY":y2};
self.scaleData();
}
this.zoomTo = function(x1, y1, x2, y2) {
/* zoom to rectangle defined by two pixel points */
// make sure that x1 1500)
return zr;
debug("x min max "+zr.minX+" "+zr.maxX);
debug("y min max "+zr.minY+" "+zr.maxY);
self.port.zoomRange = newRange;
self.scaleData();
// a special case for connected plots that are not sharing our pixel coordinates
if (self.childPlot && self.coords!==self.childPlot.coords) {
self.childPlot.zoomBy(zoomFact, xPx, yPx);
}
return newRange;
};
this.movePerc = function(xDiffFrac, yDiffFrac) {
/* move a certain percentage of current view. xDiff/yDiff are floats, e.g. 0.1 is 10% up */
var zr = self.port.zoomRange;
var xRange = Math.abs(zr.maxX-zr.minX);
var yRange = Math.abs(zr.maxY-zr.minY);
var xDiffAbs = xRange*xDiffFrac;
var yDiffAbs = yRange*yDiffFrac;
var newRange = {};
newRange.minX = zr.minX + xDiffAbs;
newRange.maxX = zr.maxX + xDiffAbs;
newRange.minY = zr.minY + yDiffAbs;
newRange.maxY = zr.maxY + yDiffAbs;
copyObj(newRange, self.port.zoomRange);
self.scaleData();
};
this.panStart = function() {
/* called when starting a panning sequence, makes a snapshop of the current image */
self.panCopy = document.createElement('canvas'); // not added to DOM, will be gc'ed
self.panCopy.width = self.canvas.width;
self.panCopy.height = self.canvas.height;
var destCtx = self.panCopy.getContext("2d", { alpha: false });
destCtx.drawImage(self.canvas, 0, 0);
}
this.panBy = function(xDiff, yDiff) {
/* pan current image by x/y pixels */
debug('panning by '+xDiff+' '+yDiff);
//var srcCtx = self.panCopy.getContext("2d", { alpha: false });
clearCanvas(self.ctx, self.canvas.width, self.canvas.height);
self.ctx.drawImage(self.panCopy, -xDiff, -yDiff);
// keep these for panEnd
self.panDiffX = xDiff;
self.panDiffY = yDiff;
}
this.panEnd = function() {
/* end a sequence of panBy calls, called when the mouse is released */
self.moveBy(self.panDiffX, -self.panDiffY); // -1 because of flipY
self.panCopy = null;
self.panDiffX = null;
self.panDiffY = null;
}
// BEGIN SELECTION METHODS (could be an object?)
this.selectClear = function(skipNotify) {
/* clear selection */
self.selCells.clear();
setStatus("");
if (self.onSelChange!==null && skipNotify!==true)
self.onSelChange(self.selCells);
};
this.selectSet = function(cellIds) {
/* set selection to an array of integer cellIds */
self.selCells.clear();
//self.selCells.push(...cellIds); // "extend" = array spread syntax, https://davidwalsh.name/spread-operator
let selCells = self.selCells;
for (let i=0; i= x1) && (x <= x2) && (y >= y1) && (y <= y2)) {
//console.timeEnd("labelCheck");
var labelText = clusterLabels[i][2];
return [labelText, i];
}
}
//console.timeEnd("labelCheck");
return null;
};
this.lineAt = function(x, y) {
/* check if there is a line at x,y and return its label if so or null if not */
var pxLines = self.coords.pxLines;
for (var i=0; i < pxLines.length; i++) {
var line = pxLines[i];
var x1 = line[0];
var y1 = line[1];
var x2 = line[2];
var y2 = line[3];
if (pDistance(x, y, x1, y1, x2, y2) <= 2) {
var lineLabel = self.coords.lineLabels[i];
return lineLabel;
}
}
return null;
};
this.cellsAt = function(x, y) {
/* check which cell's bounding boxes contain (x, y), return a list of the cell IDs, sorted by distance */
//console.time("cellSearch");
var pxCoords = self.coords.px;
if (pxCoords===null)
return null;
var possIds = [];
var radius = self.port.radius;
for (var i = 0; i < pxCoords.length/2; i++) {
var pxX = pxCoords[2*i];
var pxY = pxCoords[2*i+1];
if (isHidden(pxX, pxY))
continue;
var x1 = pxX - radius;
var y1 = pxY - radius;
var x2 = pxX + radius;
var y2 = pxY + radius;
if ((x >= x1) && (x <= x2) && (y >= y1) && (y <= y2)) {
var dist = Math.sqrt(Math.pow(x-pxX, 2) + Math.pow(y-pxY, 2));
possIds.push([dist, i]);
}
}
//console.timeEnd("cellSearch");
if (possIds.length===0)
return null;
else {
possIds.sort(function(a,b) { return a[0]-b[0]} ); // sort by distance
// strip the distance information
var ret = [];
for (let i=0; i < possIds.length; i++) {
ret.push(possIds[i][1]);
}
return ret;
}
};
this.resetMarquee = function() {
/* make the marquee disappear and reset its internal status */
if (!self.interact)
return;
self.mouseDownX = null;
self.mouseDownY = null;
self.lastPanX = null;
self.lastPanY = null;
self.selectBox.style.display = "none";
self.selectBox.style.width = 0;
self.selectBox.style.height = 0;
};
this.drawMarquee = function(x1, y1, x2, y2, forceAspect) {
/* draw the selection or zooming marquee using the DIVs created by setupMouse */
var selectWidth = Math.abs(x1 - x2);
var selectHeight = 0;
if (forceAspect) {
var aspectRatio = self.width / self.canvas.height;
selectHeight = selectWidth/aspectRatio;
} else
selectHeight = Math.abs(y1 - y2);
var minX = Math.min(x1, x2);
var minY = Math.min(y1, y2);
var div = self.selectBox;
div.style.left = (minX-self.left)+"px";
div.style.top = (minY-self.top)+"px";
div.style.width = selectWidth+"px";
div.style.height = selectHeight+"px";
div.style.display = "block";
};
this.activatePlot = function() {
/* draw black border around plot, remove black border from all connected plots, call onActive */
if (self.parentPlot===null)
return false;
// only need to do something if we're not already the active plot
self.canvas.style["border"] = "2px solid black";
self.parentPlot.canvas.style["border"] = "2px solid white";
// flip the parent/child relationship
self.childPlot = self.parentPlot;
self.childPlot.parentPlot = self;
self.parentPlot = null;
self.childPlot.childPlot = null;
// hide/show the tool and zoom buttons
self.childPlot.zoomDiv.style.display = "none";
self.childPlot.toolDiv.style.display = "none";
self.zoomDiv.style.display = "block";
self.toolDiv.style.display = "block";
// notify the UI
self.onActiveChange(self);
return true;
}
this.isSplit = function() {
/* return true if this renderer has either a parent or a child plot = is in split screen mode */
return (isValid(self.parentPlot) || isValid(self.childPlot))
}
// https://stackoverflow.com/questions/73187456/canvas-determine-if-point-is-on-line
//function distancePointFromLine(x0, y0, x1, y1, x2, y2) {
//return Math.abs((x2 - x1) * (y1 - y0) - (x1 - x0) * (y2 - y1)) / Math.sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2)
//}
function pDistance(x, y, x1, y1, x2, y2) {
/* distance of point from line segment, copied from https://stackoverflow.com/questions/849211/shortest-distance-between-a-point-and-a-line-segment */
var A = x - x1;
var B = y - y1;
var C = x2 - x1;
var D = y2 - y1;
var dot = A * C + B * D;
var len_sq = C * C + D * D;
var param = -1;
if (len_sq != 0) //in case of 0 length line
param = dot / len_sq;
var xx, yy;
if (param < 0) {
xx = x1;
yy = y1;
}
else if (param > 1) {
xx = x2;
yy = y2;
}
else {
xx = x1 + param * C;
yy = y1 + param * D;
}
var dx = x - xx;
var dy = y - yy;
return Math.sqrt(dx * dx + dy * dy);
}
this.onMouseMove = function(ev) {
/* called when the mouse is moved over the Canvas */
// set a timer so we can get "hover" functionality without too much CPU
if (self.timer!==null)
clearTimeout(self.timer);
self.timer = setTimeout(self.onNoMouseMove, 130);
// save mouse pos for onNoMouseMove timer handler
self.lastMouseX = ev.clientX;
self.lastMouseY = ev.clientY;
// label hit check requires canvas coordinates x/y
var clientX = ev.clientX;
var clientY = ev.clientY;
var canvasTop = self.top;
var canvasLeft = self.left;
var xCanvas = clientX - canvasLeft;
var yCanvas = clientY - canvasTop;
// is there just white space under the mouse, do nothing,
// from https://stackoverflow.com/questions/15325283/how-to-detect-if-a-mouse-pointer-hits-a-line-already-drawn-on-an-html-5-canvas
//var imageData = self.ctx.getImageData(0, 0, self.width, self.height);
//var inputData = imageData.data;
//var pData = (~~xCanvas + (~~yCanvas * self.width)) * 4;
//if (!inputData[pData + 3]) {
//console.log("just white space under mouse");
//return;
//}
// when the cursor is over a label, change it to a hand, but only when there is no marquee
if (self.coords.labelBbox!==null && self.mouseDownX === null) {
var labelInfo = self.labelAt(xCanvas, yCanvas);
if (labelInfo===null) {
self.canvas.style.cursor = self.canvasCursor;
self.onNoLabelHover(ev);
} else {
self.canvas.style.cursor = 'pointer'; // not 'hand' anymore ! and not 'grab' yet!
if (self.onLabelHover!==null)
self.onLabelHover(labelInfo[0], labelInfo[1], ev);
}
}
// when the cursor is over a line, trigger callback
if (self.onLineHover && self.coords.lineLabels) {
var lineLabel = self.lineAt(xCanvas, yCanvas);
self.onLineHover(lineLabel, ev);
}
if (self.mouseDownX!==null) {
// we're panning
if (((ev.altKey || self.dragMode==="move")) && self.panCopy!==null) {
var xDiff = self.mouseDownX - clientX;
var yDiff = self.mouseDownY - clientY;
self.panBy(xDiff, yDiff);
}
else {
// zooming or selecting
var forceAspect = false;
var anyKey = (ev.metaKey || ev.altKey || ev.shiftKey);
if ((self.dragMode==="zoom" && !anyKey) || ev.metaKey )
forceAspect = true;
self.drawMarquee(self.mouseDownX, self.mouseDownY, clientX, clientY, forceAspect);
}
}
};
this.onNoMouseMove = function() {
/* called after some time has elapsed and the mouse has not been moved */
if (self.coords.px===null)
return;
var x = self.lastMouseX - self.left; // need canvas, not screen coordinates
var y = self.lastMouseY - self.top;
var cellIds = self.cellsAt(x, y);
// only call onNoCellHover if callback exists and there is nothing selected
if (cellIds===null && self.onNoCellHover!==null && self.selCells===null)
self.onNoCellHover();
else if (self.onCellHover!==null)
self.onCellHover(cellIds);
};
this.onMouseDown = function(ev) {
/* user clicks onto canvas */
if (self.activatePlot())
return; // ignore the first click into the plot, if it was the activating click
debug("background mouse down");
var clientX = ev.clientX;
var clientY = ev.clientY;
if ((ev.altKey || self.dragMode==="move") && !ev.shiftKey && !ev.metaKey) {
debug("alt key or move mode: starting panning");
self.panStart();
}
self.mouseDownX = clientX;
self.mouseDownY = clientY;
};
this.onMouseUp = function(ev) {
debug("background mouse up");
// these are screen coordinates
var clientX = ev.clientX;
var clientY = ev.clientY;
var mouseDidNotMove = (self.mouseDownX === clientX && self.mouseDownY === clientY);
if (self.panCopy!==null && !mouseDidNotMove) {
debug("ending panning operation");
self.panEnd();
self.mouseDownX = null;
self.mouseDownY = null;
self.drawDots();
return;
} else {
// abort panning
self.panCopy = null;
}
if (self.mouseDownX === null && self.lastPanX === null) {
// user started the click outside of the canvas: do nothing
debug("first click must have been outside of canvas");
return;
}
// the subsequent operations require canvas coordinates x/y
var canvasTop = self.top;
var canvasLeft = self.left;
var x1 = self.mouseDownX - canvasLeft;
var y1 = self.mouseDownY - canvasTop;
var x2 = clientX - canvasLeft;
var y2 = clientY - canvasTop;
// user did not move the mouse, so this is a click
if (mouseDidNotMove) {
// recognize a double click -> zoom
if (self.lastClick!==undefined && x2===self.lastClick[0] && y2===self.lastClick[1]) {
self.zoomBy(1.33);
self.lastClick = [-1,-1];
} else {
self.lastClick = [x2, y2];
}
var labelInfo = self.labelAt(x2, y2);
if (labelInfo!==null && self.doDrawLabels)
self.onLabelClick(labelInfo[0], labelInfo[1], ev);
else {
var clickedCellIds = self.cellsAt(x2, y2);
// click on a cell -> update selection and redraw
if (clickedCellIds!==null && self.onCellClick!==null) {
self.selectClear(true);
for (var i = 0; i < clickedCellIds.length; i++) {
self.selCells.add(clickedCellIds[i]);
}
self.drawDots();
self.onCellClick(clickedCellIds, ev);
}
else {
// user clicked onto background:
// reset selection and redraw
debug("not moved at all: reset "+clientX+" "+self.mouseDownX+" "+self.mouseDownY+" "+clientY);
self.selectClear();
self.drawDots();
}
self.lastPanX = null;
self.lastPanY = null;
}
self.mouseDownX = null;
self.mouseDownY = null;
return;
}
//debug("moved: reset "+x+" "+mouseDownX+" "+mouseDownY+" "+y);
// it wasn't a click, so it was a drag
var anyKey = (ev.metaKey || ev.altKey || ev.shiftKey);
// zooming
if ((self.dragMode==="zoom" && !anyKey) || ev.metaKey ) {
// get current coords of the marquee in canvas pixels
var div = self.selectBox;
let zoomX1 = parseInt(div.style.left.replace("px",""));
let zoomY1 = parseInt(div.style.top.replace("px",""));
let zoomX2 = zoomX1+parseInt(div.style.width.replace("px",""));
let zoomY2 = zoomY1+parseInt(div.style.height.replace("px",""));
zoomY1 = self.canvas.height - zoomY1;
zoomY2 = self.canvas.height - zoomY2;
self.zoomTo(zoomX1, zoomY1, zoomX2, zoomY2);
// switch back to the mode before zoom was clicked
if (self.prevMode) {
self.activateMode(self.prevMode);
self.prevMode = null;
}
}
// marquee select
else if ((self.dragMode==="select" && !anyKey) || ev.shiftKey ) {
if (! ev.shiftKey)
self.selectClear(true);
self.selectInRect(x1, y1, x2, y2);
}
else {
debug("Internal error: no mode?");
}
self.resetMarquee();
self.drawDots();
};
function drawCirclesSvg(svgLines, pxCoords, coordColors, colors, radius, alpha, selCells) {
/* add SVG text to the array svgLines */
debug("Drawing "+coordColors.length+" circles with SVG renderer");
var count = 0;
for (var i = 0; i < pxCoords.length/2; i++) {
var pxX = pxCoords[2*i];
var pxY = pxCoords[2*i+1];
if (isHidden(pxX, pxY))
continue;
var col = colors[coordColors[i]];
svgLines.push("");
count++;
}
return count;
}
this.onWheel = function(ev) {
/* called when the user moves the mouse wheel */
if (self.parentPlot!==null)
return;
debug(ev);
var normWheel = normalizeWheel(ev);
debug(normWheel);
var pxX = ev.clientX - self.left;
var pxY = ev.clientY - self.top;
var spinFact = 0.1;
if (ev.ctrlKey) // = OSX pinch and zoom gesture (and no other OS/mouse combination?)
spinFact = 0.08; // is too fast, so slow it down a little
var zoomFact = 1-(spinFact*normWheel.spinY);
debug("Wheel Zoom by "+zoomFact);
self.zoomBy(zoomFact, pxX, pxY);
self.drawDots();
ev.preventDefault();
ev.stopPropagation();
};
this.setupMouse = function() {
// setup the mouse callbacks
self.canvas.addEventListener('mousedown', self.onMouseDown);
self.canvas.addEventListener("mousemove", self.onMouseMove);
self.canvas.addEventListener("mouseup", self.onMouseUp);
// when the user moves the mouse, the mouse is often NOT on the canvas,
// but on the marquee box, so connect this one, too.
self.selectBox.addEventListener("mouseup", self.onMouseUp);
self.canvas.addEventListener("wheel", self.onWheel);
};
this.setShowLabels = function(trueOrFalse) {
/* this is separate from setLabelField, so you can switch it off and on quickly */
self.doDrawLabels = trueOrFalse;
}
this.setLabelField = function(fieldName) {
/* this is only to keep track of what the current label field is.
Switches off label drawing if fieldName is null */
self.activeLabelField = fieldName;
self.setShowLabels( fieldName!==null )
};
this.getLabelField = function(fieldName) {
return self.activeLabelField;
};
this.getLabels = function() {
/* get current labels */
var ret = [];
var labels = self.coords.labels;
for (var i = 0; i 4) {
var lineLabels = []
for (var i=0; i array of [x, y, count]
* if names is undefined, will use numNames and convert to two decimals
* */
var calc = {};
for (var i = 0, I = values.length; i < I; i++) {
var label = null;
if (names) {
label = names[values[i]];
} else {
label = numNames[i].toFixed(2);
}
if (calc[label] === undefined) {
calc[label] = [[], [], 0]; // all X, all Y, count
}
var x = coords[i * 2];
var y = coords[i * 2 + 1];
if (isHidden(x, y))
continue;
calc[label][0].push(x);
calc[label][1].push(y);
calc[label][2] += 1;
}
return calc;
}
// object constructor code
self.initCanvas(div, top, left, width, height, args);
self.initPlot(args);
}