StackGenVis: Alignment of Data, Algorithms, and Models for Stacking Ensemble Learning Using Performance Metrics
https://doi.org/10.1109/TVCG.2020.3030352
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
538 lines
16 KiB
538 lines
16 KiB
import Interactable from "../../core/Interactable.js";
|
|
import { ActionName, Scope } from "../../core/scope.js";
|
|
import * as utils from "../../utils/index.js";
|
|
import drag from "../drag.js";
|
|
import DropEvent from "./DropEvent.js";
|
|
|
|
function install(scope) {
|
|
const {
|
|
actions,
|
|
|
|
/** @lends module:interact */
|
|
interact,
|
|
|
|
/** @lends Interactable */
|
|
Interactable,
|
|
// eslint-disable-line no-shadow
|
|
defaults
|
|
} = scope;
|
|
scope.usePlugin(drag);
|
|
/**
|
|
*
|
|
* ```js
|
|
* interact('.drop').dropzone({
|
|
* accept: '.can-drop' || document.getElementById('single-drop'),
|
|
* overlap: 'pointer' || 'center' || zeroToOne
|
|
* }
|
|
* ```
|
|
*
|
|
* Returns or sets whether draggables can be dropped onto this target to
|
|
* trigger drop events
|
|
*
|
|
* Dropzones can receive the following events:
|
|
* - `dropactivate` and `dropdeactivate` when an acceptable drag starts and ends
|
|
* - `dragenter` and `dragleave` when a draggable enters and leaves the dropzone
|
|
* - `dragmove` when a draggable that has entered the dropzone is moved
|
|
* - `drop` when a draggable is dropped into this dropzone
|
|
*
|
|
* Use the `accept` option to allow only elements that match the given CSS
|
|
* selector or element. The value can be:
|
|
*
|
|
* - **an Element** - only that element can be dropped into this dropzone.
|
|
* - **a string**, - the element being dragged must match it as a CSS selector.
|
|
* - **`null`** - accept options is cleared - it accepts any element.
|
|
*
|
|
* Use the `overlap` option to set how drops are checked for. The allowed
|
|
* values are:
|
|
*
|
|
* - `'pointer'`, the pointer must be over the dropzone (default)
|
|
* - `'center'`, the draggable element's center must be over the dropzone
|
|
* - a number from 0-1 which is the `(intersection area) / (draggable area)`.
|
|
* e.g. `0.5` for drop to happen when half of the area of the draggable is
|
|
* over the dropzone
|
|
*
|
|
* Use the `checker` option to specify a function to check if a dragged element
|
|
* is over this Interactable.
|
|
*
|
|
* @param {boolean | object | null} [options] The new options to be set.
|
|
* @return {boolean | Interactable} The current setting or this Interactable
|
|
*/
|
|
|
|
Interactable.prototype.dropzone = function (options) {
|
|
return dropzoneMethod(this, options);
|
|
};
|
|
/**
|
|
* ```js
|
|
* interact(target)
|
|
* .dropChecker(function(dragEvent, // related dragmove or dragend event
|
|
* event, // TouchEvent/PointerEvent/MouseEvent
|
|
* dropped, // bool result of the default checker
|
|
* dropzone, // dropzone Interactable
|
|
* dropElement, // dropzone elemnt
|
|
* draggable, // draggable Interactable
|
|
* draggableElement) {// draggable element
|
|
*
|
|
* return dropped && event.target.hasAttribute('allow-drop')
|
|
* }
|
|
* ```
|
|
*/
|
|
|
|
|
|
Interactable.prototype.dropCheck = function (dragEvent, event, draggable, draggableElement, dropElement, rect) {
|
|
return dropCheckMethod(this, dragEvent, event, draggable, draggableElement, dropElement, rect);
|
|
};
|
|
/**
|
|
* Returns or sets whether the dimensions of dropzone elements are calculated
|
|
* on every dragmove or only on dragstart for the default dropChecker
|
|
*
|
|
* @param {boolean} [newValue] True to check on each move. False to check only
|
|
* before start
|
|
* @return {boolean | interact} The current setting or interact
|
|
*/
|
|
|
|
|
|
interact.dynamicDrop = function (newValue) {
|
|
if (utils.is.bool(newValue)) {
|
|
// if (dragging && scope.dynamicDrop !== newValue && !newValue) {
|
|
// calcRects(dropzones)
|
|
// }
|
|
scope.dynamicDrop = newValue;
|
|
return interact;
|
|
}
|
|
|
|
return scope.dynamicDrop;
|
|
};
|
|
|
|
utils.arr.merge(actions.eventTypes, ['dragenter', 'dragleave', 'dropactivate', 'dropdeactivate', 'dropmove', 'drop']);
|
|
actions.methodDict.drop = 'dropzone';
|
|
scope.dynamicDrop = false;
|
|
defaults.actions.drop = drop.defaults;
|
|
}
|
|
|
|
function collectDrops({
|
|
interactables
|
|
}, draggableElement) {
|
|
const drops = []; // collect all dropzones and their elements which qualify for a drop
|
|
|
|
for (const dropzone of interactables.list) {
|
|
if (!dropzone.options.drop.enabled) {
|
|
continue;
|
|
}
|
|
|
|
const accept = dropzone.options.drop.accept; // test the draggable draggableElement against the dropzone's accept setting
|
|
|
|
if (utils.is.element(accept) && accept !== draggableElement || utils.is.string(accept) && !utils.dom.matchesSelector(draggableElement, accept) || utils.is.func(accept) && !accept({
|
|
dropzone,
|
|
draggableElement
|
|
})) {
|
|
continue;
|
|
} // query for new elements if necessary
|
|
|
|
|
|
const dropElements = utils.is.string(dropzone.target) ? dropzone._context.querySelectorAll(dropzone.target) : utils.is.array(dropzone.target) ? dropzone.target : [dropzone.target];
|
|
|
|
for (const dropzoneElement of dropElements) {
|
|
if (dropzoneElement !== draggableElement) {
|
|
drops.push({
|
|
dropzone,
|
|
element: dropzoneElement
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
return drops;
|
|
}
|
|
|
|
function fireActivationEvents(activeDrops, event) {
|
|
// loop through all active dropzones and trigger event
|
|
for (const {
|
|
dropzone,
|
|
element
|
|
} of activeDrops.slice()) {
|
|
event.dropzone = dropzone; // set current element as event target
|
|
|
|
event.target = element;
|
|
dropzone.fire(event);
|
|
event.propagationStopped = event.immediatePropagationStopped = false;
|
|
}
|
|
} // return a new array of possible drops. getActiveDrops should always be
|
|
// called when a drag has just started or a drag event happens while
|
|
// dynamicDrop is true
|
|
|
|
|
|
function getActiveDrops(scope, dragElement) {
|
|
// get dropzones and their elements that could receive the draggable
|
|
const activeDrops = collectDrops(scope, dragElement);
|
|
|
|
for (const activeDrop of activeDrops) {
|
|
activeDrop.rect = activeDrop.dropzone.getRect(activeDrop.element);
|
|
}
|
|
|
|
return activeDrops;
|
|
}
|
|
|
|
function getDrop({
|
|
dropState,
|
|
interactable: draggable,
|
|
element: dragElement
|
|
}, dragEvent, pointerEvent) {
|
|
const validDrops = []; // collect all dropzones and their elements which qualify for a drop
|
|
|
|
for (const {
|
|
dropzone,
|
|
element: dropzoneElement,
|
|
rect
|
|
} of dropState.activeDrops) {
|
|
validDrops.push(dropzone.dropCheck(dragEvent, pointerEvent, draggable, dragElement, dropzoneElement, rect) ? dropzoneElement : null);
|
|
} // get the most appropriate dropzone based on DOM depth and order
|
|
|
|
|
|
const dropIndex = utils.dom.indexOfDeepestElement(validDrops);
|
|
return dropState.activeDrops[dropIndex] || null;
|
|
}
|
|
|
|
function getDropEvents(interaction, _pointerEvent, dragEvent) {
|
|
const {
|
|
dropState
|
|
} = interaction;
|
|
const dropEvents = {
|
|
enter: null,
|
|
leave: null,
|
|
activate: null,
|
|
deactivate: null,
|
|
move: null,
|
|
drop: null
|
|
};
|
|
|
|
if (dragEvent.type === 'dragstart') {
|
|
dropEvents.activate = new DropEvent(dropState, dragEvent, 'dropactivate');
|
|
dropEvents.activate.target = null;
|
|
dropEvents.activate.dropzone = null;
|
|
}
|
|
|
|
if (dragEvent.type === 'dragend') {
|
|
dropEvents.deactivate = new DropEvent(dropState, dragEvent, 'dropdeactivate');
|
|
dropEvents.deactivate.target = null;
|
|
dropEvents.deactivate.dropzone = null;
|
|
}
|
|
|
|
if (dropState.rejected) {
|
|
return dropEvents;
|
|
}
|
|
|
|
if (dropState.cur.element !== dropState.prev.element) {
|
|
// if there was a previous dropzone, create a dragleave event
|
|
if (dropState.prev.dropzone) {
|
|
dropEvents.leave = new DropEvent(dropState, dragEvent, 'dragleave');
|
|
dragEvent.dragLeave = dropEvents.leave.target = dropState.prev.element;
|
|
dragEvent.prevDropzone = dropEvents.leave.dropzone = dropState.prev.dropzone;
|
|
} // if dropzone is not null, create a dragenter event
|
|
|
|
|
|
if (dropState.cur.dropzone) {
|
|
dropEvents.enter = new DropEvent(dropState, dragEvent, 'dragenter');
|
|
dragEvent.dragEnter = dropState.cur.element;
|
|
dragEvent.dropzone = dropState.cur.dropzone;
|
|
}
|
|
}
|
|
|
|
if (dragEvent.type === 'dragend' && dropState.cur.dropzone) {
|
|
dropEvents.drop = new DropEvent(dropState, dragEvent, 'drop');
|
|
dragEvent.dropzone = dropState.cur.dropzone;
|
|
dragEvent.relatedTarget = dropState.cur.element;
|
|
}
|
|
|
|
if (dragEvent.type === 'dragmove' && dropState.cur.dropzone) {
|
|
dropEvents.move = new DropEvent(dropState, dragEvent, 'dropmove');
|
|
dropEvents.move.dragmove = dragEvent;
|
|
dragEvent.dropzone = dropState.cur.dropzone;
|
|
}
|
|
|
|
return dropEvents;
|
|
}
|
|
|
|
function fireDropEvents(interaction, events) {
|
|
const {
|
|
dropState
|
|
} = interaction;
|
|
const {
|
|
activeDrops,
|
|
cur,
|
|
prev
|
|
} = dropState;
|
|
|
|
if (events.leave) {
|
|
prev.dropzone.fire(events.leave);
|
|
}
|
|
|
|
if (events.move) {
|
|
cur.dropzone.fire(events.move);
|
|
}
|
|
|
|
if (events.enter) {
|
|
cur.dropzone.fire(events.enter);
|
|
}
|
|
|
|
if (events.drop) {
|
|
cur.dropzone.fire(events.drop);
|
|
}
|
|
|
|
if (events.deactivate) {
|
|
fireActivationEvents(activeDrops, events.deactivate);
|
|
}
|
|
|
|
dropState.prev.dropzone = cur.dropzone;
|
|
dropState.prev.element = cur.element;
|
|
}
|
|
|
|
function onEventCreated({
|
|
interaction,
|
|
iEvent,
|
|
event
|
|
}, scope) {
|
|
if (iEvent.type !== 'dragmove' && iEvent.type !== 'dragend') {
|
|
return;
|
|
}
|
|
|
|
const {
|
|
dropState
|
|
} = interaction;
|
|
|
|
if (scope.dynamicDrop) {
|
|
dropState.activeDrops = getActiveDrops(scope, interaction.element);
|
|
}
|
|
|
|
const dragEvent = iEvent;
|
|
const dropResult = getDrop(interaction, dragEvent, event); // update rejected status
|
|
|
|
dropState.rejected = dropState.rejected && !!dropResult && dropResult.dropzone === dropState.cur.dropzone && dropResult.element === dropState.cur.element;
|
|
dropState.cur.dropzone = dropResult && dropResult.dropzone;
|
|
dropState.cur.element = dropResult && dropResult.element;
|
|
dropState.events = getDropEvents(interaction, event, dragEvent);
|
|
}
|
|
|
|
function dropzoneMethod(interactable, options) {
|
|
if (utils.is.object(options)) {
|
|
interactable.options.drop.enabled = options.enabled !== false;
|
|
|
|
if (options.listeners) {
|
|
const normalized = utils.normalizeListeners(options.listeners); // rename 'drop' to '' as it will be prefixed with 'drop'
|
|
|
|
const corrected = Object.keys(normalized).reduce((acc, type) => {
|
|
const correctedType = /^(enter|leave)/.test(type) ? `drag${type}` : /^(activate|deactivate|move)/.test(type) ? `drop${type}` : type;
|
|
acc[correctedType] = normalized[type];
|
|
return acc;
|
|
}, {});
|
|
interactable.off(interactable.options.drop.listeners);
|
|
interactable.on(corrected);
|
|
interactable.options.drop.listeners = corrected;
|
|
}
|
|
|
|
if (utils.is.func(options.ondrop)) {
|
|
interactable.on('drop', options.ondrop);
|
|
}
|
|
|
|
if (utils.is.func(options.ondropactivate)) {
|
|
interactable.on('dropactivate', options.ondropactivate);
|
|
}
|
|
|
|
if (utils.is.func(options.ondropdeactivate)) {
|
|
interactable.on('dropdeactivate', options.ondropdeactivate);
|
|
}
|
|
|
|
if (utils.is.func(options.ondragenter)) {
|
|
interactable.on('dragenter', options.ondragenter);
|
|
}
|
|
|
|
if (utils.is.func(options.ondragleave)) {
|
|
interactable.on('dragleave', options.ondragleave);
|
|
}
|
|
|
|
if (utils.is.func(options.ondropmove)) {
|
|
interactable.on('dropmove', options.ondropmove);
|
|
}
|
|
|
|
if (/^(pointer|center)$/.test(options.overlap)) {
|
|
interactable.options.drop.overlap = options.overlap;
|
|
} else if (utils.is.number(options.overlap)) {
|
|
interactable.options.drop.overlap = Math.max(Math.min(1, options.overlap), 0);
|
|
}
|
|
|
|
if ('accept' in options) {
|
|
interactable.options.drop.accept = options.accept;
|
|
}
|
|
|
|
if ('checker' in options) {
|
|
interactable.options.drop.checker = options.checker;
|
|
}
|
|
|
|
return interactable;
|
|
}
|
|
|
|
if (utils.is.bool(options)) {
|
|
interactable.options.drop.enabled = options;
|
|
return interactable;
|
|
}
|
|
|
|
return interactable.options.drop;
|
|
}
|
|
|
|
function dropCheckMethod(interactable, dragEvent, event, draggable, draggableElement, dropElement, rect) {
|
|
let dropped = false; // if the dropzone has no rect (eg. display: none)
|
|
// call the custom dropChecker or just return false
|
|
|
|
if (!(rect = rect || interactable.getRect(dropElement))) {
|
|
return interactable.options.drop.checker ? interactable.options.drop.checker(dragEvent, event, dropped, interactable, dropElement, draggable, draggableElement) : false;
|
|
}
|
|
|
|
const dropOverlap = interactable.options.drop.overlap;
|
|
|
|
if (dropOverlap === 'pointer') {
|
|
const origin = utils.getOriginXY(draggable, draggableElement, ActionName.Drag);
|
|
const page = utils.pointer.getPageXY(dragEvent);
|
|
page.x += origin.x;
|
|
page.y += origin.y;
|
|
const horizontal = page.x > rect.left && page.x < rect.right;
|
|
const vertical = page.y > rect.top && page.y < rect.bottom;
|
|
dropped = horizontal && vertical;
|
|
}
|
|
|
|
const dragRect = draggable.getRect(draggableElement);
|
|
|
|
if (dragRect && dropOverlap === 'center') {
|
|
const cx = dragRect.left + dragRect.width / 2;
|
|
const cy = dragRect.top + dragRect.height / 2;
|
|
dropped = cx >= rect.left && cx <= rect.right && cy >= rect.top && cy <= rect.bottom;
|
|
}
|
|
|
|
if (dragRect && utils.is.number(dropOverlap)) {
|
|
const overlapArea = Math.max(0, Math.min(rect.right, dragRect.right) - Math.max(rect.left, dragRect.left)) * Math.max(0, Math.min(rect.bottom, dragRect.bottom) - Math.max(rect.top, dragRect.top));
|
|
const overlapRatio = overlapArea / (dragRect.width * dragRect.height);
|
|
dropped = overlapRatio >= dropOverlap;
|
|
}
|
|
|
|
if (interactable.options.drop.checker) {
|
|
dropped = interactable.options.drop.checker(dragEvent, event, dropped, interactable, dropElement, draggable, draggableElement);
|
|
}
|
|
|
|
return dropped;
|
|
}
|
|
|
|
const drop = {
|
|
id: 'actions/drop',
|
|
install,
|
|
listeners: {
|
|
'interactions:before-action-start': ({
|
|
interaction
|
|
}) => {
|
|
if (interaction.prepared.name !== 'drag') {
|
|
return;
|
|
}
|
|
|
|
interaction.dropState = {
|
|
cur: {
|
|
dropzone: null,
|
|
element: null
|
|
},
|
|
prev: {
|
|
dropzone: null,
|
|
element: null
|
|
},
|
|
rejected: null,
|
|
events: null,
|
|
activeDrops: []
|
|
};
|
|
},
|
|
'interactions:after-action-start': ({
|
|
interaction,
|
|
event,
|
|
iEvent: dragEvent
|
|
}, scope) => {
|
|
if (interaction.prepared.name !== 'drag') {
|
|
return;
|
|
}
|
|
|
|
const {
|
|
dropState
|
|
} = interaction; // reset active dropzones
|
|
|
|
dropState.activeDrops = null;
|
|
dropState.events = null;
|
|
dropState.activeDrops = getActiveDrops(scope, interaction.element);
|
|
dropState.events = getDropEvents(interaction, event, dragEvent);
|
|
|
|
if (dropState.events.activate) {
|
|
fireActivationEvents(dropState.activeDrops, dropState.events.activate);
|
|
scope.fire('actions/drop:start', {
|
|
interaction,
|
|
dragEvent
|
|
});
|
|
}
|
|
},
|
|
// FIXME proper signal types
|
|
'interactions:action-move': onEventCreated,
|
|
'interactions:action-end': onEventCreated,
|
|
'interactions:after-action-move': function fireDropAfterMove({
|
|
interaction,
|
|
iEvent: dragEvent
|
|
}, scope) {
|
|
if (interaction.prepared.name !== 'drag') {
|
|
return;
|
|
}
|
|
|
|
fireDropEvents(interaction, interaction.dropState.events);
|
|
scope.fire('actions/drop:move', {
|
|
interaction,
|
|
dragEvent
|
|
});
|
|
interaction.dropState.events = {};
|
|
},
|
|
'interactions:after-action-end': ({
|
|
interaction,
|
|
iEvent: dragEvent
|
|
}, scope) => {
|
|
if (interaction.prepared.name !== 'drag') {
|
|
return;
|
|
}
|
|
|
|
fireDropEvents(interaction, interaction.dropState.events);
|
|
scope.fire('actions/drop:end', {
|
|
interaction,
|
|
dragEvent
|
|
});
|
|
},
|
|
'interactions:stop': ({
|
|
interaction
|
|
}) => {
|
|
if (interaction.prepared.name !== 'drag') {
|
|
return;
|
|
}
|
|
|
|
const {
|
|
dropState
|
|
} = interaction;
|
|
|
|
if (dropState) {
|
|
dropState.activeDrops = null;
|
|
dropState.events = null;
|
|
dropState.cur.dropzone = null;
|
|
dropState.cur.element = null;
|
|
dropState.prev.dropzone = null;
|
|
dropState.prev.element = null;
|
|
dropState.rejected = false;
|
|
}
|
|
}
|
|
},
|
|
getActiveDrops,
|
|
getDrop,
|
|
getDropEvents,
|
|
fireDropEvents,
|
|
defaults: {
|
|
enabled: false,
|
|
accept: null,
|
|
overlap: 'pointer'
|
|
}
|
|
};
|
|
export default drop;
|
|
//# sourceMappingURL=index.js.map
|