oncoprintjs 6.1.3 → 6.1.5

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.
@@ -58,6 +58,7 @@ export type CustomTrackOption = {
58
58
  weight?: string;
59
59
  disabled?: boolean;
60
60
  gapLabelsFn?: (model: OncoprintModel) => OncoprintGapConfig[];
61
+ children?: CustomTrackOption[];
61
62
  };
62
63
  export type CustomTrackGroupOption = {
63
64
  label?: string;
@@ -103,6 +104,10 @@ export type UserTrackSpec<D> = {
103
104
  $track_info_tooltip_elt?: JQuery;
104
105
  track_can_show_gaps?: boolean;
105
106
  show_gaps_on_init?: boolean;
107
+ on_move_up?: () => void;
108
+ on_move_down?: () => void;
109
+ move_up_disabled?: boolean;
110
+ move_down_disabled?: boolean;
106
111
  };
107
112
  export type LibraryTrackSpec<D> = UserTrackSpec<D> & {
108
113
  rule_set: RuleSet;
@@ -198,6 +203,10 @@ export default class OncoprintModel {
198
203
  private track_removable;
199
204
  private track_remove_callback;
200
205
  private track_remove_option_callback;
206
+ private track_on_move_up;
207
+ private track_on_move_down;
208
+ private track_move_up_disabled;
209
+ private track_move_down_disabled;
201
210
  private track_sort_cmp_fn;
202
211
  private track_sort_direction_changeable;
203
212
  private track_sort_direction;
@@ -365,6 +374,10 @@ export default class OncoprintModel {
365
374
  getTrackGroupPadding(base?: boolean): number;
366
375
  isTrackRemovable(track_id: TrackId): boolean;
367
376
  getTrackRemoveOptionCallback(track_id: TrackId): (track_id: number) => void;
377
+ getTrackOnMoveUp(track_id: TrackId): () => void;
378
+ getTrackOnMoveDown(track_id: TrackId): () => void;
379
+ isTrackMoveUpDisabled(track_id: TrackId): boolean;
380
+ isTrackMoveDownDisabled(track_id: TrackId): boolean;
368
381
  isTrackSortDirectionChangeable(track_id: TrackId): boolean;
369
382
  isTrackExpandable(track_id: TrackId): boolean;
370
383
  expandTrack(track_id: TrackId): void;
@@ -58,6 +58,7 @@ export interface IStackedBarRuleSetParams extends IGeneralRuleSetParams {
58
58
  value_key: string;
59
59
  categories: string[];
60
60
  fills?: RGBAColor[];
61
+ max_total?: number;
61
62
  }
62
63
  export interface IGeneticAlterationRuleSetParams extends IGeneralRuleSetParams {
63
64
  type: RuleSetType.GENE;
@@ -31,6 +31,7 @@ export default class OncoprintTrackOptionsView {
31
31
  private hideMenusExcept;
32
32
  private static $makeDropdownOption;
33
33
  private static $makeDropdownSeparator;
34
+ private static $makeDropdownSubmenuOption;
34
35
  private static renderSortArrow;
35
36
  private renderTrackOptions;
36
37
  enableInteraction(): void;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "oncoprintjs",
3
- "version": "6.1.3",
3
+ "version": "6.1.5",
4
4
  "description": "A data visualization for cancer genomic data.",
5
5
  "types": "./dist/js/oncoprint.d.ts",
6
6
  "main": "dist/index.js",
@@ -57,5 +57,5 @@
57
57
  "tayden-clusterfck": "^0.7.0",
58
58
  "typescript": "4.0.3"
59
59
  },
60
- "gitHead": "77f7d81ab0b6e1c0f5b6846d2b39851593f6e6e7"
60
+ "gitHead": "a7270762c0180a3e41ebce361df3c1417072c72d"
61
61
  }
@@ -79,6 +79,9 @@ export type CustomTrackOption = {
79
79
  weight?: string;
80
80
  disabled?: boolean;
81
81
  gapLabelsFn?: (model: OncoprintModel) => OncoprintGapConfig[];
82
+ // When set, this option is a parent item: hovering shows a submenu of the
83
+ // nested options. Mutually exclusive with onClick.
84
+ children?: CustomTrackOption[];
82
85
  };
83
86
  export type CustomTrackGroupOption = {
84
87
  label?: string;
@@ -124,6 +127,12 @@ export type UserTrackSpec<D> = {
124
127
  $track_info_tooltip_elt?: JQuery;
125
128
  track_can_show_gaps?: boolean;
126
129
  show_gaps_on_init?: boolean;
130
+ // Optional overrides for Move up / Move down: a callback fully replaces the
131
+ // default within-group move; the disabled flags gray out the item.
132
+ on_move_up?: () => void;
133
+ on_move_down?: () => void;
134
+ move_up_disabled?: boolean;
135
+ move_down_disabled?: boolean;
127
136
  };
128
137
  export type LibraryTrackSpec<D> = UserTrackSpec<D> & {
129
138
  rule_set: RuleSet;
@@ -305,6 +314,10 @@ export default class OncoprintModel {
305
314
  private track_remove_option_callback: TrackProp<
306
315
  (track_id: TrackId) => void
307
316
  >;
317
+ private track_on_move_up: TrackProp<(() => void) | undefined>;
318
+ private track_on_move_down: TrackProp<(() => void) | undefined>;
319
+ private track_move_up_disabled: TrackProp<boolean>;
320
+ private track_move_down_disabled: TrackProp<boolean>;
308
321
  private track_sort_cmp_fn: TrackProp<TrackSortSpecification<Datum>>;
309
322
  private track_sort_direction_changeable: TrackProp<boolean>;
310
323
  private track_sort_direction: TrackProp<TrackSortDirection>;
@@ -416,6 +429,10 @@ export default class OncoprintModel {
416
429
  this.track_removable = {};
417
430
  this.track_remove_callback = {};
418
431
  this.track_remove_option_callback = {};
432
+ this.track_on_move_up = {};
433
+ this.track_on_move_down = {};
434
+ this.track_move_up_disabled = {};
435
+ this.track_move_down_disabled = {};
419
436
  this.track_sort_cmp_fn = {};
420
437
  this.track_sort_direction_changeable = {};
421
438
  this.track_sort_direction = {}; // 1: ascending, -1: descending, 0: not
@@ -1142,11 +1159,11 @@ export default class OncoprintModel {
1142
1159
  ) {
1143
1160
  return self.track_rule_set_id[track_id];
1144
1161
  });
1145
- const unique_rule_set_ids = arrayUnique(
1146
- rule_set_ids.map(x => x.toString())
1147
- );
1162
+ // Dedupe numeric IDs directly to avoid the core-js parseInt polyfill's
1163
+ // per-call trim(), which dominated chart-type-switch cost on large studies.
1164
+ const unique_rule_set_ids = Array.from(new Set(rule_set_ids));
1148
1165
  return unique_rule_set_ids.map(function(rule_set_id) {
1149
- return self.rule_sets[parseInt(rule_set_id, 10)];
1166
+ return self.rule_sets[rule_set_id];
1150
1167
  });
1151
1168
  }
1152
1169
 
@@ -1375,6 +1392,10 @@ export default class OncoprintModel {
1375
1392
  const params = params_list[i];
1376
1393
  this.addTrack(params);
1377
1394
  }
1395
+ // Update track_tops synchronously before the (un-awaited) sort below, so
1396
+ // views reading getZoomedTrackTops() for the new track don't get
1397
+ // `undefined` and compute NaN heights — i.e. a zero-height canvas.
1398
+ this.track_tops.update();
1378
1399
  if (this.rendering_suppressed_depth === 0) {
1379
1400
  if (this.keep_sorted) {
1380
1401
  await this.sort();
@@ -1424,6 +1445,16 @@ export default class OncoprintModel {
1424
1445
  this.track_remove_option_callback[
1425
1446
  track_id
1426
1447
  ] = ifndef(params.onClickRemoveInTrackMenu, function() {});
1448
+ this.track_on_move_up[track_id] = params.on_move_up;
1449
+ this.track_on_move_down[track_id] = params.on_move_down;
1450
+ this.track_move_up_disabled[track_id] = ifndef(
1451
+ params.move_up_disabled,
1452
+ false
1453
+ );
1454
+ this.track_move_down_disabled[track_id] = ifndef(
1455
+ params.move_down_disabled,
1456
+ false
1457
+ );
1427
1458
 
1428
1459
  if (typeof params.expandCallback !== 'undefined') {
1429
1460
  this.track_expand_callback[track_id] = params.expandCallback;
@@ -1493,7 +1524,8 @@ export default class OncoprintModel {
1493
1524
  const group_arrays = [this.track_groups[params.target_group].tracks];
1494
1525
  if (
1495
1526
  this.sort_config.type === 'cluster' &&
1496
- this.sort_config.track_group_index === params.target_group
1527
+ this.sort_config.track_group_index === params.target_group &&
1528
+ this.unclustered_track_group_order
1497
1529
  ) {
1498
1530
  // if target group is clustered, also add track to unclustered order
1499
1531
  group_arrays.push(this.unclustered_track_group_order);
@@ -1633,6 +1665,10 @@ export default class OncoprintModel {
1633
1665
  delete this.track_movable[track_id];
1634
1666
  delete this.track_removable[track_id];
1635
1667
  delete this.track_remove_callback[track_id];
1668
+ delete this.track_on_move_up[track_id];
1669
+ delete this.track_on_move_down[track_id];
1670
+ delete this.track_move_up_disabled[track_id];
1671
+ delete this.track_move_down_disabled[track_id];
1636
1672
  delete this.track_sort_cmp_fn[track_id];
1637
1673
  delete this.track_sort_direction_changeable[track_id];
1638
1674
  delete this.track_sort_direction[track_id];
@@ -1708,9 +1744,14 @@ export default class OncoprintModel {
1708
1744
  return null;
1709
1745
  }
1710
1746
 
1711
- // Next, see if it's in a track
1747
+ // Next, see if it's in a track.
1748
+ // Try binary search first for performance, but fall back to linear
1749
+ // scan if the result is invalid. cell_tops may not be monotonically
1750
+ // ordered relative to the tracks array during async clustering
1751
+ // transitions, which causes binary search to return wrong results.
1712
1752
  const tracks = this.getTracks();
1713
1753
  const cell_tops = this.getCellTops() as TrackProp<number>;
1754
+ let nearest_track: TrackId | undefined;
1714
1755
  const nearest_track_index = binarysearch(
1715
1756
  tracks,
1716
1757
  y,
@@ -1719,12 +1760,26 @@ export default class OncoprintModel {
1719
1760
  },
1720
1761
  true
1721
1762
  );
1722
- if (nearest_track_index === -1) {
1723
- return null;
1763
+ if (nearest_track_index !== -1) {
1764
+ const candidate = tracks[nearest_track_index];
1765
+ if (
1766
+ y >= cell_tops[candidate] &&
1767
+ y < cell_tops[candidate] + this.getCellHeight(candidate)
1768
+ ) {
1769
+ nearest_track = candidate;
1770
+ }
1771
+ }
1772
+ if (nearest_track === undefined) {
1773
+ // Binary search failed (tracks out of order) - linear fallback
1774
+ for (let t = 0; t < tracks.length; t++) {
1775
+ const top = cell_tops[tracks[t]];
1776
+ if (y >= top && y < top + this.getCellHeight(tracks[t])) {
1777
+ nearest_track = tracks[t];
1778
+ break;
1779
+ }
1780
+ }
1724
1781
  }
1725
- const nearest_track = tracks[nearest_track_index];
1726
- if (y >= cell_tops[nearest_track] + this.getCellHeight(nearest_track)) {
1727
- // we know y is past the top of the track (>= cell_tops[nearest_track]), so this checks if y is past the bottom of the track
1782
+ if (nearest_track === undefined) {
1728
1783
  return null;
1729
1784
  }
1730
1785
 
@@ -2060,6 +2115,18 @@ export default class OncoprintModel {
2060
2115
  public getTrackRemoveOptionCallback(track_id: TrackId) {
2061
2116
  return this.track_remove_option_callback[track_id];
2062
2117
  }
2118
+ public getTrackOnMoveUp(track_id: TrackId) {
2119
+ return this.track_on_move_up[track_id];
2120
+ }
2121
+ public getTrackOnMoveDown(track_id: TrackId) {
2122
+ return this.track_on_move_down[track_id];
2123
+ }
2124
+ public isTrackMoveUpDisabled(track_id: TrackId) {
2125
+ return this.track_move_up_disabled[track_id] === true;
2126
+ }
2127
+ public isTrackMoveDownDisabled(track_id: TrackId) {
2128
+ return this.track_move_down_disabled[track_id] === true;
2129
+ }
2063
2130
 
2064
2131
  public isTrackSortDirectionChangeable(track_id: TrackId) {
2065
2132
  return this.track_sort_direction_changeable[track_id];
@@ -116,6 +116,10 @@ export interface IStackedBarRuleSetParams extends IGeneralRuleSetParams {
116
116
  value_key: string;
117
117
  categories: string[];
118
118
  fills?: RGBAColor[];
119
+ // When set, bar heights scale against this constant instead of each datum's
120
+ // own total, so height reflects absolute magnitude rather than per-sample
121
+ // composition. Typically max(sum(datum values)) across all data.
122
+ max_total?: number;
119
123
  }
120
124
 
121
125
  export interface IGeneticAlterationRuleSetParams extends IGeneralRuleSetParams {
@@ -1169,6 +1173,7 @@ class StackedBarRuleSet extends ConditionRuleSet {
1169
1173
  const value_key = params.value_key;
1170
1174
  const fills = params.fills || [];
1171
1175
  const categories = params.categories || [];
1176
+ const max_total = params.max_total;
1172
1177
  const getUnusedColor = makeUniqueColorGetter(fills.map(rgbaToHex));
1173
1178
 
1174
1179
  // Initialize with default values
@@ -1196,41 +1201,95 @@ class StackedBarRuleSet extends ConditionRuleSet {
1196
1201
  fill: fills[I],
1197
1202
  width: 100,
1198
1203
  height: function(d) {
1199
- var total = 0;
1204
+ var denom;
1205
+ if (max_total) {
1206
+ denom = max_total;
1207
+ return (
1208
+ (+d[value_key][categories[I]] *
1209
+ 100) /
1210
+ denom
1211
+ );
1212
+ }
1213
+ // Composition mode: rows fill the full cell
1214
+ // height. Snap the last rect to
1215
+ // "100 - prev_sum_pct" so float drift in the
1216
+ // fractions never leaves a gap at the bottom.
1217
+ denom = 0;
1200
1218
  for (
1201
1219
  var j = 0;
1202
1220
  j < categories.length;
1203
1221
  j++
1204
1222
  ) {
1205
- total += parseFloat(
1206
- d[value_key][categories[j]]
1207
- );
1223
+ denom += +d[value_key][categories[j]];
1224
+ }
1225
+ if (denom === 0) {
1226
+ return 0;
1227
+ }
1228
+ if (I === categories.length - 1) {
1229
+ var prev_pct = 0;
1230
+ for (var k = 0; k < I; k++) {
1231
+ prev_pct +=
1232
+ (+d[value_key][categories[k]] *
1233
+ 100) /
1234
+ denom;
1235
+ }
1236
+ return 100 - prev_pct;
1208
1237
  }
1209
1238
  return (
1210
- (parseFloat(
1211
- d[value_key][categories[I]]
1212
- ) *
1213
- 100) /
1214
- total
1239
+ (+d[value_key][categories[I]] * 100) /
1240
+ denom
1215
1241
  );
1216
1242
  },
1217
1243
  y: function(d) {
1218
- var total = 0;
1244
+ var denom;
1219
1245
  var prev_vals_sum = 0;
1220
- for (
1221
- var j = 0;
1222
- j < categories.length;
1223
- j++
1224
- ) {
1225
- var new_val = parseFloat(
1226
- d[value_key][categories[j]]
1246
+ if (max_total) {
1247
+ // Absolute mode: anchor bars to the
1248
+ // bottom baseline. empty_pct is the
1249
+ // whitespace above a short bar.
1250
+ denom = max_total;
1251
+ var total = 0;
1252
+ for (
1253
+ var j = 0;
1254
+ j < categories.length;
1255
+ j++
1256
+ ) {
1257
+ total += +d[value_key][
1258
+ categories[j]
1259
+ ];
1260
+ }
1261
+ var empty_pct =
1262
+ ((max_total - total) * 100) /
1263
+ max_total;
1264
+ for (var j = 0; j < I; j++) {
1265
+ prev_vals_sum += +d[value_key][
1266
+ categories[j]
1267
+ ];
1268
+ }
1269
+ return (
1270
+ empty_pct +
1271
+ (prev_vals_sum * 100) / denom
1227
1272
  );
1228
- if (j < I) {
1229
- prev_vals_sum += new_val;
1273
+ } else {
1274
+ denom = 0;
1275
+ for (
1276
+ var j = 0;
1277
+ j < categories.length;
1278
+ j++
1279
+ ) {
1280
+ var new_val = +d[value_key][
1281
+ categories[j]
1282
+ ];
1283
+ if (j < I) {
1284
+ prev_vals_sum += new_val;
1285
+ }
1286
+ denom += new_val;
1287
+ }
1288
+ if (denom === 0) {
1289
+ return 0;
1230
1290
  }
1231
- total += new_val;
1291
+ return (prev_vals_sum * 100) / denom;
1232
1292
  }
1233
- return (prev_vals_sum * 100) / total;
1234
1293
  },
1235
1294
  },
1236
1295
  ],
@@ -1,6 +1,7 @@
1
1
  import $ from 'jquery';
2
2
  import menuDotsIcon from '../img/menudots.svg';
3
3
  import OncoprintModel, {
4
+ CustomTrackOption,
4
5
  GAP_MODE_ENUM,
5
6
  TrackId,
6
7
  TrackProp,
@@ -176,12 +177,31 @@ export default class OncoprintTrackOptionsView {
176
177
  'font-weight': weight,
177
178
  'font-size': 12,
178
179
  'border-bottom': '1px solid rgba(0,0,0,0.3)',
180
+ // Disables the iOS 300ms tap delay and lets click fire on first tap.
181
+ 'touch-action': 'manipulation',
179
182
  });
180
183
  if (!disabled) {
181
184
  if (callback) {
182
185
  li.addClass('clickable');
183
186
  li.css({ cursor: 'pointer' });
184
- li.click(callback).hover(
187
+ // Also fire on touchend so taps work when the synthetic click
188
+ // doesn't arrive (sticky hover on iOS). preventDefault on the
189
+ // touch stops the follow-up click so the callback runs once.
190
+ let fired = false;
191
+ const invoke = function(evt: any) {
192
+ if (fired) return;
193
+ fired = true;
194
+ setTimeout(() => {
195
+ fired = false;
196
+ }, 400);
197
+ callback(evt);
198
+ };
199
+ li.on('click', invoke);
200
+ li.on('touchend', function(evt) {
201
+ evt.preventDefault();
202
+ invoke(evt);
203
+ });
204
+ li.hover(
185
205
  function() {
186
206
  $(this).css({ 'background-color': 'rgb(200,200,200)' });
187
207
  },
@@ -209,6 +229,110 @@ export default class OncoprintTrackOptionsView {
209
229
  .addClass(SEPARATOR_CLASS);
210
230
  }
211
231
 
232
+ // Hover-expanding parent item that reveals a floating submenu with leaf
233
+ // options. Used for nested "Sort by > <category>" style menus without
234
+ // cluttering the top-level dropdown.
235
+ private static $makeDropdownSubmenuOption(
236
+ text: string,
237
+ children: CustomTrackOption[],
238
+ track_id: TrackId,
239
+ closeOuterMenu: () => void
240
+ ) {
241
+ const $li = $('<li>')
242
+ .text(text + ' \u25B8')
243
+ .css({
244
+ 'font-weight': 'normal',
245
+ 'font-size': 12,
246
+ 'border-bottom': '1px solid rgba(0,0,0,0.3)',
247
+ cursor: 'default',
248
+ position: 'relative',
249
+ })
250
+ .addClass('has-submenu');
251
+ const $submenu = $('<ul>')
252
+ .appendTo($li)
253
+ .css({
254
+ position: 'absolute',
255
+ left: '100%',
256
+ top: 0,
257
+ display: 'none',
258
+ 'list-style-type': 'none',
259
+ padding: 0,
260
+ margin: 0,
261
+ 'padding-left': '6px',
262
+ 'padding-right': '6px',
263
+ 'background-color': 'rgb(255,255,255)',
264
+ border: '1px solid rgba(0,0,0,0.2)',
265
+ 'z-index': 100,
266
+ 'min-width': '140px',
267
+ });
268
+ let clickedOpen = false;
269
+ for (const child of children) {
270
+ if (child.separator) {
271
+ $submenu.append(
272
+ OncoprintTrackOptionsView.$makeDropdownSeparator()
273
+ );
274
+ continue;
275
+ }
276
+ $submenu.append(
277
+ OncoprintTrackOptionsView.$makeDropdownOption(
278
+ child.label || '',
279
+ child.weight || 'normal',
280
+ child.disabled,
281
+ child.onClick &&
282
+ function(evt) {
283
+ evt.stopPropagation();
284
+ clickedOpen = false;
285
+ $submenu.hide();
286
+ child.onClick!(track_id);
287
+ closeOuterMenu();
288
+ }
289
+ )
290
+ );
291
+ }
292
+ $li.hover(
293
+ function() {
294
+ $(this).css({ 'background-color': 'rgb(200,200,200)' });
295
+ $submenu.show();
296
+ },
297
+ function() {
298
+ $(this).css({ 'background-color': 'rgba(255,255,255,0)' });
299
+ if (!clickedOpen) $submenu.hide();
300
+ }
301
+ );
302
+ // Touch devices have no hover — tapping the parent toggles the submenu
303
+ // and keeps it open until a child is picked or the outer menu closes.
304
+ $li.css({ 'touch-action': 'manipulation' });
305
+ let parentFired = false;
306
+ const toggleSubmenu = function(evt: any) {
307
+ if (
308
+ $(evt.target)
309
+ .closest('ul')
310
+ .is($submenu)
311
+ )
312
+ return;
313
+ if (parentFired) return;
314
+ parentFired = true;
315
+ setTimeout(() => {
316
+ parentFired = false;
317
+ }, 400);
318
+ evt.stopPropagation();
319
+ clickedOpen = !clickedOpen;
320
+ $submenu.toggle(clickedOpen);
321
+ };
322
+ $li.on('click', toggleSubmenu);
323
+ $li.on('touchend', function(evt) {
324
+ if (
325
+ $(evt.target)
326
+ .closest('ul')
327
+ .is($submenu)
328
+ )
329
+ return;
330
+ evt.preventDefault();
331
+ toggleSubmenu(evt);
332
+ });
333
+ return $li;
334
+ }
335
+
212
336
  // 11/2/2023 we are removing sort arrow
213
337
  // leaving commented out if it needs to be restored based on complaint
214
338
  private static renderSortArrow(
@@ -302,7 +426,14 @@ export default class OncoprintTrackOptionsView {
302
426
  }
303
427
  }
304
428
  );
305
- $img.click(function(evt) {
429
+ $img.css({ 'touch-action': 'manipulation' });
430
+ let imgFired = false;
431
+ const toggleImgMenu = function(evt: any) {
432
+ if (imgFired) return;
433
+ imgFired = true;
434
+ setTimeout(() => {
435
+ imgFired = false;
436
+ }, 400);
306
437
  evt.stopPropagation();
307
438
  if ($dropdown.is(':visible')) {
308
439
  $img.addClass(TOGGLE_BTN_OPEN_CLASS);
@@ -312,21 +443,40 @@ export default class OncoprintTrackOptionsView {
312
443
  self.showTrackMenu(track_id);
313
444
  }
314
445
  self.hideMenusExcept(track_id);
446
+ };
447
+ $img.on('click', toggleImgMenu);
448
+ $img.on('touchend', function(evt) {
449
+ evt.preventDefault();
450
+ toggleImgMenu(evt);
315
451
  });
316
452
 
453
+ // Gray out Move up/down only when the track belongs to a clustered
454
+ // group AND there are siblings to move against. A single-track group
455
+ // can never be "clustered" in any meaningful way, and showing the
456
+ // items as disabled there is confusing (users see a grayed-out option
457
+ // with no explanation).
458
+ const containingTracks = model.getContainingTrackGroup(track_id) || [];
317
459
  const movingDisabled =
318
460
  model.getTrackMovable(track_id) &&
319
- model.isTrackInClusteredGroup(track_id);
461
+ model.isTrackInClusteredGroup(track_id) &&
462
+ containingTracks.length > 1;
320
463
 
321
464
  if (model.getTrackMovable(track_id)) {
465
+ const customMoveUp = model.getTrackOnMoveUp(track_id);
466
+ const customMoveDown = model.getTrackOnMoveDown(track_id);
467
+ const moveUpDisabled =
468
+ movingDisabled || model.isTrackMoveUpDisabled(track_id);
469
+ const moveDownDisabled =
470
+ movingDisabled || model.isTrackMoveDownDisabled(track_id);
322
471
  $dropdown.append(
323
472
  OncoprintTrackOptionsView.$makeDropdownOption(
324
473
  'Move up',
325
474
  'normal',
326
- movingDisabled,
475
+ moveUpDisabled,
327
476
  function(evt) {
328
477
  evt.stopPropagation();
329
- self.moveUpCallback(track_id);
478
+ if (customMoveUp) customMoveUp();
479
+ else self.moveUpCallback(track_id);
330
480
  }
331
481
  )
332
482
  );
@@ -334,10 +484,11 @@ export default class OncoprintTrackOptionsView {
334
484
  OncoprintTrackOptionsView.$makeDropdownOption(
335
485
  'Move down',
336
486
  'normal',
337
- movingDisabled,
487
+ moveDownDisabled,
338
488
  function(evt) {
339
489
  evt.stopPropagation();
340
- self.moveDownCallback(track_id);
490
+ if (customMoveDown) customMoveDown();
491
+ else self.moveDownCallback(track_id);
341
492
  }
342
493
  )
343
494
  );
@@ -509,6 +660,15 @@ export default class OncoprintTrackOptionsView {
509
660
  $dropdown.append(
510
661
  OncoprintTrackOptionsView.$makeDropdownSeparator()
511
662
  );
663
+ } else if (option.children && option.children.length) {
664
+ $dropdown.append(
665
+ OncoprintTrackOptionsView.$makeDropdownSubmenuOption(
666
+ option.label || '',
667
+ option.children,
668
+ track_id,
669
+ () => self.hideTrackMenu(track_id)
670
+ )
671
+ );
512
672
  } else {
513
673
  $dropdown.append(
514
674
  OncoprintTrackOptionsView.$makeDropdownOption(
@@ -630,9 +630,13 @@ export default class OncoprintWebGLCellView {
630
630
  self.ctx.viewportWidth,
631
631
  self.ctx.viewportHeight,
632
632
  0,
633
- -5,
633
+ -1000,
634
634
  1000
635
- ); // y axis inverted so that y increases down like SVG
635
+ ); // y axis inverted so that y increases down like SVG.
636
+ // z_index in packed vertex positions equals shape-list index, which
637
+ // for stacked-bar rule sets is one-per-category. A wide near plane
638
+ // (-1000) keeps shapes with z>5 from being frustum-clipped, which
639
+ // would shorten bars for tracks with 7+ categories.
636
640
  self.pMatrix = pMatrix;
637
641
  })(this);
638
642
  }