Skip to content

Commit

Permalink
Merge branch 'release/0.7.2'
Browse files Browse the repository at this point in the history
  • Loading branch information
abought committed Apr 23, 2018
2 parents 7a38aea + 279a3ec commit aacedfc
Show file tree
Hide file tree
Showing 19 changed files with 9,192 additions and 9,225 deletions.
14 changes: 7 additions & 7 deletions assets/css/locuszoom.scss
Original file line number Diff line number Diff line change
Expand Up @@ -77,41 +77,41 @@ svg.#{$namespace}-locuszoom {
stroke-width: 1px;
}

path.#{$namespace}-data_layer-scatter {
path.#{$namespace}-data_layer-scatter, path.#{$namespace}-data_layer-category_scatter {
stroke: #{$default_black_shadow};
stroke-opacity: #{$default_black_shadow_opacity};
stroke-width: 1px;
cursor: pointer;
}

path.#{$namespace}-data_layer-scatter-highlighted {
path.#{$namespace}-data_layer-scatter-highlighted, path.#{$namespace}-data_layer-category_scatter-highlighted {
stroke: #{$default_black_shadow};
stroke-opacity: #{$default_black_shadow_opacity};
stroke-width: 4px;
}

path.#{$namespace}-data_layer-scatter-selected {
path.#{$namespace}-data_layer-scatter-selected, path.#{$namespace}-data_layer-category_scatter-selected {
stroke: #{$default_black};
stroke-opacity: #{$default_black_opacity};
stroke-width: 4px;
}

path.#{$namespace}-data_layer-scatter-faded {
path.#{$namespace}-data_layer-scatter-faded, path.#{$namespace}-data_layer-category_scatter-faded {
fill-opacity: 0.1;
stroke-opacity: 0.1;
}

path.#{$namespace}-data_layer-scatter-hidden {
path.#{$namespace}-data_layer-scatter-hidden, path.#{$namespace}-data_layer-category_scatter-hidden {
display: none;
}

text.#{$namespace}-data_layer-scatter-label {
text.#{$namespace}-data_layer-scatter-label, text.#{$namespace}-data_layer-category_scatter-label {
fill: #{$default_black};
fill-opacity: #{$default_black_opacity};
alignment-baseline: middle;
}

line.#{$namespace}-data_layer-scatter-label {
line.#{$namespace}-data_layer-scatter-label, line.#{$namespace}-data_layer-category_scatter-label {
stroke: #{$default_black};
stroke-opacity: #{$default_black_opacity};
stroke-width: 1px;
Expand Down
29 changes: 24 additions & 5 deletions assets/js/app/Data.js
Original file line number Diff line number Diff line change
Expand Up @@ -237,8 +237,17 @@ LocusZoom.Data.Requester = function(sources) {
* @public
*/
LocusZoom.Data.Source = function() {
/** @member {Boolean} */
/**
* Whether this source should enable caching
* @member {Boolean}
*/
this.enableCache = true;
/**
* Whether this data source type is dependent on previous requests- for example, the LD source cannot annotate
* association data if no data was found for that region.
* @member {boolean}
*/
this.dependentSource = false;
};

/**
Expand Down Expand Up @@ -333,11 +342,18 @@ LocusZoom.Data.Source.prototype.getData = function(state, fields, outnames, tran
}
}

var self = this;
return function (chain) {
return this.getRequest(state, chain, fields).then(function(resp) {
return this.parseResponse(resp, chain, fields, outnames, trans);
}.bind(this));
}.bind(this);
if (self.dependentSource && chain && chain.body && !chain.body.length) {
// A "dependent" source should not attempt to fire a request if there is no data for it to act on.
// Therefore, it should simply return the previous data chain.
return Q.when(chain);
}

return self.getRequest(state, chain, fields).then(function(resp) {
return self.parseResponse(resp, chain, fields, outnames, trans);
});
};
};

/**
Expand Down Expand Up @@ -554,12 +570,15 @@ LocusZoom.Data.AssociationSource.prototype.getURL = function(state, chain, field

/**
* Data Source for LD Data, as fetched from the LocusZoom API server (or compatible)
* This source is designed to connect its results to association data, and therefore depends on association data having
* been loaded by a previous request in the data chain.
* @class
* @public
* @augments LocusZoom.Data.Source
*/
LocusZoom.Data.LDSource = LocusZoom.Data.Source.extend(function(init) {
this.parseInit(init);
this.dependentSource = true;
}, "LDLZ");

LocusZoom.Data.LDSource.prototype.preGetData = function(state, fields) {
Expand Down
84 changes: 46 additions & 38 deletions assets/js/app/DataLayer.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,12 @@ LocusZoom.DataLayer = function(layout, parent) {
if (this.layout.x_axis !== {} && typeof this.layout.x_axis.axis !== "number"){ this.layout.x_axis.axis = 1; }
if (this.layout.y_axis !== {} && typeof this.layout.y_axis.axis !== "number"){ this.layout.y_axis.axis = 1; }

/**
* Values in the layout object may change during rendering etc. Retain a copy of the original data layer state
* @member {Object}
*/
this._base_layout = JSON.parse(JSON.stringify(this.layout));

/** @member {Object} */
this.state = {};
/** @member {String} */
Expand Down Expand Up @@ -354,52 +360,54 @@ LocusZoom.DataLayer.prototype.getAxisExtent = function(dimension){
throw("Invalid dimension identifier passed to LocusZoom.DataLayer.getAxisExtent()");
}

var axis = dimension + "_axis";
var axis_name = dimension + "_axis";
var axis_layout = this.layout[axis_name];

// If a floor AND a ceiling are explicitly defined then just return that extent and be done
if (!isNaN(this.layout[axis].floor) && !isNaN(this.layout[axis].ceiling)){
return [+this.layout[axis].floor, +this.layout[axis].ceiling];
if (!isNaN(axis_layout.floor) && !isNaN(axis_layout.ceiling)){
return [+axis_layout.floor, +axis_layout.ceiling];
}

// If a field is defined for the axis and the data layer has data then generate the extent from the data set
if (this.layout[axis].field && this.data && this.data.length){

var extent = d3.extent(this.data, function(d) {
var f = new LocusZoom.Data.Field(this.layout[axis].field);
return +f.resolve(d);
}.bind(this));

// Apply floor/ceiling
if (!isNaN(this.layout[axis].floor)) {
extent[0] = this.layout[axis].floor;
extent[1] = d3.max(extent);
}
if (!isNaN(this.layout[axis].ceiling)) {
extent[1] = this.layout[axis].ceiling;
extent[0] = d3.min(extent);
}

// Apply upper/lower buffers, if applicable
var original_extent_span = extent[1] - extent[0];
if (isNaN(this.layout[axis].floor) && !isNaN(this.layout[axis].lower_buffer)) {
extent[0] -= original_extent_span * this.layout[axis].lower_buffer;
}
if (isNaN(this.layout[axis].ceiling) && !isNaN(this.layout[axis].upper_buffer)) {
extent[1] += original_extent_span * this.layout[axis].upper_buffer;
}

// Apply minimum extent
if (typeof this.layout[axis].min_extent == "object") {
if (isNaN(this.layout[axis].floor) && !isNaN(this.layout[axis].min_extent[0])) {
extent[0] = Math.min(extent[0], this.layout[axis].min_extent[0]);
var data_extent = [];
if (axis_layout.field && this.data) {
if (!this.data.length) {
// If data has been fetched (but no points in region), enforce the min_extent (with no buffers,
// because we don't need padding around an empty screen)
data_extent = axis_layout.min_extent || [];
return data_extent;
} else {
data_extent = d3.extent(this.data, function (d) {
var f = new LocusZoom.Data.Field(axis_layout.field);
return +f.resolve(d);
});

// Apply upper/lower buffers, if applicable
var original_extent_span = data_extent[1] - data_extent[0];
if (!isNaN(axis_layout.lower_buffer)) {
data_extent[0] -= original_extent_span * axis_layout.lower_buffer;
}
if (isNaN(this.layout[axis].ceiling) && !isNaN(this.layout[axis].min_extent[1])) {
extent[1] = Math.max(extent[1], this.layout[axis].min_extent[1]);
if (!isNaN(axis_layout.upper_buffer)) {
data_extent[1] += original_extent_span * axis_layout.upper_buffer;
}
}

return extent;

if (typeof axis_layout.min_extent == "object") {
// The data should span at least the range specified by min_extent, an array with [low, high]
var range_min = axis_layout.min_extent[0];
var range_max = axis_layout.min_extent[1];
if (!isNaN(range_min) && !isNaN(range_max)) {
data_extent[0] = Math.min(data_extent[0], range_min);
}
if (!isNaN(range_max)) {
data_extent[1] = Math.max(data_extent[1], range_max);
}
}
// If specified, floor and ceiling will override the actual data range
return [
isNaN(axis_layout.floor) ? data_extent[0] : axis_layout.floor,
isNaN(axis_layout.ceiling) ? data_extent[1] : axis_layout.ceiling
];
}
}

// If this is for the x axis and no extent could be generated yet but state has a defined start and end
Expand Down
76 changes: 66 additions & 10 deletions assets/js/app/DataLayers/scatter.js
Original file line number Diff line number Diff line change
Expand Up @@ -435,6 +435,11 @@ LocusZoom.DataLayers.add("scatter", function(layout){
this.flip_labels();
this.seperate_iterations = 0;
this.separate_labels();
// Apply default event emitters to selection
this.label_texts.on("click.event_emitter", function(element){
this.parent.emit("element_clicked", element);
this.parent_plot.emit("element_clicked", element);
}.bind(this));
// Extend mouse behaviors to labels
this.applyBehaviors(this.label_texts);
}
Expand Down Expand Up @@ -504,8 +509,7 @@ LocusZoom.DataLayers.extend("scatter", "category_scatter", {
},

/**
* Identify the unique categories on the plot, and update the layout with an appropriate color scheme
*
* Identify the unique categories on the plot, and update the layout with an appropriate color scheme.
* Also identify the min and max x value associated with the category, which will be used to generate ticks
* @private
* @returns {Object.<String, Number[]>} Series of entries used to build category name ticks {category_name: [min_x, max_x]}
Expand All @@ -524,16 +528,67 @@ LocusZoom.DataLayers.extend("scatter", "category_scatter", {
});

var categoryNames = Object.keys(uniqueCategories);
// Construct a color scale with a sufficient number of visually distinct colors
// TODO: This will break for more than 20 categories in a single API response payload for a single PheWAS plot
var color_scale = categoryNames.length <= 10 ? d3.scale.category10 : d3.scale.category20;
var colors = color_scale().range().slice(0, categoryNames.length); // List of hex values, should be of same length as categories array
this._setDynamicColorScheme(categoryNames);

this.layout.color.parameters.categories = categoryNames;
this.layout.color.parameters.values = colors;
return uniqueCategories;
},

/**
* Automatically define a color scheme for the layer based on data returned from the server.
* If part of the color scheme has been specified, it will fill in remaining missing information.
*
* There are three scenarios:
* 1. The layout does not specify either category names or (color) values. Dynamically build both based on
* the data and update the layout.
* 2. The layout specifies colors, but not categories. Use that exact color information provided, and dynamically
* determine what categories are present in the data. (cycle through the available colors, reusing if there
* are a lot of categories)
* 3. The layout specifies exactly what colors and categories to use (and they match the data!). This is useful to
* specify an explicit mapping between color scheme and category names, when you want to be sure that the
* plot matches a standard color scheme.
* (If the layout specifies categories that do not match the data, the user specified categories will be ignored)
*
* This method will only act if the layout defines a `categorical_bin` scale function for coloring. It may be
* overridden in a subclass to suit other types of coloring methods.
*
* @param {String[]} categoryNames
* @private
*/
_setDynamicColorScheme: function(categoryNames) {
var colorParams = this.layout.color.parameters;
var baseParams = this._base_layout.color.parameters;

// If the layout does not use a supported coloring scheme, or is already complete, this method should do nothing
if (this.layout.color.scale_function !== "categorical_bin") {
throw "This layer requires that coloring be specified as a `categorical_bin`";
}

if (baseParams.categories.length && baseParams.values.length) {
// If there are preset category/color combos, make sure that they apply to the actual dataset
var parameters_categories_hash = {};
baseParams.categories.forEach(function (category) { parameters_categories_hash[category] = 1; });
if (categoryNames.every(function (name) { return parameters_categories_hash.hasOwnProperty(name); })) {
// The layout doesn't have to specify categories in order, but make sure they are all there
colorParams.categories = baseParams.categories;
} else {
colorParams.categories = categoryNames;
}
} else {
colorParams.categories = categoryNames;
}
// Prefer user-specified colors if provided. Make sure that there are enough colors for all the categories.
var colors;
if (baseParams.values.length) {
colors = baseParams.values;
} else {
var color_scale = categoryNames.length <= 10 ? d3.scale.category10 : d3.scale.category20;
colors = color_scale().range();
}
while (colors.length < categoryNames.length) { colors = colors.concat(colors); }
colors = colors.slice(0, categoryNames.length); // List of hex values, should be of same length as categories array
colorParams.values = colors;
},

/**
*
* @param dimension
Expand Down Expand Up @@ -561,6 +616,7 @@ LocusZoom.DataLayers.extend("scatter", "category_scatter", {

if (dimension === "x") {
// If colors have been defined by this layer, use them to make tick colors match scatterplot point colors
var knownCategories = this.layout.color.parameters.categories || [];
var knownColors = this.layout.color.parameters.values || [];

return Object.keys(categoryBounds).map(function (category, index) {
Expand All @@ -584,7 +640,7 @@ LocusZoom.DataLayers.extend("scatter", "category_scatter", {
x: xPos,
text: category,
style: {
"fill": knownColors[index] || "#000000"
"fill": knownColors[knownCategories.indexOf(category)] || "#000000"
}
};
});
Expand All @@ -600,4 +656,4 @@ LocusZoom.DataLayers.extend("scatter", "category_scatter", {
this._categories = this._generateCategoryBounds();
return this;
}
});
});
4 changes: 3 additions & 1 deletion assets/js/app/Layouts.js
Original file line number Diff line number Diff line change
Expand Up @@ -361,7 +361,9 @@ LocusZoom.Layouts.add("data_layer", "phewas_pvalues", {
fields: ["{{namespace[phewas]}}id", "{{namespace[phewas]}}log_pvalue", "{{namespace[phewas]}}trait_group", "{{namespace[phewas]}}trait_label"],
x_axis: {
field: "{{namespace[phewas]}}x", // Synthetic/derived field added by `category_scatter` layer
category_field: "{{namespace[phewas]}}trait_group"
category_field: "{{namespace[phewas]}}trait_group",
lower_buffer: 0.025,
upper_buffer: 0.025
},
y_axis: {
axis: 1,
Expand Down
2 changes: 1 addition & 1 deletion assets/js/app/LocusZoom.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
* @namespace
*/
var LocusZoom = {
version: "0.7.1"
version: "0.7.2"
};

/**
Expand Down
3 changes: 2 additions & 1 deletion assets/js/app/wrapper.txt
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,8 @@
throw("Q dependency not met. Library missing.");
}

<%= contents %>
// ESTemplate: module content goes here
;%= body %;

} catch (plugin_loading_error){
console.error("LocusZoom Plugin error: " + plugin_loading_error);
Expand Down
Loading

0 comments on commit aacedfc

Please sign in to comment.