@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.
- package/es/aics-image-viewer/components/App/ChannelUpdater.js +11 -12
- package/es/aics-image-viewer/components/App/index.js +62 -30
- package/es/aics-image-viewer/components/AxisClipSliders/index.js +34 -25
- package/es/aics-image-viewer/components/BottomPanel/index.js +12 -28
- package/es/aics-image-viewer/components/CellViewerCanvasWrapper/index.js +3 -12
- package/es/aics-image-viewer/components/ChannelsWidgetRow/index.js +6 -2
- package/es/aics-image-viewer/components/ChannelsWidgetRow/styles.css +5 -1
- package/es/aics-image-viewer/components/ErrorAlert/index.js +1 -1
- package/es/aics-image-viewer/components/ErrorAlert/styles.css +3 -0
- package/es/aics-image-viewer/components/StyleProvider/index.js +1 -1
- package/es/aics-image-viewer/components/TfEditor/index.js +229 -41
- package/es/aics-image-viewer/components/TfEditor/styles.css +19 -7
- package/es/aics-image-viewer/components/Toolbar/ViewModeRadioButtons.js +1 -1
- package/es/aics-image-viewer/components/Toolbar/index.js +34 -30
- package/es/aics-image-viewer/components/ViewerStateProvider/index.js +11 -13
- package/es/aics-image-viewer/shared/constants.js +41 -1
- package/es/aics-image-viewer/shared/utils/hooks.js +3 -3
- package/es/aics-image-viewer/shared/utils/viewerState.js +3 -1
- package/package.json +4 -3
- package/type-declarations/aics-image-viewer/components/App/types.d.ts +1 -1
- package/type-declarations/aics-image-viewer/components/BottomPanel/index.d.ts +2 -2
- package/type-declarations/aics-image-viewer/components/CellViewerCanvasWrapper/index.d.ts +2 -2
- package/type-declarations/aics-image-viewer/components/TfEditor/index.d.ts +2 -0
- package/type-declarations/aics-image-viewer/components/Toolbar/index.d.ts +1 -1
- package/type-declarations/aics-image-viewer/components/ViewerStateProvider/types.d.ts +2 -0
- package/type-declarations/aics-image-viewer/shared/constants.d.ts +8 -1
- 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.
|
|
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([
|
|
213
|
-
}, [innerWidth,
|
|
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
|
-
|
|
351
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
516
|
+
var numBins = histogram.getNumBins();
|
|
517
|
+
if (numBins < 1) {
|
|
388
518
|
return;
|
|
389
519
|
}
|
|
390
|
-
var _getHistogramBinLengt = getHistogramBinLengths(
|
|
520
|
+
var _getHistogramBinLengt = getHistogramBinLengths(histogram),
|
|
391
521
|
binLengths = _getHistogramBinLengt.binLengths,
|
|
392
522
|
max = _getHistogramBinLengt.max;
|
|
393
|
-
var
|
|
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(
|
|
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,
|
|
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
|
-
}, [
|
|
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](
|
|
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
|
-
}, [
|
|
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.
|
|
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("
|
|
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")),
|
|
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
|
-
}))))),
|
|
545
|
-
className: "tf-editor-
|
|
546
|
-
},
|
|
547
|
-
value:
|
|
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 &&
|
|
711
|
+
return v !== null && changeChannelSetting({
|
|
712
|
+
plotMin: v,
|
|
713
|
+
plotMax: Math.max(v + 1, plotMax)
|
|
714
|
+
});
|
|
550
715
|
},
|
|
551
716
|
formatter: numberFormatter,
|
|
552
|
-
min:
|
|
553
|
-
max:
|
|
554
|
-
size: "small"
|
|
555
|
-
|
|
556
|
-
|
|
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 &&
|
|
724
|
+
return v !== null && changeChannelSetting({
|
|
725
|
+
plotMax: v,
|
|
726
|
+
plotMin: Math.min(v - 1, plotMin)
|
|
727
|
+
});
|
|
559
728
|
},
|
|
560
729
|
formatter: numberFormatter,
|
|
561
|
-
min:
|
|
562
|
-
max:
|
|
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
|
-
|
|
565
|
-
|
|
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-
|
|
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-
|
|
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:
|
|
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:
|
|
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 {
|
|
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
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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(
|
|
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
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
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
|
-
|
|
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);
|