diff --git a/client/src/bundleEntries.js b/client/src/bundleEntries.js
index acfb8027b076..2eec2a6b77bc 100644
--- a/client/src/bundleEntries.js
+++ b/client/src/bundleEntries.js
@@ -11,7 +11,6 @@
import $ from "jquery"; // eslint-disable-line no-unused-vars
import Client from "mvc/visualization/chart/chart-client";
import _ from "underscore"; // eslint-disable-line no-unused-vars
-import Circster from "viz/circster";
import { TracksterUIView } from "viz/trackster";
// Previously "chart"
@@ -22,18 +21,12 @@ export { default as LegacyGridView } from "legacy/grid/grid-view";
export { createTabularDatasetChunkedView } from "mvc/dataset/data";
export { create_chart, create_histogram } from "reports/run_stats";
export { Toast } from "ui/toast"; // TODO: remove when external consumers are updated/gone (IES right now)
-export { PhylovizView as phyloviz } from "viz/phyloviz";
-export { SweepsterVisualization, SweepsterVisualizationView } from "viz/sweepster";
export { TracksterUI } from "viz/trackster";
export function trackster(options) {
new TracksterUIView(options);
}
-export function circster(options) {
- new Circster.GalaxyApp(options);
-}
-
// Previously wandering around as window.thing = thing in the onload script
export { hide_modal, Modal, show_in_overlay, show_message, show_modal } from "layout/modal";
export { make_popup_menus, make_popupmenu } from "ui/popupmenu";
diff --git a/client/src/components/Grid/configs/visualizations.ts b/client/src/components/Grid/configs/visualizations.ts
index f7818e8687a7..f9623f25e4a7 100644
--- a/client/src/components/Grid/configs/visualizations.ts
+++ b/client/src/components/Grid/configs/visualizations.ts
@@ -75,7 +75,11 @@ const fields: FieldArray = [
icon: faEye,
condition: (data: VisualizationEntry) => !data.deleted,
handler: (data: VisualizationEntry) => {
- window.location.href = withPrefix(`/plugins/visualizations/${data.type}/saved?id=${data.id}`);
+ if (data.type === "trackster") {
+ window.location.href = withPrefix(`/visualization/${data.type}?id=${data.id}`);
+ } else {
+ window.location.href = withPrefix(`/plugins/visualizations/${data.type}/saved?id=${data.id}`);
+ }
},
},
{
diff --git a/client/src/viz/circster.js b/client/src/viz/circster.js
deleted file mode 100644
index 6d0bba96b7a0..000000000000
--- a/client/src/viz/circster.js
+++ /dev/null
@@ -1,1218 +0,0 @@
-import "libs/farbtastic";
-
-import { getGalaxyInstance } from "app";
-import Backbone from "backbone";
-import * as d3 from "d3v3";
-import { event as currentEvent } from "d3v3";
-import $ from "jquery";
-import mod_icon_btn from "mvc/ui/icon-button";
-import { getAppRoot } from "onload/loadConfig";
-import _ from "underscore";
-import config from "utils/config";
-import _l from "utils/localization";
-import mod_utils from "utils/utils";
-import visualization from "viz/visualization";
-
-/**
- * Utility class for working with SVG.
- */
-
-var SVGUtils = Backbone.Model.extend({
- /**
- * Returns true if element is visible.
- */
- is_visible: function (svg_elt, svg) {
- var eltBRect = svg_elt.getBoundingClientRect();
- var svgBRect = $("svg")[0].getBoundingClientRect();
-
- if (
- // To the left of screen?
- eltBRect.right < 0 ||
- // To the right of screen?
- eltBRect.left > svgBRect.right ||
- // Above screen?
- eltBRect.bottom < 0 ||
- // Below screen?
- eltBRect.top > svgBRect.bottom
- ) {
- return false;
- }
- return true;
- },
-});
-
-/**
- * Mixin for using ticks.
- */
-var UsesTicks = {
- drawTicks: function (parent_elt, data, dataHandler, textTransform, horizontal) {
- // Set up group elements for chroms and for each tick.
- var ticks = parent_elt
- .append("g")
- .selectAll("g")
- .data(data)
- .enter()
- .append("g")
- .selectAll("g")
- .data(dataHandler)
- .enter()
- .append("g")
- .attr("class", "tick")
- .attr("transform", (d) => `rotate(${(d.angle * 180) / Math.PI - 90})translate(${d.radius},0)`);
-
- // Add line + text for ticks.
- var tick_coords = [];
-
- var text_coords = [];
-
- var text_anchor = (d) => (d.angle > Math.PI ? "end" : null);
-
- if (horizontal) {
- tick_coords = [0, 0, 0, -4];
- text_coords = [4, 0, "", ".35em"];
- text_anchor = null;
- } else {
- tick_coords = [1, 0, 4, 0];
- text_coords = [0, 4, ".35em", ""];
- }
-
- ticks
- .append("line")
- .attr("x1", tick_coords[0])
- .attr("y1", tick_coords[1])
- .attr("x2", tick_coords[2])
- .attr("y1", tick_coords[3])
- .style("stroke", "#000");
-
- return ticks
- .append("text")
- .attr("x", text_coords[0])
- .attr("y", text_coords[1])
- .attr("dx", text_coords[2])
- .attr("dy", text_coords[3])
- .attr("text-anchor", text_anchor)
- .attr("transform", textTransform)
- .text((d) => d.label);
- },
-
- /**
- * Format number for display at a tick.
- */
- formatNum: function (num, sigDigits) {
- // Use default of 2 sig. digits.
- if (sigDigits === undefined) {
- sigDigits = 2;
- }
-
- // Verify input number
- if (num === null) {
- return null;
- }
-
- // Calculate return value
- var rval = null;
- if (Math.abs(num) < 1) {
- rval = num.toPrecision(sigDigits);
- } else {
- // Use round to turn string from toPrecision() back into a number.
- var roundedNum = Math.round(num.toPrecision(sigDigits));
-
- // Use abbreviations.
- num = Math.abs(num);
- if (num < 1000) {
- rval = roundedNum;
- } else if (num < 1000000) {
- // Use K.
- rval = `${Math.round((roundedNum / 1000).toPrecision(3)).toFixed(0)}K`;
- } else if (num < 1000000000) {
- // Use M.
- rval = `${Math.round((roundedNum / 1000000).toPrecision(3)).toFixed(0)}M`;
- }
- }
-
- return rval;
- },
-};
-
-/**
- * A label track.
- */
-var CircsterLabelTrack = Backbone.Model.extend({});
-
-/**
- * Renders a full circster visualization.
- */
-var CircsterView = Backbone.View.extend({
- className: "circster",
-
- initialize: function (options) {
- this.genome = options.genome;
- this.label_arc_height = 50;
- this.scale = 1;
- this.circular_views = null;
- this.chords_views = null;
-
- // When tracks added to/removed from model, update view.
- this.model.get("drawables").on("add", this.add_track, this);
- this.model.get("drawables").on("remove", this.remove_track, this);
-
- // When config settings change, update view.
- var vis_config = this.model.get("config");
- vis_config.get("arc_dataset_height").on("change:value", this.update_track_bounds, this);
- vis_config.get("track_gap").on("change:value", this.update_track_bounds, this);
- },
-
- // HACKs: using track_type for circular/chord distinction in the functions below for now.
-
- /**
- * Returns tracks to be rendered using circular view.
- */
- get_circular_tracks: function () {
- return this.model.get("drawables").filter((track) => track.get("track_type") !== "DiagonalHeatmapTrack");
- },
-
- /**
- * Returns tracks to be rendered using chords view.
- */
- get_chord_tracks: function () {
- return this.model.get("drawables").filter((track) => track.get("track_type") === "DiagonalHeatmapTrack");
- },
-
- /**
- * Returns a list of circular tracks' radius bounds.
- */
- get_tracks_bounds: function () {
- var circular_tracks = this.get_circular_tracks();
-
- var dataset_arc_height = this.model.get("config").get_value("arc_dataset_height");
-
- var track_gap = this.model.get("config").get_value("track_gap");
-
- var // Subtract 20 to make sure chrom labels are on screen.
- min_dimension = Math.min(this.$el.width(), this.$el.height()) - 20;
-
- var // Compute radius start based on model, will be centered
- // and fit entirely inside element by default.
- radius_start =
- min_dimension / 2 -
- circular_tracks.length * (dataset_arc_height + track_gap) +
- // Add track_gap back in because no gap is needed for last track.
- track_gap -
- this.label_arc_height;
-
- var // Compute range of track starting radii.
- tracks_start_radii = d3.range(radius_start, min_dimension / 2, dataset_arc_height + track_gap);
-
- // Map from track start to bounds.
- return _.map(tracks_start_radii, (radius) => [radius, radius + dataset_arc_height]);
- },
-
- /**
- * Renders circular tracks, chord tracks, and label tracks.
- */
- render: function () {
- var self = this;
- var width = self.$el.width();
- var height = self.$el.height();
- var circular_tracks = this.get_circular_tracks();
- var chords_tracks = this.get_chord_tracks();
- var total_gap = self.model.get("config").get_value("total_gap");
- var tracks_bounds = this.get_tracks_bounds();
-
- var // Set up SVG element.
- svg = d3
- .select(self.$el[0])
- .append("svg")
- .attr("width", width)
- .attr("height", height)
- .attr("pointer-events", "all")
- // Set up zooming, dragging.
- .append("svg:g")
- .call(
- d3.behavior.zoom().on("zoom", () => {
- // Do zoom, drag.
- var scale = currentEvent.scale;
- svg.attr("transform", `translate(${currentEvent.translate}) scale(${scale})`);
-
- // Propagate scale changes to views.
- if (self.scale !== scale) {
- // Use timeout to wait for zooming/dragging to stop before rendering more detail.
- if (self.zoom_drag_timeout) {
- clearTimeout(self.zoom_drag_timeout);
- }
- self.zoom_drag_timeout = setTimeout(() => {
- // Render more detail in tracks' visible elements.
- // FIXME: do not do this right now; it is not fully implemented--e.g. data bounds
- // are not updated when new data is fetched--and fetching more detailed quantitative
- // data is not that useful.
- /*
- _.each(self.circular_views, function(view) {
- view.update_scale(scale);
- });
- */
- }, 400);
- }
- })
- )
- .attr("transform", `translate(${width / 2},${height / 2})`)
- .append("svg:g")
- .attr("class", "tracks");
-
- // -- Render circular tracks. --
-
- // Create a view for each track in the visualization and render.
- this.circular_views = circular_tracks.map((track, index) => {
- var view = new CircsterBigWigTrackView({
- el: svg.append("g")[0],
- track: track,
- radius_bounds: tracks_bounds[index],
- genome: self.genome,
- total_gap: total_gap,
- });
-
- view.render();
-
- return view;
- });
-
- // -- Render chords tracks. --
-
- this.chords_views = chords_tracks.map((track) => {
- var view = new CircsterChromInteractionsTrackView({
- el: svg.append("g")[0],
- track: track,
- radius_bounds: tracks_bounds[0],
- genome: self.genome,
- total_gap: total_gap,
- });
-
- view.render();
-
- return view;
- });
-
- // -- Render label track. --
-
- // Track bounds are:
- // (a) outer radius of last circular track;
- // (b)
- var outermost_radius = this.circular_views[this.circular_views.length - 1].radius_bounds[1];
-
- var track_bounds = [outermost_radius, outermost_radius + this.label_arc_height];
-
- this.label_track_view = new CircsterChromLabelTrackView({
- el: svg.append("g")[0],
- track: new CircsterLabelTrack(),
- radius_bounds: track_bounds,
- genome: self.genome,
- total_gap: total_gap,
- });
-
- this.label_track_view.render();
- },
-
- /**
- * Render a single track on the outside of the current visualization.
- */
- add_track: function (new_track) {
- var total_gap = this.model.get("config").get_value("total_gap");
-
- if (new_track.get("track_type") === "DiagonalHeatmapTrack") {
- // Added chords track.
- var innermost_radius_bounds = this.circular_views[0].radius_bounds;
-
- var new_view = new CircsterChromInteractionsTrackView({
- el: d3.select("g.tracks").append("g")[0],
- track: new_track,
- radius_bounds: innermost_radius_bounds,
- genome: this.genome,
- total_gap: total_gap,
- });
-
- new_view.render();
- this.chords_views.push(new_view);
- } else {
- // Added circular track.
-
- // Recompute and update circular track bounds.
- var new_track_bounds = this.get_tracks_bounds();
- _.each(this.circular_views, (track_view, i) => {
- track_view.update_radius_bounds(new_track_bounds[i]);
- });
-
- // Update chords tracks.
- _.each(this.chords_views, (track_view) => {
- track_view.update_radius_bounds(new_track_bounds[0]);
- });
-
- // Render new track.
- var track_index = this.circular_views.length;
-
- var track_view = new CircsterBigWigTrackView({
- el: d3.select("g.tracks").append("g")[0],
- track: new_track,
- radius_bounds: new_track_bounds[track_index],
- genome: this.genome,
- total_gap: total_gap,
- });
-
- track_view.render();
- this.circular_views.push(track_view);
-
- // Update label track.
- /*
- FIXME: should never have to update label track because vis always expands to fit area
- within label track.
- var track_bounds = new_track_bounds[ new_track_bounds.length-1 ];
- track_bounds[1] = track_bounds[0];
- this.label_track_view.update_radius_bounds(track_bounds);
- */
- }
- },
-
- /**
- * Remove a track from the view.
- */
- remove_track: function (track, tracks, options) {
- // -- Remove track from view. --
- var track_view = this.circular_views[options.index];
- this.circular_views.splice(options.index, 1);
- track_view.$el.remove();
-
- // Recompute and update track bounds.
- var new_track_bounds = this.get_tracks_bounds();
- _.each(this.circular_views, (track_view, i) => {
- track_view.update_radius_bounds(new_track_bounds[i]);
- });
- },
-
- update_track_bounds: function () {
- // Recompute and update track bounds.
- var new_track_bounds = this.get_tracks_bounds();
- _.each(this.circular_views, (track_view, i) => {
- track_view.update_radius_bounds(new_track_bounds[i]);
- });
-
- // Update chords tracks.
- _.each(this.chords_views, (track_view) => {
- track_view.update_radius_bounds(new_track_bounds[0]);
- });
- },
-});
-
-/**
- * Renders a track in a Circster visualization.
- */
-var CircsterTrackView = Backbone.View.extend({
- tagName: "g",
-
- /* ----------------------- Public Methods ------------------------- */
-
- initialize: function (options) {
- this.bg_stroke = "#ddd";
- // Fill color when loading data.
- this.loading_bg_fill = "#ffc";
- // Fill color when data has been loaded.
- this.bg_fill = "#ddd";
- this.total_gap = options.total_gap;
- this.track = options.track;
- this.radius_bounds = options.radius_bounds;
- this.genome = options.genome;
- this.chroms_layout = this._chroms_layout();
- this.data_bounds = [];
- this.scale = 1;
- this.parent_elt = d3.select(this.$el[0]);
- },
-
- /**
- * Get fill color from config.
- */
- get_fill_color: function () {
- var color = this.track.get("config").get_value("block_color");
- if (!color) {
- color = this.track.get("config").get_value("color");
- }
- return color;
- },
-
- /**
- * Render track's data by adding SVG elements to parent.
- */
- render: function () {
- // -- Create track group element. --
- var track_parent_elt = this.parent_elt;
-
- // -- Render background arcs. --
- var genome_arcs = this.chroms_layout;
-
- var arc_gen = d3.svg.arc().innerRadius(this.radius_bounds[0]).outerRadius(this.radius_bounds[1]);
-
- var // Attach data to group element.
- chroms_elts = track_parent_elt.selectAll("g").data(genome_arcs).enter().append("svg:g");
-
- var // Draw chrom arcs/paths.
- chroms_paths = chroms_elts
- .append("path")
- .attr("d", arc_gen)
- .attr("class", "chrom-background")
- .style("stroke", this.bg_stroke)
- .style("fill", this.loading_bg_fill);
-
- // Append titles to paths.
- chroms_paths.append("title").text((d) => d.data.chrom);
-
- // -- Render track data and, when track data is rendered, apply preferences and update chrom_elts fill. --
-
- var self = this;
-
- var data_manager = self.track.get("data_manager");
-
- var // If track has a data manager, get deferred that resolves when data is ready.
- data_ready_deferred = data_manager ? data_manager.data_is_ready() : true;
-
- // When data is ready, render track.
- $.when(data_ready_deferred).then(() => {
- $.when(self._render_data(track_parent_elt)).then(() => {
- chroms_paths.style("fill", self.bg_fill);
-
- // Render labels after data is available so that data attributes are available.
- self.render_labels();
- });
- });
- },
-
- /**
- * Render track labels.
- */
- render_labels: function () {},
-
- /**
- * Update radius bounds.
- */
- update_radius_bounds: function (radius_bounds) {
- // Update bounds.
- this.radius_bounds = radius_bounds;
-
- // -- Update background arcs. --
- var new_d = d3.svg.arc().innerRadius(this.radius_bounds[0]).outerRadius(this.radius_bounds[1]);
-
- this.parent_elt.selectAll("g>path.chrom-background").transition().duration(1000).attr("d", new_d);
-
- this._transition_chrom_data();
-
- this._transition_labels();
- },
-
- /**
- * Update view scale. This fetches more data if scale is increased.
- */
- update_scale: function (new_scale) {
- // -- Update scale and return if new scale is less than old scale. --
-
- var old_scale = this.scale;
- this.scale = new_scale;
- if (new_scale <= old_scale) {
- return;
- }
-
- // -- Scale increased, so render visible data with more detail. --
-
- var self = this;
-
- var utils = new SVGUtils();
-
- // Select all chrom data and filter to operate on those that are visible.
- this.parent_elt
- .selectAll("path.chrom-data")
- .filter(function (d, i) {
- return utils.is_visible(this);
- })
- .each(function (d, i) {
- // -- Now operating on a single path element representing chromosome data. --
-
- var path_elt = d3.select(this);
-
- var chrom = path_elt.attr("chrom");
- var chrom_region = self.genome.get_chrom_region(chrom);
- var data_manager = self.track.get("data_manager");
- var data_deferred;
-
- // If can't get more detailed data, return.
- if (!data_manager.can_get_more_detailed_data(chrom_region)) {
- return;
- }
-
- // -- Get more detailed data. --
- data_deferred = self.track
- .get("data_manager")
- .get_more_detailed_data(chrom_region, "Coverage", 0, new_scale);
-
- // When more data is available, use new data to redraw path.
- $.when(data_deferred).then((data) => {
- // Remove current data path.
- path_elt.remove();
-
- // Update data bounds with new data.
- self._update_data_bounds();
-
- // Find chromosome arc to draw data on.
- var chrom_arc = _.find(self.chroms_layout, (layout) => layout.data.chrom === chrom);
-
- // Add new data path and apply preferences.
- var color = self.get_fill_color();
- self._render_chrom_data(self.parent_elt, chrom_arc, data)
- .style("stroke", color)
- .style("fill", color);
- });
- });
-
- return self;
- },
-
- /* ----------------------- Internal Methods ------------------------- */
-
- /**
- * Transitions chrom data to new values (e.g new radius or data bounds).
- */
- _transition_chrom_data: function () {
- var track = this.track;
- var chrom_arcs = this.chroms_layout;
- var chrom_data_paths = this.parent_elt.selectAll("g>path.chrom-data");
- var num_paths = chrom_data_paths[0].length;
-
- if (num_paths > 0) {
- var self = this;
- $.when(track.get("data_manager").get_genome_wide_data(this.genome)).then((genome_wide_data) => {
- // Map chrom data to path data, filtering out null values.
- var path_data = _.reject(
- _.map(genome_wide_data, (chrom_data, i) => {
- var rval = null;
-
- var path_fn = self._get_path_function(chrom_arcs[i], chrom_data);
-
- if (path_fn) {
- rval = path_fn(chrom_data.data);
- }
- return rval;
- }),
- (p_data) => p_data === null
- );
-
- // Transition each path for data and color.
- var color = track.get("config").get_value("color");
- chrom_data_paths.each(function (path, index) {
- d3.select(this)
- .transition()
- .duration(1000)
- .style("stroke", color)
- .style("fill", color)
- .attr("d", path_data[index]);
- });
- });
- }
- },
-
- /**
- * Transition labels to new values (e.g new radius or data bounds).
- */
- _transition_labels: function () {},
-
- /**
- * Update data bounds. If there are new_bounds, use them; otherwise use
- * default data bounds.
- */
- _update_data_bounds: function (new_bounds) {
- this.data_bounds =
- new_bounds || this.get_data_bounds(this.track.get("data_manager").get_genome_wide_data(this.genome));
- this._transition_chrom_data();
- },
-
- /**
- * Render data as elements attached to svg.
- */
- _render_data: function (svg) {
- var self = this;
- var chrom_arcs = this.chroms_layout;
- var track = this.track;
- var rendered_deferred = $.Deferred();
-
- // When genome-wide data is available, render data.
- $.when(track.get("data_manager").get_genome_wide_data(this.genome)).then((genome_wide_data) => {
- // Set bounds.
- self.data_bounds = self.get_data_bounds(genome_wide_data);
-
- // Set min, max value in config so that they can be adjusted. Make this silent
- // because these attributes are watched for changes and the viz is updated
- // accordingly (set up in initialize). Because we are setting up, we don't want
- // the watch to trigger events here.
- track.get("config").set_value("min_value", self.data_bounds[0], {
- silent: true,
- });
- track.get("config").set_value("max_value", self.data_bounds[1], {
- silent: true,
- });
-
- // Merge chroms layout with data.
- var layout_and_data = _.zip(chrom_arcs, genome_wide_data);
-
- // Render each chromosome's data.
- _.each(layout_and_data, (chrom_info) => {
- var chrom_arc = chrom_info[0];
- var data = chrom_info[1];
- return self._render_chrom_data(svg, chrom_arc, data);
- });
-
- // Apply prefs to all track data.
- var color = self.get_fill_color();
- self.parent_elt.selectAll("path.chrom-data").style("stroke", color).style("fill", color);
-
- rendered_deferred.resolve(svg);
- });
-
- return rendered_deferred;
- },
-
- /**
- * Render a chromosome data and attach elements to svg.
- */
- _render_chrom_data: function (svg, chrom_arc, data) {},
-
- /**
- * Returns data for creating a path for the given data using chrom_arc and data bounds.
- */
- _get_path_function: function (chrom_arc, chrom_data) {},
-
- /**
- * Returns arc layouts for genome's chromosomes/contigs. Arcs are arranged in a circle
- * separated by gaps.
- */
- _chroms_layout: function () {
- // Setup chroms layout using pie.
- var chroms_info = this.genome.get_chroms_info();
-
- var pie_layout = d3.layout
- .pie()
- .value((d) => d.len)
- .sort(null);
-
- var init_arcs = pie_layout(chroms_info);
- var gap_per_chrom = (2 * Math.PI * this.total_gap) / chroms_info.length;
-
- var chrom_arcs = _.map(init_arcs, (arc, index) => {
- // For short chroms, endAngle === startAngle.
- var new_endAngle = arc.endAngle - gap_per_chrom;
- arc.endAngle = new_endAngle > arc.startAngle ? new_endAngle : arc.startAngle;
- return arc;
- });
-
- return chrom_arcs;
- },
-});
-
-/**
- * Render chromosome labels.
- */
-var CircsterChromLabelTrackView = CircsterTrackView.extend({
- initialize: function (options) {
- CircsterTrackView.prototype.initialize.call(this, options);
- // Use a single arc for rendering data.
- this.innerRadius = this.radius_bounds[0];
- this.radius_bounds[0] = this.radius_bounds[1];
- this.bg_stroke = "#fff";
- this.bg_fill = "#fff";
-
- // Minimum arc distance for labels to be applied.
- this.min_arc_len = 0.05;
- },
-
- /**
- * Render labels.
- */
- _render_data: function (svg) {
- // -- Add chromosome label where it will fit; an alternative labeling mechanism
- // would be nice for small chromosomes. --
- var self = this;
-
- var chrom_arcs = svg.selectAll("g");
-
- chrom_arcs.selectAll("path").attr("id", (d) => `label-${d.data.chrom}`);
-
- chrom_arcs
- .append("svg:text")
- .filter((d) => d.endAngle - d.startAngle > self.min_arc_len)
- .attr("text-anchor", "middle")
- .append("svg:textPath")
- .attr("class", "chrom-label")
- .attr("xlink:href", (d) => `#label-${d.data.chrom}`)
- .attr("startOffset", "25%")
- .text((d) => d.data.chrom);
-
- // -- Add ticks to denote chromosome length. --
-
- /** Returns an array of tick angles and labels, given a chrom arc. */
- var chromArcTicks = (d) => {
- var k = (d.endAngle - d.startAngle) / d.value;
-
- var ticks = d3.range(0, d.value, 25000000).map((v, i) => ({
- radius: self.innerRadius,
- angle: v * k + d.startAngle,
- label: i === 0 ? 0 : i % 3 ? null : self.formatNum(v),
- }));
-
- // If there are fewer that 4 ticks, label last tick so that at least one non-zero tick is labeled.
- if (ticks.length < 4) {
- ticks[ticks.length - 1].label = self.formatNum(
- Math.round((ticks[ticks.length - 1].angle - d.startAngle) / k)
- );
- }
-
- return ticks;
- };
-
- /** Rotate and move text as needed. */
- var textTransform = (d) => (d.angle > Math.PI ? "rotate(180)translate(-16)" : null);
-
- // Filter chroms for only those large enough for display.
- var visibleChroms = _.filter(this.chroms_layout, (c) => c.endAngle - c.startAngle > self.min_arc_len);
-
- this.drawTicks(this.parent_elt, visibleChroms, chromArcTicks, textTransform);
- },
-});
-_.extend(CircsterChromLabelTrackView.prototype, UsesTicks);
-
-/**
- * View for quantitative track in Circster.
- */
-var CircsterQuantitativeTrackView = CircsterTrackView.extend({
- initialize: function (options) {
- CircsterTrackView.prototype.initialize.call(this, options);
-
- // When config settings change, update view.
- var track_config = this.track.get("config");
- track_config.get("min_value").on("change:value", this._update_min_max, this);
- track_config.get("max_value").on("change:value", this._update_min_max, this);
- track_config.get("color").on("change:value", this._transition_chrom_data, this);
- },
-
- /**
- * Update track when min and/or max are changed.
- */
- _update_min_max: function () {
- var track_config = this.track.get("config");
-
- var new_bounds = [track_config.get_value("min_value"), track_config.get_value("max_value")];
-
- this._update_data_bounds(new_bounds);
-
- // FIXME: this works to update tick/text bounds, but there's probably a better way to do this
- // by updating the data itself.
- this.parent_elt.selectAll(".min_max").text((d, i) => new_bounds[i]);
- },
-
- /**
- * Returns quantile for an array of numbers.
- */
- _quantile: function (numbers, quantile) {
- numbers.sort(d3.ascending);
- return d3.quantile(numbers, quantile);
- },
-
- /**
- * Renders quantitative data with the form [x, value] and assumes data is equally spaced across
- * chromosome. Attachs a dict with track and chrom name information to DOM element.
- */
- _render_chrom_data: function (svg, chrom_arc, chrom_data) {
- var path_data = this._get_path_function(chrom_arc, chrom_data);
-
- if (!path_data) {
- return null;
- }
-
- // There is path data, so render as path.
- var parent = svg.datum(chrom_data.data);
-
- var path = parent
- .append("path")
- .attr("class", "chrom-data")
- .attr("chrom", chrom_arc.data.chrom)
- .attr("d", path_data);
-
- return path;
- },
-
- /**
- * Returns function for creating a path across the chrom arc.
- */
- _get_path_function: function (chrom_arc, chrom_data) {
- // If no chrom data, return null.
- if (typeof chrom_data === "string" || !chrom_data.data || chrom_data.data.length === 0) {
- return null;
- }
-
- // Radius scaler.
- var radius = d3.scale.linear().domain(this.data_bounds).range(this.radius_bounds).clamp(true);
-
- // Scaler for placing data points across arc.
- var angle = d3.scale
- .linear()
- .domain([0, chrom_data.data.length])
- .range([chrom_arc.startAngle, chrom_arc.endAngle]);
-
- // Use line generator to create area.
- var line = d3.svg.line
- .radial()
- .interpolate("linear")
- .radius((d) => radius(d[1]))
- .angle((d, i) => angle(i));
-
- return d3.svg.area
- .radial()
- .interpolate(line.interpolate())
- .innerRadius(radius(0))
- .outerRadius(line.radius())
- .angle(line.angle());
- },
-
- /**
- * Render track min, max using ticks.
- */
- render_labels: function () {
- var self = this;
-
- var // Keep counter of visible chroms.
- textTransform = () => "rotate(90)";
-
- // FIXME:
- // (1) using min_max class below is needed for _update_min_max, which could be improved.
- // (2) showing config on tick click should be replaced by proper track config icon.
-
- // Draw min, max on first chrom only.
- var ticks = this.drawTicks(
- this.parent_elt,
- [this.chroms_layout[0]],
- this._data_bounds_ticks_fn(),
- textTransform,
- true
- ).classed("min_max", true);
-
- // Show config when ticks are clicked on.
- _.each(ticks, (tick) => {
- $(tick).click(() => {
- var view = new config.ConfigSettingCollectionView({
- collection: self.track.get("config"),
- });
- view.render_in_modal("Configure Track");
- });
- });
-
- /*
- // Filter for visible chroms, then for every third chrom so that labels attached to only every
- // third chrom.
- var visibleChroms = _.filter(this.chroms_layout, function(c) { return c.endAngle - c.startAngle > 0.08; }),
- labeledChroms = _.filter(visibleChroms, function(c, i) { return i % 3 === 0; });
- this.drawTicks(this.parent_elt, labeledChroms, this._data_bounds_ticks_fn(), textTransform, true);
- */
- },
-
- /**
- * Transition labels to new values (e.g new radius or data bounds).
- */
- _transition_labels: function () {
- // FIXME: (a) pull out function for getting labeled chroms? and (b) function used in transition below
- // is copied from UseTicks mixin, so pull out and make generally available.
-
- // If there are no data bounds, nothing to transition.
- if (this.data_bounds.length === 0) {
- return;
- }
-
- // Transition labels to new radius bounds.
- var self = this;
-
- var visibleChroms = _.filter(this.chroms_layout, (c) => c.endAngle - c.startAngle > 0.08);
-
- var labeledChroms = _.filter(visibleChroms, (c, i) => i % 3 === 0);
-
- var new_data = _.flatten(_.map(labeledChroms, (c) => self._data_bounds_ticks_fn()(c)));
-
- this.parent_elt
- .selectAll("g.tick")
- .data(new_data)
- .transition()
- .attr("transform", (d) => `rotate(${(d.angle * 180) / Math.PI - 90})translate(${d.radius},0)`);
- },
-
- /**
- * Get function for locating data bounds ticks.
- */
- _data_bounds_ticks_fn: function () {
- // Closure vars.
- var self = this;
-
- // Return function for locating ticks based on chrom arc data.
- return (
- d // Set up data to display min, max ticks.
- ) => [
- {
- radius: self.radius_bounds[0],
- angle: d.startAngle,
- label: self.formatNum(self.data_bounds[0]),
- },
- {
- radius: self.radius_bounds[1],
- angle: d.startAngle,
- label: self.formatNum(self.data_bounds[1]),
- },
- ];
- },
-
- /**
- * Returns an array with two values denoting the minimum and maximum
- * values for the track.
- */
- get_data_bounds: function (data) {},
-});
-_.extend(CircsterQuantitativeTrackView.prototype, UsesTicks);
-
-/**
- * Bigwig track view in Circster.
- */
-var CircsterBigWigTrackView = CircsterQuantitativeTrackView.extend({
- get_data_bounds: function (data) {
- // Set max across dataset by extracting all values, flattening them into a
- // single array, and getting third quartile.
- var values = _.flatten(
- _.map(data, (d) => {
- if (d) {
- // Each data point has the form [position, value], so return all values.
- return _.map(
- d.data,
- (
- p // Null is used for a lack of data; resolve null to 0 for comparison.
- ) => parseInt(p[1], 10) || 0
- );
- } else {
- return 0;
- }
- })
- );
-
- // For max, use 98% quantile in attempt to avoid very large values. However, this max may be 0
- // for sparsely populated data, so use max in that case.
- return [_.min(values), this._quantile(values, 0.98) || _.max(values)];
- },
-});
-
-/**
- * Chromosome interactions track view in Circster.
- */
-var CircsterChromInteractionsTrackView = CircsterTrackView.extend({
- render: function () {
- var self = this;
-
- // When data is ready, render track.
- $.when(self.track.get("data_manager").data_is_ready()).then(() => {
- // When data has been fetched, render track.
- $.when(self.track.get("data_manager").get_genome_wide_data(self.genome)).then((genome_wide_data) => {
- var chord_data = [];
- var chroms_info = self.genome.get_chroms_info();
- // Convert chromosome data into chord data.
- _.each(genome_wide_data, (chrom_data, index) => {
- // Map each interaction into chord data.
- var cur_chrom = chroms_info[index].chrom;
- var chrom_chord_data = _.map(chrom_data.data, (datum) => {
- // Each datum is an interaction/chord.
- var source_angle = self._get_region_angle(cur_chrom, datum[1]);
-
- var target_angle = self._get_region_angle(datum[3], datum[4]);
-
- return {
- source: {
- startAngle: source_angle,
- endAngle: source_angle + 0.01,
- },
- target: {
- startAngle: target_angle,
- endAngle: target_angle + 0.01,
- },
- };
- });
-
- chord_data = chord_data.concat(chrom_chord_data);
- });
-
- self.parent_elt
- .append("g")
- .attr("class", "chord")
- .selectAll("path")
- .data(chord_data)
- .enter()
- .append("path")
- .style("fill", self.get_fill_color())
- .attr("d", d3.svg.chord().radius(self.radius_bounds[0]))
- .style("opacity", 1);
- });
- });
- },
-
- update_radius_bounds: function (radius_bounds) {
- this.radius_bounds = radius_bounds;
- this.parent_elt.selectAll("path").transition().attr("d", d3.svg.chord().radius(this.radius_bounds[0]));
- },
-
- /**
- * Returns radians for a genomic position.
- */
- _get_region_angle: function (chrom, position) {
- // Find chrom angle data
- var chrom_angle_data = _.find(this.chroms_layout, (chrom_layout) => chrom_layout.data.chrom === chrom);
-
- // Return angle at position.
- return (
- chrom_angle_data.endAngle -
- ((chrom_angle_data.endAngle - chrom_angle_data.startAngle) * (chrom_angle_data.data.len - position)) /
- chrom_angle_data.data.len
- );
- },
-});
-
-// circster app loader
-var Circster = Backbone.View.extend({
- initialize: function () {
- // load css
- mod_utils.cssLoadFile("static/style/circster.css");
- // -- Configure visualization --
- var genome = new visualization.Genome(window.galaxy_config.app.genome);
-
- var vis = new visualization.GenomeVisualization(window.galaxy_config.app.viz_config);
-
- // Add Circster-specific config options.
- vis.get("config").add([
- {
- key: "arc_dataset_height",
- label: "Arc Dataset Height",
- type: "int",
- value: 25,
- view: "circster",
- },
- {
- key: "track_gap",
- label: "Gap Between Tracks",
- type: "int",
- value: 5,
- view: "circster",
- },
- {
- key: "total_gap",
- label: "Gap [0-1]",
- type: "float",
- value: 0.4,
- view: "circster",
- hidden: true,
- },
- ]);
-
- var viz_view = new CircsterView({
- // view pane
- el: $("#center .unified-panel-body"),
- genome: genome,
- model: vis,
- });
-
- // Render vizualization
- viz_view.render();
-
- // setup title
- $("#center .unified-panel-header-inner").append(
- `${window.galaxy_config.app.viz_config.title} ${window.galaxy_config.app.viz_config.dbkey}`
- );
-
- // setup menu
- var menu = mod_icon_btn.create_icon_buttons_menu(
- [
- {
- icon_class: "plus-button",
- title: _l("Add tracks"),
- on_click: function () {
- visualization.select_datasets({ dbkey: vis.get("dbkey") }, (tracks) => {
- vis.add_tracks(tracks);
- });
- },
- },
- {
- icon_class: "gear",
- title: _l("Settings"),
- on_click: function () {
- var view = new config.ConfigSettingCollectionView({
- collection: vis.get("config"),
- });
- view.render_in_modal("Configure Visualization");
- },
- },
- {
- icon_class: "disk--arrow",
- title: _l("Save"),
- on_click: function () {
- const Galaxy = getGalaxyInstance();
-
- // show saving dialog box
- Galaxy.modal.show({
- title: _l("Saving..."),
- body: "progress",
- });
-
- // send to server
- $.ajax({
- url: `${getAppRoot()}visualization/save`,
- type: "POST",
- dataType: "json",
- data: {
- id: vis.get("vis_id"),
- title: vis.get("title"),
- dbkey: vis.get("dbkey"),
- type: "trackster",
- vis_json: JSON.stringify(vis),
- },
- })
- .success((vis_info) => {
- Galaxy.modal.hide();
- vis.set("vis_id", vis_info.vis_id);
- })
- .error(() => {
- // show dialog
- Galaxy.modal.show({
- title: _l("Could Not Save"),
- body: "Could not save visualization. Please try again later.",
- buttons: {
- Cancel: function () {
- Galaxy.modal.hide();
- },
- },
- });
- });
- },
- },
- {
- icon_class: "cross-circle",
- title: _l("Close"),
- on_click: function () {
- window.top.location = `${getAppRoot()}visualizations/list`;
- },
- },
- ],
- { tooltip_config: { placement: "bottom" } }
- );
-
- // add menu
- menu.$el.attr("style", "float: right");
- $("#center .unified-panel-header-inner").append(menu.$el);
-
- // manual tooltip config because default gravity is S and cannot be changed
- $(".menu-button").tooltip({ placement: "bottom" });
- },
-});
-
-// Module exports.
-export default {
- GalaxyApp: Circster,
-};
diff --git a/client/src/viz/phyloviz.js b/client/src/viz/phyloviz.js
deleted file mode 100644
index 9020b0cd0c2b..000000000000
--- a/client/src/viz/phyloviz.js
+++ /dev/null
@@ -1,1092 +0,0 @@
-import Backbone from "backbone";
-import * as d3 from "d3v3";
-import $ from "jquery";
-import { hide_modal, show_message } from "layout/modal";
-import { Dataset } from "mvc/dataset/data";
-import mod_icon_btn from "mvc/ui/icon-button";
-import _l from "utils/localization";
-import visualization_mod from "viz/visualization";
-
-/**
- * Base class of any menus that takes in user interaction. Contains checking methods.
- */
-var UserMenuBase = Backbone.View.extend({
- className: "UserMenuBase",
-
- /**
- * Check if an input value is a number and falls within max min.
- */
- isAcceptableValue: function ($inputKey, min, max) {
- //TODO: use better feedback than alert
- var value = $inputKey.val();
-
- var fieldName = $inputKey.attr("displayLabel") || $inputKey.attr("id").replace("phyloViz", "");
-
- function isNumeric(n) {
- return !isNaN(parseFloat(n)) && isFinite(n);
- }
-
- if (!isNumeric(value)) {
- alert(`${fieldName} is not a number!`);
- return false;
- }
-
- if (value > max) {
- alert(`${fieldName} is too large.`);
- return false;
- } else if (value < min) {
- alert(`${fieldName} is too small.`);
- return false;
- }
- return true;
- },
-
- /**
- * Check if any user string inputs has illegal characters that json cannot accept
- */
- hasIllegalJsonCharacters: function ($inputKey) {
- if ($inputKey.val().search(/"|'|\\/) !== -1) {
- alert(
- "Named fields cannot contain these illegal characters: " +
- "double quote(\"), single guote('), or back slash(\\). "
- );
- return true;
- }
- return false;
- },
-});
-
-/**
- * -- Custom Layout call for phyloViz to suit the needs of a phylogenetic tree.
- * -- Specifically: 1) Nodes have a display display of (= evo dist X depth separation) from their parent
- * 2) Nodes must appear in other after they have expand and contracted
- */
-function PhyloTreeLayout() {
- var self = this; // maximum length of the text labels
-
- var hierarchy = d3.layout.hierarchy().sort(null).value(null);
-
- var // ! represents both the layout angle and the height of the layout, in px
- height = 360;
-
- var layoutMode = "Linear";
-
- var // height of each individual leaf node
- leafHeight = 18;
-
- var // separation between nodes of different depth, in px
- depthSeparation = 200;
-
- var // change to recurssive call
- leafIndex = 0;
-
- var // tree defaults to 0.5 dist if no dist is specified
- defaultDist = 0.5;
-
- var maxTextWidth = 50;
-
- self.leafHeight = (inputLeafHeight) => {
- if (typeof inputLeafHeight === "undefined") {
- return leafHeight;
- } else {
- leafHeight = inputLeafHeight;
- return self;
- }
- };
-
- self.layoutMode = (mode) => {
- if (typeof mode === "undefined") {
- return layoutMode;
- } else {
- layoutMode = mode;
- return self;
- }
- };
-
- // changes the layout angle of the display, which is really changing the height
- self.layoutAngle = (angle) => {
- if (typeof angle === "undefined") {
- return height;
- }
- // to use default if the user puts in strange values
- if (isNaN(angle) || angle < 0 || angle > 360) {
- return self;
- } else {
- height = angle;
- return self;
- }
- };
-
- self.separation = (dist) => {
- // changes the dist between the nodes of different depth
- if (typeof dist === "undefined") {
- return depthSeparation;
- } else {
- depthSeparation = dist;
- return self;
- }
- };
-
- self.links = (
- nodes // uses d3 native method to generate links. Done.
- ) => d3.layout.tree().links(nodes);
-
- // -- Custom method for laying out phylogeny tree in a linear fashion
- self.nodes = (d, i) => {
- //TODO: newick and phyloxml return arrays. where should this go (client (here, else), server)?
- if (toString.call(d) === "[object Array]") {
- // if d is an array, replate with the first object (newick, phyloxml)
- d = d[0];
- }
-
- // self is to find the depth of all the nodes, assumes root is passed in
- var _nodes = hierarchy.call(self, d, i);
-
- var nodes = [];
- var maxDepth = 0;
- var numLeaves = 0;
- //console.debug( JSON.stringify( _nodes, null, 2 ) )
- window._d = d;
- window._nodes = _nodes;
-
- //TODO: remove dbl-touch loop
- // changing from hierarchy's custom format for data to usable format
- _nodes.forEach((node) => {
- maxDepth = node.depth > maxDepth ? node.depth : maxDepth; //finding max depth of tree
- nodes.push(node);
- });
- // counting the number of leaf nodes and assigning max depth
- // to nodes that do not have children to flush all the leave nodes
- nodes.forEach((node) => {
- if (!node.children) {
- //&& !node._children
- numLeaves += 1;
- node.depth = maxDepth; // if a leaf has no child it would be assigned max depth
- }
- });
-
- leafHeight = layoutMode === "Circular" ? height / numLeaves : leafHeight;
- leafIndex = 0;
- layout(nodes[0], maxDepth, leafHeight, null);
-
- return nodes;
- };
-
- /**
- * -- Function with side effect of adding x0, y0 to all child; take in the root as starting point
- * assuming that the leave nodes would be sorted in presented order
- * horizontal(y0) is calculated according to (= evo dist X depth separation) from their parent
- * vertical (x0) - if leave node: find its order in all of the leave node === node.id,
- * then multiply by verticalSeparation
- * - if parent node: is place in the mid point all of its children nodes
- * -- The layout will first calculate the y0 field going towards the leaves, and x0 when returning
- */
- function layout(node, maxDepth, vertSeparation, parent) {
- var children = node.children;
- var sumChildVertSeparation = 0;
-
- // calculation of node's dist from parents, going down.
- var dist = node.dist || defaultDist;
- dist = dist > 1 ? 1 : dist; // We constrain all dist to be less than one
- node.dist = dist;
- if (parent !== null) {
- node.y0 = parent.y0 + dist * depthSeparation;
- } else {
- //root node
- node.y0 = maxTextWidth;
- }
-
- // if a node have no children, we will treat it as a leaf and start laying it out first
- if (!children) {
- node.x0 = leafIndex * vertSeparation;
- leafIndex += 1;
- } else {
- // if it has children, we will visit all its children and calculate its position from its children
- children.forEach((child) => {
- child.parent = node;
- sumChildVertSeparation += layout(child, maxDepth, vertSeparation, node);
- });
- node.x0 = sumChildVertSeparation / children.length;
- }
-
- // adding properties to the newly created node
- node.x = node.x0;
- node.y = node.y0;
- return node.x0;
- }
- return self;
-}
-
-/**
- * -- PhyloTree Model --
- */
-var PhyloTree = visualization_mod.Visualization.extend({
- defaults: {
- layout: "Linear",
- separation: 250, // px dist between nodes of different depth to represent 1 evolutionary until
- leafHeight: 18,
- type: "phyloviz", // visualization type
- title: _l("Title"),
- scaleFactor: 1,
- translate: [0, 0],
- fontSize: 12, //fontSize of node label
- selectedNode: null,
- nodeAttrChangedTime: 0,
- },
-
- initialize: function (options) {
- this.set(
- "dataset",
- new Dataset({
- id: options.dataset_id,
- })
- );
- },
-
- root: {}, // Root has to be its own independent object because it is not part of the viz_config
-
- /**
- * Mechanism to expand or contract a single node. Expanded nodes have a children list, while for
- * contracted nodes the list is stored in _children. Nodes with their children data stored in _children will not
- * have their children rendered.
- */
- toggle: function (d) {
- if (typeof d === "undefined") {
- return;
- }
- if (d.children) {
- d._children = d.children;
- d.children = null;
- } else {
- d.children = d._children;
- d._children = null;
- }
- },
-
- /**
- * Contracts the phylotree to a single node by repeatedly calling itself to place all the list
- * of children under _children.
- */
- toggleAll: function (d) {
- if (d.children && d.children.length !== 0) {
- d.children.forEach(this.toggleAll);
- this.toggle(d);
- }
- },
-
- /**
- * Return the data of the tree. Used for preserving state.
- */
- getData: function () {
- return this.root;
- },
-
- /**
- * Overriding the default save mechanism to do some clean of circular reference of the
- * phyloTree and to include phyloTree in the saved json
- */
- save: function () {
- var root = this.root;
- cleanTree(root);
- //this.set("root", root);
-
- function cleanTree(node) {
- // we need to remove parent to delete circular reference
- delete node.parent;
-
- // removing unnecessary attributes
- if (node._selected) {
- delete node._selected;
- }
-
- if (node.children) {
- node.children.forEach(cleanTree);
- }
- if (node._children) {
- node._children.forEach(cleanTree);
- }
- }
-
- var config = $.extend(true, {}, this.attributes);
- config.selectedNode = null;
-
- show_message("Saving to Galaxy", "progress");
-
- return $.ajax({
- url: this.url(),
- type: "POST",
- dataType: "json",
- data: {
- config: JSON.stringify(config),
- type: "phyloviz",
- },
- success: function (res) {
- hide_modal();
- },
- });
- },
-});
-
-// -- Views --
-/**
- * Stores the default variable for setting up the visualization
- */
-var PhylovizLayoutBase = Backbone.View.extend({
- defaults: {
- nodeRadius: 4.5, // radius of each node in the diagram
- },
-
- /**
- * Common initialization in layouts
- */
- stdInit: function (options) {
- var self = this;
- self.model.on(
- "change:separation change:leafHeight change:fontSize change:nodeAttrChangedTime",
- self.updateAndRender,
- self
- );
-
- self.vis = options.vis;
- self.i = 0;
- self.maxDepth = -1; // stores the max depth of the tree
-
- self.width = options.width;
- self.height = options.height;
- },
-
- /**
- * Updates the visualization whenever there are changes in the expansion and contraction of nodes
- * AND possibly when the tree is edited.
- */
- updateAndRender: function (source) {
- var self = this;
- source = source || self.model.root;
-
- self.renderNodes(source);
- self.renderLinks(source);
- self.addTooltips();
- },
-
- /**
- * Renders the links for the visualization.
- */
- renderLinks: function (source) {
- var self = this;
- var link = self.vis.selectAll("g.completeLink").data(self.tree.links(self.nodes), (d) => d.target.id);
-
- var calcalateLinePos = (d) => {
- // position of the source node <=> starting location of the line drawn
- d.pos0 = `${d.source.y0} ${d.source.x0}`;
- // position where the line makes a right angle bend
- d.pos1 = `${d.source.y0} ${d.target.x0}`;
- // point where the horizontal line becomes a dotted line
- d.pos2 = `${d.target.y0} ${d.target.x0}`;
- };
-
- var linkEnter = link.enter().insert("svg:g", "g.node").attr("class", "completeLink");
-
- linkEnter
- .append("svg:path")
- .attr("class", "link")
- .attr("d", (d) => {
- calcalateLinePos(d);
- return `M ${d.pos0} L ${d.pos1}`;
- });
-
- var linkUpdate = link.transition().duration(500);
-
- linkUpdate.select("path.link").attr("d", (d) => {
- calcalateLinePos(d);
- return `M ${d.pos0} L ${d.pos1} L ${d.pos2}`;
- });
-
- link.exit().remove();
- },
-
- // User Interaction methods below
-
- /**
- * Displays the information for editing
- */
- selectNode: function (node) {
- var self = this;
- d3.selectAll("g.node").classed("selectedHighlight", (d) => {
- if (node.id === d.id) {
- if (node._selected) {
- // for de=selecting node.
- delete node._selected;
- return false;
- } else {
- node._selected = true;
- return true;
- }
- }
- return false;
- });
-
- self.model.set("selectedNode", node);
- $("#phyloVizSelectedNodeName").val(node.name);
- $("#phyloVizSelectedNodeDist").val(node.dist);
- $("#phyloVizSelectedNodeAnnotation").val(node.annotation || "");
- },
-
- /**
- * Creates bootstrap tooltip for the visualization. Has to be called repeatedly due to newly generated
- * enterNodes
- */
- addTooltips: function () {
- $(".tooltip").remove(); //clean up tooltip, just in case its listeners are removed by d3
- $(".node")
- .attr("data-original-title", function () {
- var d = this.__data__;
- var annotation = d.annotation || "None";
- return d
- ? `${d.name ? `${d.name}
` : ""}Dist: ${d.dist}
Annotation1: ${annotation}${
- d.bootstrap ? `
Confidence level: ${Math.round(100 * d.bootstrap)}` : ""
- }`
- : "";
- })
- .tooltip({ placement: "top", trigger: "hover" });
- },
-});
-
-/**
- * Linea layout class of Phyloviz, is responsible for rendering the nodes
- * calls PhyloTreeLayout to determine the positions of the nodes
- */
-var PhylovizLinearView = PhylovizLayoutBase.extend({
- initialize: function (options) {
- // Default values of linear layout
- var self = this;
- self.margins = options.margins;
- self.layoutMode = "Linear";
-
- self.stdInit(options);
-
- self.layout();
- self.updateAndRender(self.model.root);
- },
-
- /**
- * Creates the basic layout of a linear tree by precalculating fixed values.
- * One of calculations are also made here
- */
- layout: function () {
- var self = this;
- self.tree = new PhyloTreeLayout().layoutMode("Linear");
- self.diagonal = d3.svg.diagonal().projection((d) => [d.y, d.x]);
- },
-
- /**
- * Renders the nodes base on Linear layout.
- */
- renderNodes: function (source) {
- var self = this;
- var fontSize = `${self.model.get("fontSize")}px`;
-
- // assigning properties from models
- self.tree.separation(self.model.get("separation")).leafHeight(self.model.get("leafHeight"));
-
- var duration = 500;
-
- var nodes = self.tree.separation(self.model.get("separation")).nodes(self.model.root);
-
- var node = self.vis.selectAll("g.node").data(nodes, (d) => d.name + d.id || (d.id = ++self.i));
-
- // These variables has to be passed into update links which are in the base methods
- self.nodes = nodes;
- self.duration = duration;
-
- // ------- D3 ENTRY --------
- // Enter any new nodes at the parent's previous position.
- var nodeEnter = node
- .enter()
- .append("svg:g")
- .attr("class", "node")
- .on("dblclick", () => {
- d3.event.stopPropagation();
- })
- .on("click", (d) => {
- if (d3.event.altKey) {
- self.selectNode(d); // display info if alt is pressed
- } else {
- if (d.children && d.children.length === 0) {
- return;
- } // there is no need to toggle leaves
- self.model.toggle(d); // contract/expand nodes at data level
- self.updateAndRender(d); // re-render the tree
- }
- });
- //TODO: newick and phyloxml return arrays. where should this go (client (here, else), server)?
- if (toString.call(source) === "[object Array]") {
- // if d is an array, replate with the first object (newick, phyloxml)
- source = source[0];
- }
- nodeEnter.attr("transform", (d) => `translate(${source.y0},${source.x0})`);
-
- nodeEnter
- .append("svg:circle")
- .attr("r", 1e-6)
- .style("fill", (d) => (d._children ? "lightsteelblue" : "#fff"));
-
- nodeEnter
- .append("svg:text")
- .attr("class", "nodeLabel")
- .attr("x", (d) => (d.children || d._children ? -10 : 10))
- .attr("dy", ".35em")
- .attr("text-anchor", (d) => (d.children || d._children ? "end" : "start"))
- .style("fill-opacity", 1e-6);
-
- // ------- D3 TRANSITION --------
- // Transition nodes to their new position.
- var nodeUpdate = node.transition().duration(duration);
-
- nodeUpdate.attr("transform", (d) => `translate(${d.y},${d.x})`);
-
- nodeUpdate
- .select("circle")
- .attr("r", self.defaults.nodeRadius)
- .style("fill", (d) => (d._children ? "lightsteelblue" : "#fff"));
-
- nodeUpdate
- .select("text")
- .style("fill-opacity", 1)
- .style("font-size", fontSize)
- .text((d) => (d.name && d.name !== "" ? d.name : d.bootstrap ? Math.round(100 * d.bootstrap) : ""));
-
- // ------- D3 EXIT --------
- // Transition exiting nodes to the parent's new position.
- var nodeExit = node.exit().transition().duration(duration).remove();
-
- nodeExit.select("circle").attr("r", 1e-6);
-
- nodeExit.select("text").style("fill-opacity", 1e-6);
-
- // Stash the old positions for transition.
- nodes.forEach((d) => {
- d.x0 = d.x; // we need the x0, y0 for parents with children
- d.y0 = d.y;
- });
- },
-});
-
-export var PhylovizView = Backbone.View.extend({
- className: "phyloviz",
-
- initialize: function (options) {
- var self = this;
- // -- Default values of the vis
- self.MIN_SCALE = 0.05; //for zooming
- self.MAX_SCALE = 5;
- self.MAX_DISPLACEMENT = 500;
- self.margins = [10, 60, 10, 80];
-
- self.width = $("#PhyloViz").width();
- self.height = $("#PhyloViz").height();
- self.radius = self.width;
- self.data = options.data;
-
- // -- Events Phyloviz view responses to
- $(window).resize(() => {
- self.width = $("#PhyloViz").width();
- self.height = $("#PhyloViz").height();
- self.render();
- });
-
- // -- Create phyloTree model
- self.phyloTree = new PhyloTree(options.config);
- self.phyloTree.root = self.data;
-
- // -- Set up UI functions of main view
- self.zoomFunc = d3.behavior.zoom().scaleExtent([self.MIN_SCALE, self.MAX_SCALE]);
- self.zoomFunc.translate(self.phyloTree.get("translate"));
- self.zoomFunc.scale(self.phyloTree.get("scaleFactor"));
-
- // -- set up header buttons, search and settings menu
- self.navMenu = new HeaderButtons(self);
- self.settingsMenu = new SettingsMenu({
- phyloTree: self.phyloTree,
- });
- self.nodeSelectionView = new NodeSelectionView({
- phyloTree: self.phyloTree,
- });
- self.search = new PhyloVizSearch();
-
- // using settimeout to call the zoomAndPan function according to the stored attributes in viz_config
- setTimeout(() => {
- self.zoomAndPan();
- }, 1000);
- },
-
- render: function () {
- // -- Creating helper function for vis. --
- var self = this;
- $("#PhyloViz").empty();
-
- // -- Layout viz. --
- self.mainSVG = d3
- .select("#PhyloViz")
- .append("svg:svg")
- .attr("width", self.width)
- .attr("height", self.height)
- .attr("pointer-events", "all")
- .call(
- self.zoomFunc.on("zoom", () => {
- self.zoomAndPan();
- })
- );
-
- self.boundingRect = self.mainSVG
- .append("svg:rect")
- .attr("class", "boundingRect")
- .attr("width", self.width)
- .attr("height", self.height)
- .attr("stroke", "black")
- .attr("fill", "white");
-
- self.vis = self.mainSVG.append("svg:g").attr("class", "vis");
-
- self.layoutOptions = {
- model: self.phyloTree,
- width: self.width,
- height: self.height,
- vis: self.vis,
- margins: self.margins,
- };
-
- // -- Creating Title
- $("#title").text(`Phylogenetic Tree from ${self.phyloTree.get("title")}:`);
-
- // -- Create Linear view instance --
- new PhylovizLinearView(self.layoutOptions);
- },
-
- /**
- * Function to zoom and pan the svg element which the entire tree is contained within
- * Uses d3.zoom events, and extend them to allow manual updates and keeping states in model
- */
- zoomAndPan: function (event) {
- var zoomParams;
- var translateParams;
- if (typeof event !== "undefined") {
- zoomParams = event.zoom;
- translateParams = event.translate;
- }
-
- var self = this;
- var scaleFactor = self.zoomFunc.scale();
- var translationCoor = self.zoomFunc.translate();
- var zoomStatement = "";
- var translateStatement = "";
-
- // Do manual scaling.
- switch (zoomParams) {
- case "reset":
- scaleFactor = 1.0;
- translationCoor = [0, 0];
- break;
- case "+":
- scaleFactor *= 1.1;
- break;
- case "-":
- scaleFactor *= 0.9;
- break;
- default:
- if (typeof zoomParams === "number") {
- scaleFactor = zoomParams;
- } else if (d3.event !== null) {
- scaleFactor = d3.event.scale;
- }
- }
- if (scaleFactor < self.MIN_SCALE || scaleFactor > self.MAX_SCALE) {
- return;
- }
- self.zoomFunc.scale(scaleFactor); //update scale Factor
- zoomStatement = `translate(${self.margins[3]},${self.margins[0]}) scale(${scaleFactor})`;
-
- // Do manual translation.
- if (d3.event !== null) {
- translateStatement = `translate(${d3.event.translate})`;
- } else {
- if (typeof translateParams !== "undefined") {
- var x = translateParams.split(",")[0];
- var y = translateParams.split(",")[1];
- if (!isNaN(x) && !isNaN(y)) {
- translationCoor = [translationCoor[0] + parseFloat(x), translationCoor[1] + parseFloat(y)];
- }
- }
- self.zoomFunc.translate(translationCoor); // update zoomFunc
- translateStatement = `translate(${translationCoor})`;
- }
-
- self.phyloTree.set("scaleFactor", scaleFactor);
- self.phyloTree.set("translate", translationCoor);
- //refers to the view that we are actually zooming
- self.vis.attr("transform", translateStatement + zoomStatement);
- },
-
- /**
- * Primes the Ajax URL to load another Nexus tree
- */
- reloadViz: function () {
- var self = this;
- var treeIndex = $("#phylovizNexSelector :selected").val();
- $.getJSON(
- self.phyloTree.get("dataset").url(),
- {
- tree_index: treeIndex,
- data_type: "raw_data",
- },
- (packedJson) => {
- self.data = packedJson.data;
- self.config = packedJson;
- self.render();
- }
- );
- },
-});
-
-var HeaderButtons = Backbone.View.extend({
- initialize: function (phylovizView) {
- var self = this;
- self.phylovizView = phylovizView;
-
- // Clean up code - if the class initialized more than once
- $("#panelHeaderRightBtns").empty();
- $("#phyloVizNavBtns").empty();
- $("#phylovizNexSelector").off();
-
- self.initNavBtns();
- self.initRightHeaderBtns();
-
- // Initial a tree selector in the case of nexus
- $("#phylovizNexSelector")
- .off()
- .on("change", () => {
- self.phylovizView.reloadViz();
- });
- },
-
- initRightHeaderBtns: function () {
- var self = this;
-
- var rightMenu = mod_icon_btn.create_icon_buttons_menu(
- [
- {
- icon_class: "gear",
- title: _l("PhyloViz Settings"),
- on_click: function () {
- $("#SettingsMenu").show();
- self.settingsMenu.updateUI();
- },
- },
- {
- icon_class: "disk",
- title: _l("Save visualization"),
- on_click: function () {
- var nexSelected = $("#phylovizNexSelector option:selected").text();
- if (nexSelected) {
- self.phylovizView.phyloTree.set("title", nexSelected);
- }
- self.phylovizView.phyloTree.save();
- },
- },
- {
- icon_class: "chevron-expand",
- title: "Search / Edit Nodes",
- on_click: function () {
- $("#nodeSelectionView").show();
- },
- },
- {
- icon_class: "information",
- title: _l("Phyloviz Help"),
- on_click: function () {
- window.open("https://galaxyproject.org/learn/visualization/phylogenetic-tree/");
- // https://docs.google.com/document/d/1AXFoJgEpxr21H3LICRs3EyMe1B1X_KFPouzIgrCz3zk/edit
- },
- },
- ],
- {
- tooltip_config: { placement: "bottom" },
- }
- );
- $("#panelHeaderRightBtns").append(rightMenu.$el);
- },
-
- initNavBtns: function () {
- var self = this;
-
- var navMenu = mod_icon_btn.create_icon_buttons_menu(
- [
- {
- icon_class: "zoom-in",
- title: _l("Zoom in"),
- on_click: function () {
- self.phylovizView.zoomAndPan({ zoom: "+" });
- },
- },
- {
- icon_class: "zoom-out",
- title: _l("Zoom out"),
- on_click: function () {
- self.phylovizView.zoomAndPan({ zoom: "-" });
- },
- },
- {
- icon_class: "arrow-circle",
- title: "Reset Zoom/Pan",
- on_click: function () {
- self.phylovizView.zoomAndPan({
- zoom: "reset",
- });
- },
- },
- ],
- {
- tooltip_config: { placement: "bottom" },
- }
- );
-
- $("#phyloVizNavBtns").append(navMenu.$el);
- },
-});
-
-var SettingsMenu = UserMenuBase.extend({
- className: "Settings",
-
- initialize: function (options) {
- // settings needs to directly interact with the phyloviz model so it will get access to it.
- var self = this;
- self.phyloTree = options.phyloTree;
- self.el = $("#SettingsMenu");
- self.inputs = {
- separation: $("#phyloVizTreeSeparation"),
- leafHeight: $("#phyloVizTreeLeafHeight"),
- fontSize: $("#phyloVizTreeFontSize"),
- };
-
- //init all buttons of settings
- $("#settingsCloseBtn")
- .off()
- .on("click", () => {
- self.el.hide();
- });
- $("#phylovizResetSettingsBtn")
- .off()
- .on("click", () => {
- self.resetToDefaults();
- });
- $("#phylovizApplySettingsBtn")
- .off()
- .on("click", () => {
- self.apply();
- });
- },
-
- /**
- * Applying user values to phylotree model.
- */
- apply: function () {
- var self = this;
- if (
- !self.isAcceptableValue(self.inputs.separation, 50, 2500) ||
- !self.isAcceptableValue(self.inputs.leafHeight, 5, 30) ||
- !self.isAcceptableValue(self.inputs.fontSize, 5, 20)
- ) {
- return;
- }
- $.each(self.inputs, (key, $input) => {
- self.phyloTree.set(key, $input.val());
- });
- },
- /**
- * Called to update the values input to that stored in the model
- */
- updateUI: function () {
- var self = this;
- $.each(self.inputs, (key, $input) => {
- $input.val(self.phyloTree.get(key));
- });
- },
- /**
- * Resets the value of the phyloTree model to its default
- */
- resetToDefaults: function () {
- $(".tooltip").remove(); // just in case the tool tip was not removed
- var self = this;
- $.each(self.phyloTree.defaults, (key, value) => {
- self.phyloTree.set(key, value);
- });
- self.updateUI();
- },
-
- render: function () {},
-});
-
-/**
- * View for inspecting node properties and editing them
- */
-var NodeSelectionView = UserMenuBase.extend({
- className: "Settings",
-
- initialize: function (options) {
- var self = this;
- self.el = $("#nodeSelectionView");
- self.phyloTree = options.phyloTree;
-
- self.UI = {
- enableEdit: $("#phylovizEditNodesCheck"),
- saveChanges: $("#phylovizNodeSaveChanges"),
- cancelChanges: $("#phylovizNodeCancelChanges"),
- name: $("#phyloVizSelectedNodeName"),
- dist: $("#phyloVizSelectedNodeDist"),
- annotation: $("#phyloVizSelectedNodeAnnotation"),
- };
-
- // temporarily stores the values in case user change their mind
- self.valuesOfConcern = {
- name: null,
- dist: null,
- annotation: null,
- };
-
- //init UI buttons
- $("#nodeSelCloseBtn")
- .off()
- .on("click", () => {
- self.el.hide();
- });
- self.UI.saveChanges.off().on("click", () => {
- self.updateNodes();
- });
- self.UI.cancelChanges.off().on("click", () => {
- self.cancelChanges();
- });
-
- (($) => {
- // extending jquery fxn for enabling and disabling nodes.
- $.fn.enable = function (isEnabled) {
- return $(this).each(function () {
- if (isEnabled) {
- $(this).removeAttr("disabled");
- } else {
- $(this).attr("disabled", "disabled");
- }
- });
- };
- })($);
-
- self.UI.enableEdit.off().on("click", () => {
- self.toggleUI();
- });
- },
-
- /**
- * For turning on and off the child elements
- */
- toggleUI: function () {
- var self = this;
- var checked = self.UI.enableEdit.is(":checked");
-
- if (!checked) {
- self.cancelChanges();
- }
-
- $.each(self.valuesOfConcern, (key, value) => {
- self.UI[key].enable(checked);
- });
- if (checked) {
- self.UI.saveChanges.show();
- self.UI.cancelChanges.show();
- } else {
- self.UI.saveChanges.hide();
- self.UI.cancelChanges.hide();
- }
- },
-
- /**
- * Reverting to previous values in case user change their minds
- */
- cancelChanges: function () {
- var self = this;
- var node = self.phyloTree.get("selectedNode");
- if (node) {
- $.each(self.valuesOfConcern, (key, value) => {
- self.UI[key].val(node[key]);
- });
- }
- },
-
- /**
- * Changing the data in the underlying tree with user-specified values
- */
- updateNodes: function () {
- var self = this;
- var node = self.phyloTree.get("selectedNode");
- if (node) {
- if (
- !self.isAcceptableValue(self.UI.dist, 0, 1) ||
- self.hasIllegalJsonCharacters(self.UI.name) ||
- self.hasIllegalJsonCharacters(self.UI.annotation)
- ) {
- return;
- }
- $.each(self.valuesOfConcern, (key, value) => {
- node[key] = self.UI[key].val();
- });
- self.phyloTree.set("nodeAttrChangedTime", new Date());
- } else {
- alert("No node selected");
- }
- },
-});
-
-/**
- * Initializes the search panel on phyloviz and handles its user interaction
- * It allows user to search the entire free based on some qualifer, like dist <= val.
- */
-var PhyloVizSearch = UserMenuBase.extend({
- initialize: function () {
- var self = this;
-
- $("#phyloVizSearchBtn").on("click", () => {
- var searchTerm = $("#phyloVizSearchTerm");
-
- var searchConditionVal = $("#phyloVizSearchCondition").val().split("-");
-
- var attr = searchConditionVal[0];
- var condition = searchConditionVal[1];
- self.hasIllegalJsonCharacters(searchTerm);
-
- if (attr === "dist") {
- self.isAcceptableValue(searchTerm, 0, 1);
- }
- self.searchTree(attr, condition, searchTerm.val());
- });
- },
-
- /**
- * Searches the entire tree and will highlight the nodes that match the condition in green
- */
- searchTree: function (attr, condition, val) {
- d3.selectAll("g.node").classed("searchHighlight", (d) => {
- var attrVal = d[attr];
- if (typeof attrVal !== "undefined" && attrVal !== null) {
- if (attr === "dist") {
- switch (condition) {
- case "greaterEqual":
- return attrVal >= +val;
- case "lesserEqual":
- return attrVal <= +val;
- default:
- return;
- }
- } else if (attr === "name" || attr === "annotation") {
- return attrVal.toLowerCase().indexOf(val.toLowerCase()) !== -1;
- }
- }
- });
- },
-});
diff --git a/client/src/viz/sweepster.js b/client/src/viz/sweepster.js
deleted file mode 100644
index 175516d83f40..000000000000
--- a/client/src/viz/sweepster.js
+++ /dev/null
@@ -1,1033 +0,0 @@
-/**
- * Visualization and components for Sweepster, a visualization for exploring a tool's parameter space via
- * genomic visualization.
- */
-
-import Backbone from "backbone";
-import * as d3 from "d3v3";
-import $ from "jquery";
-import { hide_modal, show_modal } from "layout/modal";
-import { Dataset } from "mvc/dataset/data";
-import mod_icon_btn from "mvc/ui/icon-button";
-import { getAppRoot } from "onload/loadConfig";
-import { make_popupmenu } from "ui/popupmenu";
-import _ from "underscore";
-import config from "utils/config";
-import _l from "utils/localization";
-import tools from "viz/tools";
-import tracks from "viz/trackster/tracks";
-import visualization from "viz/visualization";
-
-/**
- * A collection of tool input settings. Object is useful for keeping a list of settings
- * for future use without changing the input's value and for preserving inputs order.
- */
-var ToolInputsSettings = Backbone.Model.extend({
- defaults: {
- inputs: null,
- values: null,
- },
-});
-
-/**
- * Tree for a tool's parameters.
- */
-var ToolParameterTree = Backbone.Model.extend({
- defaults: {
- tool: null,
- tree_data: null,
- },
-
- initialize: function (options) {
- // Set up tool parameters to work with tree.
- var self = this;
- this.get("tool")
- .get("inputs")
- .each((input) => {
- // Listen for changes to input's attributes.
- input.on(
- "change:min change:max change:num_samples",
- (input) => {
- if (input.get("in_ptree")) {
- self.set_tree_data();
- }
- },
- self
- );
- input.on(
- "change:in_ptree",
- (input) => {
- if (input.get("in_ptree")) {
- self.add_param(input);
- } else {
- self.remove_param(input);
- }
- self.set_tree_data();
- },
- self
- );
- });
-
- // If there is a config, use it.
- if (options.config) {
- _.each(options.config, (input_config) => {
- var input = self
- .get("tool")
- .get("inputs")
- .find((input) => input.get("name") === input_config.name);
- self.add_param(input);
- input.set(input_config);
- });
- }
- },
-
- add_param: function (param) {
- // If parameter already present, do not add it.
- if (param.get("ptree_index")) {
- return;
- }
-
- param.set("in_ptree", true);
- param.set("ptree_index", this.get_tree_params().length);
- },
-
- remove_param: function (param) {
- // Remove param from tree.
- param.set("in_ptree", false);
- param.set("ptree_index", null);
-
- // Update ptree indices for remaining params.
- _(this.get_tree_params()).each((input, index) => {
- // +1 to use 1-based indexing.
- input.set("ptree_index", index + 1);
- });
- },
-
- /**
- * Sets tree data using tool's inputs.
- */
- set_tree_data: function () {
- // Get samples for each parameter.
- var params_samples = _.map(this.get_tree_params(), (param) => ({
- param: param,
- samples: param.get_samples(),
- }));
- var node_id = 0;
-
- var // Creates tree data recursively.
- create_tree_data = (params_samples, index) => {
- var param_samples = params_samples[index];
- var param = param_samples.param;
- var settings = param_samples.samples;
-
- // Create leaves when last parameter setting is reached.
- if (params_samples.length - 1 === index) {
- return _.map(settings, (setting) => ({
- id: node_id++,
- name: setting,
- param: param,
- value: setting,
- }));
- }
-
- // Recurse to handle other parameters.
- return _.map(settings, (setting) => ({
- id: node_id++,
- name: setting,
- param: param,
- value: setting,
- children: create_tree_data(params_samples, index + 1),
- }));
- };
-
- this.set("tree_data", {
- name: "Root",
- id: node_id++,
- children: params_samples.length !== 0 ? create_tree_data(params_samples, 0) : null,
- });
- },
-
- get_tree_params: function () {
- // Filter and sort parameters to get list in tree.
- return _(this.get("tool").get("inputs").where({ in_ptree: true })).sortBy((input) => input.get("ptree_index"));
- },
-
- /**
- * Returns number of leaves in tree.
- */
- get_num_leaves: function () {
- return this.get_tree_params().reduce((memo, param) => memo * param.get_samples().length, 1);
- },
-
- /**
- * Returns array of ToolInputsSettings objects based on a node and its subtree.
- */
- get_node_settings: function (target_node) {
- // -- Get fixed settings from tool and parent nodes.
-
- // Start with tool's settings.
- var fixed_settings = this.get("tool").get_inputs_dict();
-
- // Get fixed settings using node's parents.
- var cur_node = target_node.parent;
- if (cur_node) {
- while (cur_node.depth !== 0) {
- fixed_settings[cur_node.param.get("name")] = cur_node.value;
- cur_node = cur_node.parent;
- }
- }
-
- // Walk subtree starting at clicked node to get full list of settings.
- var self = this;
-
- var get_settings = (node, settings) => {
- // Add setting for this node. Root node does not have a param,
- // however.
- if (node.param) {
- settings[node.param.get("name")] = node.value;
- }
-
- if (!node.children) {
- // At leaf node, so return settings.
- return new ToolInputsSettings({
- inputs: self.get("tool").get("inputs"),
- values: settings,
- });
- } else {
- // At interior node: return list of subtree settings.
- return _.flatten(_.map(node.children, (c) => get_settings(c, _.clone(settings))));
- }
- };
-
- var all_settings = get_settings(target_node, fixed_settings);
-
- // If user clicked on leaf, settings is a single dict. Convert to array for simplicity.
- if (!_.isArray(all_settings)) {
- all_settings = [all_settings];
- }
-
- return all_settings;
- },
-
- /**
- * Returns all nodes connected a particular node; this includes parents and children of the node.
- */
- get_connected_nodes: function (node) {
- var get_subtree_nodes = (a_node) => {
- if (!a_node.children) {
- return a_node;
- } else {
- // At interior node: return subtree nodes.
- return _.flatten([a_node, _.map(a_node.children, (c) => get_subtree_nodes(c))]);
- }
- };
-
- // Get node's parents.
- var parents = [];
-
- var cur_parent = node.parent;
- while (cur_parent) {
- parents.push(cur_parent);
- cur_parent = cur_parent.parent;
- }
-
- return _.flatten([parents, get_subtree_nodes(node)]);
- },
-
- /**
- * Returns the leaf that corresponds to a settings collection.
- */
- get_leaf: function (settings) {
- var cur_node = this.get("tree_data");
-
- var find_child = (children) => _.find(children, (child) => settings[child.param.get("name")] === child.value);
-
- while (cur_node.children) {
- cur_node = find_child(cur_node.children);
- }
- return cur_node;
- },
-
- /**
- * Returns a list of parameters used in tree.
- */
- toJSON: function () {
- // FIXME: returning and jsonifying complete param causes trouble on the server side,
- // so just use essential attributes for now.
- return this.get_tree_params().map((param) => ({
- name: param.get("name"),
- min: param.get("min"),
- max: param.get("max"),
- num_samples: param.get("num_samples"),
- }));
- },
-});
-
-var SweepsterTrack = Backbone.Model.extend({
- defaults: {
- track: null,
- mode: "Pack",
- settings: null,
- regions: null,
- },
-
- initialize: function (options) {
- this.set("regions", options.regions);
- if (options.track) {
- // FIXME: find a better way to deal with needed URLs:
- var track_config = _.extend(
- {
- data_url: `${getAppRoot()}dummy1`,
- converted_datasets_state_url: `${getAppRoot()}dummy2`,
- },
- options.track
- );
- this.set("track", tracks.object_from_template(track_config, {}, null));
- }
- },
-
- same_settings: function (a_track) {
- var this_settings = this.get("settings");
- var other_settings = a_track.get("settings");
- for (var prop in this_settings) {
- if (!other_settings[prop] || this_settings[prop] !== other_settings[prop]) {
- return false;
- }
- }
- return true;
- },
-
- toJSON: function () {
- return {
- track: this.get("track").to_dict(),
- settings: this.get("settings"),
- regions: this.get("regions"),
- };
- },
-});
-
-var TrackCollection = Backbone.Collection.extend({
- model: SweepsterTrack,
-});
-
-/**
- * Sweepster visualization model.
- */
-export var SweepsterVisualization = visualization.Visualization.extend({
- defaults: _.extend({}, visualization.Visualization.prototype.defaults, {
- dataset: null,
- tool: null,
- parameter_tree: null,
- regions: null,
- tracks: null,
- default_mode: "Pack",
- }),
-
- initialize: function (options) {
- this.set("dataset", new Dataset(options.dataset));
- this.set("tool", new tools.Tool(options.tool));
- this.set("regions", new visualization.GenomeRegionCollection(options.regions));
- this.set("tracks", new TrackCollection(options.tracks));
-
- var tool_with_samplable_inputs = this.get("tool");
- this.set("tool_with_samplable_inputs", tool_with_samplable_inputs);
- // Remove complex parameters for now.
- tool_with_samplable_inputs.remove_inputs(["data", "hidden_data", "conditional", "text"]);
-
- this.set(
- "parameter_tree",
- new ToolParameterTree({
- tool: tool_with_samplable_inputs,
- config: options.tree_config,
- })
- );
- },
-
- add_track: function (track) {
- this.get("tracks").add(track);
- },
-
- toJSON: function () {
- return {
- id: this.get("id"),
- title: `Parameter exploration for dataset '${this.get("dataset").get("name")}'`,
- type: "sweepster",
- dataset_id: this.get("dataset").id,
- tool_id: this.get("tool").id,
- regions: this.get("regions").toJSON(),
- tree_config: this.get("parameter_tree").toJSON(),
- tracks: this.get("tracks").toJSON(),
- };
- },
-});
-
-/**
- * --- Views ---
- */
-
-/**
- * Sweepster track view.
- */
-var SweepsterTrackView = Backbone.View.extend({
- tagName: "tr",
-
- TILE_LEN: 250,
-
- initialize: function (options) {
- this.canvas_manager = options.canvas_manager;
- this.render();
- this.model.on("change:track change:mode", this.draw_tiles, this);
- },
-
- render: function () {
- // Render settings icon and popup.
- // TODO: use template.
- var settings = this.model.get("settings");
-
- var values = settings.get("values");
-
- var settings_td = $("