Skip to content

Commit

Permalink
Support exporting all barplot legends, including color gradient and l…
Browse files Browse the repository at this point in the history
…ength legends; export collapsed clade shapes and barplots (#392)

* ENH: support collecting+exporting barplot legends

* BUG: Fix legend SVG title centering; <g> => <svg>

Also added a newline after </style>

* BUG: Improve legend SVG for SVG importers

-Use "long form" font specification for the style SVG (fixes problems
 with GIMP and Inkscape)
-Put the style code higher up in the output legend SVG -- has the
 effect of applying the rect stroke to the topmost legend rect, which
 was a problem in Inkscape but not in chromium (:thonk:)
-Add note about GIMP choking on dominant-baseline (tldr not worth
 worrying abt now i think)

* STY: reorder/split up text and .title svg styles

(the text styles are now across multiple lines)

* MNT: hang on to continuous props in Legend for SVG

similar to what we do for categorical export

* ENH: add newlines to within gradient svgs

make life less painful (tm)

* MNT: abstract some code within legend export; grad svg work

* MNT: Refactor colorer/legend handling of gradients

Now, things are split up into a "Solo" and "HTML" SVG -- the gradient
shown on the page is a combo of these, and the one we export is just
the Solo one. This makes scaling it properly for the export SO MUCH
EASIER AHH

All we gotta do now is just add in value text and update rowsUsed /
maxLineWidth. think that should be good?

oh also this is gonna explode the tests ofc. that is a job for TOMORROW
MARCUS (tm)

* ENH: Finish? gradient legend SVG exporting!

IT WORKS SO WELL AOGIHDSOIGHJ

something worth noting: there seems to be some unaccounted-for
horizontal padding on the right side in the cat legend export.
i matched it in the continuous legend export b/c it looks nice but
worth looking into...?

* MNT: add explicit padding to right side of cat SVG

it looks like things are the opposite from how i thought -- looks like
the perceived extra space was just due to the boldfont used in
estimating the texts (when you make the text bold it's almost snug
with the border on the right side). may as well add the same padding
as for the continuous legends so things look consistent ish.

Still, this leaves it kinda unclear as to why continuous legends were
so comparatively snug with the border until i added padding ... maybe
boldface numbers are just not that bigger? idk

UPDATE: yeah i checked it and bold numbers are basically the same size
but bold letters are much larger. mystery solved :100:

* ENH: initial support for exporting length legends

looks not great (gotta align max and min headers like in table)
but good enough tm

* ENH: make exported length legend look purdy

* STY: pret

* ENH: export collapsed clade shapes! #303

Actually not that bad. wack.

* ENH: draw full rectangle for unrooted collapsing

removes line in the middle

* ENH/STY: use stroke on svg triangles; prettify

* MNT: also use stroke for unrooted clade export

* DOC: document todos for clade collapsing export

* ENH: support exporting barplots in SVG!

Need to use paths for circular barplot curves, and maybe better
stuff for rect barplots. but it works :D

* Update readme re: #303 fixes :D

* PERF: Only specify stroke width for thick lines

since default is 1

* MNT: apply shape-rendering to SVG

shoutouts to https://stackoverflow.com/a/53309814/10730311.

this is very easy to configure (just a line in the svg header),
so if users prefer different things we can document this.

* MNT: make polygon exporting util func

* MNT: attempt to fix bounding box stuff

export is broken (height is somehow nan?) but at least this works now

* STY: prettify

* negate y coords and declare maxy in bb

still broken but much less so

* Space out adjacent polygon pts

fixes things! hey this works now

* PERF/BUG: don't even draw 0-length barplots

previously they were showing up in the SVG, probs due to precision
pbms. should beo k now

* STY: prett

* DOC: remove svg export disclaimer :D #303

* BUG: Fix #421

still gotta test, tho, which will likely need to be deferred until
after fixing the other pbms with this pr and tests .______.

* TST: fix a gradient svg test

* specify combined svg

* TST: split up test ref svgs by solo/html

* TST: Fix most of the colorer tests

* MNT: redo public attr stuff with a func approach

is safer -- delegates checking that colorer is continuous to, well,
colorer.

* TST: unbreak colorer tests!

* Say missing and/or not numeric in warning

* TST: unbreak legend tests :D

* STY: prettify tests

* MNT: Drastically simplify legend exporting

At least for cat legends -- no longer do we have to worry about
rows and units and all that. each legend just returns its width and
height, and that's all we care about.

* STY: pret

* UNBREAK CONTINUOUS LEGENDS :D

* Fix length legends

* ENH: show missing/nonnumeric warn in grad legends

* DOC: fix some docs stuff

* fix stats format

* populateTreeStats fixes

* STY: whoops

* MNT: Move radius bb expansion to sep function

addresses a comment from @kwcantrell

* PERF: don't cache barplot buffer

addresses @kwcantrell comment. Required breaking up the barplot
drawing stuff into a separate function that returns the coords

* BUG: fix reversed continuous color map legends

tldr gotta reverse the stop colors. reason this worked _before_ was
that it was accessing the interpolator to build up the stop colors

* simplify grad svg code a bit

* TST: test reverse color gradient bug is fixed

* STY

* MNT: Simplify gradient ID / suffix stuff

Addresses @kwcantrell comment.

* DOC: fix documentation stuff wrt #400
  • Loading branch information
fedarko authored Nov 5, 2020
1 parent fd159e1 commit 13438f9
Show file tree
Hide file tree
Showing 12 changed files with 1,347 additions and 541 deletions.
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -313,9 +313,10 @@ This was a brief introduction to some of the barplot functionality available in

## Exporting Plots

Once you are done customizing your tree, you can export the tree as an SVG or PNG file by going to the *Export* section in the main menu and clicking on `Export tree as SVG` or `Export tree as PNG`. You can also export the legend used for tree coloring, if the tree has been colored, using the `Export legend as SVG` button.
Once you are done customizing your tree, you can export the current visualization of the tree as an SVG or PNG file by going to the *Export* section in the main menu and clicking on `Export tree as SVG` or `Export tree as PNG`. You can also export the legend(s) used for tree and/or barplot coloring, if applicable, using the `Export legends as SVG` button.

Currently, certain elements of the display (e.g. barplots, collapsed clades) are not included in the SVG export; we're working on making this functionality more comprehensive. Also, note that the SVG export does not change as you zoom / pan the tree, while the PNG export will change as you zoom / pan the tree.
Note that SVG export will always include the entire tree display, while the
contents of the PNG export will change as you zoom / pan the tree.

## Empire plots! Side-by-side integration of tree and PCoA plots

Expand Down
25 changes: 23 additions & 2 deletions empress/support_files/js/barplot-layer.js
Original file line number Diff line number Diff line change
Expand Up @@ -678,8 +678,7 @@ define([
this.colorByFMContinuous &&
!this.colorByFMColorMapDiscrete
) {
var gradInfo = colorer.getGradientSVG();
this.colorLegend.addContinuousKey(title, gradInfo[0], gradInfo[1]);
this.colorLegend.addContinuousKey(title, colorer.getGradientInfo());
} else {
this.colorLegend.addCategoricalKey(title, colorer.getMapHex());
}
Expand Down Expand Up @@ -745,6 +744,28 @@ define([
this.lengthLegend.clear();
};

/**
* Returns an Array containing all of the active legends in this layer.
*
* Currently, this just returns the color legend and length legends
* (assuming both are in use), since those are the only legends barplot
* layers own. However, if more sorts of encodings could be used at the
* same time (e.g. encoding bars' color, length, and ... opacity, I
* guess?), then this Array should be expanded.
*
* Inactive legends (e.g. the length legend, if no length encoding is in
* effect) will be excluded from the Array. However, if all legends are
* active, the order in the Array will always be color then length.
*
* @return {Array} Array of Legend objects
*/
BarplotLayer.prototype.getLegends = function () {
var containedLegends = [this.colorLegend, this.lengthLegend];
return _.filter(containedLegends, function (legend) {
return legend.isActive();
});
};

/**
* Updates the text in the layer's header based on the layer's number.
*
Expand Down
18 changes: 18 additions & 0 deletions empress/support_files/js/barplot-panel-handler.js
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,24 @@ define([
});
};

/**
* Returns an Array containing all active legends owned by barplot layers.
*
* This Array is ordered (it starts with the first barplot layer's legends,
* then the second layer's legends, etc.). The ordering of legends within a
* layer (e.g. color then length legends) is dependent on how
* BarplotLayer.getLegends() works.
*
* @return {Array} Array of Legend objects
*/
BarplotPanel.prototype.getLegends = function () {
var legends = [];
_.each(this.layers, function (layer) {
legends.push(...layer.getLegends());
});
return legends;
};

/**
* Array containing the names of layouts compatible with barplots.
*/
Expand Down
172 changes: 108 additions & 64 deletions empress/support_files/js/colorer.js
Original file line number Diff line number Diff line change
Expand Up @@ -52,18 +52,37 @@ define(["chroma", "underscore", "util"], function (chroma, _, util) {
// This object will describe a mapping of unique field values to colors
this.__valueToColor = {};

// Will be set to a string containing the SVG for a gradient, if
// continuous scaling is done (i.e. useQuantScale is true and the
// input color map is sequential / diverging)
/*** Continuous-scaling-specific things ***/

// We append a number to the gradient ID so that multiple gradients can
// be present on the same page without overriding each other. (It's the
// caller's responsibility to ensure that gradient ID suffixes are
// unique, at least across Colorers created for continuous scaling.)
this._gradientID = useQuantScale ? "Gradient" + gradientIDSuffix : null;

// Will be set to a string containing just the SVG defining the
// gradient (i.e. just the <defs>...</defs> stuff).
this._gradientSVG = null;

this._gradientIDSuffix = gradientIDSuffix;
// Will be set to a string describing the <rect> containing the
// gradient and the <text> representations of the min/mid/max values
// alongside it. Used to display the gradient in the page HTML.
this._pageSVG = null;

// Will be set to strings describing the minimum, middle, and maximum
// values along the gradient. Designed for use with this._gradientSVG
// (so they can be scaled as needed for SVG exporting).
this._minValStr = null;
this._midValStr = null;
this._maxValStr = null;

// Will be set to true if there were any non-numeric elements included
// in the input values that couldn't be assigned colors (only if
// continuous scaling was done)
// in the input values that couldn't be assigned colors (controls
// whether or not to show a warning)
this._missingNonNumerics = false;

/*** End continuous-scaling-specific things ***/

// Based on the color map, container, type and the value of
// useQuantScale, assign colors accordingly
if (_.isObject(this.color)) {
Expand Down Expand Up @@ -192,8 +211,9 @@ define(["chroma", "underscore", "util"], function (chroma, _, util) {
* by this.color) for every value in this.sortedUniqueValues, taking into
* account the magnitudes/etc. of the numeric values in
* this.sortedUniqueValues. This will populate this.__valueToColor with
* this information. This will also populate this._gradientSVG with a
* String describing this gradient.
* this information. This will also populate this._gradientSVG,
* this._pageSVG, this._gradientID, this._missingNonNumerics,
* this._minValStr, this._midValStr, and this._maxValStr.
*
* Non-numeric values will not be assigned a color.
*
Expand All @@ -211,7 +231,6 @@ define(["chroma", "underscore", "util"], function (chroma, _, util) {
" custom colormap"
);
}

var split = util.splitNumericValues(this.sortedUniqueValues);
if (split.numeric.length < 2) {
throw new Error("Category has less than 2 unique numeric values.");
Expand All @@ -229,23 +248,26 @@ define(["chroma", "underscore", "util"], function (chroma, _, util) {
_.each(split.numeric, function (n) {
scope.__valueToColor[n] = interpolator(parseFloat(n));
});
// Figure out if we should show a warning message about missing values
// to the user
// Figure out if we should show a warning message about missing /
// non-numeric values to the user
this._missingNonNumerics = split.nonNumeric.length > 0;

// Create SVG describing the gradient
// Create SVG describing the gradient: basically, we sample the color
// map along the domain 101 times, and use these 101 colors to define
// the <linearGradient /> for each integer percentage in the inclusve
// range [0%, 100%]. See https://github.com/biocore/emperor/issues/788.
var mid = (min + max) / 2;
var step = (max - min) / 100;
var stopColors = [];
for (var s = min; s <= max; s += step) {
stopColors.push(interpolator(s).hex());
var stopColors = interpolator.colors(101);
// Calling interpolator.colors() returns the colors in order relative
// to the actual color map, which means that (if reverse is true) we
// need to reverse the color map itself.
if (this.reverse) {
stopColors.reverse();
}
var gradientSVG = "<defs>";
// We append a number to the gradient ID so that multiple gradients can
// be present on the same page without overriding each other
var gID = "Gradient" + this._gradientIDSuffix;
gradientSVG +=
'<linearGradient id="' + gID + '" x1="0" x2="0" y1="1" y2="0">';
this._gradientSVG =
'<defs><linearGradient id="' +
this._gradientID +
'" x1="0" x2="0" y1="1" y2="0">';
for (var pos = 0; pos < stopColors.length; pos++) {
// "stop" tags are written here as <stop .../>. This matches what
// Emperor does, and also the example on MDN here:
Expand All @@ -256,72 +278,94 @@ define(["chroma", "underscore", "util"], function (chroma, _, util) {
// document.createElementNS(). Anyway, we keep things shorter for
// now for consistency, but replacing '"/>' with '"></stop>' works
// fine also.
gradientSVG +=
this._gradientSVG +=
'<stop offset="' +
pos +
'%" stop-color="' +
stopColors[pos] +
'"/>';
}
gradientSVG +=
"</linearGradient></defs><rect " +
'width="20" height="95%" fill="url(#' +
gID +
')"/>';

gradientSVG +=
this._gradientSVG += "</linearGradient></defs>\n";

// Add a rect containing the gradient. This'll only be used in the
// in-app representation of the gradient (not in the SVG export!)
this._pageSVG =
'<rect width="20" height="95%" fill="url(#' +
this._gradientID +
')"/>\n';

this._minValStr = min.toString();
this._midValStr = mid.toString();
this._maxValStr = max.toString();
this._pageSVG +=
'<text x="25" y="12px" font-family="sans-serif" ' +
'font-size="12px" text-anchor="start">' +
max +
"</text>";
gradientSVG +=
this._maxValStr +
"</text>\n";
this._pageSVG +=
'<text x="25" y="50%" font-family="sans-serif" ' +
'font-size="12px" text-anchor="start">' +
mid +
"</text>";
gradientSVG +=
this._midValStr +
"</text>\n";
this._pageSVG +=
'<text x="25" y="95%" font-family="sans-serif" ' +
'font-size="12px" text-anchor="start">' +
min +
"</text>";

this._gradientSVG = gradientSVG;
this._minValStr +
"</text>\n";
};

/**
* Returns an array containing SVG information and a flag about non-numeric
* values, to be used when creating a legend based on continuous scaling.
* Returns an Object containing gradient information for a Legend.
*
* This function should only be called if, on Colorer construction,
* useQuantScale is true and the input color map is sequential or
* diverging. If this is not the case (a.k.a. assignContinuousScaledColors
* was not called during construction), then this function will throw an
* error.
* The intent with this method is to make sure that the Legend holding a
* gradient has all of the information needed to export a SVG of this
* gradient (since this may involve rescaling or otherwise altering the
* way the legend is displayed, we pass things besides the SVG).
*
* @return{Array} gradientInfo An array containing two elements:
* 1. gradientSVG: a String containing the SVG
* describing this Colorer's gradient. This
* will include information about the
* input numeric values along this gradient.
* 2. missingNonNumerics: a Boolean value that
* is true if any of the values passed to
* this Colorer were not numeric (and
* therefore not assignable to any colors on
* the gradient), and false otherwise. If
* this is true, it's recommended that the
* caller show a warning along with the
* gradient that some value(s) have been
* omitted from the color map.
* @return {Object} gradientInfo An Object with the following entries:
* -gradientSVG: SVG String containing the
* <defs> and <linearGradient> that define
* the gradient.
* -pageSVG: SVG String containing the <rect>
* and <text>s that position the gradient
* within a rectangle and place min / mid
* / max value text along it. (This is used
* for displaying the gradient in the
* application interface, but not used for
* exporting.)
* -gradientID: String ID of the gradient
* defined in gradientSVG.
* -minValStr: String representation of the
* minimum numeric value.
* -midValStr: String representation of the
* middle numeric value (halfway between the
* min and max; not necessarily present
* within the input data).
* -midValStr: String representation of the
* maximum numeric value.
* -missingNonNumerics: Boolean describing
* whether or not missing / non-numeric
* values were provided to the Colorer.
* (If true, a warning about this should
* be shown in the legend about this.)
*/
Colorer.prototype.getGradientSVG = function () {
Colorer.prototype.getGradientInfo = function () {
if (_.isNull(this._gradientSVG)) {
throw new Error(
"No gradient defined for this Colorer; check that " +
"No gradient data defined for this Colorer; check that " +
"useQuantScale is true and that the selected color map " +
"is not discrete."
);
} else {
return [this._gradientSVG, this._missingNonNumerics];
return {
gradientSVG: this._gradientSVG,
pageSVG: this._pageSVG,
gradientID: this._gradientID,
minValStr: this._minValStr,
midValStr: this._midValStr,
maxValStr: this._maxValStr,
missingNonNumerics: this._missingNonNumerics,
};
}
};

Expand Down
Loading

0 comments on commit 13438f9

Please sign in to comment.