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

ENH Feature metadata coloring command pattern #440

Conversation

gwarmstrong
Copy link
Member

@kwcantrell and I put together a proposed refactoring of feature metadata coloring using the Command Pattern. I am tagging this as progress on #439 , but there are probably also others that could apply. We were hoping to get some feedback on the general design, and if the reception is positive, would be happy to flesh out more of the docs/tests.

But the general mechanism we introduce here is the Command pattern.

Namely, the proposal would be that updates to Empress plots should be routed through Commands, where you encapsulate all of the information needed to perform an update in the create of the Command object. This can seen, for example in the lines:

            let command = new Commands.ColorFeatureTreeCommand({
                empress: scope.empress,
                collapseClades: collapseChecked,
                lineWidth: lw,
                colBy: colBy,
                col: col,
                coloringMethod: coloringMethod,
                reverse: reverse,
            });

where all of the information needed to color the tree based on it's feature metadata is passed to the constructor ColorFeatureTreeCommand.

This command can then later be executed by calling the execute method, i.e, command.execute();, which actually performs the tree coloring based on the code in ColorFeatureTreeCommand's execute method.

We have also included a CommandManager class which maintains a queue of commands to execute, and a history of commands that have been executed via the CommandManager. It also comes with utility functions for helping serialize and de-serialize the history. If all of the UI elements are routed through the command manager, this could be used to completely reconstruct the state of the empress plot, enabling save/load/playback feature (that admittedly could get memory intensive for long running sessions, given the current implementation).

Using the command pattern throughout this project could also be useful for a variety of other 'wishlist' items, such as programming custom animations, i.e.,

command1 = new Commands.ChangeTreeLayoutCommand(...);
command2 = new Commands.ColorTreeByFeatureCommand(...);
command3 = new Commands.AddBarplootCommand(...);

commands = [command1, command2, command3];
commands.forEach(function(command) {
    setTimeout(function() {
        command.execute();
    }, 5000);
});

And you could have some event bus subscriber that creates commands. E.g.,

function Subscriber(bus, ...) {
    ...
}

Subscriber.prototype.handle = function (event) {
    ...
    colorScheme = // something you are to glean from `event`
    let command = new Commands.ColorFeatureTreeCommand(..., colorScheme, ...);
    command.execute();
    ...
}

Which should hopefully allow for relatively extensible interaction code (e.g., interacting with Emperor)

Co-authored-by: kwcantrell
@dhakim87
Copy link

        let command = new Commands.ColorFeatureTreeCommand({
            empress: scope.empress,
            collapseClades: collapseChecked,
            lineWidth: lw,
            colBy: colBy,
            col: col,
            coloringMethod: coloringMethod,
            reverse: reverse,
        });

I'm not sure I totally follow the example. Is there an interface that commands must follow - Some set of fields they all share?

Why is it empress: scope.empress rather than scope: scope.empress?
Should colorFeatureTree be split into colorFeatureTree and lineWidthFeatureTree?

It looks like colBy, col, and coloringMethod are all optional? Color by (dictionary?), Color (fixed color?), Coloring Method (function?) - coloringMethod would have to be the name of a function I guess, in order to allow json serialization. Or is this intended to be an enum specifying whether colorBy or color should be used?

I think you might want a command_type: xxx added to all the commands to make parsing easy.

@emperor-helper
Copy link

The following artifacts were built for this PR: empire-biplot.qzv, empire.qzv, empress-tree.qzv, just-fm.qzv, plain.qzv

Copy link

@dhakim87 dhakim87 left a comment

Choose a reason for hiding this comment

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

I really like this. I think this is the right skeleton for a message passing solution. I have minor comments on removing boilerplate and optimizations that may eventually become issues.


function CommandManager(empress) {
this.empress = empress;
this.executed = [];

Choose a reason for hiding this comment

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

This queue is a little weird. If the design involves some external message bus, I would think that message bus would keep track of all commands that pass through it, or even some specific logger component. I don't see a value in having every instance of empress maintain commands that it executed.

Choose a reason for hiding this comment

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

One use case this one makes sense for is Undo/Redo functionality, but I don't think that really works with a message bus architecture and I haven't heard it being requested. (It also requires all commands to store enough state to be invertible)

Copy link
Member Author

Choose a reason for hiding this comment

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

Right so this CommandManager is intended just to queue up commands for empress only, and hopefully be able to replay some actions on empress. We would require a separate class to manage interactions with an event bus.

var scope = this;
object.forEach(function (commandObject) {
var commandConstructor = scope.commands[commandObject.command];
var args = commandObject.arguments;

Choose a reason for hiding this comment

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

I feel like this could be done with a JSON.parse(), I don't quite get the value of looking up the constructor and passing it arguments yet.

Choose a reason for hiding this comment

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

Ahhh! Commands need to know their type for the execute calls!

I'm not sure how explicitly you want to type the command objects. You might be able to have javascript handle all the serialization and deserialization without any per Command boilerplate code. It could certainly be done with some wonky prototype trickery, or by having each command hold a separate untyped CommandArgs argument. But if you want it to be as explicitly typed/type safe as possible, the current mechanism with copies is as close as you can get in javascript.

};

CommandManager.prototype.executeNext = function () {
let command = this.toExecute.shift();

Choose a reason for hiding this comment

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

This should use a linked list or an array backed queue one day before allowing replaying events from a log, otherwise we'll get burned by quadratic cost of shifting items out of the array list.

Copy link
Member Author

@gwarmstrong gwarmstrong Nov 5, 2020

Choose a reason for hiding this comment

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

Hmmmm, any pointers to places where people have written an actual linked list/dequeue in javascript? Most of the implementations I have found just use shift 😬

}
};

CommandManager.prototype.getSerializable = function () {

Choose a reason for hiding this comment

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

Useful for save/load log but I think this functionality should be moved to a separate component and events forked to both the empress command manager and the logger command manager or something.

this.reverse = arguments.reverse;
}

ColorFeatureTreeCommand.prototype.toObject = function () {

Choose a reason for hiding this comment

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

Can probably wrap this into a Command base class - just need to loop over public fields/properties

Try Object.getOwnPropertyNames, or anything nearby on this page: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/getOwnPropertyNames

};

function ColorFeatureTreeCommand(arguments) {
this.empress = arguments.empress;

Choose a reason for hiding this comment

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

It will be up to empress to actually apply this command at some point. Do you want the Command instances themselves to understand how they are applied, or do you want the empress code to parse the command and apply them?

If the first, constructing the actual command instances has some value. If not... why shouldn't empress just look at the .command field of some arbitrary json object?

Choose a reason for hiding this comment

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

Oh... looks like its the first 👍


CommandManager.prototype.executeNext = function () {
let command = this.toExecute.shift();
command.execute();

Choose a reason for hiding this comment

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

Where does execute() live? How does it avoid taking an empress object?

Choose a reason for hiding this comment

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

Ahhh, sneaks into the Command object when it gets parsed. I think it might be cleaner to pass it into the execute call rather than parsing - doing so would allow you to use json serialization and deserialization for what otherwise becomes a ton of boilerplate creation of constructors and to_object.

Copy link
Member Author

Choose a reason for hiding this comment

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

^ended up following this and switching to execute(empress) and it is a lot cleaner.

};

ColorFeatureTreeCommand.prototype.execute = function () {
this.empress.resetTree();

Choose a reason for hiding this comment

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

What are the odds that resetTree is interminably slow? Do we need this to be a separate command? Do we need FreezeLayout ResumeLayout commands like exist in UI frameworks to allow for numerous bulk operations?

Copy link
Collaborator

@fedarko fedarko left a comment

Choose a reason for hiding this comment

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

This looks great, thanks so much! I have some comments -- they're mostly small-scale things, though. By and large I think the overall structure looks great. Having this in will clean things up a lot :)

empress/support_files/js/side-panel-handler.js Outdated Show resolved Hide resolved
empress/support_files/js/side-panel-handler.js Outdated Show resolved Hide resolved
});
};

function ColorFeatureTreeCommand(arguments) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

It seems to me like the individual command classes (which as @dhakim87 suggested would be derived from a Command base class) should have their own concrete requirements for arguments, although I guess the base class wouldn't have any such requirements.

Hm, I'm not sure what the best way to set this up would be. Knowing JS, there are probably like a billion ways to do this, and you've probably already thought of some of these :) To add to the noise, one idea that should work would be (assuming a Command base class will be eventually added) adding a method to that base class that goes through the stuff in arguments and checks it against, say, a Command-subclass-specific array of needed arguments, and raises an error if some needed stuff isn't specified (using some of the property stuff Dan suggested -- and/or we can probably use some of underscore.js to do this easily, e.g. _.keys() and _.intersection()).

return objects;
};

CommandManager.prototype.parse = function (object) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

One of those "not a huge deal but" things -- I think we should try to avoid object as a variable name, just because Object is a keyword in JS.

I'm also a bit confused by how this function works, since it seems to me on first reading that the object parameter is actually an Array of Command objects (since forEach() isn't a property of Objects in JS). If this is the case, I would suggest renaming object to something like commands (I know docs aren't a big focus of the initial work here but this would be good to make clear).

Copy link
Member Author

Choose a reason for hiding this comment

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

Make sense and updated now!

@gwarmstrong gwarmstrong changed the title WIP: ENH Feature metadata coloring command pattern ENH Feature metadata coloring command pattern Nov 10, 2020
@fedarko fedarko self-requested a review November 11, 2020 01:34
Copy link
Collaborator

@fedarko fedarko left a comment

Choose a reason for hiding this comment

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

I haven't finished going through all of the new functionality yet (will do that later this week), but here's what I have by now.

@@ -0,0 +1,285 @@
define(["underscore"], function (_) {
function CommandManager(empress) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Not all that urgent, but it would be good to add a comment here similar to what the Command class has below (just to make it clear that this is also a "class", at least kinda). There are examples of this (for non-explicitly-declared JS classes in Empress) in bp-tree.js and empress.js.

Copy link
Member Author

Choose a reason for hiding this comment

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

Added in 00baae9

this.toExecute = [];
}

CommandManager.prototype.push = function (command) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

I guess now that we're at the stage of getting ready to merge this in, it'd be nice to add some basic docs for these functions (sorry, I realize this is going to be like 4 separate comments ._.)

At the very least, it'd be good to be very clear that a first-in first-out queue is used (... at least that is what I think is done from reading this).

Copy link
Member Author

Choose a reason for hiding this comment

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

Added in 00baae9

empress/support_files/js/commands.js Outdated Show resolved Hide resolved
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants