# SPDX-License-Identifier: BSD-3-Clause
# Copyright (c) 2023 Scipp contributors (https://github.com/scipp)
import numpy as np
import pythreejs as p3
from matplotlib import ticker
from scipp import Variable
from ...core.utils import value_to_string
def _get_delta(x: Variable, axis: int) -> float:
"""
Compute the difference between two bounds along a given axis.
"""
return (x[axis][1] - x[axis][0]).value
def _get_offsets(
limits: tuple[Variable, Variable, Variable], axis: int, ind: int
) -> np.ndarray:
"""
Compute offsets for 3 dimensions, along the edges of the box.
"""
offsets = np.array([limits[i][ind].value for i in range(3)])
offsets[axis] = 0
return offsets
def _make_geometry(limits: tuple[Variable, Variable, Variable]) -> p3.EdgesGeometry:
"""
Make a geometry to represent the edges of a cubic box.
"""
return p3.EdgesGeometry(
p3.BoxBufferGeometry(
width=_get_delta(limits, axis=0),
height=_get_delta(limits, axis=1),
depth=_get_delta(limits, axis=2),
)
)
def _make_sprite(
string: str,
position: tuple[float, float, float],
color: str = "black",
size: float = 1.0,
) -> p3.Sprite:
"""
Make a text-based sprite for axis tick.
"""
sm = p3.SpriteMaterial(
map=p3.TextTexture(string=string, color=color, size=300, squareTexture=True),
transparent=True,
)
return p3.Sprite(material=sm, position=position, scale=[size, size, size])
[docs]
class Outline(p3.Group):
"""
Create an object that draws a rectangular outline, given some limits for the XYZ
dimensions. Along the lower edges of the cube are added some tick labels and axes
labels, according to the dimension extents, dimension names, and units given in
the limits.
Parameters
----------
limits:
A tuple of variables, each of length 2, which contain the lower and upper bounds
for the outline. Each variable also has a dimension, which is to be used as the
dimension for that direction, as well as a unit, which will be used to label
the corresponding axis.
tick_size:
A number to scale the tick size.
"""
[docs]
def __init__(
self,
limits: tuple[Variable, Variable, Variable],
tick_size: float | None = None,
):
center = [var.mean().value for var in limits]
if tick_size is None:
tick_size = 0.05 * np.mean([_get_delta(limits, axis=i) for i in range(3)])
self.box = p3.LineSegments(
geometry=_make_geometry(limits),
material=p3.LineBasicMaterial(color='#000000'),
position=center,
)
self.ticks = self._make_ticks(limits=limits, tick_size=tick_size)
self.labels = self._make_labels(
limits=limits, center=center, tick_size=tick_size
)
super().__init__()
for obj in (self.box, self.ticks, self.labels):
self.add(obj)
def _make_ticks(
self, limits: tuple[Variable, Variable, Variable], tick_size: float
) -> p3.Group:
"""
Create tick labels on outline edges
"""
ticks_group = p3.Group()
iden = np.identity(3, dtype=np.float32)
ticker_ = ticker.MaxNLocator(5)
for axis in range(3):
ticks = ticker_.tick_values(limits[axis][0].value, limits[axis][1].value)
for tick in ticks:
if limits[axis][0].value <= tick <= limits[axis][1].value:
tick_pos = iden[axis] * tick + _get_offsets(limits, axis, 0)
ticks_group.add(
_make_sprite(
string=value_to_string(tick, precision=1),
position=tick_pos.tolist(),
size=tick_size,
)
)
return ticks_group
def _make_labels(
self,
limits: tuple[Variable, Variable, Variable],
center: list[float],
tick_size: float,
) -> p3.Group:
"""
Create axes labels (coord dimension and unit) on outline edges
"""
labels_group = p3.Group()
for axis in range(3):
axis_label = f'{limits[axis].dim} [{limits[axis].unit}]'
# Offset labels 5% beyond axis ticks to reduce overlap
delta = 0.05
labels_group.add(
_make_sprite(
string=axis_label,
position=(
np.roll([1, 0, 0], axis) * center[axis]
+ (1.0 + delta) * _get_offsets(limits, axis, 0)
- delta * _get_offsets(limits, axis, 1)
).tolist(),
size=tick_size * 0.3 * len(axis_label),
)
)
return labels_group