7df99a27a400ec5627663c3a0692e9ab7d04796c
max
Fri May 9 09:25:54 2025 -0700
fix: null on recent genes, split screen titles, scroll bar coming up without reason, zooming on split screen, checkbox on both sides updating on change event, hide checkbox when not in split screen mode
diff --git src/cbPyLib/cellbrowser/cbWeb/js/maxPlot.js src/cbPyLib/cellbrowser/cbWeb/js/maxPlot.js
index 94ffd6a..6d6ee2f 100644
--- src/cbPyLib/cellbrowser/cbWeb/js/maxPlot.js
+++ src/cbPyLib/cellbrowser/cbWeb/js/maxPlot.js
@@ -1,3263 +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.style.cssText = 'pointer-events: none;position: absolute; width: 1000px; opacity: 0.8; 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;
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) {
+ 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);
}