@aics/vole-app 2.11.0 → 2.13.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.
Files changed (27) hide show
  1. package/es/aics-image-viewer/components/App/ChannelUpdater.js +11 -12
  2. package/es/aics-image-viewer/components/App/index.js +62 -30
  3. package/es/aics-image-viewer/components/AxisClipSliders/index.js +34 -25
  4. package/es/aics-image-viewer/components/BottomPanel/index.js +12 -28
  5. package/es/aics-image-viewer/components/CellViewerCanvasWrapper/index.js +3 -12
  6. package/es/aics-image-viewer/components/ChannelsWidgetRow/index.js +6 -2
  7. package/es/aics-image-viewer/components/ChannelsWidgetRow/styles.css +5 -1
  8. package/es/aics-image-viewer/components/ErrorAlert/index.js +1 -1
  9. package/es/aics-image-viewer/components/ErrorAlert/styles.css +3 -0
  10. package/es/aics-image-viewer/components/StyleProvider/index.js +1 -1
  11. package/es/aics-image-viewer/components/TfEditor/index.js +229 -41
  12. package/es/aics-image-viewer/components/TfEditor/styles.css +19 -7
  13. package/es/aics-image-viewer/components/Toolbar/ViewModeRadioButtons.js +1 -1
  14. package/es/aics-image-viewer/components/Toolbar/index.js +34 -30
  15. package/es/aics-image-viewer/components/ViewerStateProvider/index.js +11 -13
  16. package/es/aics-image-viewer/shared/constants.js +41 -1
  17. package/es/aics-image-viewer/shared/utils/hooks.js +3 -3
  18. package/es/aics-image-viewer/shared/utils/viewerState.js +3 -1
  19. package/package.json +4 -3
  20. package/type-declarations/aics-image-viewer/components/App/types.d.ts +1 -1
  21. package/type-declarations/aics-image-viewer/components/BottomPanel/index.d.ts +2 -2
  22. package/type-declarations/aics-image-viewer/components/CellViewerCanvasWrapper/index.d.ts +2 -2
  23. package/type-declarations/aics-image-viewer/components/TfEditor/index.d.ts +2 -0
  24. package/type-declarations/aics-image-viewer/components/Toolbar/index.d.ts +1 -1
  25. package/type-declarations/aics-image-viewer/components/ViewerStateProvider/types.d.ts +2 -0
  26. package/type-declarations/aics-image-viewer/shared/constants.d.ts +8 -1
  27. package/type-declarations/aics-image-viewer/shared/types.d.ts +2 -2
@@ -1,10 +1,10 @@
1
1
  function _typeof(o) { "@babel/helpers - typeof"; return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (o) { return typeof o; } : function (o) { return o && "function" == typeof Symbol && o.constructor === Symbol && o !== Symbol.prototype ? "symbol" : typeof o; }, _typeof(o); }
2
- function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; }
3
- function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), !0).forEach(function (r) { _defineProperty(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; }
4
2
  function _toConsumableArray(r) { return _arrayWithoutHoles(r) || _iterableToArray(r) || _unsupportedIterableToArray(r) || _nonIterableSpread(); }
5
3
  function _nonIterableSpread() { throw new TypeError("Invalid attempt to spread non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); }
6
4
  function _iterableToArray(r) { if ("undefined" != typeof Symbol && null != r[Symbol.iterator] || null != r["@@iterator"]) return Array.from(r); }
7
5
  function _arrayWithoutHoles(r) { if (Array.isArray(r)) return _arrayLikeToArray(r); }
6
+ function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; }
7
+ function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), !0).forEach(function (r) { _defineProperty(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; }
8
8
  function _defineProperty(e, r, t) { return (r = _toPropertyKey(r)) in e ? Object.defineProperty(e, r, { value: t, enumerable: !0, configurable: !0, writable: !0 }) : e[r] = t, e; }
9
9
  function _toPropertyKey(t) { var i = _toPrimitive(t, "string"); return "symbol" == _typeof(i) ? i : i + ""; }
10
10
  function _toPrimitive(t, r) { if ("object" != _typeof(t) || !t) return t; var e = t[Symbol.toPrimitive]; if (void 0 !== e) { var i = e.call(t, r || "default"); if ("object" != _typeof(i)) return i; throw new TypeError("@@toPrimitive must return a primitive value."); } return ("string" === r ? String : Number)(t); }
@@ -20,7 +20,7 @@ import * as d3 from "d3";
20
20
  import "nouislider/distribute/nouislider.css";
21
21
  import React, { useCallback, useMemo, useRef, useState } from "react";
22
22
  import { SketchPicker } from "react-color";
23
- import { LUT_MAX_PERCENTILE, LUT_MIN_PERCENTILE, TFEDITOR_DEFAULT_COLOR } from "../../shared/constants";
23
+ import { DTYPE_RANGE, LUT_MAX_PERCENTILE, LUT_MIN_PERCENTILE, TFEDITOR_DEFAULT_COLOR } from "../../shared/constants";
24
24
  import { colorArrayToObject, colorArrayToString, colorObjectToArray } from "../../shared/utils/colorRepresentations";
25
25
  import { controlPointsToRamp, rampToControlPoints } from "../../shared/utils/controlPointsToLut";
26
26
  import { useRefWithSetter } from "../../shared/utils/hooks";
@@ -31,8 +31,17 @@ import "./styles.css";
31
31
  var TFEDITOR_COLOR_PICKER_MARGIN_X_PX = 2;
32
32
  /** If a control point is within this distance of the bottom of the screen, open the color picker upward */
33
33
  var TFEDITOR_COLOR_PICKER_OPEN_UPWARD_MARGIN_PX = 310;
34
- var TFEDITOR_GRADIENT_MAX_OPACITY = 0.9;
34
+ var TFEDITOR_GRADIENT_MAX_OPACITY = 0.75;
35
35
  var TFEDITOR_NUM_TICKS = 4;
36
+ /**
37
+ * If the first or last "round" tick mark is within this ratio of the end of the x axis, remove it to get it out of the
38
+ * way of the tick mark right at the end.
39
+ *
40
+ * For instance, if the x range is [0, 255], the last tick mark d3 generates will likely be at 250. That should be
41
+ * removed to get it out of the way of the tick mark at 255! But if the range is [0, 390], it may be that the last tick
42
+ * mark is at 300. It would make no sense to remove that tick mark to make space for one at 390.
43
+ */
44
+ var TFEDITOR_END_TICK_MARGIN = 0.1;
36
45
  var TFEDITOR_MARGINS = {
37
46
  top: 18,
38
47
  right: 20,
@@ -112,6 +121,82 @@ function controlPointToAbsolute(cp, channel) {
112
121
  return u8ToAbsolute(cp.x, channel);
113
122
  }
114
123
 
124
+ /** For when all control points are outside the plot's range: just fill the plot with the settings from 1 point */
125
+ var coverRangeWithPoint = function coverRangeWithPoint(point, plotMin, plotMax) {
126
+ return [_objectSpread(_objectSpread({}, point), {}, {
127
+ x: plotMin
128
+ }), _objectSpread(_objectSpread({}, point), {}, {
129
+ x: plotMax
130
+ })];
131
+ };
132
+
133
+ /** For when some control points are outside the plot's range: create an intermediate point on the edge of the range */
134
+ var createPointOnRangeBoundary = function createPointOnRangeBoundary(outOfRangePt, inRangePt, x) {
135
+ var rangeRatio = (x - outOfRangePt.x) / (inRangePt.x - outOfRangePt.x);
136
+ var opacity = outOfRangePt.opacity + (inRangePt.opacity - outOfRangePt.opacity) * rangeRatio;
137
+ var color = outOfRangePt.color.map(function (c, i) {
138
+ return c + (inRangePt.color[i] - c) * rangeRatio;
139
+ });
140
+ return {
141
+ x: x,
142
+ opacity: opacity,
143
+ color: color
144
+ };
145
+ };
146
+
147
+ /**
148
+ * Ensures the list of `controlPoints` exactly covers the range from `plotMin` to `plotMax` by removing any
149
+ * out-of-range points and adding new points right at the edges of the range.
150
+ */
151
+ var fitControlPointsToRange = function fitControlPointsToRange(controlPoints, plotMin, plotMax) {
152
+ var points = controlPoints.slice();
153
+ var firstPoint = points[0];
154
+ var lastPoint = points[points.length - 1];
155
+
156
+ // If all control points are outside the range, just fill the range with the first or last point.
157
+ if (lastPoint.x < plotMin) {
158
+ return coverRangeWithPoint(lastPoint, plotMin, plotMax);
159
+ }
160
+ if (firstPoint.x > plotMax) {
161
+ return coverRangeWithPoint(firstPoint, plotMin, plotMax);
162
+ }
163
+ if (firstPoint.x > plotMin) {
164
+ // If the control points don't go all the way to the min, add a new point at the min.
165
+ points.unshift(_objectSpread(_objectSpread({}, firstPoint), {}, {
166
+ x: plotMin
167
+ }));
168
+ } else {
169
+ // If some control points are out of range, remove those points...
170
+ var outOfRangePoint = undefined;
171
+ while (points[0].x < plotMin && points.length > 1) {
172
+ outOfRangePoint = points.shift();
173
+ }
174
+
175
+ // ...and create a new point at the edge of the range.
176
+ if (outOfRangePoint !== undefined) {
177
+ points.unshift(createPointOnRangeBoundary(outOfRangePoint, points[0], plotMin));
178
+ }
179
+ }
180
+ if (lastPoint.x < plotMax) {
181
+ // If the control points don't go all the way to the max, add a new point at the max.
182
+ points.push(_objectSpread(_objectSpread({}, lastPoint), {}, {
183
+ x: plotMax
184
+ }));
185
+ } else {
186
+ // If some control points are out of range, remove those points...
187
+ var _outOfRangePoint = undefined;
188
+ while (points[points.length - 1].x > plotMax && points.length > 1) {
189
+ _outOfRangePoint = points.pop();
190
+ }
191
+
192
+ // ...and create a new point at the edge of the range.
193
+ if (_outOfRangePoint !== undefined) {
194
+ points.push(createPointOnRangeBoundary(_outOfRangePoint, points[points.length - 1], plotMax));
195
+ }
196
+ }
197
+ return points;
198
+ };
199
+
115
200
  /** Defines an SVG gradient with id `id` based on the provided `controlPoints` */
116
201
  var ControlPointGradientDef = function ControlPointGradientDef(_ref) {
117
202
  var controlPoints = _ref.controlPoints,
@@ -165,7 +250,9 @@ var clamp = function clamp(value, min, max) {
165
250
  return Math.min(Math.max(value, min), max);
166
251
  };
167
252
  var TfEditor = function TfEditor(props) {
168
- var changeChannelSetting = props.changeChannelSetting;
253
+ var changeChannelSetting = props.changeChannelSetting,
254
+ plotMin = props.plotMin,
255
+ plotMax = props.plotMax;
169
256
  var innerWidth = props.width - TFEDITOR_MARGINS.left - TFEDITOR_MARGINS.right;
170
257
  var innerHeight = props.height - TFEDITOR_MARGINS.top - TFEDITOR_MARGINS.bottom;
171
258
  var _useState = useState(null),
@@ -206,11 +293,30 @@ var TfEditor = function TfEditor(props) {
206
293
  var lastColorRef = useRef(TFEDITOR_DEFAULT_COLOR);
207
294
  var svgRef = useRef(null); // need access to SVG element to measure mouse position
208
295
 
296
+ var _props$channelData = props.channelData,
297
+ rawMin = _props$channelData.rawMin,
298
+ rawMax = _props$channelData.rawMax,
299
+ dtype = _props$channelData.dtype,
300
+ histogram = _props$channelData.histogram;
301
+ var typeRange = DTYPE_RANGE[dtype];
302
+
209
303
  // d3 scales define the mapping between data and screen space (and do the heavy lifting of generating plot axes)
210
304
  /** `xScale` is in raw intensity range, not U8 range. We use `u8ToAbsolute` and `absoluteToU8` to translate to U8. */
211
305
  var xScale = useMemo(function () {
212
- return d3.scaleLinear().domain([props.channelData.rawMin, props.channelData.rawMax]).range([0, innerWidth]);
213
- }, [innerWidth, props.channelData.rawMin, props.channelData.rawMax]);
306
+ return d3.scaleLinear().domain([plotMin, plotMax]).range([0, innerWidth]);
307
+ }, [innerWidth, plotMin, plotMax]);
308
+ var plotMinU8 = useMemo(function () {
309
+ return absoluteToU8(plotMin, {
310
+ rawMin: rawMin,
311
+ rawMax: rawMax
312
+ });
313
+ }, [plotMin, rawMin, rawMax]);
314
+ var plotMaxU8 = useMemo(function () {
315
+ return absoluteToU8(plotMax, {
316
+ rawMin: rawMin,
317
+ rawMax: rawMax
318
+ });
319
+ }, [plotMax, rawMin, rawMax]);
214
320
  var yScale = useMemo(function () {
215
321
  return d3.scaleLinear().domain([0, 1]).range([innerHeight, 0]);
216
322
  }, [innerHeight]);
@@ -347,19 +453,23 @@ var TfEditor = function TfEditor(props) {
347
453
  }
348
454
  };
349
455
  var controlPointsToRender = useMemo(function () {
350
- return props.useControlPoints ? props.controlPoints : rampToControlPoints(props.ramp);
351
- }, [props.controlPoints, props.ramp, props.useControlPoints]);
456
+ var points = props.useControlPoints ? props.controlPoints.slice() : rampToControlPoints(props.ramp);
457
+ return fitControlPointsToRange(points, plotMinU8, plotMaxU8);
458
+ }, [props.controlPoints, props.ramp, props.useControlPoints, plotMinU8, plotMaxU8]);
352
459
 
353
460
  /** d3-generated svg data string representing both the line between points and the region filled with gradient */
354
461
  var areaPath = useMemo(function () {
355
462
  var _areaGenerator;
356
463
  var areaGenerator = d3.area().x(function (d) {
357
- return xScale(controlPointToAbsolute(d, props.channelData));
464
+ return xScale(controlPointToAbsolute(d, {
465
+ rawMin: rawMin,
466
+ rawMax: rawMax
467
+ }));
358
468
  }).y0(function (d) {
359
469
  return yScale(d.opacity);
360
470
  }).y1(innerHeight).curve(d3.curveLinear);
361
471
  return (_areaGenerator = areaGenerator(controlPointsToRender)) !== null && _areaGenerator !== void 0 ? _areaGenerator : undefined;
362
- }, [controlPointsToRender, xScale, yScale, innerHeight]);
472
+ }, [controlPointsToRender, xScale, yScale, innerHeight, rawMin, rawMax]);
363
473
 
364
474
  /** d3-generated svg data string representing the "basic mode" min/max slider handles */
365
475
  var sliderHandlePath = useMemo(function () {
@@ -372,8 +482,27 @@ var TfEditor = function TfEditor(props) {
372
482
  // identity changes (i.e. whenever their dependencies change).
373
483
 
374
484
  var xAxisRef = useCallback(function (el) {
485
+ // generate tick marks
375
486
  var ticks = xScale.ticks(TFEDITOR_NUM_TICKS);
376
- ticks[ticks.length - 1] = xScale.domain()[1];
487
+
488
+ // make sure we have sensible tick marks right at the min and max of the x axis
489
+ var _xScale$domain = xScale.domain(),
490
+ _xScale$domain2 = _slicedToArray(_xScale$domain, 2),
491
+ min = _xScale$domain2[0],
492
+ max = _xScale$domain2[1];
493
+ var domain = max - min;
494
+ if ((ticks[0] - min) / domain < TFEDITOR_END_TICK_MARGIN) {
495
+ ticks[0] = min;
496
+ } else {
497
+ ticks.unshift(min);
498
+ }
499
+ if ((ticks[ticks.length - 1] - min) / domain > 1 - TFEDITOR_END_TICK_MARGIN) {
500
+ ticks[ticks.length - 1] = max;
501
+ } else {
502
+ ticks.push(max);
503
+ }
504
+
505
+ // now make the axis!
377
506
  d3.select(el).call(d3.axisBottom(xScale).tickValues(ticks).tickPadding(props.useControlPoints ? 3 : 10) // get tick labels out of the way of sliders in "basic" mode
378
507
  );
379
508
  }, [xScale, props.useControlPoints]);
@@ -384,30 +513,37 @@ var TfEditor = function TfEditor(props) {
384
513
  if (el === null) {
385
514
  return;
386
515
  }
387
- if (props.channelData.histogram.getNumBins() < 1) {
516
+ var numBins = histogram.getNumBins();
517
+ if (numBins < 1) {
388
518
  return;
389
519
  }
390
- var _getHistogramBinLengt = getHistogramBinLengths(props.channelData.histogram),
520
+ var _getHistogramBinLengt = getHistogramBinLengths(histogram),
391
521
  binLengths = _getHistogramBinLengt.binLengths,
392
522
  max = _getHistogramBinLengt.max;
393
- var barWidth = innerWidth / props.channelData.histogram.getNumBins();
523
+ var start = Math.max(0, Math.ceil(plotMinU8));
524
+ var end = Math.min(numBins, Math.floor(plotMaxU8));
525
+ var binLengthsToRender = binLengths.slice(start, end);
526
+ var barWidth = innerWidth / (plotMaxU8 - plotMinU8);
394
527
  var binScale = d3.scaleLog().domain([0.1, max]).range([innerHeight, 0]).base(2).clamp(true);
395
528
  d3.select(el).selectAll(".bar") // select all the bars of the histogram
396
- .data(binLengths) // bind the histogram bins to this selection
529
+ .data(binLengthsToRender) // bind the histogram bins to this selection
397
530
  .join("rect") // ensure we have exactly as many bound `rect` elements in the DOM as we have histogram bins
398
531
  .attr("class", "bar").attr("width", barWidth).attr("x", function (_len, idx) {
399
- return xScale(u8ToAbsolute(idx, props.channelData));
532
+ return xScale(u8ToAbsolute(idx + start, {
533
+ rawMin: rawMin,
534
+ rawMax: rawMax
535
+ }));
400
536
  }) // set position and height from data
401
537
  .attr("y", function (len) {
402
538
  return binScale(len);
403
539
  }).attr("height", function (len) {
404
540
  return innerHeight - binScale(len);
405
541
  });
406
- }, [props.channelData.histogram, innerWidth, innerHeight]);
542
+ }, [xScale, rawMin, rawMax, histogram, innerWidth, innerHeight, plotMinU8, plotMaxU8]);
407
543
  var applyTFGenerator = useCallback(function (generator) {
408
544
  setSelectedPointIdx(null);
409
545
  lastColorRef.current = TFEDITOR_DEFAULT_COLOR;
410
- var lut = TF_GENERATORS[generator](props.channelData.histogram);
546
+ var lut = TF_GENERATORS[generator](histogram);
411
547
  if (props.useControlPoints) {
412
548
  setControlPoints(lut.controlPoints.map(function (cp) {
413
549
  return _objectSpread(_objectSpread({}, cp), {}, {
@@ -417,12 +553,13 @@ var TfEditor = function TfEditor(props) {
417
553
  } else {
418
554
  setRamp(controlPointsToRamp(lut.controlPoints));
419
555
  }
420
- }, [props.channelData.histogram, props.useControlPoints]);
556
+ }, [histogram, props.useControlPoints, setControlPoints, setRamp]);
421
557
  var createTFGeneratorButton = function createTFGeneratorButton(generator, name, description) {
422
558
  return /*#__PURE__*/React.createElement(Tooltip, {
423
559
  title: description,
424
560
  placement: "top"
425
561
  }, /*#__PURE__*/React.createElement(Button, {
562
+ size: "small",
426
563
  onClick: function onClick() {
427
564
  return applyTFGenerator(generator);
428
565
  }
@@ -430,7 +567,10 @@ var TfEditor = function TfEditor(props) {
430
567
  };
431
568
 
432
569
  // create one svg circle element for each control point
433
- var controlPointCircles = props.useControlPoints ? props.controlPoints.map(function (cp, i) {
570
+ var controlPointCircles = props.useControlPoints ? props.controlPoints.filter(function (cp) {
571
+ return plotMinU8 <= cp.x && cp.x <= plotMaxU8;
572
+ }) // filter out-of-range points
573
+ .map(function (cp, i) {
434
574
  return /*#__PURE__*/React.createElement("circle", {
435
575
  key: i,
436
576
  className: i === selectedPointIdx ? "selected" : "",
@@ -446,14 +586,14 @@ var TfEditor = function TfEditor(props) {
446
586
  onContextMenu: handleControlPointContextMenu
447
587
  });
448
588
  }) : null;
449
- // move selected control point to the end so it's not occluded by other nearby points
589
+ // move selected control point to the end so it's drawn last and not occluded by other nearby points
450
590
  if (controlPointCircles !== null && selectedPointIdx !== null) {
451
591
  controlPointCircles.push(controlPointCircles.splice(selectedPointIdx, 1)[0]);
452
592
  }
453
593
  var viewerModeString = props.useControlPoints ? "advanced" : "basic";
454
594
  return /*#__PURE__*/React.createElement("div", null, /*#__PURE__*/React.createElement("div", {
455
595
  className: "button-row"
456
- }, createTFGeneratorButton("resetXF", "None", "Reset transfer function to full range."), createTFGeneratorButton("auto98XF", "Default", "Ramp from 50th percentile to 98th."), createTFGeneratorButton("auto2XF", "IJ Auto", "Emulates ImageJ's \"auto\" button."), createTFGeneratorButton("bestFitXF", "Auto 2", "Ramp over the middle 80% of data."), /*#__PURE__*/React.createElement(Checkbox, {
596
+ }, createTFGeneratorButton("auto98XF", "Default", "Ramp from 50th percentile to 98th"), createTFGeneratorButton("auto2XF", "IJ Auto", "Emulates ImageJ's \"auto\" button"), createTFGeneratorButton("resetXF", "Auto 1", "Ramp over the full data range (0% to 100%)"), createTFGeneratorButton("bestFitXF", "Auto 2", "Ramp over the middle 80% of data"), /*#__PURE__*/React.createElement(Checkbox, {
457
597
  checked: props.useControlPoints,
458
598
  onChange: function onChange(e) {
459
599
  return changeChannelSetting({
@@ -463,7 +603,29 @@ var TfEditor = function TfEditor(props) {
463
603
  style: {
464
604
  marginLeft: "auto"
465
605
  }
466
- }, "Advanced")), colorPickerPosition !== null && /*#__PURE__*/React.createElement("div", {
606
+ }, "Advanced")), !props.useControlPoints && /*#__PURE__*/React.createElement("div", {
607
+ className: "tf-editor-control-row ramp-row"
608
+ }, "Levels min/max", /*#__PURE__*/React.createElement(InputNumber, {
609
+ value: u8ToAbsolute(props.ramp[0], props.channelData),
610
+ onChange: function onChange(v) {
611
+ return v !== null && setRamp([absoluteToU8(v, props.channelData), props.ramp[1]]);
612
+ },
613
+ formatter: numberFormatter,
614
+ min: typeRange.min,
615
+ max: Math.min(u8ToAbsolute(props.ramp[1], props.channelData), typeRange.max),
616
+ size: "small",
617
+ controls: false
618
+ }), /*#__PURE__*/React.createElement(InputNumber, {
619
+ value: u8ToAbsolute(props.ramp[1], props.channelData),
620
+ onChange: function onChange(v) {
621
+ return v !== null && setRamp([props.ramp[0], absoluteToU8(v, props.channelData)]);
622
+ },
623
+ formatter: numberFormatter,
624
+ min: Math.max(typeRange.min, u8ToAbsolute(props.ramp[0], props.channelData)),
625
+ max: typeRange.max,
626
+ size: "small",
627
+ controls: false
628
+ })), colorPickerPosition !== null && /*#__PURE__*/React.createElement("div", {
467
629
  className: "tf-editor-popover"
468
630
  }, /*#__PURE__*/React.createElement("div", {
469
631
  className: "tf-editor-cover",
@@ -504,7 +666,7 @@ var TfEditor = function TfEditor(props) {
504
666
  className: "axis"
505
667
  }), controlPointCircles, !props.useControlPoints && /*#__PURE__*/React.createElement("g", {
506
668
  className: "ramp-sliders"
507
- }, /*#__PURE__*/React.createElement("g", {
669
+ }, plotMinU8 <= props.ramp[0] && props.ramp[0] <= plotMaxU8 && /*#__PURE__*/React.createElement("g", {
508
670
  transform: "translate(".concat(xScale(u8ToAbsolute(props.ramp[0], props.channelData)), ")")
509
671
  }, /*#__PURE__*/React.createElement("line", {
510
672
  y1: innerHeight,
@@ -523,7 +685,7 @@ var TfEditor = function TfEditor(props) {
523
685
  onPointerDown: function onPointerDown() {
524
686
  return setDraggedPointIdx(TfEditorRampSliderHandle.Min);
525
687
  }
526
- })), /*#__PURE__*/React.createElement("g", {
688
+ })), plotMinU8 <= props.ramp[1] && props.ramp[1] <= plotMaxU8 && /*#__PURE__*/React.createElement("g", {
527
689
  transform: "translate(".concat(xScale(u8ToAbsolute(props.ramp[1], props.channelData)), ")")
528
690
  }, /*#__PURE__*/React.createElement("line", {
529
691
  y1: innerHeight,
@@ -541,28 +703,54 @@ var TfEditor = function TfEditor(props) {
541
703
  onPointerDown: function onPointerDown() {
542
704
  return setDraggedPointIdx(TfEditorRampSliderHandle.Max);
543
705
  }
544
- }))))), !props.useControlPoints && /*#__PURE__*/React.createElement("div", {
545
- className: "tf-editor-numeric-input-row"
546
- }, /*#__PURE__*/React.createElement("span", null, "Min", " ", /*#__PURE__*/React.createElement(InputNumber, {
547
- value: u8ToAbsolute(props.ramp[0], props.channelData),
706
+ }))))), /*#__PURE__*/React.createElement("div", {
707
+ className: "tf-editor-control-row plot-range-row"
708
+ }, "Plot min/max", /*#__PURE__*/React.createElement(InputNumber, {
709
+ value: plotMin,
548
710
  onChange: function onChange(v) {
549
- return v !== null && setRamp([absoluteToU8(v, props.channelData), props.ramp[1]]);
711
+ return v !== null && changeChannelSetting({
712
+ plotMin: v,
713
+ plotMax: Math.max(v + 1, plotMax)
714
+ });
550
715
  },
551
716
  formatter: numberFormatter,
552
- min: 0,
553
- max: Math.min(u8ToAbsolute(props.ramp[1], props.channelData), props.channelData.rawMax),
554
- size: "small"
555
- })), /*#__PURE__*/React.createElement("span", null, "Max", " ", /*#__PURE__*/React.createElement(InputNumber, {
556
- value: u8ToAbsolute(props.ramp[1], props.channelData),
717
+ min: typeRange.min,
718
+ max: typeRange.max - 1,
719
+ size: "small",
720
+ controls: false
721
+ }), /*#__PURE__*/React.createElement(InputNumber, {
722
+ value: plotMax,
557
723
  onChange: function onChange(v) {
558
- return v !== null && setRamp([props.ramp[0], absoluteToU8(v, props.channelData)]);
724
+ return v !== null && changeChannelSetting({
725
+ plotMax: v,
726
+ plotMin: Math.min(v - 1, plotMin)
727
+ });
559
728
  },
560
729
  formatter: numberFormatter,
561
- min: Math.max(0, u8ToAbsolute(props.ramp[0], props.channelData)),
562
- max: props.channelData.rawMax,
730
+ min: typeRange.min + 1,
731
+ max: typeRange.max,
732
+ size: "small",
733
+ controls: false
734
+ }), /*#__PURE__*/React.createElement(Button, {
735
+ size: "small",
736
+ style: {
737
+ marginLeft: "12px"
738
+ },
739
+ onClick: function onClick() {
740
+ return changeChannelSetting({
741
+ plotMin: rawMin,
742
+ plotMax: rawMax
743
+ });
744
+ }
745
+ }, "Fit to data"), /*#__PURE__*/React.createElement(Button, {
563
746
  size: "small",
564
- width: 45
565
- }))), /*#__PURE__*/React.createElement(SliderRow, {
747
+ onClick: function onClick() {
748
+ return changeChannelSetting({
749
+ plotMin: typeRange.min,
750
+ plotMax: typeRange.max
751
+ });
752
+ }
753
+ }, "Full range")), /*#__PURE__*/React.createElement(SliderRow, {
566
754
  label: /*#__PURE__*/React.createElement(Checkbox, {
567
755
  checked: props.colorizeEnabled,
568
756
  onChange: function onChange(e) {
@@ -20,19 +20,31 @@
20
20
  bottom:0;
21
21
  }
22
22
 
23
- .channel-row .tf-editor-numeric-input-row{
23
+ .channel-row .tf-editor-control-row{
24
24
  display:flex;
25
- align-items:center;
26
- justify-content:space-between;
27
- margin-bottom:20px;
28
25
  color:var(--color-controlpanel-text);
26
+ align-items:center;
27
+ min-height:24px;
28
+ gap:5px;
29
29
  }
30
30
 
31
- .channel-row .tf-editor-numeric-input-row .ant-input-number{
31
+ .channel-row .tf-editor-control-row.ramp-row{
32
+ margin-top:15px;
33
+ }
34
+
35
+ .channel-row .tf-editor-control-row.plot-range-row{
36
+ margin-bottom:20px;
37
+ }
38
+
39
+ .channel-row .tf-editor-control-row .ant-input-number{
32
40
  border-color:var(--color-button-tertiary-outline);
33
- width:60px;
41
+ width:64px;
34
42
  }
35
43
 
44
+ .channel-row .tf-editor-control-row .ant-input-number input{
45
+ text-align:right;
46
+ }
47
+
36
48
  .channel-row .tick text{
37
49
  fill:var(--color-controlpanel-text);
38
50
  user-select:none;
@@ -58,7 +70,7 @@
58
70
 
59
71
  .channel-row .line{
60
72
  stroke:#aeacae;
61
- stroke-width:1.5px;
73
+ stroke-width:1px;
62
74
  }
63
75
 
64
76
  .channel-row .ramp-sliders path{
@@ -1,5 +1,5 @@
1
- import React from "react";
2
1
  import { Radio } from "antd";
2
+ import React from "react";
3
3
  import { ViewMode } from "../../shared/enums";
4
4
  var viewModes = [ViewMode.threeD, ViewMode.xy, ViewMode.xz, ViewMode.yz];
5
5
  var ViewModeRadioButtons = function ViewModeRadioButtons(props) {
@@ -4,15 +4,15 @@ function _unsupportedIterableToArray(r, a) { if (r) { if ("string" == typeof r)
4
4
  function _arrayLikeToArray(r, a) { (null == a || a > r.length) && (a = r.length); for (var e = 0, n = Array(a); e < a; e++) n[e] = r[e]; return n; }
5
5
  function _iterableToArrayLimit(r, l) { var t = null == r ? null : "undefined" != typeof Symbol && r[Symbol.iterator] || r["@@iterator"]; if (null != t) { var e, n, i, u, a = [], f = !0, o = !1; try { if (i = (t = t.call(r)).next, 0 === l) { if (Object(t) !== t) return; f = !1; } else for (; !(f = (e = i.call(t)).done) && (a.push(e.value), a.length !== l); f = !0); } catch (r) { o = !0, n = r; } finally { try { if (!f && null != t["return"] && (u = t["return"](), Object(u) !== u)) return; } finally { if (o) throw n; } } return a; } }
6
6
  function _arrayWithHoles(r) { if (Array.isArray(r)) return r; }
7
- import { UndoOutlined } from "@ant-design/icons";
7
+ import { ReloadOutlined } from "@ant-design/icons";
8
8
  import { Button, Radio, Select, Tooltip } from "antd";
9
9
  import { debounce } from "lodash";
10
10
  import React from "react";
11
- import ViewModeRadioButtons from "./ViewModeRadioButtons";
12
- import DownloadButton from "./DownloadButton";
13
- import { connectToViewerState } from "../ViewerStateProvider";
14
11
  import { ImageType, RenderMode, ViewMode } from "../../shared/enums";
15
12
  import ViewerIcon from "../shared/ViewerIcon";
13
+ import { connectToViewerState } from "../ViewerStateProvider";
14
+ import DownloadButton from "./DownloadButton";
15
+ import ViewModeRadioButtons from "./ViewModeRadioButtons";
16
16
  import "./styles.css";
17
17
  var RESIZE_DEBOUNCE_DELAY = 50;
18
18
  var visuallyHiddenStyle = {
@@ -55,34 +55,38 @@ var Toolbar = function Toolbar(props) {
55
55
  setScrollBtnLeft(barEl.scrollLeft > 0);
56
56
  setScrollBtnRight(barEl.scrollLeft < barEl.scrollWidth - barEl.clientWidth);
57
57
  }, []);
58
- var checkSize = React.useCallback(debounce(function () {
59
- if (!leftRef.current || !centerRef.current || !rightRef.current || !barRef.current) {
60
- return;
61
- }
62
- var leftRect = leftRef.current.getBoundingClientRect();
63
- var centerRect = centerRef.current.getBoundingClientRect();
64
- var rightRect = rightRef.current.getBoundingClientRect();
65
58
 
66
- // when calculating width required to leave scroll mode, add a bit of extra width to ensure that triggers
67
- // for entering and leaving scroll mode never overlap (causing toolbar to rapidly switch when resizing)
68
- var SCROLL_OFF_EXTRA_WIDTH = 15;
69
- if (scrollMode) {
70
- // Leave scroll mode if there is enough space for centered controls not to overlap left/right-aligned ones
71
- var barWidth = barRef.current.getBoundingClientRect().width;
72
- var requiredWidth = Math.max(leftRect.width, rightRect.width) * 2 + centerRect.width + SCROLL_OFF_EXTRA_WIDTH;
73
- if (barWidth > requiredWidth) {
74
- setScrollMode(false);
75
- }
76
- } else {
77
- // Enter scroll mode if centered controls are overlapping either left/right-aligned ones
78
- if (leftRect.right > centerRect.left || centerRect.right > rightRect.left) {
79
- setScrollMode(true);
59
+ // This is effectively a `useCallback` - it memoizes a function - but since we're feeding the whole function into
60
+ // lodash's `debounce`, using `useMemo` memoizes the transformation done by `debounce` as well.
61
+ var checkSize = React.useMemo(function () {
62
+ return debounce(function () {
63
+ if (!leftRef.current || !centerRef.current || !rightRef.current || !barRef.current) {
64
+ return;
80
65
  }
81
- }
82
- checkScrollBtnVisible();
83
- }, RESIZE_DEBOUNCE_DELAY), [scrollMode, checkScrollBtnVisible]);
84
- React.useEffect(checkSize, []);
66
+ var leftRect = leftRef.current.getBoundingClientRect();
67
+ var centerRect = centerRef.current.getBoundingClientRect();
68
+ var rightRect = rightRef.current.getBoundingClientRect();
69
+
70
+ // when calculating width required to leave scroll mode, add a bit of extra width to ensure that triggers
71
+ // for entering and leaving scroll mode never overlap (causing toolbar to rapidly switch when resizing)
72
+ var SCROLL_OFF_EXTRA_WIDTH = 15;
73
+ setScrollMode(function (mode) {
74
+ if (mode) {
75
+ // Leave scroll mode if there is enough space for centered controls not to overlap left/right-aligned ones
76
+ var barWidth = barRef.current.getBoundingClientRect().width;
77
+ var requiredWidth = Math.max(leftRect.width, rightRect.width) * 2 + centerRect.width + SCROLL_OFF_EXTRA_WIDTH;
78
+ return barWidth <= requiredWidth;
79
+ } else {
80
+ // Enter scroll mode if centered controls are overlapping either left/right-aligned ones
81
+ return leftRect.right > centerRect.left || centerRect.right > rightRect.left;
82
+ }
83
+ });
84
+ checkScrollBtnVisible();
85
+ }, RESIZE_DEBOUNCE_DELAY);
86
+ }, [checkScrollBtnVisible]);
85
87
  React.useEffect(function () {
88
+ // Make sure `checkSize` is run once on mount
89
+ checkSize();
86
90
  window.addEventListener("resize", checkSize);
87
91
  return function () {
88
92
  return window.removeEventListener("resize", checkSize);
@@ -157,7 +161,7 @@ var Toolbar = function Toolbar(props) {
157
161
  }, /*#__PURE__*/React.createElement(Button, {
158
162
  className: "ant-btn-icon-only btn-borderless",
159
163
  onClick: resetToSavedViewerState
160
- }, /*#__PURE__*/React.createElement(UndoOutlined, null), /*#__PURE__*/React.createElement("span", {
164
+ }, /*#__PURE__*/React.createElement(ReloadOutlined, null), /*#__PURE__*/React.createElement("span", {
161
165
  style: visuallyHiddenStyle
162
166
  }, "Reset to initial settings")))), /*#__PURE__*/React.createElement("div", {
163
167
  className: "viewer-toolbar-center",
@@ -11,6 +11,7 @@ function _defineProperty(e, r, t) { return (r = _toPropertyKey(r)) in e ? Object
11
11
  function _toPropertyKey(t) { var i = _toPrimitive(t, "string"); return "symbol" == _typeof(i) ? i : i + ""; }
12
12
  function _toPrimitive(t, r) { if ("object" != _typeof(t) || !t) return t; var e = t[Symbol.toPrimitive]; if (void 0 !== e) { var i = e.call(t, r || "default"); if ("object" != _typeof(i)) return i; throw new TypeError("@@toPrimitive must return a primitive value."); } return ("string" === r ? String : Number)(t); }
13
13
  function _typeof(o) { "@babel/helpers - typeof"; return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (o) { return typeof o; } : function (o) { return o && "function" == typeof Symbol && o.constructor === Symbol && o !== Symbol.prototype ? "symbol" : typeof o; }, _typeof(o); }
14
+ import { isEqual } from "lodash";
14
15
  import React, { useCallback, useContext, useEffect, useMemo, useReducer, useRef } from "react";
15
16
  import { getDefaultViewerChannelSettings, getDefaultViewerState } from "../../shared/constants";
16
17
  import { RenderMode, ViewMode } from "../../shared/enums";
@@ -150,7 +151,7 @@ var ViewerStateProvider = function ViewerStateProvider(props) {
150
151
  });
151
152
  useEffect(function () {
152
153
  resetProvider.setSavedViewerState(props.viewerSettings || {});
153
- }, [props.viewerSettings]);
154
+ }, [props.viewerSettings, resetProvider]);
154
155
  var changeViewerSetting = useCallback(function (key, value) {
155
156
  return viewerDispatch({
156
157
  key: key,
@@ -178,20 +179,17 @@ var ViewerStateProvider = function ViewerStateProvider(props) {
178
179
  });
179
180
  }, []);
180
181
 
181
- // Sync viewer settings prop with state
182
- // React docs seem to be fine with syncing state with props directly in the render function, but that caused an
183
- // infinite render loop, so now it's in a `useMemo`:
184
182
  // https://react.dev/learn/you-might-not-need-an-effect#adjusting-some-state-when-a-prop-changes
185
- useMemo(function () {
186
- if (props.viewerSettings) {
187
- for (var _i = 0, _arr = Object.keys(props.viewerSettings); _i < _arr.length; _i++) {
188
- var key = _arr[_i];
189
- if (viewerSettings[key] !== props.viewerSettings[key]) {
190
- changeViewerSetting(key, props.viewerSettings[key]);
191
- }
183
+ var prevViewerSettingsPropsRef = useRef(undefined);
184
+ if (props.viewerSettings && !isEqual(props.viewerSettings, prevViewerSettingsPropsRef.current)) {
185
+ for (var _i = 0, _arr = Object.keys(props.viewerSettings); _i < _arr.length; _i++) {
186
+ var key = _arr[_i];
187
+ if (viewerSettings[key] !== props.viewerSettings[key]) {
188
+ changeViewerSetting(key, props.viewerSettings[key]);
192
189
  }
193
190
  }
194
- }, [props.viewerSettings]);
191
+ prevViewerSettingsPropsRef.current = props.viewerSettings;
192
+ }
195
193
  var context = useMemo(function () {
196
194
  ref.current = _objectSpread(_objectSpread({}, viewerSettings), {}, {
197
195
  channelSettings: channelSettings,
@@ -214,7 +212,7 @@ var ViewerStateProvider = function ViewerStateProvider(props) {
214
212
  return {
215
213
  ref: ref
216
214
  };
217
- }, [viewerSettings, channelSettings]);
215
+ }, [viewerSettings, channelSettings, applyColorPresets, changeViewerSetting, changeChannelSetting, setChannelSettings, resetProvider]);
218
216
  return /*#__PURE__*/React.createElement(ViewerStateContext.Provider, {
219
217
  value: context
220
218
  }, props.children);