• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

eweitz / ideogram / 12131714885

03 Dec 2024 02:21AM UTC coverage: 82.273% (-1.1%) from 83.356%
12131714885

push

github

web-flow
Merge pull request #375 from eweitz/smooth-repeat-hover

Avoid hiding tooltip on first hover of previous clicked annotation

2359 of 3225 branches covered (73.15%)

Branch coverage included in aggregate %.

10 of 15 new or added lines in 3 files covered. (66.67%)

455 existing lines in 18 files now uncovered.

5410 of 6218 relevant lines covered (87.01%)

27653.67 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

85.64
/src/js/annotations/annotations.js
1
/**
2
 * @fileoverview Methods for ideogram annotations.
3
 * Annotations are graphical objects that represent features of interest
4
 * located on the chromosomes, e.g. genes or variations.  They can
5
 * appear beside a chromosome, overlaid on top of it, or between multiple
6
 * chromosomes.
7
 */
8

9
import {BedParser} from '../parsers/bed-parser';
10
import {TsvParser} from '../parsers/tsv-parser';
11
import {drawHeatmaps, deserializeAnnotsForHeatmap} from './heatmap';
12
import {inflateThresholds} from './heatmap-lib';
13
import {inflateHeatmaps} from './heatmap-collinear';
14
import {
15
  onLoadAnnots, onDrawAnnots, startHideAnnotTooltipTimeout,
16
  onWillShowAnnotTooltip, onDidShowAnnotTooltip, showAnnotTooltip, onClickAnnot
17
} from './events';
18

19
import {
20
  addAnnotLabel, removeAnnotLabel, fillAnnotLabels, clearAnnotLabels
21
  // fadeOutAnnotLabels
22
} from './labels';
23

24
import {drawAnnots, drawProcessedAnnots} from './draw';
25
import {getHistogramBars} from './histogram';
26
import {drawSynteny} from './synteny';
27
import {
28
  restoreDefaultTracks, setOriginalTrackIndexes, updateDisplayedTracks
29
} from './filter';
30
import {processAnnotData} from './process';
31
import {ExpressionMatrixParser} from '../parsers/expression-matrix-parser';
32
import {downloadAnnotations} from './download';
33

34
function initNumTracksAndBarWidth(ideo, config) {
35

36
  if (config.annotationTracks) {
31✔
37
    ideo.config.numAnnotTracks = config.annotationTracks.length;
8✔
38
  } else if (config.annotationsNumTracks) {
23✔
39
    ideo.config.numAnnotTracks = config.annotationsNumTracks;
5✔
40
  } else {
41
    ideo.config.numAnnotTracks = 1;
18✔
42
  }
43
  ideo.config.annotTracksHeight =
31✔
44
    config.annotationHeight * config.numAnnotTracks;
45

46
  if (typeof config.barWidth === 'undefined') {
31✔
47
    ideo.config.barWidth = 3;
28✔
48
  }
49
}
50

51
function initTooltip(ideo, config) {
52
  if (config.showAnnotTooltip !== false) {
89!
53
    ideo.config.showAnnotTooltip = true;
89✔
54
  }
55

56
  if (config.onWillShowAnnotTooltip) {
89!
57
    ideo.onWillShowAnnotTooltipCallback = config.onWillShowAnnotTooltip;
×
58
  }
59

60
  if (config.onDidShowAnnotTooltip) {
89!
61
    ideo.onDidShowAnnotTooltipCallback = config.onDidShowAnnotTooltip;
×
62
  }
63
}
64

65
function initAnnotLabel(ideo, config) {
66
  if (config.addAnnotLabel !== false) {
89!
67
    ideo.config.addAnnotLabel = true;
89✔
68
  }
69

70
  if (config.onWillAddAnnotLabel) {
89!
71
    ideo.onWillAddAnnotLabelCallback = config.onWillAddAnnotLabel;
×
72
  }
73
}
74

75
function initAnnotHeight(ideo) {
76
  var config = ideo.config;
89✔
77
  var annotHeight;
78

79
  if (!config.annotationHeight) {
89✔
80
    if (config.annotationsLayout === 'heatmap') {
66✔
81
      annotHeight = config.chrWidth - 1;
4✔
82
    } else {
83
      annotHeight = Math.round(config.chrHeight / 100);
62✔
84
      if (annotHeight < 3) annotHeight = 3;
62✔
85
    }
86
    ideo.config.annotationHeight = annotHeight;
66✔
87
  }
88
}
89

90
/**
91
 * Initializes various annotation settings.  Constructor help function.
92
 */
93
function initAnnotSettings() {
94
  var ideo = this,
89✔
95
    config = ideo.config;
89✔
96

97
  initAnnotHeight(ideo);
89✔
98

99
  if (
89✔
100
    config.annotationsPath || config.localAnnotationsPath ||
287✔
101
    ideo.annots || config.annotations
102
  ) {
103
    initNumTracksAndBarWidth(ideo, config);
31✔
104
  } else {
105
    ideo.config.annotTracksHeight = 0;
58✔
106
    ideo.config.numAnnotTracks = 0;
58✔
107
  }
108

109
  if (typeof config.annotationsColor === 'undefined') {
89!
110
    ideo.config.annotationsColor = '#F00';
89✔
111
  }
112

113
  if (config.onClickAnnot) {
89!
114
    ideo.onClickAnnotCallback = config.onClickAnnot;
×
115
  }
116

117
  initTooltip(ideo, config);
89✔
118
  initAnnotLabel(ideo, config);
89✔
119
}
120

121
function validateAnnotsUrl(annotsUrl) {
122
  var tmp, extension;
123

124
  tmp = annotsUrl.split('?')[0].split('.');
23✔
125
  extension = tmp[tmp.length - 1];
23✔
126

127
  if (['bed', 'json', 'tsv'].includes(extension) === false) {
23✔
128
    extension = extension.toUpperCase();
1✔
129
    alert(
1✔
130
      'Ideogram.js only supports BED and Ideogram JSON and TSV ' +
131
      'at the moment.  ' +
132
      'Sorry, check back soon for ' + extension + ' support!'
133
    );
134
    return;
1✔
135
  }
136
  return extension;
22✔
137
}
138

139
/** Find redundant chromosomes in raw annotations */
140
function detectDuplicateChrsInRawAnnots(ideo) {
141
  const seen = {};
22✔
142
  const duplicates = [];
22✔
143
  const chrs = ideo.rawAnnots.annots.map(annot => annot.chr);
465✔
144

145
  chrs.forEach((chr) => {
22✔
146
    if (chr in seen) duplicates.push(chr);
465✔
147
    seen[chr] = 1;
465✔
148
  });
149

150
  if (duplicates.length > 0) {
22✔
151
    const message =
152
      `Duplicate chromosomes detected.\n` +
1✔
153
      `Chromosome list: ${chrs}.  Duplicates: ${duplicates}.\n` +
154
      `To fix this, edit your raw annotations JSON data to remove redundant ` +
155
      `chromosomes.`;
156
    throw Error(message);
1✔
157
  }
158
}
159

160
function afterRawAnnots() {
161
  var ideo = this,
23✔
162
    config = ideo.config;
23✔
163

164
  // Ensure annots are ordered by chromosome
165
  ideo.rawAnnots.annots = ideo.rawAnnots.annots.sort(Ideogram.sortChromosomes);
23✔
166

167
  if (ideo.onLoadAnnotsCallback) {
23!
168
    ideo.onLoadAnnotsCallback();
×
169
  }
170

171
  if (
23✔
172
    'heatmapThresholds' in config ||
49✔
173
    'metadata' in ideo.rawAnnots &&
174
    'heatmapThresholds' in ideo.rawAnnots.metadata
175
  ) {
176
    if (config.annotationsLayout === 'heatmap') {
2!
177
      inflateHeatmaps(ideo);
2✔
UNCOV
178
    } else if (config.annotationsLayout === 'heatmap-2d') {
×
UNCOV
179
      ideo.config.heatmapThresholds = inflateThresholds(ideo);
×
180
    }
181
  }
182

183
  if (config.heatmaps) {
22✔
184
    ideo.deserializeAnnotsForHeatmap(ideo.rawAnnots);
3✔
185
  }
186

187
  detectDuplicateChrsInRawAnnots(ideo);
22✔
188
}
189

190
/**
191
 * Converts list of annotation-by-chromosome objects to list of annot objects
192
 */
193
function flattenAnnots() {
194
  const ideo = this;
35✔
195
  return ideo.annots.reduce((accumulator, annots) => {
35✔
196
    return [...accumulator, ...annots.annots];
832✔
197
  }, []);
198
}
199

200
/**
201
 * Requests annotations URL via HTTP, sets ideo.rawAnnots for downstream
202
 * processing.
203
 *
204
 * @param annotsUrl Absolute or relative URL for native or BED annotations file
205
 */
206
function fetchAnnots(annotsUrl) {
207
  var extension, is2dHeatmap,
208
    ideo = this,
23✔
209
    config = ideo.config;
23✔
210

211
  is2dHeatmap = config.annotationsLayout === 'heatmap-2d';
23✔
212

213
  var extension = validateAnnotsUrl(annotsUrl);
23✔
214

215
  if (annotsUrl.slice(0, 4) !== 'http' && !is2dHeatmap && extension !== 'tsv') {
23✔
216
    ideo.fetch(annotsUrl)
18✔
217
      .then(function(data) {
218
        ideo.rawAnnotsResponse = data; // Preserve truly raw response content
18✔
219
        ideo.rawAnnots = data; // Sometimes gets partially processed
18✔
220
        ideo.afterRawAnnots();
18✔
221
      });
222
    return;
18✔
223
  }
224

225
  extension = (is2dHeatmap ? '' : extension);
5✔
226

227
  ideo.fetch(annotsUrl, 'text')
5✔
228
    .then(function(text) {
229
      ideo.rawAnnotsResponse = text;
4✔
230
      if (is2dHeatmap) {
4!
UNCOV
231
        var parser = new ExpressionMatrixParser(text, ideo);
×
UNCOV
232
        parser.setRawAnnots().then(function(d) {
×
UNCOV
233
          ideo.rawAnnots = d;
×
UNCOV
234
          ideo.afterRawAnnots();
×
235
        });
236
      } else {
237
        if (extension === 'tsv') {
4✔
238
          ideo.rawAnnots = new TsvParser(text, ideo).rawAnnots;
2✔
239
        } else if (extension === 'bed') {
2✔
240
          ideo.rawAnnots = new BedParser(text, ideo).rawAnnots;
1✔
241
        } else {
242
          ideo.rawAnnots = JSON.parse(text);
1✔
243
        }
244
        ideo.afterRawAnnots();
4✔
245
      }
246
    });
247
}
248

249
/**
250
 * Fills out annotations data structure such that its top-level list of arrays
251
 * matches that of this ideogram's chromosomes list in order and number
252
 * Fixes https://github.com/eweitz/ideogram/issues/66
253
 */
254
function fillAnnots(annots) {
255
  var filledAnnots, chrs, chrArray, i, chr, annot, chrIndex;
256

257
  filledAnnots = [];
81✔
258
  chrs = [];
81✔
259
  chrArray = this.chromosomesArray;
81✔
260

261
  for (i = 0; i < chrArray.length; i++) {
81✔
262
    chr = chrArray[i].name;
1,867✔
263
    chrs.push(chr);
1,867✔
264
    filledAnnots.push({chr: chr, annots: []});
1,867✔
265
  }
266

267
  for (i = 0; i < annots.length; i++) {
81✔
268
    annot = annots[i];
1,808✔
269
    chrIndex = chrs.indexOf(annot.chr);
1,808✔
270
    if (chrIndex !== -1) {
1,808!
271
      filledAnnots[chrIndex] = annot;
1,808✔
272
    }
273
  }
274

275
  return filledAnnots;
81✔
276
}
277

278
export function applyRankCutoff(annots, cutoff, ideo) {
279
  const rankedAnnots = sortAnnotsByRank(annots, ideo);
×
280

281
  // Take the top N ranked genes, where N is `cutoff`
282
  annots = rankedAnnots.slice(0, cutoff);
×
283

284
  return annots;
×
285
}
286

287
export function setAnnotRanks(annots, ideo) {
288
  if (annots.length === 0) return annots;
2,010✔
289
  if ('initRank' in annots[0] === false) {
961✔
290
    if ('geneCache' in ideo === false) return annots;
948!
291

292
    const ranks = ideo.geneCache.interestingNames;
948✔
293

294
    return annots.map(annot => {
948✔
295
      if (ranks.includes(annot.name)) {
4,237✔
296
        annot.rank = ranks.indexOf(annot.name) + 1;
2,673✔
297
      } else {
298
        annot.rank = 1E10;
1,564✔
299
      }
300
      return annot;
4,237✔
301
    });
302
  } else {
303
    return annots.map(annot => {
13✔
304
      annot.rank = annot.initRank;
16✔
305
      return annot;
16✔
306
    });
307
  }
308
}
309

310
export function sortAnnotsByRank(annots, ideo) {
311

312
  if (ideo) {
746!
313
    annots = setAnnotRanks(annots, ideo);
746✔
314
  }
315
  // Ranks annots by popularity
316
  return annots.sort((a, b) => {
746✔
317

318
    // // Search gene is most important, regardless of popularity
319
    // if (a.color === 'red') return -1;
320
    // if (b.color === 'red') return 1;
321

322
    // Rank 3 is more important than rank 30
323
    return a.rank - b.rank;
6,940✔
324
  });
325
}
326

327
export {
328
  onLoadAnnots, onDrawAnnots, processAnnotData, restoreDefaultTracks,
329
  updateDisplayedTracks, initAnnotSettings, fetchAnnots, drawAnnots,
330
  getHistogramBars, drawHeatmaps, deserializeAnnotsForHeatmap, fillAnnots,
331
  drawProcessedAnnots, drawSynteny, startHideAnnotTooltipTimeout,
332
  showAnnotTooltip, onWillShowAnnotTooltip, onDidShowAnnotTooltip,
333
  setOriginalTrackIndexes,
334
  afterRawAnnots, onClickAnnot, downloadAnnotations, addAnnotLabel,
335
  removeAnnotLabel, fillAnnotLabels, clearAnnotLabels, flattenAnnots
336
  // fadeOutAnnotLabels
337
};
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc