PageRenderTime 52ms CodeModel.GetById 20ms RepoModel.GetById 1ms app.codeStats 0ms

/client/galaxy/scripts/mvc/history/history-structure-view.js

https://bitbucket.org/remy_d1/galaxy-central-manageapi
JavaScript | 585 lines | 374 code | 80 blank | 131 comment | 11 complexity | 780c39e49f700bc5dc4606d6793812e7 MD5 | raw file
Possible License(s): CC-BY-3.0
  1. define([
  2. 'mvc/history/job-dag',
  3. 'mvc/job/job-model',
  4. 'mvc/job/job-li',
  5. 'mvc/history/history-content-model',
  6. 'mvc/dataset/dataset-li',
  7. 'mvc/base-mvc',
  8. 'utils/localization',
  9. 'libs/d3'
  10. ], function( JobDAG, JOB, JOB_LI, HISTORY_CONTENT, DATASET_LI, BASE_MVC, _l ){
  11. // ============================================================================
  12. /*
  13. TODO:
  14. disruptive:
  15. handle collections
  16. retain contents to job relationships (out/input name)
  17. display when *only* copied datasets
  18. need to change when/how joblessVertices are created
  19. components should be full height containers that scroll individually
  20. use history contents views for job outputCollection, not vanilla datasets
  21. need hid
  22. show datasets when job not expanded
  23. make them external to the job display
  24. connect jobs by dataset
  25. which datasets from job X are which inputs in job Y?
  26. make job data human readable (needs tool data)
  27. show only tool.inputs with labels (w/ job.params as values)
  28. input datasets are special
  29. they don't appear in job.params
  30. have to connect to datasets in the dag
  31. connect job.inputs to any tool.inputs by tool.input.name (in params)
  32. API: seems like this could be handled there - duplicating the input data in the proper param space
  33. collections
  34. use cases:
  35. operations by thread:
  36. copy to new history
  37. rerun
  38. to workflow
  39. operations by branch (all descendants):
  40. copy to new history
  41. rerun
  42. to workflow
  43. signal to noise:
  44. collapse/expand branch
  45. hide jobs
  46. visually isolate branch (hide other jobs) of thread
  47. zoom (somehow)
  48. layout changes:
  49. move branch to new column in component
  50. complicated
  51. pyramid
  52. circular
  53. sources on inner radius
  54. expansion in vertical:
  55. obscures relations due to height
  56. could move details to side panel
  57. difficult to compare two+ jobs/datasets when at different points in the topo
  58. (other) controls:
  59. (optionally) filter all deleted
  60. (optionally) filter all hidden
  61. //(optionally) filter __SET_METADATA__
  62. //(optionally) filter error'd jobs
  63. help and explanation
  64. filtering/searching of jobs
  65. challenges:
  66. difficult to scale dom (for zoomout)
  67. possible to use css transforms?
  68. transform svg and dom elements
  69. it is possible to use css transforms on svg nodes
  70. use transform-origin to select origin to top left
  71. on larger histories the svg section may become extremely large due to distance from output to input
  72. how-to:
  73. descendant ids: _.keys( component.depth/breadthFirstSearchTree( start ).vertices )
  74. in-panel view of anc desc
  75. */
  76. // ============================================================================
  77. /**
  78. *
  79. */
  80. window.JobDAG = JobDAG;
  81. var HistoryStructureComponent = Backbone.View.extend( BASE_MVC.LoggableMixin ).extend({
  82. //logger : console,
  83. className : 'history-structure-component',
  84. _INITIAL_ZOOM_LEVEL : 1.0,
  85. _MIN_ZOOM_LEVEL : 0.25,
  86. _LINK_ID_SEP : '-to-',
  87. _VERTEX_NAME_DATA_KEY : 'vertex-name',
  88. JobItemClass : JOB_LI.JobListItemView,
  89. ContentItemClass : DATASET_LI.DatasetListItemView,
  90. initialize : function( attributes ){
  91. this.log( this + '(HistoryStructureComponent).initialize:', attributes );
  92. this.component = attributes.component;
  93. this._liMap = {};
  94. this._createVertexItems();
  95. this.zoomLevel = attributes.zoomLevel || this._INITIAL_ZOOM_LEVEL;
  96. this.layout = this._createLayout( attributes.layoutOptions );
  97. },
  98. _createVertexItems : function(){
  99. var view = this;
  100. view.component.eachVertex( function( vertex ){
  101. //TODO: hack
  102. var type = vertex.data.job? 'job' : 'copy',
  103. li;
  104. if( type === 'job' ){
  105. li = view._createJobListItem( vertex );
  106. } else if( type === 'copy' ){
  107. li = view._createContentListItem( vertex );
  108. }
  109. view._liMap[ vertex.name ] = li;
  110. });
  111. view.debug( '_liMap:', view._liMap );
  112. },
  113. _createJobListItem : function( vertex ){
  114. this.debug( '_createJobListItem:', vertex );
  115. var view = this,
  116. jobData = vertex.data,
  117. job = new JOB.Job( jobData.job );
  118. // get the models of the outputs for this job from the history
  119. var outputModels = _.map( job.get( 'outputs' ), function( output ){
  120. //note: output is { src: 'hda/dataset_collection', id: <some id> }
  121. // job output doesn't *quite* match up to normal typeId
  122. var type = output.src === 'hda'? 'dataset' : 'dataset_collection',
  123. typeId = HISTORY_CONTENT.typeIdStr( type, output.id );
  124. return view.model.contents.get( typeId );
  125. });
  126. // set the collection (HistoryContents) for the job to that json (setting historyId for proper ajax urls)
  127. job.outputCollection.reset( outputModels );
  128. job.outputCollection.historyId = view.model.id;
  129. //this.debug( job.outputCollection );
  130. // create the bbone view for the job (to be positioned later accrd. to the layout) and cache
  131. var li = new view.JobItemClass({ model: job, tool: jobData.tool, jobData: jobData });
  132. li.on( 'expanding expanded collapsing collapsed', view.renderGraph, view );
  133. li.foldout.on( 'view:expanding view:expanded view:collapsing view:collapsed', view.renderGraph, view );
  134. return li;
  135. },
  136. _createContentListItem : function( vertex ){
  137. this.debug( '_createContentListItem:', vertex );
  138. var view = this,
  139. content = vertex.data,
  140. typeId = HISTORY_CONTENT.typeIdStr( content.history_content_type, content.id );
  141. content = view.model.contents.get( typeId );
  142. var li = new view.ContentItemClass({ model: content });
  143. li.on( 'expanding expanded collapsing collapsed', view.renderGraph, view );
  144. return li;
  145. },
  146. layoutDefaults : {
  147. linkSpacing : 16,
  148. linkWidth : 0,
  149. linkHeight : 0,
  150. jobWidth : 300,
  151. jobHeight : 300,
  152. jobSpacing : 12,
  153. linkAdjX : 4,
  154. linkAdjY : 0
  155. },
  156. _createLayout : function( options ){
  157. options = _.defaults( _.clone( options || {} ), this.layoutDefaults );
  158. var view = this,
  159. vertices = _.values( view.component.vertices ),
  160. layout = _.extend( options, {
  161. nodeMap : {},
  162. links : [],
  163. svg : { width: 0, height: 0 }
  164. });
  165. vertices.forEach( function( v, j ){
  166. var node = { name: v.name, x: 0, y: 0 };
  167. layout.nodeMap[ v.name ] = node;
  168. });
  169. view.component.edges( function( e ){
  170. var link = {
  171. source: e.source,
  172. target: e.target
  173. };
  174. layout.links.push( link );
  175. });
  176. //this.debug( JSON.stringify( layout, null, ' ' ) );
  177. return layout;
  178. },
  179. render : function( options ){
  180. this.debug( this + '.render:', options );
  181. var view = this;
  182. view.$el.html([
  183. '<header></header>',
  184. '<nav class="controls"></nav>',
  185. '<figure class="graph"></figure>',
  186. '<footer></footer>'
  187. ].join( '' ) );
  188. var $graph = view.$graph();
  189. view.component.eachVertex( function( vertex ){
  190. view._liMap[ vertex.name ].render( 0 ).$el.appendTo( $graph )
  191. // store the name in the DOM and cache by that name
  192. .data( view._VERTEX_NAME_DATA_KEY, vertex.name );
  193. });
  194. view.renderGraph();
  195. return this;
  196. },
  197. $graph : function(){
  198. return this.$( '.graph' );
  199. },
  200. renderGraph : function( options ){
  201. this.debug( this + '.renderGraph:', options );
  202. var view = this;
  203. function _render(){
  204. view._updateLayout();
  205. // set up the display containers
  206. view.$graph()
  207. // use css3 transform to scale component graph
  208. .css( 'transform', [ 'scale(', view.zoomLevel, ',', view.zoomLevel, ')' ].join( '' ) )
  209. .width( view.layout.svg.width )
  210. .height( view.layout.svg.height );
  211. view.renderSVG();
  212. // position the job views accrd. to the layout
  213. view.component.eachVertex( function( v ){
  214. //TODO:?? liMap needed - can't we attach to vertex?
  215. var li = view._liMap[ v.name ],
  216. position = view.layout.nodeMap[ v.name ];
  217. //this.debug( position );
  218. li.$el.css({ top: position.y, left: position.x });
  219. });
  220. }
  221. //TODO: hack - li's invisible in updateLayout without this delay
  222. if( !this.$el.is( ':visible' ) ){
  223. _.delay( _render, 0 );
  224. } else {
  225. _render();
  226. }
  227. return this;
  228. },
  229. _updateLayout : function(){
  230. this.debug( this + '._updateLayout:' );
  231. var view = this,
  232. layout = view.layout;
  233. layout.linkHeight = layout.linkSpacing * _.size( layout.nodeMap );
  234. layout.svg.height = layout.linkHeight + layout.jobHeight;
  235. // reset for later max comparison
  236. layout.svg.width = 0;
  237. //TODO:?? can't we just alter the component v and e's directly?
  238. // layout the job views putting jobSpacing btwn each
  239. var x = 0,
  240. y = layout.linkHeight;
  241. _.each( layout.nodeMap, function( node, jobId ){
  242. //this.debug( node, jobId );
  243. node.x = x;
  244. node.y = y;
  245. x += layout.jobWidth + layout.jobSpacing;
  246. });
  247. layout.svg.width = layout.linkWidth = Math.max( layout.svg.width, x );
  248. // layout the links - connecting each job by it's main coords (currently)
  249. //TODO: somehow adjust the svg height based on the largest distance the longest connection needs
  250. layout.links.forEach( function( link ){
  251. var source = layout.nodeMap[ link.source ],
  252. target = layout.nodeMap[ link.target ];
  253. link.x1 = source.x + layout.linkAdjX;
  254. link.y1 = source.y + layout.linkAdjY;
  255. link.x2 = target.x + layout.linkAdjX;
  256. link.y2 = target.y + layout.linkAdjY;
  257. });
  258. this.debug( JSON.stringify( layout, null, ' ' ) );
  259. return this.layout;
  260. },
  261. renderSVG : function(){
  262. this.debug( this + '.renderSVG:' );
  263. var view = this,
  264. layout = view.layout;
  265. var svg = d3.select( this.$graph().get(0) ).select( 'svg' );
  266. if( svg.empty() ){
  267. svg = d3.select( this.$graph().get(0) ).append( 'svg' );
  268. }
  269. svg
  270. .attr( 'width', layout.svg.width )
  271. .attr( 'height', layout.svg.height );
  272. function highlightConnect( d ){
  273. d3.select( this ).classed( 'highlighted', true );
  274. view._liMap[ d.source ].$el.addClass( 'highlighted' );
  275. view._liMap[ d.target ].$el.addClass( 'highlighted' );
  276. }
  277. function unhighlightConnect( d ){
  278. d3.select( this ).classed( 'highlighted', false );
  279. view._liMap[ d.source ].$el.removeClass( 'highlighted' );
  280. view._liMap[ d.target ].$el.removeClass( 'highlighted' );
  281. }
  282. var connections = svg.selectAll( '.connection' )
  283. .data( layout.links );
  284. connections
  285. .enter().append( 'path' )
  286. .attr( 'class', 'connection' )
  287. .attr( 'id', function( d ){ return [ d.source, d.target ].join( view._LINK_ID_SEP ); })
  288. .on( 'mouseover', highlightConnect )
  289. .on( 'mouseout', unhighlightConnect );
  290. connections
  291. .attr( 'd', function( d ){ return view._connectionPath( d ); });
  292. return svg.node();
  293. },
  294. _connectionPath : function( d ){
  295. var CURVE_X = 0,
  296. controlY = ( ( d.x2 - d.x1 ) / this.layout.svg.width ) * this.layout.linkHeight;
  297. return [
  298. 'M', d.x1, ',', d.y1, ' ',
  299. 'C',
  300. d.x1 + CURVE_X, ',', d.y1 - controlY, ' ',
  301. d.x2 - CURVE_X, ',', d.y2 - controlY, ' ',
  302. d.x2, ',', d.y2
  303. ].join( '' );
  304. },
  305. events : {
  306. 'mouseover .graph > .list-item' : function( ev ){ this.highlightConnected( ev.currentTarget, true ); },
  307. 'mouseout .graph > .list-item' : function( ev ){ this.highlightConnected( ev.currentTarget, false ); }
  308. },
  309. highlightConnected : function( jobElement, highlight ){
  310. this.debug( 'highlightConnected', jobElement, highlight );
  311. highlight = highlight !== undefined? highlight : true;
  312. var view = this,
  313. component = view.component,
  314. jobClassFn = highlight? jQuery.prototype.addClass : jQuery.prototype.removeClass,
  315. connectionClass = highlight? 'connection highlighted' : 'connection';
  316. //console.debug( 'mouseover', this );
  317. var $hoverTarget = jobClassFn.call( $( jobElement ), 'highlighted' ),
  318. id = $hoverTarget.data( view._VERTEX_NAME_DATA_KEY );
  319. // immed. ancestors
  320. component.edges({ target: id }).forEach( function( edge ){
  321. var ancestorId = edge.source,
  322. ancestorLi = view._liMap[ ancestorId ];
  323. //view.debug( '\t ancestor:', ancestorId, ancestorLi );
  324. jobClassFn.call( ancestorLi.$el, 'highlighted' );
  325. view.$( '#' + ancestorId + view._LINK_ID_SEP + id ).attr( 'class', connectionClass );
  326. });
  327. // descendants
  328. component.vertices[ id ].eachEdge( function( edge ){
  329. var descendantId = edge.target,
  330. descendantLi = view._liMap[ descendantId ];
  331. //view.debug( '\t descendant:', descendantId, descendantLi );
  332. jobClassFn.call( descendantLi.$el, 'highlighted' );
  333. view.$( '#' + id + view._LINK_ID_SEP + descendantId ).attr( 'class', connectionClass );
  334. });
  335. },
  336. zoom : function( level ){
  337. this.zoomLevel = Math.min( 1.0, Math.max( this._MIN_ZOOM_LEVEL, level ) );
  338. return this.renderGraph();
  339. },
  340. toString : function(){
  341. return 'HistoryStructureComponent(' + this.model.id + ')';
  342. }
  343. });
  344. // ============================================================================
  345. /**
  346. *
  347. */
  348. var VerticalHistoryStructureComponent = HistoryStructureComponent.extend({
  349. //logger : console,
  350. className : HistoryStructureComponent.prototype.className + ' vertical',
  351. layoutDefaults : _.extend( _.clone( HistoryStructureComponent.prototype.layoutDefaults ), {
  352. linkAdjX : 0,
  353. linkAdjY : 4
  354. }),
  355. //TODO: how can we use the dom height of the job li's - they're not visible when this is called?
  356. _updateLayout : function(){
  357. this.debug( this + '._updateLayout:' );
  358. var view = this,
  359. layout = view.layout;
  360. //this.info( this.cid, '_updateLayout' )
  361. layout.linkWidth = layout.linkSpacing * _.size( layout.nodeMap );
  362. layout.svg.width = layout.linkWidth + layout.jobWidth;
  363. // reset height - we'll get the max Y below to assign to it
  364. layout.svg.height = 0;
  365. //TODO:?? can't we just alter the component v and e's directly?
  366. var x = layout.linkWidth,
  367. y = 0;
  368. _.each( layout.nodeMap, function( node, nodeId ){
  369. node.x = x;
  370. node.y = y;
  371. var li = view._liMap[ nodeId ];
  372. y += li.$el.height() + layout.jobSpacing;
  373. });
  374. layout.linkHeight = layout.svg.height = Math.max( layout.svg.height, y );
  375. // layout the links - connecting each job by it's main coords (currently)
  376. layout.links.forEach( function( link ){
  377. var source = layout.nodeMap[ link.source ],
  378. target = layout.nodeMap[ link.target ];
  379. link.x1 = source.x + layout.linkAdjX;
  380. link.y1 = source.y + layout.linkAdjY;
  381. link.x2 = target.x + layout.linkAdjX;
  382. link.y2 = target.y + layout.linkAdjY;
  383. //view.debug( 'link:', link.x1, link.y1, link.x2, link.y2, link );
  384. });
  385. this.debug( JSON.stringify( layout, null, ' ' ) );
  386. return layout;
  387. },
  388. _connectionPath : function( d ){
  389. var CURVE_Y = 0,
  390. controlX = ( ( d.y2 - d.y1 ) / this.layout.svg.height ) * this.layout.linkWidth;
  391. return [
  392. 'M', d.x1, ',', d.y1, ' ',
  393. 'C',
  394. d.x1 - controlX, ',', d.y1 + CURVE_Y, ' ',
  395. d.x2 - controlX, ',', d.y2 - CURVE_Y, ' ',
  396. d.x2, ',', d.y2
  397. ].join( '' );
  398. },
  399. toString : function(){
  400. return 'VerticalHistoryStructureComponent(' + this.model.id + ')';
  401. }
  402. });
  403. // ============================================================================
  404. /**
  405. *
  406. */
  407. var HistoryStructureView = Backbone.View.extend( BASE_MVC.LoggableMixin ).extend({
  408. //logger : console,
  409. className : 'history-structure',
  410. _layoutToComponentClass : {
  411. 'horizontal' : HistoryStructureComponent,
  412. 'vertical' : VerticalHistoryStructureComponent
  413. },
  414. //_DEFAULT_LAYOUT : 'horizontal',
  415. _DEFAULT_LAYOUT : 'vertical',
  416. initialize : function( attributes ){
  417. this.layout = _.contains( attributes.layout, _.keys( this._layoutToComponentClass ) )?
  418. attributes.layout : this._DEFAULT_LAYOUT;
  419. this.log( this + '(HistoryStructureView).initialize:', attributes, this.model );
  420. //TODO:?? to model - maybe glom jobs onto model in order to persist
  421. // cache jobs since we need to re-create the DAG if settings change
  422. this._processTools( attributes.tools );
  423. this._processJobs( attributes.jobs );
  424. this._createDAG();
  425. },
  426. _processTools : function( tools ){
  427. this.tools = tools || {};
  428. return this.tools;
  429. },
  430. _processJobs : function( jobs ){
  431. this.jobs = jobs || [];
  432. return this.jobs;
  433. },
  434. _createDAG : function(){
  435. this.dag = new JobDAG({
  436. historyContents : this.model.contents.toJSON(),
  437. tools : this.tools,
  438. jobs : this.jobs,
  439. excludeSetMetadata : true,
  440. excludeErroredJobs : true
  441. });
  442. this.debug( this + '.dag:', this.dag );
  443. this._createComponents();
  444. },
  445. _createComponents : function(){
  446. this.log( this + '._createComponents' );
  447. var structure = this;
  448. structure.componentViews = structure.dag.weakComponentGraphArray().map( function( componentGraph ){
  449. return structure._createComponent( componentGraph );
  450. });
  451. return structure.componentViews;
  452. },
  453. _createComponent : function( component ){
  454. this.log( this + '._createComponent:', component );
  455. var ComponentClass = this._layoutToComponentClass[ this.layout ];
  456. return new ComponentClass({
  457. model : this.model,
  458. component : component
  459. });
  460. },
  461. render : function( options ){
  462. this.log( this + '.render:', options );
  463. var structure = this;
  464. structure.$el.addClass( 'clear' ).html([
  465. '<div class="controls"></div>',
  466. '<div class="components"></div>'
  467. ].join( '' ));
  468. structure.componentViews.forEach( function( component ){
  469. component.render().$el.appendTo( structure.$components() );
  470. });
  471. return structure;
  472. },
  473. $components : function(){
  474. return this.$( '.components' );
  475. },
  476. changeLayout : function( layout ){
  477. if( !( layout in this._layoutToComponentClass ) ){
  478. throw new Error( this + ': unknown layout: ' + layout );
  479. }
  480. this.layout = layout;
  481. this._createComponents();
  482. return this.render();
  483. },
  484. toString : function(){
  485. return 'HistoryStructureView(' + this.model.id + ')';
  486. }
  487. });
  488. // ============================================================================
  489. return HistoryStructureView;
  490. });