import { extent } from 'd3-array';
import { format } from 'd3-format';
import { select, mouse, event as d3event } from 'd3-selection';
import { zoom as d3zoom, zoomIdentity } from 'd3-zoom';
import { drag as d3drag } from 'd3-drag';
import { scaleLinear } from 'd3-scale';
import { quadtree } from 'd3-quadtree';
import { ObjectUtils } from './ObjectUtils';
import { QuadtreeUtils } from './quadtree';
import { Lasso } from './lasso';
import { cssprefix, debuglog } from './constants';
import { TooltipUtils } from './tooltip';
import { EventEmitter } from 'eventemitter3';
export var EScaleAxes;
(function (EScaleAxes) {
EScaleAxes[EScaleAxes["x"] = 0] = "x";
EScaleAxes[EScaleAxes["y"] = 1] = "y";
EScaleAxes[EScaleAxes["xy"] = 2] = "xy";
})(EScaleAxes || (EScaleAxes = {}));
/**
* reasons why a new render pass is needed
*/
export var ERenderReason;
(function (ERenderReason) {
ERenderReason[ERenderReason["DIRTY"] = 0] = "DIRTY";
ERenderReason[ERenderReason["SELECTION_CHANGED"] = 1] = "SELECTION_CHANGED";
ERenderReason[ERenderReason["ZOOMED"] = 2] = "ZOOMED";
ERenderReason[ERenderReason["PERFORM_SCALE_AND_TRANSLATE"] = 3] = "PERFORM_SCALE_AND_TRANSLATE";
ERenderReason[ERenderReason["AFTER_SCALE_AND_TRANSLATE"] = 4] = "AFTER_SCALE_AND_TRANSLATE";
ERenderReason[ERenderReason["PERFORM_TRANSLATE"] = 5] = "PERFORM_TRANSLATE";
ERenderReason[ERenderReason["AFTER_TRANSLATE"] = 6] = "AFTER_TRANSLATE";
ERenderReason[ERenderReason["PERFORM_SCALE"] = 7] = "PERFORM_SCALE";
ERenderReason[ERenderReason["AFTER_SCALE"] = 8] = "AFTER_SCALE";
})(ERenderReason || (ERenderReason = {}));
/**
* @internal
*/
export function fixScale(current, acc, data, given, givenLimits) {
if (given) {
return given;
}
if (givenLimits) {
return current.domain(givenLimits);
}
const ex = extent(data, acc);
return current.domain([ex[0], ex[1]]);
}
function defaultProps() {
return {
marginLeft: 48,
marginTop: 10,
marginBottom: 32,
marginRight: 10,
canvasBorder: 0,
clickRadius: 10,
x: (d) => d.x,
y: (d) => d.y,
xlabel: 'x',
ylabel: 'y',
xscale: scaleLinear().domain([0, 100]),
xlim: null,
yscale: scaleLinear().domain([0, 100]),
ylim: null,
symbol: 'o',
scale: EScaleAxes.xy,
zoomDelay: 300,
zoomScaleExtent: [1, +Infinity],
zoomWindow: null,
zoomScaleTo: 1,
zoomTranslateBy: [0, 0],
format: {},
ticks: {},
tooltipDelay: 500,
showTooltip: TooltipUtils.showTooltip,
isSelectEvent: (event) => event.ctrlKey || event.altKey,
lasso: Object.assign({
interval: 100
}, Lasso.defaultOptions()),
extras: null,
renderBackground: null,
aspectRatio: 1
};
}
/**
* an class for rendering a scatterplot in a canvas
*/
export class AScatterplot extends EventEmitter {
constructor(root, props) {
super();
this.canvasDataLayer = null;
this.canvasSelectionLayer = null;
this.tree = null;
this.selectionTree = null;
/**
* timout handle when the tooltip is shown
* @type {number}
*/
this.showTooltipHandle = -1;
this.lasso = new Lasso();
this.currentTransform = zoomIdentity;
this.zoomStartTransform = zoomIdentity;
this.zoomHandle = -1;
this.dragHandle = -1;
this.props = ObjectUtils.merge(defaultProps(), props);
this.parent = root.ownerDocument.createElement('div');
//need to use d3 for d3.mouse to work
const $parent = select(this.parent);
root.appendChild(this.parent);
if (this.props.scale !== null) {
//register zoom
this.zoomBehavior = d3zoom()
.on('start', this.onZoomStart.bind(this))
.on('zoom', this.onZoom.bind(this))
.on('end', this.onZoomEnd.bind(this))
.scaleExtent(this.props.zoomScaleExtent)
.translateExtent([[0, 0], [+Infinity, +Infinity]])
.filter(() => d3event.button === 0 && (typeof this.props.isSelectEvent !== 'function' || !this.props.isSelectEvent(d3event)));
if (this.props.zoomWindow != null) {
this.window = this.props.zoomWindow;
}
else {
const z = zoomIdentity.scale(this.props.zoomScaleTo).translate(this.props.zoomTranslateBy[0], this.props.zoomTranslateBy[1]);
this.setTransform(z);
}
}
else {
this.zoomBehavior = null;
}
if (typeof this.props.isSelectEvent === 'function') {
const drag = d3drag()
.container(function () { return this; })
.on('start', this.onDragStart.bind(this))
.on('drag', this.onDrag.bind(this))
.on('end', this.onDragEnd.bind(this))
.filter(() => d3event.button === 0 && typeof this.props.isSelectEvent === 'function' && this.props.isSelectEvent(d3event));
$parent.call(drag)
.on('click', () => this.onClick(d3event));
}
if (this.hasTooltips()) {
$parent.on('mouseleave', () => this.onMouseLeave(d3event))
.on('mousemove', () => this.onMouseMove(d3event));
}
this.parent.classList.add(cssprefix);
}
get node() {
return this.parent;
}
initDOM(extraMarkup = '') {
//init dom
this.parent.innerHTML = `
${extraMarkup}
`;
if (!this.zoomBehavior) {
return;
}
select(this.parent).select(`.${cssprefix}-draw-area`)
.call(this.zoomBehavior)
.on('wheel', () => d3event.preventDefault());
}
get data() {
return this.tree ? this.tree.data() : [];
}
setDataImpl(data) {
//generate a quad tree out of the data
//work on a normalized dimension within the quadtree to
// * be independent of the current pixel size
// * but still consider the mapping function (linear, pow, log) from the data domain
const domain2normalizedX = this.props.xscale.copy().range(this.normalized2pixel.x.domain());
const domain2normalizedY = this.props.yscale.copy().range(this.normalized2pixel.y.domain());
this.tree = quadtree(data, (d) => domain2normalizedX(this.props.x(d)), (d) => domain2normalizedY(this.props.y(d)));
}
set data(data) {
this.setDataImpl(data);
this.selectionTree = quadtree([], this.tree.x(), this.tree.y());
this.render(ERenderReason.DIRTY);
}
/**
* returns the total domain
* @returns {{xMinMax: number[], yMinMax: number[]}}
*/
get domain() {
return {
xMinMax: this.props.xscale.domain(),
yMinMax: this.props.yscale.domain(),
};
}
hasTooltips() {
return this.props.showTooltip != null && this.props.showTooltip !== false;
}
resized() {
this.render(ERenderReason.DIRTY);
}
getMouseNormalizedPos(canvasPixelPox = this.mousePosAtCanvas()) {
const { n2pX, n2pY } = this.transformedNormalized2PixelScales();
function range(range) {
return Math.abs(range[1] - range[0]);
}
const computeClickRadius = () => {
//compute the data domain radius based on xscale and the scaling factor
const view = this.props.clickRadius;
const transform = this.currentTransform;
const scale = this.props.scale;
const kX = (scale === EScaleAxes.x || scale === EScaleAxes.xy) ? transform.k : 1;
const kY = (scale === EScaleAxes.y || scale === EScaleAxes.xy) ? transform.k : 1;
const viewSizeX = kX * range(this.normalized2pixel.x.range());
const viewSizeY = kY * range(this.normalized2pixel.y.range());
//transform from view to data without translation
const normalizedRangeX = range(this.normalized2pixel.x.domain());
const normalizedRangeY = range(this.normalized2pixel.y.domain());
const normalizedX = view / viewSizeX * normalizedRangeX;
const normalizedY = view / viewSizeY * normalizedRangeY;
//const view = this.props.xscale(base)*transform.k - this.props.xscale.range()[0]; //skip translation
//debuglog(view, viewSize, transform.k, normalizedSize, normalized);
return [normalizedX, normalizedY];
};
const [clickRadiusX, clickRadiusY] = computeClickRadius();
return { x: n2pX.invert(canvasPixelPox[0]), y: n2pY.invert(canvasPixelPox[1]), clickRadiusX, clickRadiusY };
}
/**
* returns the current selection
*/
get selection() {
if (typeof this.props.isSelectEvent !== 'function') {
return [];
}
return this.selectionTree.data();
}
/**
* sets the current selection
* @param selection
*/
set selection(selection) {
this.setSelection(selection);
}
setSelection(selection) {
return this.setSelectionImpl(selection);
}
setSelectionImpl(selection, inProgress = false) {
if (typeof this.props.isSelectEvent !== 'function') {
return false;
}
if (selection == null) {
selection = []; //ensure valid value
}
//this.lasso.clear();
if (selection.length === 0) {
return this.clearSelectionImpl(inProgress);
}
//find the delta
let changed = false;
const s = this.selection.slice();
selection.forEach((sNew) => {
const i = s.indexOf(sNew);
if (i < 0) { //new
this.selectionTree.add(sNew);
changed = true;
}
else {
s.splice(i, 1); //mark as used
}
});
changed = changed || s.length > 0;
//remove removed items
this.selectionTree.removeAll(s);
if (changed) {
this.emit(inProgress ? AScatterplot.EVENT_SELECTION_IN_PROGRESS_CHANGED : AScatterplot.EVENT_SELECTION_CHANGED, this);
this.render(ERenderReason.SELECTION_CHANGED);
}
return changed;
}
/**
* clears the selection, same as .selection=[]
*/
clearSelection() {
return this.clearSelection();
}
clearSelectionImpl(inProgress = false) {
const changed = this.selectionTree !== null && this.selectionTree.size() > 0;
if (changed) {
this.selectionTree = quadtree([], this.tree.x(), this.tree.y());
this.emit(inProgress ? AScatterplot.EVENT_SELECTION_IN_PROGRESS_CHANGED : AScatterplot.EVENT_SELECTION_CHANGED, this);
this.render(ERenderReason.SELECTION_CHANGED);
}
return changed;
}
/**
* shortcut to add items to the selection
* @param items
*/
addToSelection(items) {
if (items.length === 0 || typeof this.props.isSelectEvent !== 'function') {
return false;
}
this.selectionTree.addAll(items);
this.emit(AScatterplot.EVENT_SELECTION_CHANGED, this);
this.render(ERenderReason.SELECTION_CHANGED);
return true;
}
/**
* shortcut to remove items from the selection
* @param items
*/
removeFromSelection(items) {
if (items.length === 0 || typeof this.props.isSelectEvent !== 'function') {
return false;
}
this.selectionTree.removeAll(items);
this.emit(AScatterplot.EVENT_SELECTION_CHANGED, this);
this.render(ERenderReason.SELECTION_CHANGED);
return true;
}
selectWithTester(tester, inProgress = false) {
const selection = QuadtreeUtils.findByTester(this.tree, tester);
return this.setSelectionImpl(selection, inProgress);
}
checkResize() {
const c = this.canvasDataLayer;
if (c.width !== c.clientWidth || c.height !== c.clientHeight) {
const oldWidth = this.canvasSelectionLayer.width;
const oldHeight = this.canvasSelectionLayer.height;
this.canvasSelectionLayer.width = c.width = c.clientWidth;
this.canvasSelectionLayer.height = c.height = c.clientHeight;
this.adaptMaxTranslation(oldWidth, oldHeight);
return true;
}
return false;
}
/**
* adapt the current translation (is absolute in pixels) and consider if the dimensions of the canvas element have changed
*/
adaptMaxTranslation(oldWidth, oldHeight) {
if (!this.zoomBehavior) {
return;
}
const availableWidth = this.canvasDataLayer.width - this.props.marginLeft - this.props.marginRight;
const availableHeight = this.canvasDataLayer.height - this.props.marginTop - this.props.marginBottom;
const oldAvailableWidth = oldWidth - this.props.marginLeft - this.props.marginRight;
const oldAvailableHeight = oldHeight - this.props.marginTop - this.props.marginBottom;
const current = this.currentTransform;
// compute factors to consider the element's new dimensions
const factorX = availableWidth / oldAvailableWidth;
const factorY = availableHeight / oldAvailableHeight;
this.zoomBehavior
.extent([[0, 0], [availableWidth, availableHeight]])
.translateExtent([[0, 0], [availableWidth, availableHeight]]);
// set the new transform by considering the factors
this.setTransform(zoomIdentity.translate(current.x * factorX, current.y * factorY).scale(current.k));
}
rescale(axis, scale) {
const c = this.currentTransform;
const p = this.props.scale;
switch (axis) {
case EScaleAxes.x:
return p === EScaleAxes.x || p === EScaleAxes.xy ? c.rescaleX(scale) : scale;
case EScaleAxes.y:
return p === EScaleAxes.y || p === EScaleAxes.xy ? c.rescaleY(scale) : scale;
}
throw new Error('Not Implemented');
}
mousePosAtCanvas() {
const pos = mouse(this.parent);
// shift by the margin since the scales doesn't include them for better scaling experience
return [pos[0] - this.props.marginLeft, pos[1] - this.props.marginTop];
}
/**
* sets the current visible window
* @param window
*/
set window(window) {
if (!this.zoomBehavior) {
return;
}
const { k, tx, ty } = this.window2transform(window);
this.setTransform(zoomIdentity.scale(k).translate(tx, ty));
this.node.classList.toggle(`${EScaleAxes[this.props.scale]}-zoomed`, this.isZoomed());
this.render();
}
setTransform(transform) {
if (!this.zoomBehavior) {
return;
}
const $zoom = select(this.parent).select(`.${cssprefix}-draw-area`);
this.zoomBehavior
.on('start', null)
.on('zoom', null)
.on('end', null);
this.zoomBehavior.transform($zoom, this.currentTransform = transform);
this.zoomBehavior
.on('start', this.onZoomStart.bind(this))
.on('zoom', this.onZoom.bind(this))
.on('end', this.onZoomEnd.bind(this));
}
window2transform(window) {
const range2transform = (minMax, scale, coordinateSystemOrigin) => {
const scaledWindowAxisMin = scale(minMax[0]);
const scaledWindowAxisMax = scale(minMax[1]);
const pmin = Math.min(scaledWindowAxisMin, scaledWindowAxisMax);
const pmax = Math.max(scaledWindowAxisMin, scaledWindowAxisMax);
const rangeMin = Math.min(scale.range()[0], scale.range()[1]);
const rangeMax = Math.max(scale.range()[0], scale.range()[1]);
const k = (rangeMax - rangeMin) / (pmax - pmin);
return { k, t: (coordinateSystemOrigin - pmin) };
};
const s = this.props.scale;
const x = (s === EScaleAxes.x || s === EScaleAxes.xy) ? range2transform(window.xMinMax, this.props.xscale, this.props.xscale.range()[0]) : null;
const y = (s === EScaleAxes.y || s === EScaleAxes.xy) ? range2transform(window.yMinMax, this.props.yscale, this.props.yscale.range()[1]) : null;
let k = 1;
if (x && y) {
k = Math.min(x.k, y.k);
}
else if (x) {
k = x.k;
}
else if (y) {
k = y.k;
}
return {
k,
tx: x ? x.t : 0,
ty: y ? y.t : 0
};
}
/**
* returns the current visible window
* @returns {{xMinMax: [number,number], yMinMax: [number,number]}}
*/
get window() {
const { x: xscale, y: yscale } = this.transformedScales();
return {
xMinMax: (xscale.range().map(xscale.invert.bind(xscale))),
yMinMax: (yscale.range().map(yscale.invert.bind(yscale)))
};
}
onZoomStart() {
this.zoomStartTransform = this.currentTransform;
}
isZoomed() {
return this.currentTransform.k !== 1;
}
shiftTransform(t) {
// zoom transform is over the whole canvas an not just the center part in which the scales are defined
return t;
}
onZoom() {
const evt = d3event;
const newValue = this.shiftTransform(evt.transform);
const oldValue = this.currentTransform;
this.currentTransform = newValue;
const scale = this.props.scale;
const tchanged = ((scale !== EScaleAxes.y && oldValue.x !== newValue.x) || (scale !== EScaleAxes.x && oldValue.y !== newValue.y));
const schanged = (oldValue.k !== newValue.k);
const delta = {
x: (scale === EScaleAxes.x || scale === EScaleAxes.xy) ? newValue.x - oldValue.x : 0,
y: (scale === EScaleAxes.y || scale === EScaleAxes.xy) ? newValue.y - oldValue.y : 0,
kx: (scale === EScaleAxes.x || scale === EScaleAxes.xy) ? newValue.k / oldValue.k : 1,
ky: (scale === EScaleAxes.y || scale === EScaleAxes.xy) ? newValue.k / oldValue.k : 1
};
if (tchanged && schanged) {
this.emit(AScatterplot.EVENT_WINDOW_CHANGED, this.window, this.transformedScales());
this.render(ERenderReason.PERFORM_SCALE_AND_TRANSLATE, delta);
}
else if (schanged) {
this.emit(AScatterplot.EVENT_WINDOW_CHANGED, this.window, this.transformedScales());
this.render(ERenderReason.PERFORM_SCALE, delta);
}
else if (tchanged) {
this.emit(AScatterplot.EVENT_WINDOW_CHANGED, this.window, this.transformedScales());
this.render(ERenderReason.PERFORM_TRANSLATE, delta);
}
//nothing if no change
this.emit(AScatterplot.EVENT_ZOOM_CHANGED, d3event);
}
onZoomEnd() {
const start = this.zoomStartTransform;
const end = this.currentTransform;
const tchanged = (start.x !== end.x || start.y !== end.y);
const schanged = (start.k !== end.k);
this.node.classList.toggle(`${EScaleAxes[this.props.scale]}-zoomed`, this.isZoomed());
if (tchanged && schanged) {
this.render(ERenderReason.AFTER_SCALE_AND_TRANSLATE);
}
else if (schanged) {
this.render(ERenderReason.AFTER_SCALE);
}
else if (tchanged) {
this.render(ERenderReason.AFTER_TRANSLATE);
}
}
onDragStart() {
this.lasso.start(d3event.x, d3event.y);
if (!this.clearSelectionImpl(true)) {
this.render(ERenderReason.SELECTION_CHANGED);
}
}
onDrag() {
if (this.dragHandle < 0) {
this.dragHandle = window.setInterval(this.updateDrag.bind(this), this.props.lasso.interval);
}
this.lasso.setCurrent(d3event.x, d3event.y);
this.render(ERenderReason.SELECTION_CHANGED);
this.emit(AScatterplot.EVENT_DRAGGED, d3event);
}
updateDrag() {
if (this.lasso.pushCurrent()) {
this.retestLasso();
}
}
onDragEnd() {
clearInterval(this.dragHandle);
this.dragHandle = -1;
this.lasso.end(d3event.x, d3event.y);
if (!this.retestLasso()) {
this.render(ERenderReason.SELECTION_CHANGED);
}
this.lasso.clear();
this.emit(AScatterplot.EVENT_SELECTION_CHANGED, this);
}
retestLasso() {
const { n2pX, n2pY } = this.transformedNormalized2PixelScales();
// shift by the margin since the scales doesn't include them for better scaling experience
const tester = this.lasso.tester(n2pX.invert.bind(n2pX), n2pY.invert.bind(n2pY), -this.props.marginLeft, -this.props.marginTop);
return tester && this.selectWithTester(tester, true);
}
onClick(event) {
if (event.button > 0) {
//right button or something like that = reset
this.selection = [];
return;
}
const { x, y, clickRadiusX, clickRadiusY } = this.getMouseNormalizedPos();
//find closest data item
const tester = QuadtreeUtils.ellipseTester(x, y, clickRadiusX, clickRadiusY);
this.selectWithTester(tester);
this.emit(AScatterplot.EVENT_MOUSE_CLICKED, event);
}
showTooltip(canvasPos, event) {
const items = this.findItems(canvasPos);
// canvas pos doesn't include the margin
this.props.showTooltip(this.parent, items, canvasPos[0] + this.props.marginLeft, canvasPos[1] + this.props.marginTop, event);
this.showTooltipHandle = -1;
}
findItems(canvasPos) {
const { x, y, clickRadiusX, clickRadiusY } = this.getMouseNormalizedPos(canvasPos);
const tester = QuadtreeUtils.ellipseTester(x, y, clickRadiusX, clickRadiusY);
return QuadtreeUtils.findByTester(this.tree, tester);
}
onMouseMove(event) {
if (this.showTooltipHandle >= 0) {
this.onMouseLeave(event);
}
const pos = this.mousePosAtCanvas();
//TODO find a more efficient way or optimize the timing
this.showTooltipHandle = window.setTimeout(this.showTooltip.bind(this, pos, event), this.props.tooltipDelay);
this.emit(AScatterplot.EVENT_MOUSE_MOVED, event);
}
onMouseLeave(event) {
clearTimeout(this.showTooltipHandle);
this.showTooltipHandle = -1;
this.props.showTooltip(this.parent, [], 0, 0, event);
}
traverseTree(ctx, tree, renderer, xscale, yscale, isNodeVisible, debug = false, x, y) {
//debug stats
let rendered = 0, aggregated = 0, hidden = 0;
const { n2pX, n2pY } = this.transformedNormalized2PixelScales();
const visitTree = (node, x0, y0, x1, y1) => {
if (!isNodeVisible(x0, y0, x1, y1)) {
hidden += debug ? QuadtreeUtils.getTreeSize(node) : 0;
return QuadtreeUtils.ABORT_TRAVERSAL;
}
if (this.useAggregation(n2pX, n2pY, x0, y0, x1, y1)) {
const d = QuadtreeUtils.getFirstLeaf(node);
//debuglog('aggregate', getTreeSize(node));
rendered++;
aggregated += debug ? (QuadtreeUtils.getTreeSize(node) - 1) : 0;
renderer.render(xscale(x(d)), yscale(y(d)), d);
return QuadtreeUtils.ABORT_TRAVERSAL;
}
if (QuadtreeUtils.isLeafNode(node)) { //is a leaf
rendered += QuadtreeUtils.forEachLeaf(node, (d) => renderer.render(xscale(x(d)), yscale(y(d)), d));
}
return QuadtreeUtils.CONTINUE_TRAVERSAL;
};
ctx.save();
tree.visit(visitTree);
renderer.done();
if (debug) {
debuglog('rendered', rendered, 'aggregated', aggregated, 'hidden', hidden, 'total', this.tree.size());
}
//a dummy path to clear the 'to draw' state
ctx.beginPath();
ctx.closePath();
ctx.restore();
}
setAxisFormat(axis, key) {
const f = this.props.format[key];
if (f != null) {
axis.tickFormat(typeof f === 'string' ? format(f) : f);
}
const t = this.props.ticks[key];
if (t != null) {
axis.tickValues(Array.isArray(t) ? t : t(axis.scale()));
}
}
transformData(c, bounds, boundsWidth, boundsHeight, x, y, kx, ky) {
//idea copy the data layer to selection layer in a transformed way and swap
const ctx = this.canvasSelectionLayer.getContext('2d');
ctx.clearRect(0, 0, c.width, c.height);
ctx.save();
ctx.rect(bounds.x0, bounds.y0, boundsWidth, boundsHeight);
ctx.clip();
//ctx.translate(bounds.x0, bounds.y0+bounds_height); //move to visible area
//debuglog(x,y,k, bounds.x0, bounds.y0, n2pX(0), n2pY(100), this.currentTransform.x, this.currentTransform.y);
//ctx.scale(k,k);
//ctx.translate(0, -bounds_height); //move to visible area
ctx.translate(x, y);
//copy just the visible area
//canvas, clip area, target area
//see http://www.w3schools.com/tags/canvas_drawimage.asp
ctx.drawImage(this.canvasDataLayer, bounds.x0, bounds.y0, boundsWidth, boundsHeight, bounds.x0, bounds.y0, boundsWidth * kx, boundsHeight * ky);
ctx.restore();
//swap and update class names
[this.canvasDataLayer, this.canvasSelectionLayer] = [this.canvasSelectionLayer, this.canvasDataLayer];
this.canvasDataLayer.className = `${cssprefix}-data-layer`;
this.canvasSelectionLayer.className = `${cssprefix}-selection-layer`;
}
useAggregation(n2pX, n2pY, x0, y0, x1, y1) {
x0 = n2pX(x0);
y0 = n2pY(y0);
x1 = n2pX(x1);
y1 = n2pY(y1);
const minSize = Math.max(Math.abs(x0 - x1), Math.abs(y0 - y1));
return minSize < 5; //TODO tune depend on visual impact
}
}
AScatterplot.EVENT_SELECTION_CHANGED = 'selectionChanged';
AScatterplot.EVENT_SELECTION_IN_PROGRESS_CHANGED = 'selectionInProgressChanged';
AScatterplot.EVENT_RENDER = 'render';
AScatterplot.EVENT_WINDOW_CHANGED = 'windowChanged';
AScatterplot.EVENT_MOUSE_CLICKED = 'mouseClicked';
AScatterplot.EVENT_DRAGGED = 'dragged';
AScatterplot.EVENT_MOUSE_MOVED = 'mouseMoved';
AScatterplot.EVENT_ZOOM_CHANGED = 'zoomChanged';
//# sourceMappingURL=AScatterplot.js.map