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

Actually implement boxplots #137

Merged
merged 14 commits into from
May 21, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion rankratioviz/generate.py
Original file line number Diff line number Diff line change
Expand Up @@ -421,11 +421,17 @@ def gen_sample_plot(table, metadata):
.interactive()
)

# Replace the "mark": "circle" definition with a more explicit one. This
# will be useful when adding attributes to the boxplot mark in the
# visualization. (We have to resort to this hack because I haven't been
# able to successfully use alt.MarkDef in the alt.Chart definition above.)
sample_chart_dict = sample_chart.to_dict()
sample_chart_dict["mark"] = {"type": "circle"}
# Return the JSONs as dicts for 1) the sample plot JSON (which only
# contains sample metadata), and 2) the feature counts per sample (which
# will be stored separately from the sample plot JSON in order to not hit
# performance too terribly).
return sample_chart.to_dict(), table.to_dict()
return sample_chart_dict, table.to_dict()


def gen_visualization(V, processed_table, df_sample_metadata, output_dir):
Expand Down
15 changes: 13 additions & 2 deletions rankratioviz/support_files/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -58,14 +58,25 @@
</select>
</div>
<div class="centeredBlock">
<label for="colorField">Color field</label>
<label id="colorFieldLabel" for="colorField"
>Color field</label
>
<select id="colorField"> </select>
<label for="colorScale">Color scale type</label>
<label id="colorScaleLabel" for="colorScale"
>Color scale type</label
>
<select id="colorScale">
<option value="nominal">Categorical</option>
<option value="quantitative">Quantitative</option>
</select>
</div>
<div class="centeredBlock">
<label for="boxplotCheckbox"
>(Experimental) Use boxplots for categorical
data?</label
>
<input type="checkbox" id="boxplotCheckbox" />
</div>
</div>
</div>
</div>
Expand Down
138 changes: 124 additions & 14 deletions rankratioviz/support_files/js/display.js
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,9 @@ define(["./feature_computation", "vega", "vega-embed"], function(
},
barSize: function() {
display.updateRankPlotBarSize(true);
},
boxplotCheckbox: function() {
display.updateSamplePlotBoxplot();
}
},
"onchange"
Expand Down Expand Up @@ -463,27 +466,48 @@ define(["./feature_computation", "vega", "vega-embed"], function(
}

updateSamplePlotTooltips() {
// NOTE: this should be safe from duplicate entries within tooltips
// so long as you don't change the field titles displayed.
this.samplePlotJSON.encoding.tooltip = [
{ type: "nominal", field: "Sample ID" },
{ type: "quantitative", field: "rankratioviz_balance" },
{
type: this.samplePlotJSON.encoding.x.type,
field: this.samplePlotJSON.encoding.x.field
},
{
type: this.samplePlotJSON.encoding.color.type,
field: this.samplePlotJSON.encoding.color.field
}
];
if (!document.getElementById("boxplotCheckbox").checked) {
// NOTE: this should be safe from duplicate entries within
// tooltips so long as you don't change the field titles
// displayed.
this.samplePlotJSON.encoding.tooltip = [
{ type: "nominal", field: "Sample ID" },
{ type: "quantitative", field: "rankratioviz_balance" },
{
type: this.samplePlotJSON.encoding.x.type,
field: this.samplePlotJSON.encoding.x.field
},
{
type: this.samplePlotJSON.encoding.color.type,
field: this.samplePlotJSON.encoding.color.field
}
];
}
}

/* Update color so that color encoding matches the x-axis encoding
* (due to how box plots work in Vega-Lite). To be clear, we also
* update the color field <select> to show the user what's going on.
*/
setColorForBoxplot() {
var category = this.samplePlotJSON.encoding.x.field;
this.samplePlotJSON.encoding.color.field = category;
document.getElementById("colorField").value = category;
document.getElementById("colorScale").value = "nominal";
this.samplePlotJSON.encoding.color.type = "nominal";
}

updateSamplePlotField(vizAttribute) {
if (vizAttribute === "xAxis") {
this.samplePlotJSON.encoding.x.field = document.getElementById(
"xAxisField"
).value;
if (
document.getElementById("boxplotCheckbox").checked &&
this.samplePlotJSON.encoding.x.type === "nominal"
) {
this.setColorForBoxplot();
}
} else {
this.samplePlotJSON.encoding.color.field = document.getElementById(
"colorField"
Expand Down Expand Up @@ -513,7 +537,11 @@ define(["./feature_computation", "vega", "vega-embed"], function(
// labelAngle parameter.
if (newScale === "nominal") {
this.samplePlotJSON.encoding.x.axis = { labelAngle: -45 };
if (document.getElementById("boxplotCheckbox").checked) {
this.changeSamplePlotToBoxplot(false);
}
} else {
this.changeSamplePlotFromBoxplot(false);
// This should work even if the axis property is undefined
// -- it just won't do anything in that case.
delete this.samplePlotJSON.encoding.x.axis;
Expand All @@ -526,6 +554,88 @@ define(["./feature_computation", "vega", "vega-embed"], function(
this.remakeSamplePlot();
}

updateSamplePlotBoxplot() {
// We only bother changing up anything if the sample plot x-axis
// is currently categorical.
if (this.samplePlotJSON.encoding.x.type === "nominal") {
if (document.getElementById("boxplotCheckbox").checked) {
this.changeSamplePlotToBoxplot(true);
} else {
this.changeSamplePlotFromBoxplot(true);
}
}
}

static changeColorElementEnabled(enable) {
// List of DOM elements that have to do with the color controls. We
// disable these when in "boxplot mode" because Vega-Lite gets
// grumpy when you try to apply colors to a boxplot that have
// different granularity than the boxplot's current x-axis.
// (It does the same thing with tooltips.)
var colorEles = ["colorField", "colorScale"];
var e;
if (enable) {
for (e = 0; e < colorEles.length; e++) {
document.getElementById(colorEles[e]).disabled = false;
}
} else {
for (e = 0; e < colorEles.length; e++) {
document.getElementById(colorEles[e]).disabled = true;
}
}
}

/* Changes the sample plot JSON and DOM elements to get ready for
* switching to "boxplot mode." If callRemakeSamplePlot is truthy, this
* will actually call this.remakeSamplePlot(); otherwise, this won't do
* anything.
*
* callRemakeSamplePlot should be false if this is called in the
* middle of remaking the sample plot, anyway -- e.g. if the user
* switched the x-axis scale type from quantitative to categorical, and
* the "use boxplots" checkbox was already checked.
*
* callRemakeSamplePlot should be true if this is called as the only
* update to the sample plot that's going to be made -- i.e. the user
* was already using a categorical x-axis scale, and they just clicked
* the "use boxplots" checkbox.
*/
changeSamplePlotToBoxplot(callRemakeSamplePlot) {
this.samplePlotJSON.mark.type = "boxplot";
// Make the middle tick of the boxplot black. This makes boxes for
// which only one sample is available show up on the white
// background and light-gray axis.
this.samplePlotJSON.mark.median = { color: "#000000" };
RRVDisplay.changeColorElementEnabled(false);
this.setColorForBoxplot();
delete this.samplePlotJSON.encoding.tooltip;
if (callRemakeSamplePlot) {
this.remakeSamplePlot();
}
}

/* Like changeSamplePlotToBoxplot(), but the other way around. This is
* a bit simpler, since (as of writing) we have to do less to go back
* to a normal circle mark from the boxplot mark.
*
* callRemakeSamplePlot works the same way as in
* changeSamplePlotToBoxplot().
*/
changeSamplePlotFromBoxplot(callRemakeSamplePlot) {
this.samplePlotJSON.mark.type = "circle";
delete this.samplePlotJSON.mark.median;
RRVDisplay.changeColorElementEnabled(true);
// No need to explicitly adjust color or tooltips here; tooltips
// will be auto-added in updateSamplePlotTooltips() (since it will
// detect that boxplot mode is off, and therefore try to add
// tooltips), and color should have been kept up-to-date every time
// the field was changed while boxplot mode was going on (as well
// as at the start of boxplot mode), in setColorForBoxplot().
if (callRemakeSamplePlot) {
this.remakeSamplePlot();
}
}

static identifyMetadataColumns(samplePlotSpec) {
// Given a Vega-Lite sample plot specification, find all the metadata cols.
// Just uses whatever the first available sample's keys are as a
Expand Down
2 changes: 1 addition & 1 deletion rankratioviz/tests/testing_utilities.py
Original file line number Diff line number Diff line change
Expand Up @@ -271,7 +271,7 @@ def validate_rank_plot_json(input_ranks_loc, rank_json):
def validate_sample_plot_json(
biom_table_loc, metadata_loc, sample_json, count_json
):
assert sample_json["mark"] == "circle"
assert sample_json["mark"] == {"type": "circle"}
assert sample_json["title"] == "Log Ratio of Abundances in Samples"
basic_vegalite_json_validation(sample_json)
dn = sample_json["data"]["name"]
Expand Down
1 change: 1 addition & 0 deletions rankratioviz/tests/web_tests/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
<option value="nominal"></option>
<option value="quantitative"></option>
</select>
<input type="checkbox" id="boxplotCheckbox" />
<button id="multiFeatureButton"></button>
<button id="exportDataButton"></button>
<a id="downloadHelper"></a>
Expand Down
2 changes: 1 addition & 1 deletion rankratioviz/tests/web_tests/tests/test_data_export.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ define(["display", "mocha", "chai"], function(display, mocha, chai) {
// prettier-ignore
var rankPlotJSON = {"config": {"view": {"width": 400, "height": 300}, "mark": {"tooltip": null}, "axis": {"gridColor": "#f2f2f2", "labelBound": true}}, "data": {"name": "data-36414add24a5bf1eab9b625a76ffc1b9"}, "mark": "bar", "autosize": {"resize": true}, "background": "#FFFFFF", "encoding": {"color": {"type": "nominal", "field": "Classification", "scale": {"domain": ["None", "Numerator", "Denominator", "Both"], "range": ["#e0e0e0", "#f00", "#00f", "#949"]}}, "tooltip": [{"type": "quantitative", "field": "rankratioviz_x", "title": "Current Ranking"}, {"type": "nominal", "field": "Classification"}, {"type": "nominal", "field": "Feature ID"}], "x": {"type": "ordinal", "axis": {"labelAngle": -45, "ticks": false}, "field": "rankratioviz_x", "scale": {"paddingInner": 0, "paddingOuter": 1, "rangeStep": 1}, "title": "Sorted Features"}, "y": {"type": "quantitative", "field": "Intercept"}}, "selection": {"selector013": {"type": "interval", "bind": "scales", "encodings": ["x", "y"]}}, "title": "Feature Ranks", "transform": [{"window": [{"op": "row_number", "as": "rankratioviz_x"}], "sort": [{"field": "Intercept", "order": "ascending"}]}], "$schema": "https://vega.github.io/schema/vega-lite/v3.2.1.json", "datasets": {"data-36414add24a5bf1eab9b625a76ffc1b9": [{"Feature ID": "Taxon1", "Intercept": 5.0, "Rank 1": 6.0, "Rank 2": 7.0, "Classification": "None"}, {"Feature ID": "Taxon2", "Intercept": 1.0, "Rank 1": 2.0, "Rank 2": 3.0, "Classification": "None"}, {"Feature ID": "Taxon3 | Yeet | 100", "Intercept": 4.0, "Rank 1": 5.0, "Rank 2": 6.0, "Classification": "None"}, {"Feature ID": "Taxon4", "Intercept": 9.0, "Rank 1": 8.0, "Rank 2": 7.0, "Classification": "None"}, {"Feature ID": "Taxon5", "Intercept": 6.0, "Rank 1": 5.0, "Rank 2": 4.0, "Classification": "None"}], "rankratioviz_rank_ordering": ["Intercept", "Rank 1", "Rank 2"]}};
// prettier-ignore
var samplePlotJSON = {"config": {"view": {"width": 400, "height": 300}, "mark": {"tooltip": null}, "axis": {"labelBound": true}}, "data": {"name": "data-587975575c35e2a2f0cf84839938cac8"}, "mark": "circle", "autosize": {"resize": true}, "background": "#FFFFFF", "encoding": {"color": {"type": "nominal", "field": "Metadata1"}, "tooltip": [{"type": "nominal", "field": "Sample ID"}, {"type": "quantitative", "field": "rankratioviz_balance"}], "x": {"type": "quantitative", "field": "rankratioviz_balance"}, "y": {"type": "quantitative", "field": "rankratioviz_balance", "title": "log(Numerator / Denominator)"}}, "selection": {"selector014": {"type": "interval", "bind": "scales", "encodings": ["x", "y"]}}, "title": "Log Ratio of Abundances in Samples", "$schema": "https://vega.github.io/schema/vega-lite/v3.2.1.json", "datasets": {"data-587975575c35e2a2f0cf84839938cac8": [{"Sample ID": "Sample1", "rankratioviz_balance": null, "Metadata1": 1, "Metadata2": 2, "Metadata3": 3}, {"Sample ID": "Sample2", "rankratioviz_balance": null, "Metadata1": 4, "Metadata2": 5, "Metadata3": 6}, {"Sample ID": "Sample3", "rankratioviz_balance": null, "Metadata1": 7, "Metadata2": 8, "Metadata3": 9}, {"Sample ID": "Sample5", "rankratioviz_balance": null, "Metadata1": 13, "Metadata2": 14, "Metadata3": 15}, {"Sample ID": "Sample6", "rankratioviz_balance": null, "Metadata1": 16, "Metadata2": 17, "Metadata3": 18}, {"Sample ID": "Sample7", "rankratioviz_balance": null, "Metadata1": 19, "Metadata2": 20, "Metadata3": 21}]}};
var samplePlotJSON = {"config": {"view": {"width": 400, "height": 300}, "mark": {"tooltip": null}, "axis": {"labelBound": true}}, "data": {"name": "data-587975575c35e2a2f0cf84839938cac8"}, "mark": {"type": "circle"}, "autosize": {"resize": true}, "background": "#FFFFFF", "encoding": {"color": {"type": "nominal", "field": "Metadata1"}, "tooltip": [{"type": "nominal", "field": "Sample ID"}, {"type": "quantitative", "field": "rankratioviz_balance"}], "x": {"type": "quantitative", "field": "rankratioviz_balance"}, "y": {"type": "quantitative", "field": "rankratioviz_balance", "title": "log(Numerator / Denominator)"}}, "selection": {"selector014": {"type": "interval", "bind": "scales", "encodings": ["x", "y"]}}, "title": "Log Ratio of Abundances in Samples", "$schema": "https://vega.github.io/schema/vega-lite/v3.2.1.json", "datasets": {"data-587975575c35e2a2f0cf84839938cac8": [{"Sample ID": "Sample1", "rankratioviz_balance": null, "Metadata1": 1, "Metadata2": 2, "Metadata3": 3}, {"Sample ID": "Sample2", "rankratioviz_balance": null, "Metadata1": 4, "Metadata2": 5, "Metadata3": 6}, {"Sample ID": "Sample3", "rankratioviz_balance": null, "Metadata1": 7, "Metadata2": 8, "Metadata3": 9}, {"Sample ID": "Sample5", "rankratioviz_balance": null, "Metadata1": 13, "Metadata2": 14, "Metadata3": 15}, {"Sample ID": "Sample6", "rankratioviz_balance": null, "Metadata1": 16, "Metadata2": 17, "Metadata3": 18}, {"Sample ID": "Sample7", "rankratioviz_balance": null, "Metadata1": 19, "Metadata2": 20, "Metadata3": 21}]}};
// prettier-ignore
var countJSON = {"Taxon5": {"Sample6": 0.0, "Sample2": 0.0, "Sample5": 2.0, "Sample3": 1.0, "Sample7": 0.0, "Sample1": 0.0}, "Taxon4": {"Sample6": 1.0, "Sample2": 1.0, "Sample5": 1.0, "Sample3": 1.0, "Sample7": 1.0, "Sample1": 1.0}, "Taxon1": {"Sample6": 5.0, "Sample2": 1.0, "Sample5": 4.0, "Sample3": 2.0, "Sample7": 6.0, "Sample1": 0.0}, "Taxon2": {"Sample6": 1.0, "Sample2": 5.0, "Sample5": 2.0, "Sample3": 4.0, "Sample7": 0.0, "Sample1": 6.0}, "Taxon3 | Yeet | 100": {"Sample6": 3.0, "Sample2": 3.0, "Sample5": 4.0, "Sample3": 4.0, "Sample7": 2.0, "Sample1": 2.0}};
var rrv = new display.RRVDisplay(rankPlotJSON, samplePlotJSON, countJSON);
Expand Down
Loading