sequenceserver 3.1.3 → 3.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/lib/sequenceserver/api_errors.rb +18 -0
- data/lib/sequenceserver/blast/hit.rb +1 -2
- data/lib/sequenceserver/blast/job.rb +15 -5
- data/lib/sequenceserver/blast/report.rb +5 -1
- data/lib/sequenceserver/database.rb +5 -1
- data/lib/sequenceserver/job.rb +26 -10
- data/lib/sequenceserver/makeblastdb.rb +2 -1
- data/lib/sequenceserver/routes.rb +37 -12
- data/lib/sequenceserver/version.rb +1 -1
- data/lib/sequenceserver/zip_file_generator.rb +1 -1
- data/public/404.html +8 -16
- data/public/css/app.css +794 -111
- data/public/css/app.min.css +1 -1
- data/public/css/sequenceserver.min.css +1 -8
- data/public/js/circos.js +103 -96
- data/public/{packages/circosJS@1.7.0.js → js/circosjs.js} +156 -778
- data/public/js/cloud_share_modal.js +55 -58
- data/public/js/collapse_preferences.js +13 -15
- data/public/js/databases.js +4 -3
- data/public/js/databases_tree.js +51 -30
- data/public/js/error_modal.js +35 -22
- data/public/js/exporter.js +2 -0
- data/public/js/form.js +43 -7
- data/public/js/grapher.js +94 -92
- data/public/js/hit.js +21 -19
- data/public/js/hits.js +45 -38
- data/public/js/hits_overview.js +8 -27
- data/public/js/hsp.js +140 -144
- data/public/js/jquery_world.js +24 -15
- data/public/js/kablammo.js +14 -10
- data/public/js/length_distribution.js +23 -37
- data/public/js/null_plugins/report_plugins.js +3 -5
- data/public/js/options.js +17 -19
- data/public/js/query.js +36 -35
- data/public/js/report/alignment_results.js +18 -0
- data/public/js/report/graphical_overview.js +38 -0
- data/public/js/report/report_header.js +28 -0
- data/public/js/report/run_summary.js +43 -0
- data/public/js/report.js +62 -102
- data/public/js/report_root.js +3 -3
- data/public/js/search.js +5 -2
- data/public/js/search_button.js +85 -78
- data/public/js/sequence.js +11 -22
- data/public/js/sequence_modal.js +38 -37
- data/public/js/share_url.js +5 -5
- data/public/js/sidebar.js +148 -86
- data/public/js/svgExporter.js +8 -3
- data/public/js/tests/form.spec.js +1 -1
- data/public/js/tests/visualisation_helpers.spec.js +28 -0
- data/public/js/visualisation_helpers.js +24 -27
- data/public/sequenceserver-report.min.js +1 -1
- data/public/sequenceserver-report.min.js.LICENSE.txt +1 -88
- data/public/sequenceserver-report.min.js.map +1 -1
- data/public/sequenceserver-search.min.js +1 -1
- data/public/sequenceserver-search.min.js.LICENSE.txt +0 -30
- data/public/sequenceserver-search.min.js.map +1 -1
- data/views/error.erb +1 -1
- data/views/layout.erb +123 -130
- data/views/report.erb +3 -2
- data/views/search.erb +10 -14
- metadata +13 -26
- data/public/css/fonts.css +0 -31
- data/public/css/grapher.css +0 -670
- data/public/css/sequenceserver.css +0 -509
- data/public/vendor/github/mbostock/d3@3.5.6/d3.js +0 -9506
- data/public/vendor/github/mbostock/d3@3.5.6.js +0 -1
- data/views/search_layout.erb +0 -153
data/public/js/sequence_modal.js
CHANGED
|
@@ -13,6 +13,7 @@ export default class SequenceModal extends React.Component {
|
|
|
13
13
|
error_msgs: [],
|
|
14
14
|
sequences: [],
|
|
15
15
|
requestCompleted: false,
|
|
16
|
+
isModalVisible: false,
|
|
16
17
|
};
|
|
17
18
|
this.modalRef = createRef();
|
|
18
19
|
}
|
|
@@ -20,44 +21,43 @@ export default class SequenceModal extends React.Component {
|
|
|
20
21
|
// Lifecycle methods. //
|
|
21
22
|
|
|
22
23
|
render() {
|
|
24
|
+
const { isModalVisible, requestCompleted } = this.state;
|
|
25
|
+
|
|
23
26
|
return (
|
|
24
|
-
<div className="
|
|
25
|
-
<
|
|
26
|
-
<div className="
|
|
27
|
-
<div className="
|
|
28
|
-
<h3
|
|
27
|
+
<div className="relative sequence-viewer-wrap">
|
|
28
|
+
<dialog ref={this.modalRef} className="sequence-viewer fixed p-4 w-full max-w-2xl bg-transparent focus:outline-none overflow-visible z-50">
|
|
29
|
+
<div className="relative flex flex-col rounded-lg bg-white shadow">
|
|
30
|
+
<div className="flex items-start justify-between rounded-t border-b p-5">
|
|
31
|
+
<h3 className="text-xl font-medium text-gray-900">
|
|
32
|
+
View sequence
|
|
33
|
+
</h3>
|
|
34
|
+
<button className="sequence-viewer-close ml-auto inline-flex items-center rounded-lg bg-transparent p-1.5 text-gray-400 hover:bg-gray-200" onClick={this.hide}>
|
|
35
|
+
<i className="fa-solid fa-xmark hover:text-black"></i>
|
|
36
|
+
</button>
|
|
37
|
+
</div>
|
|
38
|
+
<div className="sequence-viewer-content max-h-[80vh] overflow-y-scroll">
|
|
39
|
+
{(requestCompleted && this.resultsJSX()) || this.loadingJSX()}
|
|
29
40
|
</div>
|
|
30
|
-
|
|
31
|
-
{(this.state.requestCompleted && this.resultsJSX()) ||
|
|
32
|
-
this.loadingJSX()}
|
|
33
41
|
</div>
|
|
34
|
-
</
|
|
42
|
+
</dialog>
|
|
35
43
|
</div>
|
|
36
44
|
);
|
|
37
45
|
}
|
|
38
46
|
|
|
39
|
-
/*
|
|
40
|
-
* Returns jQuery reference to the main modal container.
|
|
41
|
-
*/
|
|
42
|
-
modal() {
|
|
43
|
-
return $(this.modalRef.current);
|
|
44
|
-
}
|
|
45
|
-
|
|
46
47
|
/**
|
|
47
48
|
* Shows sequence viewer.
|
|
48
49
|
*/
|
|
49
|
-
show(url) {
|
|
50
|
-
this.
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
});
|
|
50
|
+
show = (url) => {
|
|
51
|
+
this.modalRef.current?.showModal();
|
|
52
|
+
this.setState({ requestCompleted: false });
|
|
53
|
+
this.loadJSON(url);
|
|
54
54
|
}
|
|
55
55
|
|
|
56
56
|
/**
|
|
57
57
|
* Hide sequence viewer.
|
|
58
58
|
*/
|
|
59
|
-
hide() {
|
|
60
|
-
this.
|
|
59
|
+
hide = () => {
|
|
60
|
+
this.modalRef.current?.close();
|
|
61
61
|
}
|
|
62
62
|
|
|
63
63
|
/**
|
|
@@ -66,29 +66,30 @@ export default class SequenceModal extends React.Component {
|
|
|
66
66
|
async loadJSON(url) {
|
|
67
67
|
// Fetch sequence and update state.
|
|
68
68
|
try {
|
|
69
|
-
const response = await
|
|
69
|
+
const response = await fetch(url);
|
|
70
|
+
const data = await response.json();
|
|
70
71
|
this.setState({
|
|
71
|
-
sequences:
|
|
72
|
-
error_msgs:
|
|
72
|
+
sequences: data.sequences,
|
|
73
|
+
error_msgs: data.error_msgs,
|
|
73
74
|
requestCompleted: true,
|
|
74
75
|
});
|
|
75
76
|
} catch (error) {
|
|
76
77
|
console.log('Error fetching sequence:', error);
|
|
77
78
|
this.hide();
|
|
78
|
-
this.props.showErrorModal(error
|
|
79
|
+
this.props.showErrorModal(error);
|
|
79
80
|
}
|
|
80
81
|
}
|
|
81
82
|
|
|
82
83
|
resultsJSX() {
|
|
83
84
|
return (
|
|
84
|
-
<div className="
|
|
85
|
+
<div className="pt-2 px-6 pb-6 mt-2">
|
|
85
86
|
{this.state.error_msgs.map((error_msg, index) => (
|
|
86
87
|
<div key={`error-message-${index}`} className="fastan">
|
|
87
|
-
<div className="section-header">
|
|
88
|
-
<h4>{error_msg[0]}</h4>
|
|
88
|
+
<div className="section-header border-b border-seqorange pl-px table mb-0 w-full pb-2">
|
|
89
|
+
<h4 className="text-sm table-cell">{error_msg[0]}</h4>
|
|
89
90
|
</div>
|
|
90
|
-
<div className="
|
|
91
|
-
<pre className="pre-
|
|
91
|
+
<div className="pt-0 px-0 pb-px">
|
|
92
|
+
<pre className="m-0 p-0 rounded-none border-0 bg-inherit whitespace-pre-wrap break-keep">{error_msg[1]}</pre>
|
|
92
93
|
</div>
|
|
93
94
|
</div>
|
|
94
95
|
))}
|
|
@@ -101,7 +102,7 @@ export default class SequenceModal extends React.Component {
|
|
|
101
102
|
|
|
102
103
|
loadingJSX() {
|
|
103
104
|
return (
|
|
104
|
-
<div className="
|
|
105
|
+
<div className="my-4 text-center">
|
|
105
106
|
<i className="fa fa-spinner fa-3x fa-spin"></i>
|
|
106
107
|
</div>
|
|
107
108
|
);
|
|
@@ -122,13 +123,13 @@ class SequenceViewer extends React.Component {
|
|
|
122
123
|
|
|
123
124
|
return (
|
|
124
125
|
<div className="fastan">
|
|
125
|
-
<div className="section-header">
|
|
126
|
-
<h4>
|
|
126
|
+
<div className="section-header border-b border-seqorange pl-px table mb-0 w-full pb-2">
|
|
127
|
+
<h4 className="text-sm table-cell">
|
|
127
128
|
{this.props.sequence.id}
|
|
128
|
-
<small> {this.props.sequence.title}</small>
|
|
129
|
+
<small className="text-inherit"> {this.props.sequence.title}</small>
|
|
129
130
|
</h4>
|
|
130
131
|
</div>
|
|
131
|
-
<div className="
|
|
132
|
+
<div className="fastan-content relative pt-0 px-0 pb-px">
|
|
132
133
|
<div className={this.widgetClass} id={this.widgetID}></div>
|
|
133
134
|
</div>
|
|
134
135
|
</div>
|
data/public/js/share_url.js
CHANGED
|
@@ -10,14 +10,14 @@ const ShareURLComponent = ({ querydb, program, queryLength, url }) => {
|
|
|
10
10
|
};
|
|
11
11
|
|
|
12
12
|
return (
|
|
13
|
-
<div className="share-url-component">
|
|
14
|
-
<input name="shareableUrl" type="text" value={url} readOnly />
|
|
15
|
-
<div className="
|
|
16
|
-
<button className="
|
|
13
|
+
<div className="px-6 share-url-component">
|
|
14
|
+
<input className="w-full mb-3 pt-3" name="shareableUrl" type="text" value={url} readOnly />
|
|
15
|
+
<div className="py-4 flex justify-between items-center">
|
|
16
|
+
<button className="py-2 px-3 border border-transparent rounded-md shadow-sm text-white bg-seqblue hover:bg-seqorange focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-seqorange" onClick={copyToClipboard}>{copied ? 'Copied!' : 'Copy to Clipboard'}</button>
|
|
17
17
|
<a href={asMailtoHref(querydb, program, queryLength, url, true)}>Share via email</a>
|
|
18
18
|
</div>
|
|
19
19
|
</div>
|
|
20
20
|
);
|
|
21
21
|
};
|
|
22
22
|
|
|
23
|
-
export default ShareURLComponent;
|
|
23
|
+
export default ShareURLComponent;
|
data/public/js/sidebar.js
CHANGED
|
@@ -41,14 +41,6 @@ export default class extends Component {
|
|
|
41
41
|
}
|
|
42
42
|
|
|
43
43
|
componentDidMount() {
|
|
44
|
-
/**
|
|
45
|
-
* Fixes tooltips in the sidebar, allows tooltip display on click
|
|
46
|
-
*/
|
|
47
|
-
$(function () {
|
|
48
|
-
$('.sidebar [data-toggle="tooltip"]').tooltip({ placement: 'right' });
|
|
49
|
-
$('#copyURL').tooltip({ title: 'Copied!', trigger: 'click', placement: 'right', delay: 0 });
|
|
50
|
-
});
|
|
51
|
-
|
|
52
44
|
//keep track of the current queryIndex so it doesn't get lost on page reload
|
|
53
45
|
const urlMatch = window.location.href.match(/#Query_(\d+)/);
|
|
54
46
|
if (urlMatch && urlMatch.length > 1) {
|
|
@@ -181,25 +173,25 @@ export default class extends Component {
|
|
|
181
173
|
return false;
|
|
182
174
|
}
|
|
183
175
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
176
|
/**
|
|
188
177
|
* Handles copying the URL into the user's clipboard. Modified from: https://stackoverflow.com/a/49618964/18117380
|
|
189
178
|
* Hides the 'Copied!' tooltip after 3 seconds
|
|
190
179
|
*/
|
|
191
180
|
|
|
192
181
|
copyURL() {
|
|
193
|
-
|
|
194
|
-
|
|
182
|
+
const element = document.createElement('input');
|
|
183
|
+
const url = window.location.href;
|
|
195
184
|
document.body.appendChild(element);
|
|
196
185
|
element.value = url;
|
|
197
186
|
element.select();
|
|
198
187
|
document.execCommand('copy');
|
|
199
188
|
document.body.removeChild(element);
|
|
200
189
|
|
|
201
|
-
|
|
202
|
-
|
|
190
|
+
const tooltip = document.getElementById('tooltip');
|
|
191
|
+
tooltip.classList.remove('hidden');
|
|
192
|
+
|
|
193
|
+
setTimeout(() => {
|
|
194
|
+
tooltip.classList.add('hidden');
|
|
203
195
|
}, 3000);
|
|
204
196
|
}
|
|
205
197
|
|
|
@@ -216,20 +208,20 @@ export default class extends Component {
|
|
|
216
208
|
var rootURL = path.join('/');
|
|
217
209
|
return (
|
|
218
210
|
<div className="sidebar-top-panel">
|
|
219
|
-
<div className="
|
|
220
|
-
<h4>
|
|
211
|
+
<div className="pl-px table mb-0 w-full">
|
|
212
|
+
<h4 className="text-sm font-bold mb-0 mt-0.5">
|
|
221
213
|
{this.summaryString()}
|
|
222
214
|
</h4>
|
|
223
215
|
</div>
|
|
224
216
|
{this.props.data.queries.length > 12 && this.queryIndexButtons()}
|
|
225
217
|
<div>
|
|
226
|
-
<a href={`${rootURL}/?job_id=${job_id}`}>
|
|
218
|
+
<a href={`${rootURL}/?job_id=${job_id}`} className="text-sm text-seqblue hover:text-seqorange cursor-pointer">
|
|
227
219
|
<i className="fa fa-pencil"></i> Edit search
|
|
228
220
|
</a>
|
|
229
|
-
<span className="
|
|
221
|
+
<span className="text-seqorange px-1">|</span>
|
|
230
222
|
<a href={`${rootURL}/`}
|
|
231
|
-
onClick={this.clearSession}>
|
|
232
|
-
<i className="fa fa-file
|
|
223
|
+
onClick={this.clearSession} className="text-sm text-seqblue hover:text-seqorange cursor-pointer">
|
|
224
|
+
<i className="fa-regular fa-file"></i> New search
|
|
233
225
|
</a>
|
|
234
226
|
</div>
|
|
235
227
|
{this.props.shouldShowIndex && this.indexJSX()}
|
|
@@ -253,7 +245,7 @@ export default class extends Component {
|
|
|
253
245
|
const buttonStyle = {
|
|
254
246
|
outline: 'none', border: 'none', background: 'none'
|
|
255
247
|
};
|
|
256
|
-
const buttonClasses = '
|
|
248
|
+
const buttonClasses = 'text-sm text-seqblue hover:text-seqorange hover:bg-gray-200';
|
|
257
249
|
|
|
258
250
|
const handlePreviousBtnClick = () => this.handleQueryIndexChange(this.state.queryIndex - 1);
|
|
259
251
|
const handleNextBtnClick = () => this.handleQueryIndexChange(this.state.queryIndex + 1);
|
|
@@ -264,15 +256,15 @@ export default class extends Component {
|
|
|
264
256
|
);
|
|
265
257
|
return <div style={{ display: 'flex', width: '100%', margin: '7px 0' }}>
|
|
266
258
|
{this.state.queryIndex > 1 && <NavButton text="Previous Query" onClick={handlePreviousBtnClick} />}
|
|
267
|
-
{this.state.queryIndex > 1 && this.state.queryIndex < this.props.data.queries.length && <span className="
|
|
259
|
+
{this.state.queryIndex > 1 && this.state.queryIndex < this.props.data.queries.length && <span className="text-seqorange px-1">|</span>}
|
|
268
260
|
{this.state.queryIndex < this.props.data.queries.length && <NavButton onClick={handleNextBtnClick} text="Next Query" />}
|
|
269
261
|
</div>;
|
|
270
262
|
}
|
|
271
263
|
indexJSX() {
|
|
272
|
-
return <ul className="
|
|
264
|
+
return <ul className="w-full"> {
|
|
273
265
|
_.map(this.props.data.queries, (query) => {
|
|
274
266
|
return <li key={'Side_bar_' + query.id}>
|
|
275
|
-
<a className="
|
|
267
|
+
<a className="side-nav text-sm text-seqblue hover:text-seqorange focus:text-seqorange active:text-seqorange cursor-pointer hover-bold line-clamp-1 mb-1.5"
|
|
276
268
|
title={'Query= ' + query.id + ' ' + query.title}
|
|
277
269
|
href={'#Query_' + query.number}>
|
|
278
270
|
{'Query= ' + query.id}
|
|
@@ -286,77 +278,113 @@ export default class extends Component {
|
|
|
286
278
|
downloadsPanelJSX() {
|
|
287
279
|
return (
|
|
288
280
|
<div className="downloads">
|
|
289
|
-
<div className="
|
|
290
|
-
<h4>
|
|
281
|
+
<div className="pl-px table mb-0 w-full">
|
|
282
|
+
<h4 className="text-sm font-bold mb-0 mt-2.5">
|
|
291
283
|
Download FASTA, XML, TSV
|
|
292
284
|
</h4>
|
|
293
285
|
</div>
|
|
294
|
-
<ul
|
|
286
|
+
<ul>
|
|
295
287
|
{
|
|
296
|
-
!(this.props.data.imported_xml || this.props.data.non_parse_seqids) && <li>
|
|
297
|
-
<a
|
|
298
|
-
|
|
299
|
-
|
|
288
|
+
!(this.props.data.imported_xml || this.props.data.non_parse_seqids) && <li className="hover:bg-gray-200 mb-1">
|
|
289
|
+
<a
|
|
290
|
+
href="#"
|
|
291
|
+
className={`text-sm text-seqblue download-fasta-of-all hover:text-seqorange cursor-pointer py-0.5 px-0.5 ${!this.props.atLeastOneHit && 'disabled'}`}
|
|
292
|
+
onClick={this.props.atLeastOneHit ? this.downloadFastaOfAll : (e) => e.preventDefault()}>
|
|
293
|
+
FASTA of all hits
|
|
300
294
|
</a>
|
|
301
295
|
</li>
|
|
302
296
|
}
|
|
303
297
|
{
|
|
304
|
-
!(this.props.data.imported_xml || this.props.data.non_parse_seqids) && <li>
|
|
305
|
-
<a
|
|
298
|
+
!(this.props.data.imported_xml || this.props.data.non_parse_seqids) && <li className="mb-1">
|
|
299
|
+
<a
|
|
300
|
+
href="#"
|
|
301
|
+
className="flex text-sm download-fasta-of-selected text-seqblue disabled py-0.5 px-0.5"
|
|
306
302
|
onClick={this.downloadFastaOfSelected}>
|
|
307
|
-
FASTA of <span className="
|
|
303
|
+
FASTA of <span className="font-bold px-0.5"></span> selected hit(s)
|
|
308
304
|
</a>
|
|
309
305
|
</li>
|
|
310
306
|
}
|
|
311
|
-
<li>
|
|
312
|
-
<a href="#" className={`
|
|
307
|
+
<li className="hover:bg-gray-200 mb-1">
|
|
308
|
+
<a href="#" className={`text-sm text-seqblue download-alignment-of-all hover:text-seqorange cursor-pointer py-0.5 px-0.5 ${!this.props.atLeastOneHit && 'disabled'}`}>
|
|
313
309
|
Alignment of all hits
|
|
314
310
|
</a>
|
|
315
311
|
</li>
|
|
316
|
-
<li>
|
|
317
|
-
<a href="#" className="
|
|
318
|
-
Alignment of <span className="
|
|
312
|
+
<li className="mb-1">
|
|
313
|
+
<a href="#" className="flex text-sm download-alignment-of-selected text-seqblue disabled py-0.5 px-0.5">
|
|
314
|
+
Alignment of <span className="font-bold px-0.5"></span> selected hit(s)
|
|
319
315
|
</a>
|
|
320
316
|
</li>
|
|
321
317
|
{
|
|
322
|
-
!this.props.data.imported_xml && <li>
|
|
323
|
-
<a
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
318
|
+
!this.props.data.imported_xml && <li className="hover:bg-gray-200 mb-1">
|
|
319
|
+
<a href={'download/' + this.props.data.search_id + '.std_tsv'}>
|
|
320
|
+
<div className="relative flex flex-col items-center group">
|
|
321
|
+
<div className="flex items-center w-full">
|
|
322
|
+
<span className="w-full text-sm text-seqblue hover:text-seqorange download cursor-pointer py-0.5 px-0.5">Standard tabular report</span>
|
|
323
|
+
<div className="absolute hidden left-full ml-2 items-center group-hover:flex tooltip-wrap">
|
|
324
|
+
<div className="w-0 h-0 border-y-8 border-r-8 border-t-transparent border-b-transparent border-r-black -mr-px"></div>
|
|
325
|
+
<span className="relative z-10 p-2 side-tooltip-text leading-4 text-center text-white whitespace-no-wrap bg-black shadow-lg rounded-md">
|
|
326
|
+
15 columns: query and subject ID; scientific
|
|
327
|
+
name, alignment length, mismatches, gaps, identity,
|
|
328
|
+
start and end coordinates, e value, bitscore, query
|
|
329
|
+
coverage per subject and per HSP.
|
|
330
|
+
</span>
|
|
331
|
+
</div>
|
|
332
|
+
</div>
|
|
333
|
+
</div>
|
|
330
334
|
</a>
|
|
331
335
|
</li>
|
|
332
336
|
}
|
|
333
337
|
{
|
|
334
|
-
!this.props.data.imported_xml && <li>
|
|
335
|
-
<a
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
338
|
+
!this.props.data.imported_xml && <li className="hover:bg-gray-200 mb-1">
|
|
339
|
+
<a href={'download/' + this.props.data.search_id + '.full_tsv'}>
|
|
340
|
+
<div className="relative flex flex-col items-center group">
|
|
341
|
+
<div className="flex items-center w-full">
|
|
342
|
+
<span className="w-full text-sm text-seqblue hover:text-seqorange download cursor-pointer py-0.5 px-0.5">Full tabular report</span>
|
|
343
|
+
<div className="absolute hidden left-full ml-2 items-center group-hover:flex tooltip-wrap">
|
|
344
|
+
<div className="w-0 h-0 border-y-8 border-r-8 border-t-transparent border-b-transparent border-r-black -mr-px"></div>
|
|
345
|
+
<span className="relative z-10 p-2 side-tooltip-text leading-4 text-center text-white whitespace-no-wrap bg-black shadow-lg rounded-md">
|
|
346
|
+
44 columns: query and subject ID, GI,
|
|
347
|
+
accessions, and length; alignment details;
|
|
348
|
+
taxonomy details of subject sequence(s) and
|
|
349
|
+
query coverage per subject and per HSP.
|
|
350
|
+
</span>
|
|
351
|
+
</div>
|
|
352
|
+
</div>
|
|
353
|
+
</div>
|
|
342
354
|
</a>
|
|
343
355
|
</li>
|
|
344
356
|
}
|
|
345
357
|
{
|
|
346
|
-
!this.props.data.imported_xml && <li>
|
|
347
|
-
<a
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
358
|
+
!this.props.data.imported_xml && <li className="hover:bg-gray-200 mb-1">
|
|
359
|
+
<a href={'download/' + this.props.data.search_id + '.xml'}>
|
|
360
|
+
<div className="relative flex flex-col items-center group">
|
|
361
|
+
<div className="flex items-center w-full">
|
|
362
|
+
<span className="w-full text-sm text-seqblue hover:text-seqorange download cursor-pointer py-0.5 px-0.5">Full XML report</span>
|
|
363
|
+
<div className="absolute hidden left-full ml-2 items-center group-hover:flex tooltip-wrap">
|
|
364
|
+
<div className="w-0 h-0 border-y-8 border-r-8 border-t-transparent border-b-transparent border-r-black -mr-px"></div>
|
|
365
|
+
<span className="relative z-10 p-2 side-tooltip-text leading-4 text-center text-white whitespace-no-wrap bg-black shadow-lg rounded-md">
|
|
366
|
+
Results in XML format.
|
|
367
|
+
</span>
|
|
368
|
+
</div>
|
|
369
|
+
</div>
|
|
370
|
+
</div>
|
|
351
371
|
</a>
|
|
352
372
|
</li>
|
|
353
373
|
}
|
|
354
374
|
{
|
|
355
|
-
!this.props.data.imported_xml && <li>
|
|
356
|
-
<a
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
375
|
+
!this.props.data.imported_xml && <li className="hover:bg-gray-200 mb-1">
|
|
376
|
+
<a href={'download/' + this.props.data.search_id + '.pairwise'}>
|
|
377
|
+
<div className="relative flex flex-col items-center group">
|
|
378
|
+
<div className="flex items-center w-full">
|
|
379
|
+
<span className="w-full text-sm text-seqblue hover:text-seqorange download cursor-pointer py-0.5 px-0.5">Full Text report</span>
|
|
380
|
+
<div className="absolute hidden left-full ml-2 items-center group-hover:flex tooltip-wrap">
|
|
381
|
+
<div className="w-0 h-0 border-y-8 border-r-8 border-t-transparent border-b-transparent border-r-black -mr-px"></div>
|
|
382
|
+
<span className="relative z-10 p-2 side-tooltip-text leading-4 text-center text-white whitespace-no-wrap bg-black shadow-lg rounded-md">
|
|
383
|
+
Results in text format.
|
|
384
|
+
</span>
|
|
385
|
+
</div>
|
|
386
|
+
</div>
|
|
387
|
+
</div>
|
|
360
388
|
</a>
|
|
361
389
|
</li>
|
|
362
390
|
}
|
|
@@ -369,35 +397,69 @@ export default class extends Component {
|
|
|
369
397
|
sharingPanelJSX() {
|
|
370
398
|
return (
|
|
371
399
|
<div className="sharing-panel">
|
|
372
|
-
<div className="
|
|
373
|
-
<h4>
|
|
400
|
+
<div className="pl-px table mb-0 w-full">
|
|
401
|
+
<h4 className="text-sm font-bold mb-0 mt-2.5">
|
|
374
402
|
Share results
|
|
375
403
|
</h4>
|
|
376
404
|
</div>
|
|
377
|
-
<ul
|
|
405
|
+
<ul>
|
|
378
406
|
{!this.props.cloudSharingEnabled &&
|
|
379
|
-
<li>
|
|
380
|
-
<a id="copyURL" className="
|
|
381
|
-
|
|
382
|
-
|
|
407
|
+
<li className="hover:text-seqorange hover:bg-gray-200">
|
|
408
|
+
<a id="copyURL" className="flex text-sm text-seqblue hover:text-seqorange copy-URL cursor-pointer py-0.5 px-0.5 w-full" onClick={this.copyURL}>
|
|
409
|
+
<div className="relative flex gap-2 items-center group w-full">
|
|
410
|
+
<i className="fa fa-copy"></i>
|
|
411
|
+
<div className="flex items-center">
|
|
412
|
+
<span className="w-full">Copy URL to clipboard</span>
|
|
413
|
+
<div id="tooltip" className="absolute hidden left-full ml-2 items-center">
|
|
414
|
+
<div className="flex items-center">
|
|
415
|
+
<div className="w-0 h-0 border-y-8 border-r-8 border-t-transparent border-b-transparent border-r-black -mr-px"></div>
|
|
416
|
+
<span className="relative z-10 p-2 side-tooltip-text leading-4 text-center text-white whitespace-no-wrap bg-black shadow-lg rounded-md">
|
|
417
|
+
Copied!
|
|
418
|
+
</span>
|
|
419
|
+
</div>
|
|
420
|
+
</div>
|
|
421
|
+
</div>
|
|
422
|
+
</div>
|
|
383
423
|
</a>
|
|
384
424
|
</li>
|
|
385
425
|
}
|
|
386
426
|
{!this.props.cloudSharingEnabled &&
|
|
387
|
-
<li>
|
|
388
|
-
<a id="sendEmail" className="
|
|
389
|
-
|
|
427
|
+
<li className="hover:text-seqorange hover:bg-gray-200">
|
|
428
|
+
<a id="sendEmail" className="flex text-sm text-seqblue hover:text-seqorange email-URL cursor-pointer py-0.5 px-0.5 w-full"
|
|
429
|
+
href={asMailtoHref(this.props.data.querydb, this.props.data.program, this.props.data.queries.length, window.location.href)}
|
|
390
430
|
target="_blank" rel="noopener noreferrer">
|
|
391
|
-
<
|
|
431
|
+
<div className="relative flex gap-2 items-center group w-full">
|
|
432
|
+
<i className="fa fa-envelope"></i>
|
|
433
|
+
<div className="flex items-center w-full">
|
|
434
|
+
<span className="w-full">Send by email</span>
|
|
435
|
+
<div className="absolute hidden left-full ml-2 items-center group-hover:flex tooltip-wrap">
|
|
436
|
+
<div className="w-0 h-0 border-y-8 border-r-8 border-t-transparent border-b-transparent border-r-black -mr-px"></div>
|
|
437
|
+
<span className="relative z-10 p-2 side-tooltip-text leading-4 text-center text-white whitespace-no-wrap bg-black shadow-lg rounded-md">
|
|
438
|
+
Send by email
|
|
439
|
+
</span>
|
|
440
|
+
</div>
|
|
441
|
+
</div>
|
|
442
|
+
</div>
|
|
392
443
|
</a>
|
|
393
444
|
</li>
|
|
394
445
|
}
|
|
395
446
|
{this.props.cloudSharingEnabled &&
|
|
396
|
-
<li>
|
|
397
|
-
<button className="
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
447
|
+
<li className="hover:text-seqorange hover:bg-gray-200">
|
|
448
|
+
<button className="flex text-sm text-seqblue hover:text-seqorange cloud-Post cursor-pointer py-0.5 px-0.5 w-full" onClick={this.shareCloudInit}>
|
|
449
|
+
<div className="relative flex gap-2 items-center group w-full">
|
|
450
|
+
<i className="fa fa-cloud"></i>
|
|
451
|
+
<div className="flex items-center">
|
|
452
|
+
<span className="w-full">Share to cloud</span>
|
|
453
|
+
<div className="absolute hidden left-full ml-2 items-center group-hover:flex tooltip-wrap">
|
|
454
|
+
<div className="w-0 h-0 border-y-8 border-r-8 border-t-transparent border-b-transparent border-r-black -mr-px"></div>
|
|
455
|
+
<span className="relative z-10 p-2 side-tooltip-text leading-4 text-center text-white whitespace-no-wrap bg-black shadow-lg rounded-md">
|
|
456
|
+
Results in pairwise format
|
|
457
|
+
Upload results to SequenceServer Cloud where it will become accessable
|
|
458
|
+
to everyone who has a link.
|
|
459
|
+
</span>
|
|
460
|
+
</div>
|
|
461
|
+
</div>
|
|
462
|
+
</div>
|
|
401
463
|
</button>
|
|
402
464
|
</li>
|
|
403
465
|
}
|
|
@@ -416,14 +478,14 @@ export default class extends Component {
|
|
|
416
478
|
|
|
417
479
|
render() {
|
|
418
480
|
return (
|
|
419
|
-
<div className="sidebar">
|
|
481
|
+
<div className="sidebar sticky top-0 print:hidden">
|
|
420
482
|
{this.topPanelJSX()}
|
|
421
483
|
{this.downloadsPanelJSX()}
|
|
422
484
|
{this.sharingPanelJSX()}
|
|
423
485
|
<div className="referral-panel">
|
|
424
|
-
<div className="
|
|
425
|
-
<h4>Recommend SequenceServer</h4>
|
|
426
|
-
<p><a href="https://sequenceserver.com/referral-program" target="_blank">Earn up to $400 per signup</a></p>
|
|
486
|
+
<div className="pl-px table mb-0 w-full text-sm">
|
|
487
|
+
<h4 className="font-bold mb-0 mt-2.5">Recommend SequenceServer</h4>
|
|
488
|
+
<p><a href="https://sequenceserver.com/referral-program" target="_blank" className="text-seqblue hover:text-seqorange">Earn up to $400 per signup</a></p>
|
|
427
489
|
</div>
|
|
428
490
|
</div>
|
|
429
491
|
</div>
|
data/public/js/svgExporter.js
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
*
|
|
9
9
|
*/
|
|
10
10
|
import * as Exporter from './exporter';
|
|
11
|
+
import * as d3 from 'd3';
|
|
11
12
|
|
|
12
13
|
/**
|
|
13
14
|
* Exports the given <svg> DOM node as a .svg file.
|
|
@@ -44,7 +45,11 @@ var export_as_png = function (svg, filename) {
|
|
|
44
45
|
Exporter.download_url(canvas.toDataURL('image/png'), filename);
|
|
45
46
|
};
|
|
46
47
|
|
|
47
|
-
|
|
48
|
+
var svgString = serialize_svg(svg);
|
|
49
|
+
var encodedSvg = encodeURIComponent(svgString).replace(/%([0-9A-F]{2})/gi, function(match, p1) {
|
|
50
|
+
return String.fromCharCode('0x' + p1);
|
|
51
|
+
});
|
|
52
|
+
img.src = 'data:image/svg+xml;base64,' + window.btoa(encodedSvg);
|
|
48
53
|
};
|
|
49
54
|
|
|
50
55
|
var serialize_svg = function(svg) {
|
|
@@ -61,8 +66,8 @@ var serialize_svg = function(svg) {
|
|
|
61
66
|
|
|
62
67
|
svg.removeAttribute('xmlns');
|
|
63
68
|
svg.removeAttribute('xlink');
|
|
64
|
-
svg.setAttributeNS(d3.
|
|
65
|
-
svg.setAttributeNS(d3.
|
|
69
|
+
svg.setAttributeNS(d3.namespaces.xmlns, 'xmlns', d3.namespaces.svg);
|
|
70
|
+
svg.setAttributeNS(d3.namespaces.xmlns, 'xmlns:xlink', d3.namespaces.xlink);
|
|
66
71
|
|
|
67
72
|
var source = (new XMLSerializer()).serializeToString(svg);
|
|
68
73
|
var doctype = '<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC ' +
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// Import the necessary functions
|
|
2
|
+
import { tick_formatter, formatNumberUnits } from '../visualisation_helpers';
|
|
3
|
+
|
|
4
|
+
describe('tick_formatter', () => {
|
|
5
|
+
test('formats tick for amino_acid sequence', () => {
|
|
6
|
+
const formatter = tick_formatter(null, 'amino_acid');
|
|
7
|
+
expect(formatter(1_000)).toBe('1 kaa');
|
|
8
|
+
expect(formatter(25_286_936)).toBe('25.3 Maa');
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
test('formats tick for nucleic_acid sequence', () => {
|
|
12
|
+
const formatter = tick_formatter(null, 'nucleic_acid');
|
|
13
|
+
expect(formatter(1_000)).toBe('1 kbp');
|
|
14
|
+
expect(formatter(25_965_000)).toBe('26 Mbp');
|
|
15
|
+
});
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
describe('formatNumberUnits', () => {
|
|
19
|
+
test('formats number with units correctly', () => {
|
|
20
|
+
expect(formatNumberUnits('1.9k')).toBe('1.9 k');
|
|
21
|
+
expect(formatNumberUnits('1.5k')).toBe('1.5 k');
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test('rounds up correctly based on threshold', () => {
|
|
25
|
+
expect(formatNumberUnits('2.95k')).toBe('3 k');
|
|
26
|
+
expect(formatNumberUnits('2.94k')).toBe('2.9 k');
|
|
27
|
+
});
|
|
28
|
+
});
|