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

vanvalenlab / deepcell-label / 4930566609

pending completion
4930566609

Pull #461

github

GitHub
Merge a5131932f into 18db3ee56
Pull Request #461: Celltype UI toolbar with new toggles

464 of 1193 branches covered (38.89%)

Branch coverage included in aggregate %.

17 of 36 new or added lines in 7 files covered. (47.22%)

1 existing line in 1 file now uncovered.

3212 of 5468 relevant lines covered (58.74%)

535.59 hits per line

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

50.57
/frontend/src/Project/service/labels/cellTypesMachine.js
1
/** Manages cell type labels.
2
 * Broadcasts CELLTYPES event on cellTypes event bus.
3
 * Updates cellTypes based on edits to cells with CELLS, REPLACE, DELETE, NEW, and SWAP events.
4
 * Edits cellTypes with ADD_CELL, ADD_CELLTYPE, REMOVE_CELL, REMOVE_CELLTYPE, EDIT_COLOR, EDIT_NAME events.
5
 */
6

7
import equal from 'fast-deep-equal';
8
import { assign, Machine, send } from 'xstate';
9
import { pure } from 'xstate/lib/actions';
10
import Cells from '../../cells';
11
import { markerPanel } from '../../EditControls/CellTypeControls/CellTypeUI/CellMarkerPanel';
12
import { fromEventBus } from '../eventBus';
13

14
// Adapted from https://stackoverflow.com/questions/5623838/rgb-to-hex-and-hex-to-rgb
15
export const hexToRgb = (hex) => {
19✔
16
  var result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
172✔
17
  return [
172✔
18
    parseInt(result[1], 16) / 255,
19
    parseInt(result[2], 16) / 255,
20
    parseInt(result[3], 16) / 255,
21
    1,
22
  ];
23
};
24

25
/** Removes cell from all cellTypes. */
26
function remove(cellTypes, cell) {
27
  return cellTypes.map((cellType) => {
×
28
    let cells = cellType.cells;
×
29
    cells = cells.filter((oldCell) => oldCell !== cell);
×
30
    return { ...cellType, cells };
×
31
  });
32
}
33

34
function updateFromCells(cellTypes, cells) {
35
  return (
×
36
    cellTypes
37
      // remove cells that no longer exist
38
      .map((cellType) => {
39
        let cellList = cellType.cells;
×
40
        cellList = cellList.filter((cell) => cells.some((newCell) => newCell.cell === cell));
×
41
        return { ...cellType, cells: cellList };
×
42
      })
43
  );
44
}
45

46
const createCellTypesMachine = ({ eventBuses, undoRef }) =>
19✔
47
  Machine(
866✔
48
    {
49
      id: 'cellTypes',
50
      entry: send('REGISTER_LABELS', { to: undoRef }),
51
      invoke: [
52
        { id: 'eventBus', src: fromEventBus('cellTypes', () => eventBuses.cellTypes) },
18✔
53
        { src: fromEventBus('cellTypes', () => eventBuses.load, 'LOADED') },
18✔
54
        { src: fromEventBus('cellTypes', () => eventBuses.labeled, 'SET_FEATURE') },
18✔
55
        { src: fromEventBus('cellTypes', () => eventBuses.cells) }, // listen for edit events (REPLACE, SWAP, REMOVE) and CELLS (generic updates)
18✔
56
      ],
57
      context: {
58
        cellTypes: [],
59
        feature: 0,
60
        numCells: null,
61
        colorMap: null,
62
        markerPanel: markerPanel,
63
        isOn: [],
64
        opacities: [],
65
        undoRef: undoRef,
66
        historyRef: null,
67
        edit: null,
68
      },
69
      initial: 'loading',
70
      states: {
71
        loading: {
72
          type: 'parallel',
73
          states: {
74
            getCellTypes: {
75
              initial: 'waiting',
76
              states: {
77
                waiting: {
78
                  on: {
79
                    LOADED: {
80
                      actions: [
81
                        'setCellTypes',
82
                        'setCells',
83
                        'setMaxId',
84
                        'setOpacities',
85
                        'setIsOn',
86
                        'updateColorMap',
87
                      ],
88
                      target: 'done',
89
                    },
90
                  },
91
                },
92
                done: { type: 'final' },
93
              },
94
            },
95
            getHistoryRef: {
96
              initial: 'waiting',
97
              states: {
98
                waiting: {
99
                  on: {
100
                    LABEL_HISTORY: { target: 'done', actions: 'setHistoryRef' },
101
                  },
102
                },
103
                done: { type: 'final' },
104
              },
105
            },
106
          },
107
          onDone: { target: 'loaded' },
108
        },
109
        loaded: {
110
          type: 'parallel',
111
          states: {
112
            edit: {
113
              initial: 'idle',
114
              states: {
115
                idle: {
116
                  entry: ['sendCellTypes', 'updateColorMap'],
117
                  on: {
118
                    // From editCellTypesMachine
119
                    ADD_CELL: { actions: 'addCell', target: 'editing' },
120
                    MULTI_ADD_CELLS: { actions: 'addCells', target: 'editing' },
121
                    ADD_PREDICTIONS: { actions: 'addPredictions', target: 'editing' },
122
                    MULTI_REMOVE_CELLS: { actions: 'removeCells', target: 'editing' },
123
                    ADD_CELLTYPE: {
124
                      actions: ['addCellType', 'addIsOn', 'addOpacity', 'setMaxId'],
125
                      target: 'editing',
126
                    },
127
                    REMOVE_CELLTYPE: {
128
                      actions: ['removeCellType', 'removeIsOn'],
129
                      target: 'editing',
130
                    },
131
                    REMOVE_CELL: { actions: 'removeCell', target: 'editing' },
132
                    EDIT_COLOR: { actions: 'editColor', target: 'editing' },
133
                    EDIT_NAME: { actions: 'editName', target: 'editing' },
134
                  },
135
                },
136
                editing: {
137
                  entry: 'setEditEvent',
138
                  type: 'parallel',
139
                  states: {
140
                    getEdit: {
141
                      entry: 'startEdit',
142
                      initial: 'idle',
143
                      states: {
144
                        idle: { on: { SAVE: { target: 'done', actions: 'setEdit' } } },
145
                        done: { type: 'final' },
146
                      },
147
                    },
148
                    getEdits: {
149
                      initial: 'editing',
150
                      states: {
151
                        editing: {
152
                          on: {
153
                            EDITED_CELLTYPES: { target: 'done', actions: 'setEditedCellTypes' },
154
                          },
155
                        },
156
                        done: { type: 'final' },
157
                      },
158
                    },
159
                  },
160
                  onDone: {
161
                    target: 'idle',
162
                    actions: 'finishEditing',
163
                  },
164
                },
165
              },
166
            },
167
            update: {
168
              on: {
169
                // from CELLS event bus
170
                // REPLACE: { actions: ['replace', 'sendCellTypes'] },
171
                DELETE: { actions: ['delete', 'sendCellTypes'] },
172
                // SWAP: { actions: ['swap', 'sendCellTypes'] },
173
                EDITED_CELLS: { actions: ['setCells', 'updateFromCells', 'updateColorMap'] },
174
                EDIT_IS_ON: { actions: ['editIsOn', 'updateColorMap'] },
175
                TOGGLE_ALL_ON: { actions: ['toggleAll', 'updateColorMap'] },
176
                TOGGLE_ALL_OFF: { actions: ['untoggleAll', 'updateColorMap'] },
177
                EDIT_OPACITY: { actions: ['editOpacities', 'updateColorMap'] },
178
                EDIT_MARKER_PANEL: { actions: ['editMarkerPanel'] },
179
                CELLS: { actions: 'setCells' },
180
                RESTORE: { actions: ['restore', 'updateColorMap'] },
181
                SET_FEATURE: { actions: ['setFeature', 'updateColorMap'] },
182
              },
183
            },
184
          },
185
        },
186
      },
187
    },
188
    {
189
      actions: {
190
        // Set specified context or parameters
191
        setHistoryRef: assign({ historyRef: (_, __, meta) => meta._event.origin }),
18✔
192
        setCellTypes: assign({ cellTypes: (_, evt) => evt.cellTypes }),
13✔
193
        setCells: assign({ numCells: (_, evt) => new Cells(evt.cells).getNewCell() }),
39✔
194
        setIsOn: assign({ isOn: (ctx) => Array(ctx.maxId + 1).fill(true) }),
13✔
195
        setOpacities: assign({ opacities: (ctx) => Array(ctx.maxId + 1).fill(0.3) }),
13✔
196
        setEditEvent: assign({ editEvent: (_, evt) => evt }),
12✔
197
        setFeature: assign({ feature: (_, evt) => evt.feature }),
×
198
        startEdit: send('SAVE', { to: (ctx) => ctx.undoRef }),
12✔
199
        setEdit: assign({ edit: (_, evt) => evt.edit }),
12✔
200
        setEditedCellTypes: assign({ editedCellTypes: (_, evt) => evt.cellTypes }),
12✔
201

202
        // Get the next highest id for the next cell type to add
203
        setMaxId: assign({
204
          maxId: (ctx) => {
205
            const ids = ctx.cellTypes.map((cellType) => cellType.id);
19✔
206
            if (ids.length === 0) {
19✔
207
              return 0;
18✔
208
            }
209
            return Math.max.apply(null, ids);
1✔
210
          },
211
        }),
212

213
        // Send an event to event bus that cell types have been edited
214
        sendCellTypes: send((ctx) => ({ type: 'CELLTYPES', cellTypes: ctx.cellTypes }), {
25✔
215
          to: 'eventBus',
216
        }),
217

218
        // Re-calculate the color map for rendering cell types
219
        updateColorMap: pure(() =>
220
          assign({
41✔
221
            colorMap: (ctx) => {
222
              let cellTypes = ctx.cellTypes.filter((cellType) => cellType.feature === ctx.feature);
41✔
223
              let numTypes = cellTypes.length;
41✔
224
              let newColorMap = Array(ctx.numCells).fill([0, 0, 0, 0]);
41✔
225
              for (let i = 0; i < numTypes; i++) {
41✔
226
                const cellType = cellTypes[i];
18✔
227
                let numCells = cellType.cells.length;
18✔
228
                if (ctx.isOn[cellType.id]) {
18✔
229
                  for (let j = 0; j < numCells; j++) {
15✔
230
                    let oldColor = newColorMap[cellType.cells[j]];
4✔
231
                    let newColor = hexToRgb(cellType.color);
4✔
232
                    const sr = newColor[0];
4✔
233
                    const sg = newColor[1];
4✔
234
                    const sb = newColor[2];
4✔
235
                    const sa = ctx.opacities[cellType.id];
4✔
236
                    const r = oldColor[0] + sr - oldColor[0] * sr;
4✔
237
                    const g = oldColor[1] + sg - oldColor[1] * sg;
4✔
238
                    const b = oldColor[2] + sb - oldColor[2] * sb;
4✔
239
                    const a = oldColor[3];
4✔
240
                    newColorMap[cellType.cells[j]] = [r, g, b, Math.max(sa, a)];
4✔
241
                  }
242
                }
243
              }
244
              return newColorMap;
41✔
245
            },
246
          })
247
        ),
248

249
        delete: pure((ctx, evt) => {
250
          let cellTypes = remove(ctx.cellTypes, evt.cell);
×
251
          const before = { type: 'RESTORE', cellTypes: ctx.cellTypes };
×
252
          const after = { type: 'RESTORE', cellTypes: cellTypes };
×
253
          return [
×
254
            assign({ cellTypes }),
255
            send({ type: 'SNAPSHOT', edit: evt.edit, before, after }, { to: ctx.historyRef }),
256
          ];
257
        }),
258

259
        updateFromCells: pure((ctx, evt) => {
260
          let cellTypes = updateFromCells(ctx.cellTypes, evt.cells);
×
261
          if (!equal(cellTypes, ctx.cellTypes)) {
×
262
            const before = { type: 'RESTORE', cellTypes: ctx.cellTypes };
×
263
            const after = { type: 'RESTORE', cellTypes: cellTypes };
×
264
            return [
×
265
              assign({ cellTypes }),
266
              send({ type: 'SNAPSHOT', edit: evt.edit, before, after }, { to: ctx.historyRef }),
267
              send({ type: 'CELLTYPES', cellTypes }, { to: 'eventBus' }),
268
            ];
269
          }
270
          return [];
×
271
        }),
272

273
        finishEditing: pure((ctx) => {
274
          return [
12✔
275
            assign({ cellTypes: (ctx) => ctx.editedCellTypes }),
12✔
276
            send(
277
              {
278
                type: 'SNAPSHOT',
279
                before: { type: 'RESTORE', cellTypes: ctx.cellTypes },
280
                after: { type: 'RESTORE', cellTypes: ctx.editedCellTypes },
281
                edit: ctx.edit,
282
              },
283
              { to: ctx.historyRef }
284
            ),
285
          ];
286
        }),
287

288
        // Add one cell to a specified cell type
289
        addCell: send((ctx, evt) => {
290
          let cellTypes;
291
          if (evt.mode === 'overwrite') {
2!
292
            cellTypes = ctx.cellTypes.map((cellType) =>
2✔
293
              cellType.id === evt.cellType && !cellType.cells.includes(evt.cell)
2!
294
                ? {
295
                    ...cellType,
296
                    cells: [...cellType.cells, evt.cell].sort(function (a, b) {
297
                      return a - b;
1✔
298
                    }),
299
                  }
300
                : {
301
                    ...cellType,
NEW
302
                    cells: cellType.cells.filter((cell) => cell !== evt.cell),
×
303
                  }
304
            );
NEW
305
          } else if (evt.mode === 'multiLabel') {
×
NEW
306
            cellTypes = ctx.cellTypes.map((cellType) =>
×
NEW
307
              cellType.id === evt.cellType && !cellType.cells.includes(evt.cell)
×
308
                ? {
309
                    ...cellType,
310
                    cells: [...cellType.cells, evt.cell].sort(function (a, b) {
NEW
311
                      return a - b;
×
312
                    }),
313
                  }
314
                : cellType
315
            );
316
          }
317
          return { type: 'EDITED_CELLTYPES', cellTypes };
2✔
318
        }),
319

320
        // Add a list of cells to a specified cell type
321
        addCells: send((ctx, evt) => {
322
          let cellTypes;
323
          const oldCells = ctx.cellTypes.filter((cellType) => cellType.id === evt.cellType)[0]
×
324
            .cells;
325
          const newCells = evt.cells.filter((cell) => !oldCells.includes(cell));
×
NEW
326
          if (evt.mode === 'overwrite') {
×
NEW
327
            cellTypes = ctx.cellTypes.map((cellType) =>
×
NEW
328
              cellType.id === evt.cellType
×
329
                ? {
330
                    ...cellType,
331
                    cells: [...cellType.cells, ...newCells].sort(function (a, b) {
NEW
332
                      return a - b;
×
333
                    }),
334
                  }
335
                : {
336
                    ...cellType,
NEW
337
                    cells: cellType.cells.filter((cell) => !evt.cells.includes(cell)),
×
338
                  }
339
            );
NEW
340
          } else if (evt.mode === 'multiLabel') {
×
NEW
341
            cellTypes = ctx.cellTypes.map((cellType) =>
×
NEW
342
              cellType.id === evt.cellType
×
343
                ? {
344
                    ...cellType,
345
                    cells: [...cellType.cells, ...newCells].sort(function (a, b) {
NEW
346
                      return a - b;
×
347
                    }),
348
                  }
349
                : cellType
350
            );
351
          }
UNCOV
352
          return { type: 'EDITED_CELLTYPES', cellTypes };
×
353
        }),
354

355
        // Add a prediction map of cells with their label
356
        addPredictions: send((ctx, evt) => {
357
          let cellTypes = ctx.cellTypes;
×
358
          const pred = evt.predictions;
×
359
          for (let cell in pred) {
×
360
            cellTypes = cellTypes.map((cellType) =>
×
361
              cellType.id === pred[cell] + 1 && !cellType.cells.includes(cell) && cell > 0
×
362
                ? {
363
                    ...cellType,
364
                    cells: [...cellType.cells, parseInt(cell)].sort(function (a, b) {
365
                      return a - b;
×
366
                    }),
367
                  }
368
                : cellType
369
            );
370
          }
371
          return { type: 'EDITED_CELLTYPES', cellTypes };
×
372
        }),
373

374
        // Remove a single cell from a specified cell type
375
        removeCell: send((ctx, evt) => {
376
          let cellTypes;
377
          cellTypes = ctx.cellTypes.map((cellType) =>
1✔
378
            cellType.id === evt.cellType
1!
379
              ? { ...cellType, cells: cellType.cells.filter((cell) => !(cell === evt.cell)) }
2✔
380
              : cellType
381
          );
382
          return { type: 'EDITED_CELLTYPES', cellTypes };
1✔
383
        }),
384

385
        // Removes a list of cells from a specified cell type
386
        removeCells: send((ctx, evt) => {
387
          let cellTypes;
388
          const removedCells = evt.cells;
×
389
          cellTypes = ctx.cellTypes.map((cellType) =>
×
390
            cellType.id === evt.cellType
×
391
              ? {
392
                  ...cellType,
393
                  cells: cellType.cells.filter((cell) => !removedCells.includes(cell)),
×
394
                }
395
              : cellType
396
          );
397
          return { type: 'EDITED_CELLTYPES', cellTypes };
×
398
        }),
399

400
        // Add a new empty cell type with a specified color
401
        addCellType: send((ctx, evt) => {
402
          let cellTypes;
403
          cellTypes = [
6✔
404
            ...ctx.cellTypes,
405
            {
406
              id: ctx.maxId + 1,
407
              feature: ctx.feature,
408
              name: `Untitled ${ctx.maxId + 1}`,
409
              color: evt.color,
410
              cells: [],
411
            },
412
          ];
413
          return { type: 'EDITED_CELLTYPES', cellTypes };
6✔
414
        }),
415

416
        // Remove a specified cell type
417
        removeCellType: send((ctx, evt) => {
418
          let cellTypes;
419
          cellTypes = ctx.cellTypes.filter((item) => !(item.id === evt.cellType));
1✔
420
          return { type: 'EDITED_CELLTYPES', cellTypes };
1✔
421
        }),
422

423
        // Edit the color of a specified cell type
424
        editColor: send((ctx, evt) => {
425
          let cellTypes;
426
          cellTypes = ctx.cellTypes.map((cellType) =>
1✔
427
            cellType.id === evt.cellType ? { ...cellType, color: evt.color } : cellType
1!
428
          );
429
          return { type: 'EDITED_CELLTYPES', cellTypes };
1✔
430
        }),
431

432
        // Edit the name of a specified cell type
433
        editName: send((ctx, evt) => {
434
          let cellTypes;
435
          cellTypes = ctx.cellTypes.map((cellType) =>
1✔
436
            cellType.id === evt.cellType ? { ...cellType, name: evt.name } : cellType
1!
437
          );
438
          return { type: 'EDITED_CELLTYPES', cellTypes };
1✔
439
        }),
440

441
        // Toggle a specified cell type on/off for the color map
442
        editIsOn: assign({
443
          isOn: (ctx, evt) => {
444
            let isOn = ctx.isOn;
2✔
445
            isOn[evt.cellType] = !isOn[evt.cellType];
2✔
446
            return isOn;
2✔
447
          },
448
        }),
449

450
        // Make a specified edit to a row in the marker panel
451
        editMarkerPanel: assign({
452
          markerPanel: (ctx, evt) => {
453
            const editedPanel = ctx.markerPanel.map((row) =>
×
454
              row.id === evt.id ? { ...row, [evt.field]: evt.data } : row
×
455
            );
456
            return editedPanel;
×
457
          },
458
        }),
459

460
        // Toggle all cell types on for the color map
461
        toggleAll: assign({
462
          isOn: (ctx) => {
463
            let isOn = ctx.isOn.map((t) => 1);
×
464
            return isOn;
×
465
          },
466
        }),
467

468
        // Toggle all cell types off for the color map
469
        untoggleAll: assign({
470
          isOn: (ctx) => {
471
            let isOn = ctx.isOn.map((t) => 0);
3✔
472
            return isOn;
1✔
473
          },
474
        }),
475

476
        // Edit the opacity of a specified cell type
477
        editOpacities: assign({
478
          opacities: (ctx, evt) => {
479
            let opacities = ctx.opacities;
×
480
            opacities[evt.cellType] = evt.opacity;
×
481
            return opacities;
×
482
          },
483
        }),
484

485
        // Add a new cell type to track for color map toggling
486
        addIsOn: assign({
487
          isOn: (ctx) => {
488
            let isOn = ctx.isOn;
6✔
489
            isOn.push(true);
6✔
490
            return isOn;
6✔
491
          },
492
        }),
493

494
        // Remove a cell type to track for color map toggling
495
        removeIsOn: assign({
496
          isOn: (ctx, evt) => {
497
            const isOn = ctx.isOn.filter((e, i) => i !== evt.cellType);
2✔
498
            return isOn;
1✔
499
          },
500
        }),
501

502
        // Add a new opacity to track for color map
503
        addOpacity: assign({
504
          opacities: (ctx) => {
505
            let opacities = ctx.opacities;
6✔
506
            opacities.push(0.3);
6✔
507
            return opacities;
6✔
508
          },
509
        }),
510

511
        restore: pure((_, evt) => {
512
          return [
×
513
            assign({ cellTypes: evt.cellTypes }),
514
            send({ type: 'CELLTYPES', cellTypes: evt.cellTypes }, { to: 'eventBus' }),
515
          ];
516
        }),
517
      },
518
    }
519
  );
520

521
export default createCellTypesMachine;
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

© 2025 Coveralls, Inc