Skip to main content
Module

x/billboardjs/interactions/interaction.js

πŸ“Š Re-usable, easy interface JavaScript chart library based on D3.js
Go to Latest
File
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676
/** * Copyright (c) 2017 ~ present NAVER Corp. * billboard.js project is licensed under the MIT license */import { mouse as d3Mouse, select as d3Select, event as d3Event} from "d3-selection";import {drag as d3Drag} from "d3-drag";import ChartInternal from "../internals/ChartInternal";import {document} from "../internals/browser";import CLASS from "../config/classes";import {emulateEvent, extend, isBoolean, isNumber, isObject} from "../internals/util";
extend(ChartInternal.prototype, { /** * Initialize the area that detects the event. * Add a container for the zone that detects the event. * @private */ initEventRect() { const $$ = this;
$$.main.select(`.${CLASS.chart}`) .append("g") .attr("class", CLASS.eventRects) .style("fill-opacity", "0"); },
/** * Redraws the area that detects the event. * @private */ redrawEventRect() { const $$ = this; const config = $$.config; const isMultipleX = $$.isMultipleX(); let eventRectUpdate;
const zoomEnabled = config.zoom_enabled; const eventRects = $$.main.select(`.${CLASS.eventRects}`) .style("cursor", zoomEnabled && zoomEnabled.type !== "drag" ? ( config.axis_rotated ? "ns-resize" : "ew-resize" ) : null) .classed(CLASS.eventRectsMultiple, isMultipleX) .classed(CLASS.eventRectsSingle, !isMultipleX);
// clear old rects eventRects.selectAll(`.${CLASS.eventRect}`).remove();
// open as public constiable $$.eventRect = eventRects.selectAll(`.${CLASS.eventRect}`);
if (isMultipleX) { eventRectUpdate = $$.eventRect.data([0]); // update // enter: only one rect will be added // exit: not needed because always only one rect exists eventRectUpdate = $$.generateEventRectsForMultipleXs(eventRectUpdate.enter()) .merge(eventRectUpdate); } else { // Set data and update $$.eventRect const xAxisTickValues = $$.getMaxDataCountTarget();
// update data's index value to be alinged with the x Axis $$.updateDataIndexByX(xAxisTickValues); $$.updateXs(xAxisTickValues); $$.updatePointClass(true);
eventRects.datum(xAxisTickValues);
$$.eventRect = eventRects.selectAll(`.${CLASS.eventRect}`); eventRectUpdate = $$.eventRect.data(d => d);
// exit eventRectUpdate.exit().remove();
// update eventRectUpdate = $$.generateEventRectsForSingleX(eventRectUpdate.enter()) .merge(eventRectUpdate); }
$$.eventRect = eventRectUpdate; $$.updateEventRect(eventRectUpdate);
if ($$.inputType === "touch" && !$$.svg.on("touchstart.eventRect") && !$$.hasArcType()) { $$.bindTouchOnEventRect(isMultipleX); } },
bindTouchOnEventRect(isMultipleX) { const $$ = this; const config = $$.config;
const getEventRect = () => { const touch = d3Event.changedTouches[0];
return d3Select(document.elementFromPoint(touch.clientX, touch.clientY)); };
const getIndex = eventRect => { let index = eventRect && eventRect.attr("class") && eventRect.attr("class") .replace(new RegExp(`(${CLASS.eventRect}-?|s)`, "g"), "") * 1;
if (isNaN(index) || index === null) { index = -1; }
return index; };
const selectRect = context => { if (isMultipleX) { $$.selectRectForMultipleXs(context); } else { const eventRect = getEventRect(); const index = getIndex(eventRect);
$$.callOverOutForTouch(index);
index === -1 ? $$.unselectRect() : $$.selectRectForSingle(context, eventRect, index); } };
// call event.preventDefault() // according 'interaction.inputType.touch.preventDefault' option const preventDefault = config.interaction_inputType_touch.preventDefault; const isPrevented = (isBoolean(preventDefault) && preventDefault) || false; const preventThreshold = (!isNaN(preventDefault) && preventDefault) || null; let startPx;
const preventEvent = event => { const eventType = event.type; const touch = event.changedTouches[0]; const currentXY = touch[`client${config.axis_rotated ? "Y" : "X"}`];
// prevent document scrolling if (eventType === "touchstart") { if (isPrevented) { event.preventDefault(); } else if (preventThreshold !== null) { startPx = currentXY; } } else if (eventType === "touchmove") { if (isPrevented || startPx === true || ( preventThreshold !== null && Math.abs(startPx - currentXY) >= preventThreshold )) { // once prevented, keep prevented during whole 'touchmove' context startPx = true; event.preventDefault(); } } };
// bind touch events $$.svg .on("touchstart.eventRect touchmove.eventRect", function() { const eventRect = getEventRect(); const event = d3Event;
if (!eventRect.empty() && eventRect.classed(CLASS.eventRect)) { // if touch points are > 1, means doing zooming interaction. In this case do not execute tooltip codes. if ($$.dragging || $$.flowing || $$.hasArcType() || event.touches.length > 1) { return; }
preventEvent(event); selectRect(this); } else { $$.unselectRect(); $$.callOverOutForTouch(); } }, true) .on("touchend.eventRect", () => { const eventRect = getEventRect();
if (!eventRect.empty() && eventRect.classed(CLASS.eventRect)) { if ($$.hasArcType() || !$$.toggleShape || $$.cancelClick) { $$.cancelClick && ($$.cancelClick = false); } } }, true); },
/** * Updates the location and size of the eventRect. * @private * @param {Object} d3.select(CLASS.eventRects) object. */ updateEventRect(eventRectUpdate) { const $$ = this; const config = $$.config; const xScale = $$.zoomScale || $$.x; const eventRectData = eventRectUpdate || $$.eventRect.data(); // set update selection if null const isRotated = config.axis_rotated; let x; let y; let w; let h;
if ($$.isMultipleX()) { // TODO: rotated not supported yet x = 0; y = 0; w = $$.width; h = $$.height; } else { let rectW; let rectX;
if ($$.isCategorized()) { rectW = $$.getEventRectWidth(); rectX = d => xScale(d.x) - (rectW / 2); } else { const getPrevNextX = d => { const index = d.index;
return { prev: $$.getPrevX(index), next: $$.getNextX(index) }; };
rectW = d => { const x = getPrevNextX(d);
// if there this is a single data point make the eventRect full width (or height) if (x.prev === null && x.next === null) { return isRotated ? $$.height : $$.width; }
if (x.prev === null) { x.prev = xScale.domain()[0]; }
if (x.next === null) { x.next = xScale.domain()[1]; }
return Math.max(0, (xScale(x.next) - xScale(x.prev)) / 2); };
rectX = d => { const x = getPrevNextX(d); const thisX = d.x;
// if there this is a single data point position the eventRect at 0 if (x.prev === null && x.next === null) { return 0; }
if (x.prev === null) { x.prev = xScale.domain()[0]; }
return (xScale(thisX) + xScale(x.prev)) / 2; }; }
x = isRotated ? 0 : rectX; y = isRotated ? rectX : 0; w = isRotated ? $$.width : rectW; h = isRotated ? rectW : $$.height; }
eventRectData.attr("class", $$.classEvent.bind($$)) .attr("x", x) .attr("y", y) .attr("width", w) .attr("height", h); },
selectRectForSingle(context, eventRect, index) { const $$ = this; const config = $$.config; const isSelectionEnabled = config.data_selection_enabled; const isSelectionGrouped = config.data_selection_grouped; const isTooltipGrouped = config.tooltip_grouped; const selectedData = $$.getAllValuesOnIndex(index);
if (isTooltipGrouped) { $$.showTooltip(selectedData, context); $$.showGridFocus(selectedData);
if (!isSelectionEnabled || isSelectionGrouped) { return; } }
$$.main.selectAll(`.${CLASS.shape}-${index}`) .each(function() { d3Select(this).classed(CLASS.EXPANDED, true);
if (isSelectionEnabled) { eventRect.style("cursor", isSelectionGrouped ? "pointer" : null); }
if (!isTooltipGrouped) { $$.hideGridFocus(); $$.hideTooltip();
!isSelectionGrouped && $$.expandCirclesBars(index); } }) .filter(function(d) { return $$.isWithinShape(this, d); }) .call(selected => { const d = selected.data();
if (isSelectionEnabled && (isSelectionGrouped || config.data_selection_isselectable(d))) { eventRect.style("cursor", "pointer"); }
if (!isTooltipGrouped) { $$.showTooltip(d, context); $$.showGridFocus(d);
$$.unexpandCircles(); selected.each(d => $$.expandCirclesBars(index, d.id)); } }); },
expandCirclesBars(index, id, reset) { const $$ = this; const config = $$.config;
config.point_focus_expand_enabled && $$.expandCircles(index, id, reset);
$$.expandBars(index, id, reset); },
selectRectForMultipleXs(context) { const $$ = this; const config = $$.config; const targetsToShow = $$.filterTargetsToShow($$.data.targets);
// do nothing when dragging if ($$.dragging || $$.hasArcType(targetsToShow)) { return; }
const mouse = d3Mouse(context); const closest = $$.findClosestFromTargets(targetsToShow, mouse);
if ($$.mouseover && (!closest || closest.id !== $$.mouseover.id)) { config.data_onout.call($$.api, $$.mouseover); $$.mouseover = undefined; }
if (!closest) { $$.unselectRect(); return; }
const sameXData = ( $$.isBubbleType(closest) || $$.isScatterType(closest) || !config.tooltip_grouped ) ? [closest] : $$.filterByX(targetsToShow, closest.x);
// show tooltip when cursor is close to some point const selectedData = sameXData.map(d => $$.addName(d));
$$.showTooltip(selectedData, context);
// expand points $$.expandCirclesBars(closest.index, closest.id, true);
// Show xgrid focus line $$.showGridFocus(selectedData);
// Show cursor as pointer if point is close to mouse position if ($$.isBarType(closest.id) || $$.dist(closest, mouse) < config.point_sensitivity) { $$.svg.select(`.${CLASS.eventRect}`).style("cursor", "pointer");
if (!$$.mouseover) { config.data_onover.call($$.api, closest); $$.mouseover = closest; } } },
/** * Unselect EventRect. * @private */ unselectRect() { const $$ = this;
$$.svg.select(`.${CLASS.eventRect}`).style("cursor", null); $$.hideGridFocus(); $$.hideTooltip(); $$._handleLinkedCharts(false); $$.unexpandCircles(); $$.unexpandBars(); },
/** * Handle data.onover/out callback options * @param {Boolean} isOver * @param {Number|Object} d * @private */ setOverOut(isOver, d) { const $$ = this; const config = $$.config; const isArc = isObject(d);
// Call event handler if (isArc || d !== -1) { let callback = config[isOver ? "data_onover" : "data_onout"].bind($$.api);
config.color_onover && $$.setOverColor(isOver, d, isArc);
if (isArc) { callback(d, $$.main.select(`.${CLASS.arc}${$$.getTargetSelectorSuffix(d.id)}`).node()); } else if (!config.tooltip_grouped) { const callee = $$.setOverOut; let last = callee.last || [];
const shape = $$.main.selectAll(`.${CLASS.shape}-${d}`) .filter(function(d) { return $$.isWithinShape(this, d); });
shape .each(function(d) { if (last.length === 0 || last.every(v => v !== this)) { callback(d, this); last.push(this); } });
if (last.length > 0 && shape.empty()) { callback = config.data_onout.bind($$.api);
last.forEach(v => callback(d3Select(v).datum(), v)); last = []; }
callee.last = last; } else { isOver && $$.expandCirclesBars(d, null, true);
!$$.isMultipleX() && $$.main.selectAll(`.${CLASS.shape}-${d}`) .each(function(d) { callback(d, this); }); } } },
/** * Call data.onover/out callback for touch event * @param {Number|Object} d target index or data object for Arc type * @private */ callOverOutForTouch(d) { const $$ = this; const callee = $$.callOverOutForTouch; const last = callee.last;
if (isObject(d) && last ? d.id !== last.id : (d !== last)) { (last || isNumber(last)) && $$.setOverOut(false, last); (d || isNumber(d)) && $$.setOverOut(true, d);
callee.last = d; } },
/** * Return draggable selection function * @return {Function} * @private */ getDraggableSelection() { const $$ = this; const config = $$.config;
return config.interaction_enabled && config.data_selection_draggable && $$.drag ? d3Drag() .on("drag", function() { $$.drag(d3Mouse(this)); }) .on("start", function() { $$.dragstart(d3Mouse(this)); }) .on("end", () => { $$.dragend(); }) : () => {}; },
/** * Create eventRect for each data on the x-axis. * Register touch and drag events. * @private * @param {Object} d3.select(CLASS.eventRects) object. * @returns {Object} d3.select(CLASS.eventRects) object. */ generateEventRectsForSingleX(eventRectEnter) { const $$ = this; const config = $$.config;
const rect = eventRectEnter.append("rect") .attr("class", $$.classEvent.bind($$)) .style("cursor", config.data_selection_enabled && config.data_selection_grouped ? "pointer" : null) .on("click", function(d) { $$.clickHandlerForSingleX.bind(this)(d, $$); }) .call($$.getDraggableSelection());
if ($$.inputType === "mouse") { rect .on("mouseover", d => { // do nothing while dragging/flowing if ($$.dragging || $$.flowing || $$.hasArcType()) { return; }
$$.config.tooltip_grouped && $$.setOverOut(true, d.index); }) .on("mousemove", function(d) { // do nothing while dragging/flowing if ($$.dragging || $$.flowing || $$.hasArcType()) { return; }
let index = d.index; const eventRect = $$.svg.select(`.${CLASS.eventRect}-${index}`);
if ($$.isStepType(d) && $$.config.line_step_type === "step-after" && d3Mouse(this)[0] < $$.x($$.getXValue(d.id, index)) ) { index -= 1; }
index === -1 ? $$.unselectRect() : $$.selectRectForSingle(this, eventRect, index);
// As of individual data point(or <path>) element can't bind mouseover/out event // to determine current interacting element, so use 'mousemove' event instead. if (!$$.config.tooltip_grouped) { $$.setOverOut(index !== -1, d.index); } }) .on("mouseout", d => { // chart is destroyed if (!$$.config || $$.hasArcType()) { return; }
$$.unselectRect(); $$.setOverOut(false, d.index); }); }
return rect; },
clickHandlerForSingleX(d, ctx) { const $$ = ctx; const config = $$.config;
if ($$.hasArcType() || !$$.toggleShape || $$.cancelClick) { $$.cancelClick && ($$.cancelClick = false);
return; }
const index = d.index;
$$.main.selectAll(`.${CLASS.shape}-${index}`) .each(function(d2) { if (config.data_selection_grouped || $$.isWithinShape(this, d2)) { $$.toggleShape(this, d2, index); config.data_onclick.call($$.api, d2, this); } }); },
/** * Create an eventRect, * Register touch and drag events. * @private * @param {Object} d3.select(CLASS.eventRects) object. * @returns {Object} d3.select(CLASS.eventRects) object. */ generateEventRectsForMultipleXs(eventRectEnter) { const $$ = this;
const rect = eventRectEnter .append("rect") .attr("x", 0) .attr("y", 0) .attr("width", $$.width) .attr("height", $$.height) .attr("class", CLASS.eventRect) .on("click", function() { $$.clickHandlerForMultipleXS.bind(this)($$); }) .call($$.getDraggableSelection());
if ($$.inputType === "mouse") { rect .on("mouseover mousemove", function() { $$.selectRectForMultipleXs(this); }) .on("mouseout", () => { // chart is destroyed if (!$$.config || $$.hasArcType()) { return; }
$$.unselectRect(); }); }
return rect; },
clickHandlerForMultipleXS(ctx) { const $$ = ctx; const config = $$.config; const targetsToShow = $$.filterTargetsToShow($$.data.targets);
if ($$.hasArcType(targetsToShow)) { return; }
const mouse = d3Mouse(this); const closest = $$.findClosestFromTargets(targetsToShow, mouse);
if (!closest) { return; }
// select if selection enabled if ($$.isBarType(closest.id) || $$.dist(closest, mouse) < config.point_sensitivity) { $$.main.selectAll(`.${CLASS.shapes}${$$.getTargetSelectorSuffix(closest.id)}`) .selectAll(`.${CLASS.shape}-${closest.index}`) .each(function() { if (config.data_selection_grouped || $$.isWithinShape(this, closest)) { $$.toggleShape(this, closest, closest.index); config.data_onclick.call($$.api, closest, this); } }); } },
/** * Dispatch a mouse event. * @private * @param {String} type event type * @param {Number} index Index of eventRect * @param {Array} mouse x and y coordinate value */ dispatchEvent(type, index, mouse) { const $$ = this; const isMultipleX = $$.isMultipleX(); const selector = `.${isMultipleX ? CLASS.eventRect : `${CLASS.eventRect}-${index}`}`; const eventRect = $$.main.select(selector).node(); const {width, left, top} = eventRect.getBoundingClientRect(); const x = left + (mouse ? mouse[0] : 0) + ( isMultipleX || $$.config.axis_rotated ? 0 : (width / 2) ); const y = top + (mouse ? mouse[1] : 0); const params = { screenX: x, screenY: y, clientX: x, clientY: y };
emulateEvent[/^(mouse|click)/.test(type) ? "mouse" : "touch"](eventRect, type, params); }});