Skip to content

Commit

Permalink
Sensible scalebar values (#6034)
Browse files Browse the repository at this point in the history
* extend accepted length units

* snap scalebar dynamically to sensible values

* adjust or collapse scalebar according to width

* [optional] nm to pixel conversion (ultimately was not needed)

* updated unreleased changelog

* constrain scalebar with very large values
  • Loading branch information
Dagobert42 authored Feb 11, 2022
1 parent b708b3b commit dabdbfa
Show file tree
Hide file tree
Showing 4 changed files with 92 additions and 29 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.unreleased.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ For upgrade instructions, please check the [migration guide](MIGRATIONS.released
[Commits](https://github.com/scalableminds/webknossos/compare/22.02.0...HEAD)

### Added
- Viewport scale bars are now dynamically adjusted to display sensible values. [#5418](https://github.com/scalableminds/webknossos/pull/6034)
- Added the option to make a segment's ID active via the right-click context menu in the segments list. [#5935](https://github.com/scalableminds/webknossos/pull/6006)
- Added a button next to the histogram which adapts the contrast and brightness to the currently visible data. [#5961](https://github.com/scalableminds/webknossos/pull/5961)
- Running uploads can now be cancelled. [#5958](https://github.com/scalableminds/webknossos/pull/5958)
Expand Down
35 changes: 29 additions & 6 deletions frontend/javascripts/libs/format_utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -59,14 +59,37 @@ export function formatScale(scaleArr: ?Vector3, roundTo?: number = 2): string {
}
}

const nmFactorToUnit = new Map([
[1e-3, "pm"],
[1, "nm"],
[1e3, "µm"],
[1e6, "mm"],
[1e9, "m"],
[1e12, "km"],
]);
const sortedNmFactors = Array.from(nmFactorToUnit.keys()).sort((a, b) => a - b);

export function formatNumberToLength(lengthInNm: number): string {
if (lengthInNm < 1000) {
return `${lengthInNm.toFixed(0)}${ThinSpace}nm`;
} else if (lengthInNm < 1000000) {
return `${(lengthInNm / 1000).toFixed(1)}${ThinSpace}µm`;
} else {
return `${(lengthInNm / 1000000).toFixed(1)}${ThinSpace}mm`;
const closestFactor = findClosestLengthUnitFactor(lengthInNm);
const unit = nmFactorToUnit.get(closestFactor);
if (unit == null) {
throw new Error("Couldn't look up appropriate length unit.");
}
const lengthInUnit = lengthInNm / closestFactor;
if (lengthInUnit !== Math.floor(lengthInUnit)) {
return `${lengthInUnit.toFixed(1)}${ThinSpace}${unit}`;
}
return `${lengthInUnit}${ThinSpace}${unit}`;
}
export function findClosestLengthUnitFactor(lengthInNm: number): number {
let closestFactor = sortedNmFactors[0];
for (const factor of sortedNmFactors) {
if (lengthInNm >= factor) {
closestFactor = factor;
}
}
return closestFactor;
}
export function formatLengthAsVx(lengthInVx: number, roundTo?: number = 2): string {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,14 @@ export function convertPixelsToNm(
return lengthInPixel * zoomValue * getBaseVoxel(dataset.dataSource.scale);
}

export function convertNmToPixels(
lengthInNm: number,
zoomValue: number,
dataset: APIDataset,
): number {
return lengthInNm / (zoomValue * getBaseVoxel(dataset.dataSource.scale));
}

class DatasetInfoTabView extends React.PureComponent<Props, State> {
state = {
showJobsDetailsModal: null,
Expand Down
77 changes: 54 additions & 23 deletions frontend/javascripts/oxalis/view/scalebar.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,34 +22,64 @@ type OwnProps = {|
type StateProps = {|
dataset: APIDataset,
zoomValue: number,
widthInPixels: number,
heightInPixels: number,
viewportWidthInPixels: number,
viewportHeightInPixels: number,
|};

type Props = {|
...OwnProps,
...StateProps,
|};

const scalebarWidthPercentage = 0.25;
const getBestScalebarAnchorInNm = (lengthInNm: number): number => {
const closestExponent = Math.floor(Math.log10(lengthInNm));
const closestPowerOfTen = 10 ** closestExponent;
const mantissa = lengthInNm / closestPowerOfTen;

function Scalebar({ zoomValue, dataset, widthInPixels, heightInPixels }: Props) {
const widthInNm = convertPixelsToNm(widthInPixels, zoomValue, dataset);
const heightInNm = convertPixelsToNm(heightInPixels, zoomValue, dataset);
const formattedScalebarWidth = formatNumberToLength(widthInNm * scalebarWidthPercentage);
let bestAnchor = 1;
for (const anchor of [2, 5, 10]) {
if (Math.abs(anchor - mantissa) < Math.abs(bestAnchor - mantissa)) {
bestAnchor = anchor;
}
}
return bestAnchor * closestPowerOfTen;
};

// This factor describes how wide the scalebar would ideally be.
// However, this is only a rough guideline, as the actual width is changed
// so that round length values are represented.
const idealScalebarWidthFactor = 0.3;

const maxScaleBarWidthFactor = 0.45;
const minWidthToFillScalebar = 130;

function Scalebar({ zoomValue, dataset, viewportWidthInPixels, viewportHeightInPixels }: Props) {
const viewportWidthInNm = convertPixelsToNm(viewportWidthInPixels, zoomValue, dataset);
const viewportHeightInNm = convertPixelsToNm(viewportHeightInPixels, zoomValue, dataset);
const idealWidthInNm = viewportWidthInNm * idealScalebarWidthFactor;
const scalebarWidthInNm = getBestScalebarAnchorInNm(idealWidthInNm);
const scaleBarWidthFactor = Math.min(
scalebarWidthInNm / viewportWidthInNm,
maxScaleBarWidthFactor,
);

const tooltip = [
formatNumberToLength(viewportWidthInNm),
ThinSpace,
MultiplicationSymbol,
ThinSpace,
formatNumberToLength(viewportHeightInNm),
].join("");
const collapseScalebar = viewportWidthInPixels < minWidthToFillScalebar;
const limitScalebar = scaleBarWidthFactor === maxScaleBarWidthFactor;
const padding = 4;

return (
<Tooltip
title={
<div>
<div>Viewport Size:</div>
<div>
{formatNumberToLength(widthInNm)}
{ThinSpace}
{MultiplicationSymbol}
{ThinSpace}
{formatNumberToLength(heightInNm)}{" "}
</div>
<div>{tooltip}</div>
</div>
}
>
Expand All @@ -58,28 +88,29 @@ function Scalebar({ zoomValue, dataset, widthInPixels, heightInPixels }: Props)
position: "absolute",
bottom: "1%",
right: "1%",
// The scalebar should have a width of 25% from the actual viewport (without the borders)
width: `calc(25% - ${Math.round(
((2 * OUTER_CSS_BORDER) / constants.VIEWPORT_WIDTH) * 100,
)}%)`,
width: collapseScalebar
? 16
: `calc(${scaleBarWidthFactor * 100}% - ${Math.round(
((2 * OUTER_CSS_BORDER) / constants.VIEWPORT_WIDTH) * 100,
)}% + ${2 * padding}px)`,
height: 14,
background: "rgba(0, 0, 0, .3)",
color: "white",
textAlign: "center",
fontSize: 12,
lineHeight: "14px",
boxSizing: "content-box",
padding: 4,
padding,
}}
>
<div
style={{
borderBottom: "1px solid",
borderLeft: "1px solid",
borderLeft: limitScalebar ? "none" : "1px solid",
borderRight: "1px solid",
}}
>
{formattedScalebarWidth}
{collapseScalebar ? "i" : formatNumberToLength(scalebarWidthInNm)}
</div>
</div>
</Tooltip>
Expand All @@ -93,8 +124,8 @@ const mapStateToProps = (state: OxalisState, ownProps: OwnProps): StateProps =>
return {
zoomValue,
dataset: state.dataset,
widthInPixels: width,
heightInPixels: height,
viewportWidthInPixels: width,
viewportHeightInPixels: height,
};
};

Expand Down

0 comments on commit dabdbfa

Please sign in to comment.