diff --git a/examples/line/line.py b/examples/line/line.py index 008b0147d..45fc5eb5b 100644 --- a/examples/line/line.py +++ b/examples/line/line.py @@ -9,13 +9,8 @@ from fastplotlib import Plot import numpy as np -from wgpu.gui.offscreen import WgpuCanvas -from pygfx import WgpuRenderer -canvas = WgpuCanvas() -renderer = WgpuRenderer(canvas) - -plot = Plot(canvas=canvas, renderer=renderer) +plot = Plot() xs = np.linspace(-10, 10, 100) # sine wave diff --git a/examples/line/line_cmap.py b/examples/line/line_cmap.py new file mode 100644 index 000000000..f2fa29d79 --- /dev/null +++ b/examples/line/line_cmap.py @@ -0,0 +1,46 @@ +""" +Line Plot +============ +Example showing cosine, sine, sinc lines. +""" + +# test_example = true + +import fastplotlib as fpl +import numpy as np + + +plot = fpl.Plot() + +xs = np.linspace(-10, 10, 100) +# sine wave +ys = np.sin(xs) +sine = np.dstack([xs, ys])[0] + +# cosine wave +ys = np.cos(xs) - 5 +cosine = np.dstack([xs, ys])[0] + +# cmap_values from an array, so the colors on the sine line will be based on the sine y-values +sine_graphic = plot.add_line( + data=sine, + thickness=10, + cmap="plasma", + cmap_values=sine[:, 1] +) + +# qualitative colormaps, useful for cluster labels or other types of categorical labels +cmap_values = [0] * 25 + [5] * 10 + [1] * 35 + [2] * 30 +cosine_graphic = plot.add_line( + data=cosine, + thickness=10, + cmap="tab10", + cmap_values=cmap_values +) + +plot.show() + +plot.canvas.set_logical_size(800, 800) + +if __name__ == "__main__": + fpl.run() diff --git a/examples/line/line_colorslice.py b/examples/line/line_colorslice.py index 68ce5b71c..a82f43aa6 100644 --- a/examples/line/line_colorslice.py +++ b/examples/line/line_colorslice.py @@ -9,13 +9,8 @@ from fastplotlib import Plot import numpy as np -from wgpu.gui.offscreen import WgpuCanvas -from pygfx import WgpuRenderer -canvas = WgpuCanvas() -renderer = WgpuRenderer(canvas) - -plot = Plot(canvas=canvas, renderer=renderer) +plot = Plot() xs = np.linspace(-10, 10, 100) # sine wave diff --git a/examples/line/line_dataslice.py b/examples/line/line_dataslice.py index ed9b542b6..ddc670cd2 100644 --- a/examples/line/line_dataslice.py +++ b/examples/line/line_dataslice.py @@ -9,13 +9,8 @@ from fastplotlib import Plot import numpy as np -from wgpu.gui.offscreen import WgpuCanvas -from pygfx import WgpuRenderer -canvas = WgpuCanvas() -renderer = WgpuRenderer(canvas) - -plot = Plot(canvas=canvas, renderer=renderer) +plot = Plot() xs = np.linspace(-10, 10, 100) # sine wave diff --git a/examples/line/line_present_scaling.py b/examples/line/line_present_scaling.py index 6f40bee49..9cf2706e1 100644 --- a/examples/line/line_present_scaling.py +++ b/examples/line/line_present_scaling.py @@ -9,13 +9,8 @@ from fastplotlib import Plot import numpy as np -from wgpu.gui.offscreen import WgpuCanvas -from pygfx import WgpuRenderer -canvas = WgpuCanvas() -renderer = WgpuRenderer(canvas) - -plot = Plot(canvas=canvas, renderer=renderer) +plot = Plot() xs = np.linspace(-10, 10, 100) # sine wave diff --git a/examples/line_collection/__init__.py b/examples/line_collection/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/examples/line_collection/line_collection.py b/examples/line_collection/line_collection.py new file mode 100644 index 000000000..508aca190 --- /dev/null +++ b/examples/line_collection/line_collection.py @@ -0,0 +1,39 @@ +""" +Line Plot +============ +Example showing how to plot line collections +""" + +# test_example = true + +from itertools import product +import numpy as np +import fastplotlib as fpl + + +def make_circle(center, radius: float, n_points: int = 75) -> np.ndarray: + theta = np.linspace(0, 2 * np.pi, n_points) + xs = radius * np.sin(theta) + ys = radius * np.cos(theta) + + return np.column_stack([xs, ys]) + center + + +spatial_dims = (100, 100) + +circles = list() +for center in product(range(0, spatial_dims[0], 15), range(0, spatial_dims[1], 15)): + circles.append(make_circle(center, 5, n_points=75)) + +pos_xy = np.vstack(circles) + +plot = fpl.Plot() + +plot.add_line_collection(circles, cmap="jet", thickness=5) + +plot.show() + +plot.canvas.set_logical_size(800, 800) + +if __name__ == "__main__": + fpl.run() diff --git a/examples/line_collection/line_collection_cmap_values.py b/examples/line_collection/line_collection_cmap_values.py new file mode 100644 index 000000000..749d25b38 --- /dev/null +++ b/examples/line_collection/line_collection_cmap_values.py @@ -0,0 +1,50 @@ +""" +Line Plot +============ +Example showing how to plot line collections +""" + +# test_example = true + +from itertools import product +import numpy as np +import fastplotlib as fpl + + +def make_circle(center, radius: float, n_points: int = 75) -> np.ndarray: + theta = np.linspace(0, 2 * np.pi, n_points) + xs = radius * np.sin(theta) + ys = radius * np.cos(theta) + + return np.column_stack([xs, ys]) + center + + +spatial_dims = (50, 50) + +circles = list() +for center in product(range(0, spatial_dims[0], 15), range(0, spatial_dims[1], 15)): + circles.append(make_circle(center, 5, n_points=75)) + +pos_xy = np.vstack(circles) + +# this makes 16 circles, so we can create 16 cmap values, so it will use these values to set the +# color of the line based by using the cmap as a LUT with the corresponding cmap_value + +# highest values, lowest values, mid-high values, mid values +cmap_values = [10] * 4 + [0] * 4 + [7] * 4 + [5] * 4 + +plot = fpl.Plot() + +plot.add_line_collection( + circles, + cmap="bwr", + cmap_values=cmap_values, + thickness=10 +) + +plot.show() + +plot.canvas.set_logical_size(800, 800) + +if __name__ == "__main__": + fpl.run() diff --git a/examples/line_collection/line_collection_cmap_values_qualitative.py b/examples/line_collection/line_collection_cmap_values_qualitative.py new file mode 100644 index 000000000..f42c46ca3 --- /dev/null +++ b/examples/line_collection/line_collection_cmap_values_qualitative.py @@ -0,0 +1,56 @@ +""" +Line Plot +============ +Example showing how to plot line collections +""" + +# test_example = true + +from itertools import product +import numpy as np +import fastplotlib as fpl + + +def make_circle(center, radius: float, n_points: int = 75) -> np.ndarray: + theta = np.linspace(0, 2 * np.pi, n_points) + xs = radius * np.sin(theta) + ys = radius * np.cos(theta) + + return np.column_stack([xs, ys]) + center + + +spatial_dims = (50, 50) + +circles = list() +for center in product(range(0, spatial_dims[0], 15), range(0, spatial_dims[1], 15)): + circles.append(make_circle(center, 5, n_points=75)) + +pos_xy = np.vstack(circles) + +# this makes 16 circles, so we can create 16 cmap values, so it will use these values to set the +# color of the line based by using the cmap as a LUT with the corresponding cmap_value + +# qualitative colormap used for mapping 16 cmap values for each line +# for example, these could be cluster labels +cmap_values = [ + 0, 1, 1, 2, + 0, 0, 1, 1, + 2, 2, 3, 3, + 1, 1, 1, 5 +] + +plot = fpl.Plot() + +plot.add_line_collection( + circles, + cmap="tab10", + cmap_values=cmap_values, + thickness=10 +) + +plot.show() + +plot.canvas.set_logical_size(800, 800) + +if __name__ == "__main__": + fpl.run() diff --git a/examples/line_collection/line_collection_colors.py b/examples/line_collection/line_collection_colors.py new file mode 100644 index 000000000..bb1a2c833 --- /dev/null +++ b/examples/line_collection/line_collection_colors.py @@ -0,0 +1,43 @@ +""" +Line Plot +============ +Example showing how to plot line collections +""" + +# test_example = true + +from itertools import product +import numpy as np +import fastplotlib as fpl + + +def make_circle(center, radius: float, n_points: int = 75) -> np.ndarray: + theta = np.linspace(0, 2 * np.pi, n_points) + xs = radius * np.sin(theta) + ys = radius * np.cos(theta) + + return np.column_stack([xs, ys]) + center + + +spatial_dims = (50, 50) + +circles = list() +for center in product(range(0, spatial_dims[0], 15), range(0, spatial_dims[1], 15)): + circles.append(make_circle(center, 5, n_points=75)) + +pos_xy = np.vstack(circles) + +# set line collection colors manually +# this will produce 16 circles so we will define 16 colors +colors = ["blue"] * 4 + ["red"] * 4 + ["yellow"] * 4 + ["w"] * 4 + +plot = fpl.Plot() + +plot.add_line_collection(circles, colors=colors, thickness=10) + +plot.show() + +plot.canvas.set_logical_size(800, 800) + +if __name__ == "__main__": + fpl.run() diff --git a/examples/line_collection/line_stack.py b/examples/line_collection/line_stack.py new file mode 100644 index 000000000..282137c40 --- /dev/null +++ b/examples/line_collection/line_stack.py @@ -0,0 +1,30 @@ +""" +Line Plot +============ +Example showing how to plot line collections +""" + +# test_example = true + +import numpy as np +import fastplotlib as fpl + + +xs = np.linspace(0, 100, 1000) +# sine wave +ys = np.sin(xs) * 20 + +# make 25 lines +data = np.vstack([ys] * 25) + +plot = fpl.Plot() + +# line stack takes all the same arguments as line collection and behaves similarly +plot.add_line_stack(data, cmap="jet") + +plot.show(maintain_aspect=False) + +plot.canvas.set_logical_size(900, 600) + +if __name__ == "__main__": + fpl.run() diff --git a/examples/scatter/scatter_cmap.py b/examples/scatter/scatter_cmap.py index 8b52da767..b6ab5fb17 100644 --- a/examples/scatter/scatter_cmap.py +++ b/examples/scatter/scatter_cmap.py @@ -6,25 +6,30 @@ # test_example = true -from fastplotlib import Plot +from fastplotlib import Plot, run import numpy as np from pathlib import Path +from sklearn.cluster import AgglomerativeClustering -from wgpu.gui.offscreen import WgpuCanvas -from pygfx import WgpuRenderer -canvas = WgpuCanvas() -renderer = WgpuRenderer(canvas) - -plot = Plot(canvas=canvas, renderer=renderer) +plot = Plot() data_path = Path(__file__).parent.parent.joinpath("data", "iris.npy") data = np.load(data_path) -n_points = 50 -colors = ["yellow"] * n_points + ["cyan"] * n_points + ["magenta"] * n_points -scatter_graphic = plot.add_scatter(data=data[:, :-1], sizes=6, alpha=0.7, colors=colors) +agg = AgglomerativeClustering(n_clusters=3) + +agg.fit_predict(data) + + +scatter_graphic = plot.add_scatter( + data=data[:, :-1], + sizes=15, + alpha=0.7, + cmap="Set1", + cmap_values=agg.labels_ +) plot.show() @@ -32,9 +37,10 @@ plot.auto_scale() -scatter_graphic.cmap = "viridis" +scatter_graphic.cmap = "tab10" -img = np.asarray(plot.renderer.target.draw()) +# img = np.asarray(plot.renderer.target.draw()) if __name__ == "__main__": print(__doc__) + run() diff --git a/examples/screenshots/line_cmap.png b/examples/screenshots/line_cmap.png new file mode 100644 index 000000000..0ece6fbde --- /dev/null +++ b/examples/screenshots/line_cmap.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c6f511ffd3a10e2c653afd3b9eca8f6bb10af26759a7efc73fe16c825cc1bf15 +size 43718 diff --git a/examples/screenshots/line_collection.png b/examples/screenshots/line_collection.png new file mode 100644 index 000000000..f7be75201 --- /dev/null +++ b/examples/screenshots/line_collection.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4aa71b9b8d2c049dad951493a5f51c32da33a3e536761254cd18368d6b8cd8e8 +size 146755 diff --git a/examples/screenshots/line_collection_cmap_values.png b/examples/screenshots/line_collection_cmap_values.png new file mode 100644 index 000000000..a91e4ce69 --- /dev/null +++ b/examples/screenshots/line_collection_cmap_values.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a32210432cd8e88bec20a84b1e6839d0a2d5bb2edb1aea8ebe09569872cb16d8 +size 93580 diff --git a/examples/screenshots/line_collection_cmap_values_qualitative.png b/examples/screenshots/line_collection_cmap_values_qualitative.png new file mode 100644 index 000000000..c38e5fb96 --- /dev/null +++ b/examples/screenshots/line_collection_cmap_values_qualitative.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5be6f9343b47848d3e1be4b82315f0b71bdb1b919503f943c618ef8ba7f6272a +size 95656 diff --git a/examples/screenshots/line_collection_colors.png b/examples/screenshots/line_collection_colors.png new file mode 100644 index 000000000..1ae597033 --- /dev/null +++ b/examples/screenshots/line_collection_colors.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b1e5b913ca91293a8edb8f6898249dd62019cb827223dacf377e3fc6cda89a77 +size 82686 diff --git a/examples/screenshots/line_stack.png b/examples/screenshots/line_stack.png new file mode 100644 index 000000000..23343df32 --- /dev/null +++ b/examples/screenshots/line_stack.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bc2496c203b2994ef5b8e714e1c7619e726d0b605e0c25498f11e1154d4905ec +size 360981 diff --git a/examples/screenshots/scatter_cmap.png b/examples/screenshots/scatter_cmap.png index 8fefd0f91..10477e81b 100644 --- a/examples/screenshots/scatter_cmap.png +++ b/examples/screenshots/scatter_cmap.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2be58a41b54f29db4683692b73993309ff561c47388af667a95bb33aed65f219 -size 26894 +oid sha256:8b4b364d1cd3ab328f025030db87f8ff4fb2544c3bfb67176ea8f0acdc24f04b +size 59407 diff --git a/examples/tests/test_examples.py b/examples/tests/test_examples.py index 650c7e8cd..876533fa6 100644 --- a/examples/tests/test_examples.py +++ b/examples/tests/test_examples.py @@ -58,7 +58,7 @@ def test_example_screenshots(module, force_offscreen): example = importlib.import_module(module_name) # render a frame - img = np.asarray(example.renderer.target.draw()) + img = np.asarray(example.plot.renderer.target.draw()) # check if _something_ was rendered assert img is not None and img.size > 0 diff --git a/examples/tests/testutils.py b/examples/tests/testutils.py index 8e248e1e4..7bc271e02 100644 --- a/examples/tests/testutils.py +++ b/examples/tests/testutils.py @@ -14,7 +14,13 @@ diffs_dir = examples_dir / "diffs" # examples live in themed sub-folders -example_globs = ["image/*.py", "scatter/*.py", "line/*.py", "gridplot/*.py"] +example_globs = [ + "image/*.py", + "scatter/*.py", + "line/*.py", + "line_collection/*.py", + "gridplot/*.py" +] def get_wgpu_backend(): diff --git a/fastplotlib/graphics/features/_colors.py b/fastplotlib/graphics/features/_colors.py index f1ca3acb9..fb2a7a088 100644 --- a/fastplotlib/graphics/features/_colors.py +++ b/fastplotlib/graphics/features/_colors.py @@ -1,7 +1,7 @@ import numpy as np from ._base import GraphicFeature, GraphicFeatureIndexable, cleanup_slice, FeatureEvent, cleanup_array_slice -from ...utils import make_colors, get_cmap_texture, make_pygfx_colors +from ...utils import make_colors, get_cmap_texture, make_pygfx_colors, parse_cmap_values from pygfx import Color @@ -226,10 +226,13 @@ class CmapFeature(ColorFeature): Same event pick info as :class:`ColorFeature` """ - def __init__(self, parent, colors): + def __init__(self, parent, colors, cmap_name: str, cmap_values: np.ndarray): super(ColorFeature, self).__init__(parent, colors) - def __setitem__(self, key, value): + self._cmap_name = cmap_name + self._cmap_values = cmap_values + + def __setitem__(self, key, cmap_name): key = cleanup_slice(key, self._upper_bound) if not isinstance(key, (slice, np.ndarray)): raise TypeError("Cannot set cmap on single indices, must pass a slice object, " @@ -242,9 +245,34 @@ def __setitem__(self, key, value): # numpy array n_colors = key.size - colors = make_colors(n_colors, cmap=value).astype(self._data.dtype) + colors = parse_cmap_values( + n_colors=n_colors, + cmap_name=cmap_name, + cmap_values=self._cmap_values + ) + + self._cmap_name = cmap_name super(CmapFeature, self).__setitem__(key, colors) + @property + def values(self) -> np.ndarray: + return self._cmap_values + + @values.setter + def values(self, values: np.ndarray): + if not isinstance(values, np.ndarray): + values = np.array(values) + + colors = parse_cmap_values( + n_colors=self().shape[0], + cmap_name=self._cmap_name, + cmap_values=values + ) + + self._cmap_values = values + + super(CmapFeature, self).__setitem__(slice(None), colors) + class ImageCmapFeature(GraphicFeature): """ diff --git a/fastplotlib/graphics/line.py b/fastplotlib/graphics/line.py index 1d9db6d58..6114fdd83 100644 --- a/fastplotlib/graphics/line.py +++ b/fastplotlib/graphics/line.py @@ -7,7 +7,7 @@ from ._base import Graphic, Interaction, PreviouslyModifiedData from .features import PointsDataFeature, ColorFeature, CmapFeature, ThicknessFeature from .selectors import LinearRegionSelector, LinearSelector -from ..utils import make_colors +from ..utils import parse_cmap_values class LineGraphic(Graphic, Interaction): @@ -26,6 +26,7 @@ def __init__( colors: Union[str, np.ndarray, Iterable] = "w", alpha: float = 1.0, cmap: str = None, + cmap_values: Union[np.ndarray, List] = None, z_position: float = None, collection_index: int = None, *args, @@ -49,6 +50,9 @@ def __init__( cmap: str, optional apply a colormap to the line instead of assigning colors manually, this overrides any argument passed to "colors" + + cmap_values: 1D array-like or list of numerical values, optional + if provided, these values are used to map the colors from the cmap alpha: float, optional, default 1.0 alpha value for the colors @@ -81,7 +85,13 @@ def __init__( self.data = PointsDataFeature(self, data, collection_index=collection_index) if cmap is not None: - colors = make_colors(n_colors=self.data().shape[0], cmap=cmap, alpha=alpha) + n_datapoints = self.data().shape[0] + + colors = parse_cmap_values( + n_colors=n_datapoints, + cmap_name=cmap, + cmap_values=cmap_values + ) self.colors = ColorFeature( self, @@ -91,7 +101,12 @@ def __init__( collection_index=collection_index ) - self.cmap = CmapFeature(self, self.colors()) + self.cmap = CmapFeature( + self, + self.colors(), + cmap_name=cmap, + cmap_values=cmap_values + ) super(LineGraphic, self).__init__(*args, **kwargs) diff --git a/fastplotlib/graphics/line_collection.py b/fastplotlib/graphics/line_collection.py index 80a66e6bd..25b556784 100644 --- a/fastplotlib/graphics/line_collection.py +++ b/fastplotlib/graphics/line_collection.py @@ -1,6 +1,7 @@ from typing import * from copy import deepcopy import weakref +import traceback import numpy as np import pygfx @@ -8,7 +9,7 @@ from ._base import Interaction, PreviouslyModifiedData, GraphicCollection from .line import LineGraphic from .selectors import LinearRegionSelector, LinearSelector -from ..utils import make_colors +from ..utils import make_colors, get_cmap, QUALITATIVE_CMAPS, normalize_min_max, parse_cmap_values class LineCollection(GraphicCollection, Interaction): @@ -29,6 +30,7 @@ def __init__( colors: Union[List[np.ndarray], np.ndarray] = "w", alpha: float = 1.0, cmap: Union[List[str], str] = None, + cmap_values: Union[np.ndarray, List] = None, name: str = None, metadata: Union[list, tuple, np.ndarray] = None, *args, @@ -63,6 +65,9 @@ def __init__( | if ``list`` of ``str``, each cmap will apply to the individual lines **Note:** ``cmap`` overrides any arguments passed to ``colors`` + cmap_values: 1D array-like or list of numerical values, optional + if provided, these values are used to map the colors from the cmap + name: str, optional name of the line collection @@ -152,7 +157,7 @@ def __init__( if len(data) != len(z_position): raise ValueError("z_position must be a single float or an iterable with same length as data") - if not isinstance(thickness, float): + if not isinstance(thickness, (float, int)): if len(thickness) != len(data): raise ValueError("args must be a single float or an iterable with same length as data") @@ -163,13 +168,21 @@ def __init__( f"{len(metadata)} != {len(data)}" ) + self._cmap_values = cmap_values + self._cmap_str = cmap + # cmap takes priority over colors if cmap is not None: # cmap across lines if isinstance(cmap, str): - colors = make_colors(len(data), cmap) + colors = parse_cmap_values( + n_colors=len(data), + cmap_name=cmap, + cmap_values=cmap_values + ) single_color = False cmap = None + elif isinstance(cmap, (tuple, list)): if len(cmap) != len(data): raise ValueError("cmap argument must be a single cmap or a list of cmaps " @@ -261,6 +274,37 @@ def __init__( self.add_graphic(lg, reset_index=False) + @property + def cmap(self) -> str: + return self._cmap_str + + @cmap.setter + def cmap(self, cmap: str): + colors = parse_cmap_values( + n_colors=len(self), + cmap_name=cmap, + cmap_values=self.cmap_values + ) + + for i, g in enumerate(self.graphics): + g.colors = colors[i] + + @property + def cmap_values(self) -> np.ndarray: + return self._cmap_values + + @cmap_values.setter + def cmap_values(self, values: Union[np.ndarray, list]): + colors = parse_cmap_values( + n_colors=len(self), + cmap_name=self.cmap, + cmap_values=values + + ) + + for i, g in enumerate(self.graphics): + g.colors = colors[i] + def add_linear_selector(self, selection: int = None, padding: float = 50, **kwargs) -> LinearSelector: """ Adds a :class:`.LinearSelector` . diff --git a/fastplotlib/graphics/scatter.py b/fastplotlib/graphics/scatter.py index 5556b1de2..b2a92ea95 100644 --- a/fastplotlib/graphics/scatter.py +++ b/fastplotlib/graphics/scatter.py @@ -5,7 +5,7 @@ from ._base import Graphic from .features import PointsDataFeature, ColorFeature, CmapFeature -from ..utils import make_colors +from ..utils import make_colors, parse_cmap_values class ScatterGraphic(Graphic): @@ -16,6 +16,7 @@ def __init__( colors: np.ndarray = "w", alpha: float = 1.0, cmap: str = None, + cmap_values: Union[np.ndarray, List] = None, z_position: float = 0.0, *args, **kwargs @@ -39,6 +40,9 @@ def __init__( apply a colormap to the scatter instead of assigning colors manually, this overrides any argument passed to "colors" + cmap_values: 1D array-like or list of numerical values, optional + if provided, these values are used to map the colors from the cmap + alpha: float, optional, default 1.0 alpha value for the colors @@ -67,12 +71,22 @@ def __init__( """ self.data = PointsDataFeature(self, data) + n_datapoints = self.data().shape[0] if cmap is not None: - colors = make_colors(n_colors=self.data().shape[0], cmap=cmap, alpha=alpha) - - self.colors = ColorFeature(self, colors, n_colors=self.data().shape[0], alpha=alpha) - self.cmap = CmapFeature(self, self.colors()) + colors = parse_cmap_values( + n_colors=n_datapoints, + cmap_name=cmap, + cmap_values=cmap_values + ) + + self.colors = ColorFeature(self, colors, n_colors=n_datapoints, alpha=alpha) + self.cmap = CmapFeature( + self, + self.colors(), + cmap_name=cmap, + cmap_values=cmap_values + ) if isinstance(sizes, int): sizes = np.full(self.data().shape[0], sizes, dtype=np.float32) diff --git a/fastplotlib/utils/functions.py b/fastplotlib/utils/functions.py index fe4e09366..ce6740f71 100644 --- a/fastplotlib/utils/functions.py +++ b/fastplotlib/utils/functions.py @@ -1,3 +1,5 @@ +from typing import Union, List + import numpy as np from pygfx import Texture, Color from collections import OrderedDict @@ -6,11 +8,11 @@ # some funcs adapted from mesmerize -qual_cmaps = ['Pastel1', 'Pastel2', 'Paired', 'Accent', 'Dark2', 'Set1', +QUALITATIVE_CMAPS = ['Pastel1', 'Pastel2', 'Paired', 'Accent', 'Dark2', 'Set1', 'Set2', 'Set3', 'tab10', 'tab20', 'tab20b', 'tab20c'] -def _get_cmap(name: str, alpha: float = 1.0) -> np.ndarray: +def get_cmap(name: str, alpha: float = 1.0) -> np.ndarray: cmap_path = Path(__file__).absolute().parent.joinpath('colormaps', name) if cmap_path.is_file(): cmap = np.loadtxt(cmap_path) @@ -53,9 +55,9 @@ def make_colors(n_colors: int, cmap: str, alpha: float = 1.0) -> np.ndarray: """ name = cmap - cmap = _get_cmap(name, alpha) + cmap = get_cmap(name, alpha) - if name in qual_cmaps: + if name in QUALITATIVE_CMAPS: max_colors = cmap.shape[0] if n_colors > cmap.shape[0]: raise ValueError(f"You have requested <{n_colors}> but only <{max_colors} existing for the " @@ -67,7 +69,7 @@ def make_colors(n_colors: int, cmap: str, alpha: float = 1.0) -> np.ndarray: def get_cmap_texture(name: str, alpha: float = 1.0) -> Texture: - cmap = _get_cmap(name) + cmap = get_cmap(name) return Texture(cmap, dim=1) @@ -166,3 +168,76 @@ def calculate_gridshape(n_subplots: int) -> Tuple[int, int]: int(np.round(sr)), int(np.ceil(sr)) ) + + +def normalize_min_max(a): + """normalize an array between 0 - 1""" + return (a - np.min(a)) / (np.max(a - np.min(a))) + + +def parse_cmap_values( + n_colors: int, + cmap_name: str, + cmap_values: Union[np.ndarray, List[Union[int, float]]] = None +) -> np.ndarray: + """ + + Parameters + ---------- + n_colors: int + number of graphics in collection + + cmap_name: str + colormap name + + cmap_values: np.ndarray | List[int | float], optional + cmap values + Returns + ------- + + """ + if cmap_values is None: + # use the cmap values linearly just along the collection indices + # for example, if len(data) = 10 and the cmap is "jet", then it will + # linearly go from blue to red from data[0] to data[-1] + colors = make_colors(n_colors, cmap_name) + return colors + + else: + if not isinstance(cmap_values, np.ndarray): + cmap_values = np.array(cmap_values) + + # use the values within cmap_values to set the color of the corresponding data + # each individual data[i] has its color based on the "relative cmap_value intensity" + if len(cmap_values) != n_colors: + raise ValueError( + f"len(cmap_values) != len(data): {len(cmap_values)} != {n_colors}" + ) + + colormap = get_cmap(cmap_name) + + n_colors = colormap.shape[0] - 1 + + if cmap_name in QUALITATIVE_CMAPS: + # check that cmap_values are and within the number of colors `n_colors` + # do not scale, use directly + if not np.issubdtype(cmap_values.dtype, np.integer): + raise TypeError( + f" cmap_values should be used with qualitative colormaps, the dtype you " + f"have passed is {cmap_values.dtype}" + ) + if max(cmap_values) > n_colors: + raise IndexError( + f"You have chosen the qualitative colormap <'{cmap_name}'> which only has " + f"<{n_colors}> colors, which is lower than the max value of your `cmap_values`." + f"Choose a cmap with more colors, or a non-quantitative colormap." + ) + norm_cmap_values = cmap_values + else: + # scale between 0 - n_colors so we can just index the colormap as a LUT + norm_cmap_values = (normalize_min_max(cmap_values) * n_colors).astype(int) + + # use colormap as LUT to map the cmap_values to the colormap index + colors = np.vstack([colormap[val] for val in norm_cmap_values]) + + return colors diff --git a/setup.py b/setup.py index d07f01949..b7212fa23 100644 --- a/setup.py +++ b/setup.py @@ -27,10 +27,9 @@ "nbmake", "scipy", "imageio", - "imageio-ffmpeg>=0.4.7", "jupyterlab", "jupyter-rfb", - "Pillow", + "scikit-learn", ] }