@apollo-annotation/jbrowse-plugin-apollo 0.3.7 → 0.3.8

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.
Files changed (71) hide show
  1. package/dist/index.esm.js +2371 -1642
  2. package/dist/index.esm.js.map +1 -1
  3. package/dist/jbrowse-plugin-apollo.cjs.development.js +2384 -1641
  4. package/dist/jbrowse-plugin-apollo.cjs.development.js.map +1 -1
  5. package/dist/jbrowse-plugin-apollo.cjs.production.min.js +1 -1
  6. package/dist/jbrowse-plugin-apollo.cjs.production.min.js.map +1 -1
  7. package/dist/jbrowse-plugin-apollo.umd.development.js +4387 -2952
  8. package/dist/jbrowse-plugin-apollo.umd.development.js.map +1 -1
  9. package/dist/jbrowse-plugin-apollo.umd.production.min.js +1 -1
  10. package/dist/jbrowse-plugin-apollo.umd.production.min.js.map +1 -1
  11. package/package.json +15 -15
  12. package/src/ApolloInternetAccount/model.ts +48 -13
  13. package/src/BackendDrivers/CollaborationServerDriver.ts +23 -2
  14. package/src/ChangeManager.ts +33 -13
  15. package/src/FeatureDetailsWidget/ApolloTranscriptDetailsWidget.tsx +64 -5
  16. package/src/FeatureDetailsWidget/TranscriptSequence.tsx +70 -73
  17. package/src/FeatureDetailsWidget/TranscriptWidgetEditLocation.tsx +33 -31
  18. package/src/LinearApolloDisplay/components/LinearApolloDisplay.tsx +60 -72
  19. package/src/LinearApolloDisplay/glyphs/BoxGlyph.ts +50 -194
  20. package/src/LinearApolloDisplay/glyphs/GeneGlyph.ts +441 -180
  21. package/src/LinearApolloDisplay/glyphs/GenericChildGlyph.ts +53 -34
  22. package/src/LinearApolloDisplay/glyphs/Glyph.ts +7 -9
  23. package/src/LinearApolloDisplay/stateModel/base.ts +34 -43
  24. package/src/LinearApolloDisplay/stateModel/layouts.ts +3 -2
  25. package/src/LinearApolloDisplay/stateModel/mouseEvents.ts +32 -261
  26. package/src/LinearApolloDisplay/stateModel/rendering.ts +43 -343
  27. package/src/LinearApolloReferenceSequenceDisplay/components/LinearApolloReferenceSequenceDisplay.tsx +87 -0
  28. package/src/LinearApolloReferenceSequenceDisplay/components/index.ts +1 -0
  29. package/src/LinearApolloReferenceSequenceDisplay/configSchema.ts +7 -0
  30. package/src/LinearApolloReferenceSequenceDisplay/index.ts +3 -0
  31. package/src/LinearApolloReferenceSequenceDisplay/stateModel/base.ts +227 -0
  32. package/src/LinearApolloReferenceSequenceDisplay/stateModel/index.ts +25 -0
  33. package/src/LinearApolloReferenceSequenceDisplay/stateModel/rendering.ts +481 -0
  34. package/src/LinearApolloSixFrameDisplay/components/LinearApolloSixFrameDisplay.tsx +95 -38
  35. package/src/LinearApolloSixFrameDisplay/glyphs/GeneGlyph.ts +221 -201
  36. package/src/LinearApolloSixFrameDisplay/glyphs/Glyph.ts +12 -8
  37. package/src/LinearApolloSixFrameDisplay/stateModel/base.ts +42 -4
  38. package/src/LinearApolloSixFrameDisplay/stateModel/layouts.ts +4 -8
  39. package/src/LinearApolloSixFrameDisplay/stateModel/mouseEvents.ts +73 -97
  40. package/src/LinearApolloSixFrameDisplay/stateModel/rendering.ts +49 -61
  41. package/src/TabularEditor/HybridGrid/Feature.tsx +16 -14
  42. package/src/TabularEditor/HybridGrid/HybridGrid.tsx +7 -5
  43. package/src/components/AddAssembly.tsx +1 -1
  44. package/src/components/AddAssemblyAliases.tsx +1 -1
  45. package/src/components/AddChildFeature.tsx +5 -2
  46. package/src/components/AddFeature.tsx +9 -3
  47. package/src/components/AddRefSeqAliases.tsx +9 -9
  48. package/src/components/CopyFeature.tsx +3 -1
  49. package/src/components/CreateApolloAnnotation.tsx +1 -0
  50. package/src/components/DeleteAssembly.tsx +1 -1
  51. package/src/components/EditZoomThresholdDialog.tsx +69 -0
  52. package/src/components/FilterFeatures.tsx +7 -7
  53. package/src/components/FilterTranscripts.tsx +6 -6
  54. package/src/components/ImportFeatures.tsx +1 -1
  55. package/src/components/ManageChecks.tsx +1 -1
  56. package/src/components/MergeTranscripts.tsx +12 -15
  57. package/src/components/OntologyTermMultiSelect.tsx +11 -11
  58. package/src/components/OpenLocalFile.tsx +11 -7
  59. package/src/components/ViewCheckResults.tsx +1 -1
  60. package/src/components/index.ts +1 -0
  61. package/src/config.ts +6 -0
  62. package/src/index.ts +42 -105
  63. package/src/makeDisplayComponent.tsx +0 -1
  64. package/src/menus/index.ts +1 -0
  65. package/src/{ApolloInternetAccount/addMenuItems.ts → menus/topLevelMenu.ts} +56 -47
  66. package/src/menus/topLevelMenuAdmin.ts +154 -0
  67. package/src/session/session.ts +162 -116
  68. package/src/util/annotationFeatureUtils.ts +15 -21
  69. package/src/util/displayUtils.ts +149 -0
  70. package/src/util/glyphUtils.ts +152 -0
  71. package/src/util/mouseEventsUtils.ts +32 -0
@@ -26,8 +26,8 @@ import { autorun, observable } from 'mobx'
26
26
  import {
27
27
  type Instance,
28
28
  type SnapshotOut,
29
+ addDisposer,
29
30
  applySnapshot,
30
- flow,
31
31
  getRoot,
32
32
  getSnapshot,
33
33
  types,
@@ -54,12 +54,15 @@ export interface Collaborator {
54
54
  locations: UserLocation[]
55
55
  }
56
56
 
57
+ export interface HoveredFeature {
58
+ feature: AnnotationFeature
59
+ bp: number
60
+ }
61
+
57
62
  export function extendSession(
58
63
  pluginManager: PluginManager,
59
64
  sessionModel: ReturnType<typeof types.model>,
60
65
  ) {
61
- const aborter = new AbortController()
62
- const { signal } = aborter
63
66
  const AnnotationFeatureExtended = pluginManager.evaluateExtensionPoint(
64
67
  'Apollo-extendAnnotationFeature',
65
68
  AnnotationFeatureModel,
@@ -70,7 +73,12 @@ export function extendSession(
70
73
  apolloDataStore: types.optional(ClientDataStore, { typeName: 'Client' }),
71
74
  apolloSelectedFeature: types.safeReference(AnnotationFeatureExtended),
72
75
  jobsManager: types.optional(ApolloJobModel, {}),
76
+ isLocked: types.optional(types.boolean, false),
73
77
  })
78
+ .volatile(() => ({
79
+ apolloHoveredFeature: undefined as HoveredFeature | undefined,
80
+ abortController: new AbortController(),
81
+ }))
74
82
  .extend(() => {
75
83
  const collabs = observable.array<Collaborator>([])
76
84
 
@@ -95,10 +103,13 @@ export function extendSession(
95
103
  }
96
104
  })
97
105
  .actions((self) => ({
98
- apolloSetSelectedFeature(feature?: AnnotationFeature) {
106
+ apolloSetSelectedFeature(feature?: AnnotationFeature | string) {
99
107
  // @ts-expect-error Not sure why TS thinks these MST types don't match
100
108
  self.apolloSelectedFeature = feature
101
109
  },
110
+ apolloSetHoveredFeature(feature?: HoveredFeature) {
111
+ self.apolloHoveredFeature = feature
112
+ },
102
113
  addApolloTrackConfig(assembly: AssemblyModel, baseURL?: string) {
103
114
  const trackId = `apollo_track_${assembly.name}`
104
115
  const hasTrack = (self as unknown as AbstractSessionModel).tracks.some(
@@ -127,6 +138,18 @@ export function extendSession(
127
138
  })
128
139
  }
129
140
  },
141
+ toggleLocked() {
142
+ self.isLocked = !self.isLocked
143
+ },
144
+ getPluginConfiguration() {
145
+ const { jbrowse } = getRoot<ApolloRootModel>(self)
146
+ const pluginConfiguration =
147
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
148
+ jbrowse.configuration.ApolloPlugin as Instance<
149
+ typeof ApolloPluginConfigurationSchema
150
+ >
151
+ return pluginConfiguration
152
+ },
130
153
  broadcastLocations() {
131
154
  const { internetAccounts } = getRoot<ApolloRootModel>(self)
132
155
  const locations: {
@@ -184,132 +207,155 @@ export function extendSession(
184
207
  }
185
208
  },
186
209
  }))
210
+ .volatile((self) => ({
211
+ previousSnapshot: getSnapshot(self),
212
+ }))
187
213
  .actions((self) => ({
188
- afterCreate: flow(function* afterCreate() {
189
- autorun(
190
- () => {
191
- // broadcastLocations() // **** This is not working and therefore we need to duplicate broadcastLocations() -method code here because autorun() does not observe changes otherwise
192
- const locations: {
193
- assemblyName: string
194
- refName: string
195
- start: number
196
- end: number
197
- }[] = []
198
- for (const view of (self as unknown as AbstractSessionModel)
199
- .views) {
200
- if (view.type !== 'LinearGenomeView') {
201
- return
202
- }
203
- const lgv = view as unknown as LinearGenomeViewModel
204
- if (lgv.initialized) {
205
- const { dynamicBlocks } = lgv
206
- // eslint-disable-next-line unicorn/no-array-for-each
207
- dynamicBlocks.forEach((block) => {
208
- if (block.regionNumber !== undefined) {
209
- const { assemblyName, end, refName, start } = block
210
- const assembly =
211
- self.apolloDataStore.assemblies.get(assemblyName)
212
- if (
213
- assembly &&
214
- assembly.backendDriverType === 'CollaborationServerDriver'
215
- ) {
216
- locations.push({ assemblyName, refName, start, end })
214
+ afterCreate() {
215
+ applySnapshot(self, { name: self.name, id: self.id })
216
+ // @ts-expect-error type is missing on ApolloRootModel
217
+ const { internetAccounts, jbrowse, reloadPluginManagerCallback } =
218
+ getRoot<ApolloRootModel>(self)
219
+ addDisposer(
220
+ self,
221
+ autorun(
222
+ () => {
223
+ // broadcastLocations() // **** This is not working and therefore we need to duplicate broadcastLocations() -method code here because autorun() does not observe changes otherwise
224
+ const locations: {
225
+ assemblyName: string
226
+ refName: string
227
+ start: number
228
+ end: number
229
+ }[] = []
230
+ for (const view of (self as unknown as AbstractSessionModel)
231
+ .views) {
232
+ if (view.type !== 'LinearGenomeView') {
233
+ return
234
+ }
235
+ const lgv = view as unknown as LinearGenomeViewModel
236
+ if (lgv.initialized) {
237
+ const { dynamicBlocks } = lgv
238
+ // eslint-disable-next-line unicorn/no-array-for-each
239
+ dynamicBlocks.forEach((block) => {
240
+ if (block.regionNumber !== undefined) {
241
+ const { assemblyName, end, refName, start } = block
242
+ const assembly =
243
+ self.apolloDataStore.assemblies.get(assemblyName)
244
+ if (
245
+ assembly &&
246
+ assembly.backendDriverType ===
247
+ 'CollaborationServerDriver'
248
+ ) {
249
+ locations.push({ assemblyName, refName, start, end })
250
+ }
217
251
  }
252
+ })
253
+ }
254
+ }
255
+ if (locations.length === 0) {
256
+ for (const internetAccount of internetAccounts) {
257
+ if ('baseURL' in internetAccount) {
258
+ internetAccount.postUserLocation([])
218
259
  }
219
- })
260
+ }
261
+ return
220
262
  }
221
- }
222
- if (locations.length === 0) {
263
+
264
+ const allLocations: UserLocation[] = []
223
265
  for (const internetAccount of internetAccounts) {
224
266
  if ('baseURL' in internetAccount) {
225
- internetAccount.postUserLocation([])
267
+ for (const location of locations) {
268
+ const tmpLoc: UserLocation = {
269
+ assemblyId: location.assemblyName,
270
+ refSeq: location.refName,
271
+ start: location.start,
272
+ end: location.end,
273
+ }
274
+ allLocations.push(tmpLoc)
275
+ }
276
+ internetAccount.postUserLocation(allLocations)
226
277
  }
227
278
  }
228
- return
229
- }
279
+ },
280
+ { name: 'ApolloSessionBroadcastLocations' },
281
+ ),
282
+ )
283
+ addDisposer(
284
+ self,
285
+ autorun(
286
+ async (reaction) => {
287
+ // When the initial config.json loads, it doesn't include the Apollo
288
+ // tracks, which would result in a potentially invalid session snapshot
289
+ // if any tracks are open. Here we copy the session snapshot, apply an
290
+ // empty session snapshot, and then restore the original session
291
+ // snapshot after the updated config.json loads.
292
+ const pluginConfiguration =
293
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
294
+ jbrowse.configuration.ApolloPlugin as Instance<
295
+ typeof ApolloPluginConfigurationSchema
296
+ >
297
+ const hasRole = readConfObject(
298
+ pluginConfiguration,
299
+ 'hasRole',
300
+ ) as boolean
301
+ if (hasRole) {
302
+ // @ts-expect-error not sure why snapshot type is wrong for snapshot
303
+ applySnapshot(self, self.previousSnapshot)
304
+ reaction.dispose()
305
+ return
306
+ }
307
+
308
+ const { signal } = self.abortController
309
+ // fetch and initialize assemblies for each of our Apollo internet accounts
310
+ for (const internetAccount of internetAccounts as ApolloInternetAccountModel[]) {
311
+ if (internetAccount.type !== 'ApolloInternetAccount') {
312
+ continue
313
+ }
230
314
 
231
- const allLocations: UserLocation[] = []
232
- for (const internetAccount of internetAccounts) {
233
- if ('baseURL' in internetAccount) {
234
- for (const location of locations) {
235
- const tmpLoc: UserLocation = {
236
- assemblyId: location.assemblyName,
237
- refSeq: location.refName,
238
- start: location.start,
239
- end: location.end,
315
+ const { baseURL } = internetAccount
316
+ const uri = new URL('jbrowse/config.json', baseURL).href
317
+ const fetch = internetAccount.getFetcher({
318
+ locationType: 'UriLocation',
319
+ uri,
320
+ })
321
+ let response: Response
322
+ try {
323
+ response = await fetch(uri, { signal })
324
+ } catch (error) {
325
+ if (!self.abortController.signal.aborted) {
326
+ console.error(error)
240
327
  }
241
- allLocations.push(tmpLoc)
328
+ continue
329
+ }
330
+ if (!response.ok) {
331
+ const errorMessage = await createFetchErrorMessage(
332
+ response,
333
+ 'Failed to fetch assemblies',
334
+ )
335
+ console.error(errorMessage)
336
+ continue
242
337
  }
243
- internetAccount.postUserLocation(allLocations)
338
+ let jbrowseConfig
339
+ try {
340
+ jbrowseConfig = await response.json()
341
+ } catch (error) {
342
+ console.error(error)
343
+ continue
344
+ }
345
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-call
346
+ reloadPluginManagerCallback(
347
+ jbrowseConfig,
348
+ self.previousSnapshot,
349
+ )
350
+ reaction.dispose()
244
351
  }
245
- }
246
- },
247
- { name: 'ApolloSession' },
352
+ },
353
+ { name: 'ApolloSessionLoadConfig' },
354
+ ),
248
355
  )
249
- // When the initial config.json loads, it doesn't include the Apollo
250
- // tracks, which would result in a potentially invalid session snapshot
251
- // if any tracks are open. Here we copy the session snapshot, apply an
252
- // empty session snapshot, and then restore the original session
253
- // snapshot after the updated config.json loads.
254
- // @ts-expect-error type is missing on ApolloRootModel
255
- const { internetAccounts, jbrowse, reloadPluginManagerCallback } =
256
- getRoot<ApolloRootModel>(self)
257
- const pluginConfiguration =
258
- // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
259
- jbrowse.configuration.ApolloPlugin as Instance<
260
- typeof ApolloPluginConfigurationSchema
261
- >
262
- const hasRole = readConfObject(
263
- pluginConfiguration,
264
- 'hasRole',
265
- ) as boolean
266
- if (hasRole) {
267
- return
268
- }
269
- const sessionSnapshot = getSnapshot(self)
270
- const { id, name } = sessionSnapshot
271
- applySnapshot(self, { name, id })
272
-
273
- // fetch and initialize assemblies for each of our Apollo internet accounts
274
- for (const internetAccount of internetAccounts as ApolloInternetAccountModel[]) {
275
- if (internetAccount.type !== 'ApolloInternetAccount') {
276
- continue
277
- }
278
-
279
- const { baseURL } = internetAccount
280
- const uri = new URL('jbrowse/config.json', baseURL).href
281
- const fetch = internetAccount.getFetcher({
282
- locationType: 'UriLocation',
283
- uri,
284
- })
285
- let response: Response
286
- try {
287
- response = yield fetch(uri, { signal })
288
- } catch (error) {
289
- console.error(error)
290
- continue
291
- }
292
- if (!response.ok) {
293
- const errorMessage = yield createFetchErrorMessage(
294
- response,
295
- 'Failed to fetch assemblies',
296
- )
297
- console.error(errorMessage)
298
- continue
299
- }
300
- let jbrowseConfig
301
- try {
302
- jbrowseConfig = yield response.json()
303
- } catch (error) {
304
- console.error(error)
305
- continue
306
- }
307
- // eslint-disable-next-line @typescript-eslint/no-unsafe-call
308
- reloadPluginManagerCallback(jbrowseConfig, sessionSnapshot)
309
- }
310
- }),
356
+ },
311
357
  beforeDestroy() {
312
- aborter.abort('destroying session model')
358
+ self.abortController.abort('destroying session model')
313
359
  },
314
360
  }))
315
361
 
@@ -1,7 +1,5 @@
1
1
  import { type AnnotationFeature } from '@apollo-annotation/mst'
2
2
 
3
- import { type MousePosition } from '../LinearApolloDisplay/stateModel/mouseEvents'
4
-
5
3
  export function getFeatureName(feature: AnnotationFeature) {
6
4
  const { attributes } = feature
7
5
  const name = attributes.get('gff_name')
@@ -75,44 +73,40 @@ function getParents(feature: AnnotationFeature): AnnotationFeature[] {
75
73
  return parents
76
74
  }
77
75
 
78
- export function getFeaturesUnderClick(
79
- mousePosition: MousePosition,
76
+ export function getRelatedFeatures(
77
+ feature: AnnotationFeature,
78
+ bp: number,
80
79
  includeSiblings = false,
81
80
  ): AnnotationFeature[] {
82
- const clickedFeatures: AnnotationFeature[] = []
83
- if (!mousePosition.featureAndGlyphUnderMouse) {
84
- return clickedFeatures
85
- }
86
- clickedFeatures.push(mousePosition.featureAndGlyphUnderMouse.feature)
87
- for (const x of getParents(mousePosition.featureAndGlyphUnderMouse.feature)) {
88
- clickedFeatures.push(x)
81
+ const relatedFeatures: AnnotationFeature[] = []
82
+ relatedFeatures.push(feature)
83
+ for (const x of getParents(feature)) {
84
+ relatedFeatures.push(x)
89
85
  }
90
- const { bp } = mousePosition
91
- const children = getChildren(mousePosition.featureAndGlyphUnderMouse.feature)
86
+ const children = getChildren(feature)
92
87
  for (const child of children) {
93
88
  if (child.min < bp && child.max >= bp) {
94
- clickedFeatures.push(child)
89
+ relatedFeatures.push(child)
95
90
  }
96
91
  }
97
92
  if (!includeSiblings) {
98
- return clickedFeatures
93
+ return relatedFeatures
99
94
  }
100
95
 
101
96
  // Also add siblings , i.e. features having the same parent as the clicked
102
97
  // one and intersecting the click position
103
- if (mousePosition.featureAndGlyphUnderMouse.feature.parent) {
104
- const siblings =
105
- mousePosition.featureAndGlyphUnderMouse.feature.parent.children
98
+ if (feature.parent) {
99
+ const siblings = feature.parent.children
106
100
  if (siblings) {
107
101
  for (const [, sib] of siblings) {
108
- if (sib._id == mousePosition.featureAndGlyphUnderMouse.feature._id) {
102
+ if (sib._id == feature._id) {
109
103
  continue
110
104
  }
111
105
  if (sib.min < bp && sib.max >= bp) {
112
- clickedFeatures.push(sib)
106
+ relatedFeatures.push(sib)
113
107
  }
114
108
  }
115
109
  }
116
110
  }
117
- return clickedFeatures
111
+ return relatedFeatures
118
112
  }
@@ -0,0 +1,149 @@
1
+ import { type CheckResultIdsType } from '@apollo-annotation/mst'
2
+ import { makeStyles } from 'tss-react/mui'
3
+
4
+ export { default as EditZoomThresholdDialog } from '../components/EditZoomThresholdDialog'
5
+
6
+ export type Coord = [number, number]
7
+
8
+ export const useStyles = makeStyles()((theme) => ({
9
+ canvasContainer: {
10
+ position: 'relative',
11
+ left: 0,
12
+ },
13
+ canvas: {
14
+ position: 'absolute',
15
+ left: 0,
16
+ },
17
+ center: {
18
+ display: 'flex',
19
+ justifyContent: 'center',
20
+ },
21
+ ellipses: {
22
+ textOverflow: 'ellipsis',
23
+ overflow: 'hidden',
24
+ },
25
+ avatar: {
26
+ position: 'static',
27
+ height: '100%',
28
+ width: '100%',
29
+ overflow: 'visible',
30
+ color: theme.palette.warning.light,
31
+ backgroundColor: theme.palette.warning.contrastText,
32
+ },
33
+ box: {
34
+ position: 'absolute',
35
+ overflow: 'visible',
36
+ },
37
+ badge: {
38
+ display: 'inline-block',
39
+ },
40
+ loading: {
41
+ position: 'absolute',
42
+ right: theme.spacing(3),
43
+ zIndex: 10,
44
+ pointerEvents: 'none',
45
+ textAlign: 'right',
46
+ },
47
+ locked: {
48
+ position: 'absolute',
49
+ right: theme.spacing(3),
50
+ top: theme.spacing(6),
51
+ zIndex: 1,
52
+ pointerEvents: 'none',
53
+ textAlign: 'right',
54
+ },
55
+ }))
56
+
57
+ export interface CheckResultCluster<T> {
58
+ _id: string
59
+ message: string
60
+ start: number
61
+ count: number
62
+ members: T[]
63
+ range: { min: number; max: number }
64
+ featureIds: CheckResultIdsType
65
+ }
66
+
67
+ export function clusterResultByMessage<
68
+ T extends {
69
+ _id: string
70
+ start: number
71
+ end: number
72
+ message: string
73
+ ids: CheckResultIdsType
74
+ },
75
+ >(
76
+ items: readonly T[],
77
+ width: number,
78
+ touchesAsOverlap: boolean,
79
+ ): CheckResultCluster<T>[] {
80
+ const byMsg = new Map<string, T[]>()
81
+ for (const it of items) {
82
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
83
+ ;(byMsg.get(it.message) ?? byMsg.set(it.message, []).get(it.message)!).push(
84
+ it,
85
+ )
86
+ }
87
+
88
+ const clusters: CheckResultCluster<T>[] = []
89
+ const overlaps = (aEnd: number, bStart: number) =>
90
+ touchesAsOverlap ? bStart <= aEnd : bStart < aEnd
91
+
92
+ for (const [message, arr] of byMsg.entries()) {
93
+ if (arr.length === 0) {
94
+ continue
95
+ }
96
+
97
+ arr.sort((a, b) => a.start - b.start)
98
+
99
+ let group: T[] = [arr[0]]
100
+ let curMin = arr[0].start
101
+ let curMax = arr[0].start + width
102
+
103
+ const pushResult = () => {
104
+ const starts = group.map((d) => d.start).sort((a, b) => a - b)
105
+ const mid = Math.floor(starts.length / 2)
106
+ const median: number =
107
+ starts.length % 2 ? starts[mid] : (starts[mid - 1] + starts[mid]) / 2
108
+ const clusterId = group[0]._id
109
+ const featureIds = group[0].ids
110
+
111
+ clusters.push({
112
+ _id: clusterId,
113
+ message,
114
+ start: median,
115
+ count: group.length,
116
+ members: [...group],
117
+ range: { min: curMin, max: curMax },
118
+ featureIds,
119
+ })
120
+ }
121
+
122
+ for (let i = 1; i < arr.length; i++) {
123
+ const it = arr[i]
124
+ const itStart = it.start
125
+ const itEnd = itStart + width
126
+
127
+ if (overlaps(curMax, itStart)) {
128
+ group.push(it)
129
+ if (itStart < curMin) {
130
+ curMin = itStart
131
+ }
132
+ if (itEnd > curMax) {
133
+ curMax = itEnd
134
+ }
135
+ } else {
136
+ pushResult()
137
+ group = [it]
138
+ curMin = itStart
139
+ curMax = itEnd
140
+ }
141
+ }
142
+ pushResult()
143
+ }
144
+
145
+ clusters.sort(
146
+ (a, b) => a.message.localeCompare(b.message) || a.start - b.start,
147
+ )
148
+ return clusters
149
+ }