statgen / locuszoom

A Javascript/d3 embeddable plugin for interactively visualizing statistical genetic data from customizable sources.
https://statgen.github.io/locuszoom/
MIT License
156 stars 29 forks source link

New layout syntax #30

Closed Frencil closed 8 years ago

Frencil commented 8 years ago

The Problem

Now starting to look at how end users would integrate the LocusZoom plugin into projects it became apparent that the syntax for defining new "layouts" was unintuitive. Layouts were to be built as new Instance classes using methods and, while possible, it was cumbersome and did not lend itself to clarity or easy documentation.

Layouts as Objects

This branch introduces the concept of a layout object, a serializable object that can be built by any means and provided when an instance is created. This approach allows for easily breaking out attributes to cover any aspect of a layout we wish to be configurable, offering far greater flexibility.

Instance and Panel classes

With this change there is no longer a need to have subclasses of Instances or Panels. Each base class is still vital to compartmentalize aspects of a rendered LocusZoom object, and now lend themselves even better to broad generalizations (e.g. "An Instance represents a single LocusZoom graph and all of its elements" or "A Panel controls the axes on a graph").

DefaultInstance, PositionsPanel, and GenesPanel no longer exist.

DefaultLayout

Where before DefaultInstance created the default layout by extending the base Instance class, now DefaultLayout does the same by defining a layout as an object:

LocusZoom.DefaultLayout = {
    width: 700,
    height: 700,
    min_width: 300,
    min_height: 400,
    panels: {
        positions: {
            origin: { x: 0, y: 0 },
            width:      700,
            height:     350,
            min_width:  300,
            min_height: 200,
            proportional_width: 1,
            proportional_height: 0.5,
            margin: { top: 20, right: 20, bottom: 35, left: 50 },
            axes: {
                x: {
                    label_function: "chromosome"
                },
                y1: {
                    label: "-log10 p-value"
                }
            },
            data_layers: {
                positions: {
                    class: "PositionsDataLayer",
                    y_axis: {
                        axis: 1,
                        data: "pvalue|neglog10",
                        floor: 0,
                        upper_buffer: 0.05
                    },
                    color: {
                        function: "numeric_cut",
                        parameters: {
                            breaks: [0, 0.2, 0.4, 0.6, 0.8],
                            colors: ["#357ebd","#46b8da","#5cb85c","#eea236","#d43f3a"],
                            null_color: "#B8B8B8"
                        }
                    }
                }
            }
        },
        genes: {
            origin: { x: 0, y: 350 },
            width:      700,
            height:     350,
            min_width:  300,
            min_height: 200,
            proportional_width: 1,
            proportional_height: 0.5,
            margin: { top: 20, right: 20, bottom: 20, left: 50 },
            data_layers: {
                genes: {
                    class: "GenesDataLayer"
                }
            }
        }
    }
};

Recursive layout pattern

The classes Instance, Panel, and the base DataLayer class all keep a layout property that is their layout. Where before some of the above classes had a view property for layout-related data, such as dimensions or dimension bounds, that's all now stored in layout. Any methods that change any of those values on the screen should now be using properties in their respective layout objects to track those values under the hood.

Label functions

The Panel class now has a singleton called LabelFunctions for storing universal functions for axis labels that change based on state. This also has a robust suite of unit tests built out and ready to go.

TODO

Lots!

Frencil commented 8 years ago

UPDATE: This branch is now development complete.

This is a large refactor and is by no means finished, but in its current state it is stable and functional. As discussed today among LocusZoom devs we're better off freezing further feature development and refactoring on this branch in favor of review and merging to prevent the scope from growing much further.

MrFlick commented 8 years ago

So it doesn't look like there's actually a "serialize" method yet. Is the idea just to keep the "layout" property/object always current and have no state outside that object?

MrFlick commented 8 years ago

Lots of great stuff here. This will be immensely useful.

Frencil commented 8 years ago

Re: the serialize method: see this comment for some thoughts about that.

In general I see power in keeping the layout object on the Plot/Instance as a serializable object that's kept current as the layout is changed (good example of this in action right now would be the UI to resize an instance which changes top-level layout width/height directly and redraws the plot from the values in the layout). But we need generic methods for merging partial layouts with a fully-defined layout (that fully defined layout could be the default layout, in the case of initialization, or whatever the current layout is, in the case of on-the-fly layout editing that end users could hook up to their own custom inputs).

In general the layout on the plot/instance should always be an accurate description of what that plot/instance is. There's an important distinction between that and the state object, which describes the current query parameters for the data being displayed. The only other piece needed to essentially "clone" a LocusZoom are the data sources which presently are still built up as a separate object. This pattern is expressed explicitly in code as LocusZoom.populate and LocusZoom.populateAll take a selector for a target but then apply datasource, layout, and state.

Because these three are different, but similar, and equally important to build a precise LocusZoom visualization we can probably expect each of them to undergo a fair bit of iteration going forward.

Frencil commented 8 years ago

UPDATE: Most of our discussion today has been about directions to take layout patterns, API, and workflow beyond the scope of this branch. I've added a few issues to begin to document these needs for addressing on future branches (see #31, #32, and #33).

MrFlick commented 8 years ago

Tracking these discussions as other issues makes total sense. Unless you want to make any other changes first, i'd be happy to merge the PR now.

Frencil commented 8 years ago

Yeah, I'd say go ahead and merge if you're comfortable. I'll continue to document more issues from our discussion and try to keep future refactors smaller in scope. Thanks!