Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

New: Added support for keyboard interaction #76

Open
wants to merge 6 commits into
base: release/1.0.0
Choose a base branch
from

Conversation

parmar-abhinav
Copy link

This PR introduces and new feature that allows user to Drag/Zoom graph in Canvas using keyboard. Allowing users to control the scale of zoom using zoomInFactor and zoomOutFactor also draggingFactor allows to control dragging speed. This feature enhances the usability of the library by giving users more control over interactivity.

Copy link
Contributor

@tonilastre tonilastre left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice feature!

Comment on lines 456 to 465
_dragByArrowKey = (transform: ZoomTransform) => {
select(this._canvas)
.transition()
.duration(this._settings.zoomFitTransitionMs)
.ease(easeLinear)
.call(this._d3Zoom.transform, transform)
.call(() => {
this._renderer.render(this._graph);
});
};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
_dragByArrowKey = (transform: ZoomTransform) => {
select(this._canvas)
.transition()
.duration(this._settings.zoomFitTransitionMs)
.ease(easeLinear)
.call(this._d3Zoom.transform, transform)
.call(() => {
this._renderer.render(this._graph);
});
};
_applyTransformation = (transform: ZoomTransform, callback?: () => void) => {
select(this._canvas)
.transition()
.duration(this._settings.zoomFitTransitionMs)
.ease(easeLinear)
.call(this._d3Zoom.transform, transform)
.call(() => {
this._renderer.render(this._graph);
callback?.();
});
};

This doesn't have anything to do with arrows. It is applying the transformation to the canvas. Also, this code is pretty much the same as the one used in recenter(), so I would advise changing the one in recenter to use this:

  recenter(onRendered?: () => void) {
    const fitZoomTransform = this._renderer.getFitZoomTransform(this._graph);
    this._applyTransformation(fitZoomTransform, () => {
      onRendered?.();
    });
  }

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for your feedback. I've made the necessary changes based on your suggestions. Pls check.

@@ -157,6 +163,12 @@ const defaultSettings = {
interaction: {
isDragEnabled: true;
isZoomEnabled: true;
keyboard: {
isKeyboardEnabled: true,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As far as I can see, default is false.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, you already have a group name keyboard, so this field can just be isEnabled.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes by default it is false. Renamed isKeyboardEnabled to isEnabled and also update default value

Comment on lines 257 to 287
getDragLeftTransform(draggingFactor: number): ZoomTransform {
const currentTransform = this.transform;
const newX = currentTransform.x + draggingFactor;
return zoomIdentity
.translate(newX, currentTransform.y)
.scale(currentTransform.k);
}

getDragRightTransform(draggingFactor: number): ZoomTransform {
const currentTransform = this.transform;
const newX = currentTransform.x - draggingFactor;
return zoomIdentity
.translate(newX, currentTransform.y)
.scale(currentTransform.k);
}

getDragUpTransform(draggingFactor: number): ZoomTransform {
const currentTransform = this.transform;
const newY = currentTransform.y + draggingFactor;
return zoomIdentity
.translate(currentTransform.x, newY)
.scale(currentTransform.k);
}

getDragDownTransform(draggingFactor: number): ZoomTransform {
const currentTransform = this.transform;
const newY = currentTransform.y - draggingFactor;
return zoomIdentity
.translate(currentTransform.x, newY)
.scale(currentTransform.k);
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All these are pretty much doing the same thing, so I would propose having one function that handles all 4 cases with enum, e.g. PanDirectionType.{UP, DOWN, LEFT, RIGHT}.

Btw this event should be pan, drag is used for dragging nodes/edges. Pan is when you move the canvas up/down/left/right.
With the enum, you can have just one function getPanTransform(panDirectionType, factor);

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Created single getPanTransform function, Pls check.

@@ -364,6 +390,80 @@ export class OrbView<N extends INodeBase, E extends IEdgeBase> implements IOrbVi
}, 1);
};

_handleKeyDown = (event: KeyboardEvent) => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is missing on the orb-map-view.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"orb-map-view" is already responding to key presses, so I decided not to make any changes.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Awesome! Yep, that makes sense.

Copy link
Contributor

@tonilastre tonilastre left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is better and more performant code now! Btw I have some comments mostly about moving stuff to other files, missing property, and handling key events globally.

@@ -25,6 +25,13 @@ const DEBUG_GREEN = '#3CFF33';
const DEBUG_BLUE = '#3383FF';
const DEBUG_PINK = '#F333FF';

export enum PanDirectionType {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should go to src/renderer/shared.ts because all renderer classes will share this.

Comment on lines 392 to 429
switch (event.key) {
case '-': {
const zoomOutFactor = this._settings.interaction.keyboard.zoomOutFactor;
this._zoomOut(zoomOutFactor);
break;
}
case '+': {
const zoomInFactor = this._settings.interaction.keyboard.zoomInFactor;
this._zoomIn(zoomInFactor);
break;
}
case 'ArrowLeft': {
const panFactor = this._settings.interaction.keyboard.panFactor;
const dragLeftTransform = this._renderer.getPanTransform(PanDirectionType.LEFT, panFactor);
this._applyTransformation(dragLeftTransform);
break;
}
case 'ArrowRight': {
const panFactor = this._settings.interaction.keyboard.panFactor;
const dragRightTransform = this._renderer.getPanTransform(PanDirectionType.RIGHT, panFactor);
this._applyTransformation(dragRightTransform);
break;
}
case 'ArrowUp': {
const panFactor = this._settings.interaction.keyboard.panFactor;
const dragUpTransform = this._renderer.getPanTransform(PanDirectionType.UP, panFactor);
this._applyTransformation(dragUpTransform);
break;
}
case 'ArrowDown': {
const panFactor = this._settings.interaction.keyboard.panFactor;
const dragDownTransform = this._renderer.getPanTransform(PanDirectionType.DOWN, panFactor);
this._applyTransformation(dragDownTransform);
break;
}
default:
break;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nitpicking, but because there are 6 long statements to get the zoomInFactor, zoomOutFactor and panFactor, it makes sense to have variable destruct before the switch case, e.g.

const { zoomOutFactor, zoomInFactor, panFactor } = this._settings.interaction.keyboard;

switch (event.key) {
  case '-': {
    this._zoomOut(zoomOutFactor);
    break;
  }
  ...
}

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With this, you can literally remove lines where you get the factors in each switch-case.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've made the recommended changes to improve the code readability and reduce redundancy.

_applyTransformation = (transform: ZoomTransform, callback?: () => void) => {
select(this._canvas)
.transition()
.duration(this._settings.zoomFitTransitionMs)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, I totally missed this one in the last PR check. This this._settings.zoomFitTransitionMs is actually from the recenter function that does fit to zoom.

In order to make this applyTransformation generic enough, I propose having a second argument options (that will currently have transitionMs (+ callback) only. Something like this:

interface IApplyTransitionOptions {
  transitionMs: number;
  callback: () => void;
}

_applyTransformation = (transform: ZoomTransform, options?: Partial<IApplyTransitionOptions>) => {
  const transitionMs = options?.transitionMs ?? DEFAULT_TRANSITION_MS;
  ...

Then, the recenter can send its this._settings.zoomFitTransitionMs and callback. The zoom, and pan can send their transitionMs. This transitionMs can be even added to the settings with a default value that is lower than 200, maybe 100 or 50. 200 seems a bit slow for expected keyboard movement.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've implemented your suggested approach to enhance the generic nature of the applyTransformation function.

Thank you for your valuable input.

@@ -150,6 +168,8 @@ export class OrbView<N extends INodeBase, E extends IEdgeBase> implements IOrbVi
.on('contextmenu', this.mouseRightClicked)
.on('dblclick.zoom', this.mouseDoubleClicked);

document.addEventListener('keydown', this._handleKeyDown);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, this is a bit problematic, to listen for the key down events on the whole document. If there is a form next to the Orb view, orb will get each event and even react to +, -, array keys while the user writes keys in the input.

So, this should be fixed either by:

  • Listening only for keys on the container - I don't think this is even possible
  • Having a way to listen for click events in the container to make it "focused" so we can know if we want to react to document.keydown event (for example, check the orb-view-map and how it works - key events are working as expected there: if you click on a map, it becomes "focused" in the background, and keys are working. If you click on the header, keys are not working for the graph view out of the orb view, which is the expected behavior).

If you have any other suggestions on how to approach this problem, feel free to share. Happy to discuss this.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@parmar-abhinav do you have any info on this?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tried few approach i.e.

  1. Instead of attaching the keydown event listener to the entire document. I tried attaching it to the specific container where we want to handle the key events. But not able to listen to keydown event using this approach.
this._container.addEventListener('keydown', (event) => {})
  1. Also tried listening to focusin and focusout event on the container. But it is also not working.
private readonly isContainerFocused = false;

this._container.addEventListener('focusin', () => {
    this.isContainerFocused = true;
});

this._container.addEventListener('focusout', () => {
    this.isContainerFocused = false;
});

document.addEventListener('keydown', event => {
    if (this.isContainerFocused) {
        // Handle keydown events only when the container is focused
        this._handleKeyDown(event);
    }
});

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, thanks for sharing. I will check how the leaflet did it (it is used for orb-view-map), maybe we can replicate that behavior. I will share my findings here.

@@ -364,6 +390,80 @@ export class OrbView<N extends INodeBase, E extends IEdgeBase> implements IOrbVi
}, 1);
};

_handleKeyDown = (event: KeyboardEvent) => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Awesome! Yep, that makes sense.

Copy link
Contributor

@tonilastre tonilastre left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@parmar-abhinav everything looks great, there are just two comments left: one for default and another one for document-level listening (which might be problematic when integrating orb). Please check those two and let me know.

const DEFAULT_ZOOM_IN_FACTOR = 1.2;
const DEFAULT_ZOOM_OUT_FACTOR = 0.8;
const DEFAULT_PAN_FACTOR = 25;
const DEFAULT_TRANSITION_MS = 200;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe the default can be 0 - no transition.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done, Please check

@@ -150,6 +168,8 @@ export class OrbView<N extends INodeBase, E extends IEdgeBase> implements IOrbVi
.on('contextmenu', this.mouseRightClicked)
.on('dblclick.zoom', this.mouseDoubleClicked);

document.addEventListener('keydown', this._handleKeyDown);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@parmar-abhinav do you have any info on this?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants