--- url: /get-started.md description: An overview of the most important features and functions. --- # Get Started ## What is Jupyter Scatter? Jupyter Scatter is a scalable, interactive, and interlinked scatter plot widget exploring datasets with up to several million data points that runs in Jupyter Lab/Notebook and Google Colab. It focuses on data-driven visual encodings and offers two-way pan+zoom and lasso interactions. Beyond a single plot, Jupyter Scatter can compose multiple scatter plots and synchronize their views and point selections. ### Key Features * 🖱️ **Interactive**: Pan, zoom, and select data points interactively. * 🚀 **Scalable**: Plot up to several millions data points smoothly. * 🔗 **Interlinked**: Synchronize the view, hover, and selection across multiple plots. * ✨ **Effective Defaults**: Perceptually effective point colors and opacity by default. * 📚 **Friendly API:** A readable API that integrates with Pandas, Polars, and any Arrow-compatible DataFrame. * 🛠️ **Integratable**: Use Jupyter Scatter in your own widgets by observing its traitlets. ## Simplest Example In the simplest case, you can pass the x/y coordinates to the plot function as follows: ```python import jscatter import numpy as np x = np.random.rand(500) y = np.random.rand(500) jscatter.plot(x, y) ``` ## Bind a DataFrame In most cases, however, it's more convenient to work with a DataFrame and reference the x/y columns via their names. Jupyter Scatter supports Pandas, Polars, and any DataFrame implementing the [Arrow PyCapsule Interface](https://arrow.apache.org/docs/format/CDataInterface/PyCapsuleInterface.html) (e.g., DuckDB, cuDF). ```python import jscatter import numpy as np import pandas as pd df = pd.DataFrame(np.random.rand(500, 2), columns=['mass', 'speed']) jscatter.plot(data=df, x='mass', y='speed') ``` The same works with Polars: ```python import polars as pl df = pl.DataFrame(np.random.rand(500, 2), schema=['mass', 'speed']) jscatter.plot(data=df, x='mass', y='speed') ``` ## Point Color, Opacity, and Size Often times, we want to style the points. Jupyter Scatter allows you to do this as follows: ```py{5-7} jscatter.plot( data=df, x='mass', y='speed', color='red', # static visual encoding size=10, # static visual encoding opacity=0.5 # static visual encoding ) ``` However, more commonely, one wants to use these three point attributes (color, opacity, and size) to visualize data properties. For instance, in the following we're extending the data frame with a continuous property called `pval` and a categorical property called `cat`. ```py{5,7} df = pd.DataFrame({ # Random floats "mass": np.random.rand(500), "speed": np.random.rand(500), "pval": np.random.rand(500), # Random letters A, B, C, D, E, F, G, H "cat": np.vectorize(lambda x: chr(65 + round(x * 7)))(np.random.rand(500)), }) ``` | | x | y | pval | cat | |---|------|------|------|-----| | 0 | 0.13 | 0.27 | 0.51 | G | | 1 | 0.87 | 0.93 | 0.80 | B | | 2 | 0.10 | 0.25 | 0.25 | F | | 3 | 0.03 | 0.90 | 0.01 | G | | 4 | 0.19 | 0.78 | 0.65 | D | You can visualize the two properties by referencing their columns using the `color_by`, `opacity_by`, or `size_by` arguments. ```py{5-6} jscatter.plot( data=df, x='mass', y='speed', color_by='cat', # data-driven visual encoding size_by='pval', # data-driven visual encoding ) ``` Notice how `jscatter` uses a reasonable color and size map by default. Both are based on the properties' data types. In this examples, the `jscatter` picked the color blindness safe color map from [Okabe and Ito](https://jfly.uni-koeln.de/color/#pallet) as the number of categories is less than 9. When visualizing the `pval` via the color we see how the default color map switches to Viridis given that `pval` is a continuous property. ```py{5} jscatter.plot( data=df, x='mass', y='speed', color_by='pval', # pval is continuous data size_by='pval', # pval is categorical data ) ``` You can of course customize the color map and many other parameters of the visual encoding as shown next. ```py{7-19} jscatter.plot( data=df, x='mass', y='speed', color_by='cat', size_by='pval', # Custom categorical color map color_map=dict( A='red', B='#00ff00', C=(0,0,1), D='DeepSkyBlue', E='orange', F='#702AF7', G='#2AF7C0', H='teal' ), # Custom size map (specified as a linspace) size_map=(2, 20, 10), ) ``` ## Functional API The flat API (that we used before) can get overwhelming when we customize a lot of properties. Therefore, `jscatter` provides a functional API that groups properties by type and exposes them via meaningfully-named methods that can almost be read like a sentence. For instance, in line two of the example below, the scatter plot colors points by the `mass` column by mapping its values to the plasma color map in reverse order. ```py{2} scatter = jscatter.Scatter(data=df, x='mass', y='speed') scatter.color(by='mass', map='plasma', order='reverse') scatter.opacity(by='density') scatter.size(by='pval', map=[2, 4, 6, 8, 10]) scatter.background('#1E1E20') scatter.show() ``` ## Update Properties After Plotting You don't have to specify all properties upfront. Using the functional API you can update scatter plot instances after having plotted the scatter and the plot will automatically re-render. For instance, in the following we're changing the color map to `magma` in reverse order. ```py scatter.color(map='magma', order='reverse') ``` ## Chaining Method Calls Inspired by [D3](https://d3js.org/) you can also chain methods calls as follows to update multiple property groups at once. ```py scatter.legend(True).axes(False) ``` ## Animating Point Coordinates When you update the x/y coordinates dynamically and the number of points match, the points will animate in a smooth transition from the previous to their new point location. For instance, try calling [`scatter.xy('speed', 'mass')`](./api#scatter.xy) and you will see how the points are mirrored along the diagonal. ## Retrieving Properties Lastly, all method arguments are optional. If you specify arguments, the methods will act as setters and change the properties. However, if you call a method without any arguments it will act as a getter and return the related properties. For example, `scatter.color()` will return the current coloring settings. ```py {'default': (0, 0, 0, 0.66), 'selected': (0, 0.55, 1, 1), 'hover': (0, 0, 0, 1), 'by': 'mass', 'map': [[0.001462, 0.000466, 0.013866, 1.0], [0.002258, 0.001295, 0.018331, 1.0], ... [0.987387, 0.984288, 0.742002, 1.0], [0.987053, 0.991438, 0.749504, 1.0]], 'norm': , 'order': 'reverse', 'labeling': None} ``` --- --- url: /interactions.md description: A guide on the supported mouse interactions in Jupyter Scatter. --- # Interactions Jupyter Scatter's scatter plots are interactive by default. You can pan, zoom, hover, click, and lasso with your mouse. Moreover, Jupyter Scatter offers APIs for filtering and zooming. ## Pan & Zoom the View Like in Google Maps, you can pan and zoom to adjust the view and explore different areas of the scatter plot in detail. To pan, click and hold the primary mouse button and drag your mouse. To zoom, simply engage your mouse's scroll wheel. ## Hover a Point To help locate a point and where it's located, when you mouse over a point, a reticle will appear. ## Click a Point In order to select a point you can click on it. ## Double Click into the Void To deselect points, simply double click into the background. ## Lasso Points To select more than a single point, you can lasso multiple points. To activate the lasso, click and hold down your primary mouse button. An open circle will appear and slowly close in clockwise order. Once the circle is fully closed it'll turn blue. At this point the lasso is active. Once the lasso is active, keep holding down your primary mouse button and move your mouse cursor around the points you want to select. Finally, release your primary mouse key. ::: info To permanently activate the lasso, you can click on the crosshair icon in the top-left of the scatter plot. ::: ### Add and Remove Selected Points To add more points to an existing selection, hold down the [*meta* key](https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/metaKey) as you lasso select the points you want to add. To remove points from a selection, hold down the [*alt* key](https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/altKey) as you lasso select the points you want to remove. ::: info On macOS, the meta key is `Command` or `⌘` and the alt key is `Option` or `⌥`. ::: ### Brush and Rectangle Lasso Selections Beyond this *freeform* lasso selection, Jupyter Scatter also offers *rectangular* and *brush* style lasso selections. You can change the lasso type either via the *lasso* button in the top-left corner of the plot or via `scatter.lasso(type=lasso_type)`, where `lasso_type` must be one of `'freeform'`, `'brush'`, or `'rectangle'`. Below is an example of the brush lasso selection. And finally an example of the classic rectangular lasso selection. ## Filter Points Sometimes it can be useful to exclusively focus on a subset of points and temporarily hide other points. This can be achieved using [`scatter.filter()`](/api#scatter.filter): ```py scatter.filter([0, 1, 2]) ``` The filtering is blazing fast as it's only hiding non-filtered points. To unset the filter simply call the filter function with `None`: ```py scatter.filter(None) ``` ## Zoom to Points When trying to focus on a subset of points (in particular point clusters), it can help to zoom in. To zoom to a specific set of points, you can use [`scatter.zoom()`](/api#scatter.zoom): ```py scatter.zoom([0, 1, 2]) ``` You can customize how much padding you want to leave when you zoom in as follows: ```py{3} scatter.zoom( to=[0, 1, 2], padding=1, ) ``` In this case we're instructing our scatter plot instance to have the padding be the same size as the bounding box of the first, second, and third point we're zooming to. Lastly, a cool feature of Jupyter Scatter is the ability to automatically zoom to selected or filtered points to make it as simple as possible for you to focus on a certain set of points. ```py scatter.zoom(on_selection=True) ``` ## Overview+Detail Interface Using everything we've learned so far and combining it with Jupyter Scatter's ability to [synchronize/link multiple scatter plots](/link-multiple-plots) we can easily create an overview+detail interface as follows: ```py from jscatter import Scatter, compose from numpy.random import rand x, y = rand(500), rand(500) sc_overview = Scatter(x, y) sc_detail = Scatter(x, y, zoom_on_filter=True, zoom_padding=1) def filter_on_select(change): sc_detail.filter(change['new'] if len(change['new']) > 0 else None) sc_overview.widget.observe(filter_on_select, names='selection') compose( [(sc_overview, "Overview"), (sc_detail, "Detail")], sync_hover=True, ) ``` --- --- url: /selections.md description: A guide on how to select points and clusters, and extract such selections. --- # Selections A primary way to interact with a scatter plot is through selections. Jupyter Scatter makes this easy by offering selections that synchronize between the Python and JavaScript kernel. There are two ways to select points in a scatter plot: 1. [Programmatically using the `scatter.selection()` method](#set-programmatic) 2. [Interactively using the mouse-based lasso](#set-interactive) Similarly, to act upon selected data points, you can 1. [Use the `scatter.selection()` method](#get-programmatic) 2. [Observe `scatter.widget.selection`](#get-interactive) *** To demonstrate how these approaches work, we're going to use the following DataFrame of random value: ```python import jscatter import numpy as np import pandas as pd df = pd.DataFrame({ # Random floats "mass": np.random.rand(500), "speed": np.random.rand(500), "pval": np.random.rand(500), # Random letters A, B, C, D, E, F, G, H "cat": np.vectorize(lambda x: chr(65 + round(x * 8)))(np.random.rand(500)), }) scatter = jscatter.Scatter(data=df, x="mass", y="speed", color_by="cat") scatter.show() ``` ## Programmatically Select Points {#set-programmatic} To select a specific set of points, you can use the [`scatter.selection()`](./api#scatter.selection) method which accepts as input a list of point indices. ```py scatter.selection([1, 2, 3]) ``` With the help of Panda's `query` method, we can easily select specific points matching some criteria as follows: ```py scatter.selection(df.query("cat == 'A'").index) ``` With the above call, for instance, we would select all points that belong to category `A`. ::: info By default, Jupyter Scatter references points by their range index. Meaning, `scatter.selection([0, 1, 2])` will select the first, second, and third point. This works consistently across Pandas, Polars, and other supported DataFrames. Alternatively, if you're binding a Pandas DataFrame with a custom index, you can make the `Scatter` instance reference data points by the DataFrame's label index via `Scatter(data=df, data_use_index=True)`. Note that `data_use_index` is only supported for Pandas DataFrames. ::: ## Lasso Select Points {#set-interactive} As you might have seen already in the [interactions guide](./interactions), we can also select points interactively using the lasso tool. ## Get Selected Points {#get-programmatic} Importantly, once you have selected some points, you can retrieve the interaction using the same method [`scatter.selection()`](./api#scatter.selection) that we used earlier. This time just don't pass any arguments to the function. ```py scatter.selection() # => [0, 1, 2] ``` This will return a the indices of the selected points. If you have bound a DataFrame to the scatter instance, you can use these indices to retrieve the original data records. ```py{2} scatter.selection(df.query("cat == 'A'").index) df.loc[scatter.selection()] ``` | | x | y | pval | cat | |------|------|------|------|-----| | 0 | 0.13 | 0.27 | 0.51 | A | | 42 | 0.87 | 0.93 | 0.80 | A | | … | … | … | … | … | | 1337 | 0.10 | 0.25 | 0.25 | A | ## Observe Selected Points {#get-interactive} Real *magic* can about to happen when you react to selections automatically. You can do this by observing scatter widget's `selection` property: ```py{1,17-23} import ipywidgets import jscatter import numpy as np import pandas as pd df = pd.DataFrame({ # Random floats "mass": np.random.rand(500), "speed": np.random.rand(500), "pval": np.random.rand(500), # Random letters A, B, C, D, E, F, G, H "cat": np.vectorize(lambda x: chr(65 + round(x * 8)))(np.random.rand(500)), }) scatter = jscatter.Scatter(data=df, x="mass", y="speed", color_by="cat") output = ipywidgets.Output() @output.capture(clear_output=True) def selection_change_handler(change): display(df.loc[change.new].style.hide(axis='index')) scatter.widget.observe(selection_change_handler, names=["selection"]) ipywidgets.HBox([scatter.show(), output]) ``` If you want to learn how the point selections can be used to help you explore large-scale datasets, check out our [in-depth talk+tutorial from SciPy '23](https://github.com/flekschas/jupyter-scatter-tutorial). --- --- url: /link-multiple-plots.md description: How to link and synchronize multiple Jupyter Scatter instances. --- # Link Multiple Scatter Plots There are many use cases where one wants to create two or more scatter plot and potentially link the view, selection, and hover state. For instance, we might want to * visualize different properties of a dataset * compare different facets of a dataset * compare different embedding methods of a dataset * compare multiple datasets with shared labels Jupyter Scatter can help with this by composing and linking multiple scatter instances. ## Simplest Example We'll start out with a very simple example to get familiar with the API. In the following, we'll compose two scatter plots next to each other using [`compose()`](./api#compose). ```py{7} import jscatter import numpy as np x, y = np.random.rand(500), np.random.rand(500) a, b = jscatter.Scatter(x=x, y=y), jscatter.Scatter(x=x, y=y) jscatter.compose([a, b]) ``` By default, jscatter arranges scatter plots into a single row but we can customize this of course. ```py jscatter.compose([a, b], rows=2) ``` We can also change the row height as follows to shrink or grow the plots. ```py jscatter.compose([a, b], rows=2, row_height=240) ``` ## Synchronize View, Selection, & Hover So good so far but the fun part starts when we link/synchronize the scatter plots' views and selections. ```py{3-5} jscatter.compose( [a, b], sync_view=True, sync_selection=True, sync_hover=True, ) ``` Since a common use case is to synchronize everything, `jscatter` offers the shorthand method [`link()`](./api#link): ```py jscatter.link([a, b]) ``` The result is the same as the previous [`compose()`](./api#compose) call. --- --- url: /axes-legends.md description: How to configure axes and legends. --- # Axes & Legends Now that we know how to create, configure, compose, and link scatter plots, it's time learn about axes and legends which are often essential in making sense of the visualized data. ## Axes You might have noticed already that axes are drawn by default. E.g.: ```py{7} from jscatter import Scatter from numpy.random import rand scatter = jscatter.Scatter(x=rand(500), y=rand(500)) scatter.show() ``` ::: info You can also hide the axes via `scatter.axes(False)` in case they are not informative like for t-SNE or UMAP embeddings. ::: In addition, you can also enable a grid, which can be helpful to better locate points. ```py scatter.axes(grid=True) ``` And finally, you can also label the axes ```py scatter.axes(labels=['Speed (km/h)', 'Weight (tons)']) ``` ## Legend When you encode data properties with the point color, opacity, or size, it's immensly helpful to know how the encoded data properties relate to the visual properties by showing a legend. ```py{22-24} import jscatter import numpy as np import pandas as pd df = pd.DataFrame({ # Random floats "mass": np.random.rand(500), "speed": np.random.rand(500), "pval": np.random.rand(500), # Gaussian-distributed floats "effect_size": np.random.normal(.5, .2, 500), # Random letters A, B, C, D, E, F, G, H "cat": np.vectorize(lambda x: chr(65 + round(x * 8)))(np.random.rand(500)), # Random letters X, Y, Z "group": np.vectorize(lambda x: chr(88 + round(x * 2)))(np.random.rand(500)), }) scatter = jscatter.Scatter( data=df, x="mass", y="speed", color_by="cat", size_by="pval", legend=True, ) scatter.show() ``` When you encode a categorical data property (like `cat`) using color, Jupyter Scatter will list out each category in the legend. In contrast, for continuous data properties (like `pval`), only five values are shown in the legend: the minimum, maximum, and three equally spaced values in between. ```py scatter.color(by="pval").opacity(by="cat").size(5) ``` Notice how the legend now only shows five entries for `color` as it encodes a continuous variable. In addition to just showing a mapping of data and visual properties, Jupyter Scatter can also label continuous properties. ```py scatter.color(labeling={ "variable": "p-value", "minValue": "significant", "maxValue": "insignificant", }) ``` Annotating numerical range like this can make it easier for yourself but primarily for collaborators and others to grasp the color mapping. --- --- url: /labels.md description: How to show text labels for points and clusters. --- # Labels When your points are labeled it can be helpful to show these labels in the scatterplot. You can draw text labels with Jupyter Scatter's `label()` function: ```py scatter.label(by='df_column_name') ``` The referenced column needs to be either categorical or contain strings. For all points with the same value, a text label is drawn. For instance, say we have the following DataFrame: | | x | y | cat | pval | |---|---|---|-----|------| | 0 | 0 | 0 | A | 0.51 | | 1 | 1 | 0 | A | 0.80 | | 2 | 1 | 1 | A | 0.25 | | 3 | 0 | 1 | A | 0.01 | | 4 | 2 | 2 | B | 0.65 | | 5 | 3 | 2 | B | 0.99 | | 6 | 3 | 3 | B | 1.33 | | 7 | 2 | 3 | B | 0.01 | We can label points using the `cat` column via `scatter.label(by='cat')`. When displaying labels, Jupyter Scatter automatically manages label collision and overcrowding. It uses an importance-based static placement strategy such that overlapping labels with a lower priority are visualized at a higher zoom when the collision is resolved. To handle many labels, Jupyter Scatter uses a tiling approach where only a limited number of labels (default: `100`) are shown per tile. You can control the label density with the `max_number` parameter. For instance, to show fewer labels per tile do `scatter.label(by='cat', max_number=50)`. ::: info For demo notebooks on how to use labels, see https://github.com/flekschas/jupyter-scatter-tutorial. ::: ## Importance Whenever two text labels would collide, the label with the lower priority is hidden. You can specify the importance via the `importance` parameter as shown below. If no importance information is used, the number of points labeled by a value is used. ```py scatter.label(by='cat', importance='pval') ``` Since importance values are point specific, we need to aggregate multiple values to derive the label importance. By default, Jupyter Scatter uses the mean but you can change this behavior to `'min'`, `'median'`, `'max'`, or `'sum'`. For instance, in the following we use the maximum point importance as the label importance. ```py scatter.label(by='cat', importance='pval', importance_aggregation='max') ``` Additionally, it's also possible to specify a custom aggregator function that takes as input an array of floats and must return a single float. ::: tip If you want to count the points and use this as the importance, simply omit `importance` altogether as that's the default behavior. ::: ## Customization ### Appearance You can customize the appearance and placement of labels in various ways. The `font`, `color`, and `size` parameters allow you to adjust the font face, color, and size. ```py{3-5} scatter.label( by='cat', font='arial bold', color='red', size=36, ) ``` By default, the label size is constant (i.e., zoom invariant) but you can also enlarge labels as you soon in. To do this, set `scale_function` to `"asinh"`, which enlarges the label using the inverse hyperbolic sine function. ```py{3-4} scatter.label( by='cat', size=36, scale_function='asinh' ) ``` The inverse hyperbolic sine function is only applied when zooming in and increases the label size sublinearly compared to the camera zoom as follows: ``` label_scale = asinh(zoom_scale) / asinh(1) ``` ::: info Resolving collisions with inverse hyperbolic sine-scaled labels is computationally more involved than constant scaling. Hence, if you have many labels (i.e., >=1000) we recommend using constant scaling. ::: ### Position You can also control the center position of labels, their alignment around this position, and offset using the `positioning`, `align`, and `offset` parameters. ```py{3-5} scatter.label( by='cat', positioning='largest_cluster', align='top', offset=(2, 2), ) ``` Jupyter Scatter offers three positioning algorithms with different tradeoffs as outlined below. You may want to experiment to see which one works best for your specific use case. #### Highest Density The default positioning method (`'highest_density'`) places the label at the point of highest density within the group. This algorithm: * Is fast * Calculates density based on how many points are clustered in each area * Places labels where the most points are concentrated * Works well for irregular clusters with varying densities * Gives okay results for many datasets #### Center of Mass The `'center_of_mass'` method places the label at the geometric center of all points in the group: * Is fast * Calculates the center position using the Shoelace formula * Creates a balanced label position * Works well for single-cluster and evenly-distributed points * Not recommended when your labels consist of disconnected clusters #### Largest Cluster The `'largest_cluster'` method identifies the largest sub-cluster within the group using [HDBSCAN](https://github.com/scikit-learn-contrib/hdbscan) and places the label at its center of mass: * Slowest method * Detects clusters within each group * Places the label at the center of mass of the largest cluster * Works well when points naturally form multiple clusters * Typically results in the best label placement overall ::: info `"largest_cluster"` is an additional feature that's not included in the default installation as it relies on HDBSCAN. To use the feature install Jupyter Scatter via `pip install "jupyter-scatter[all]"`. ::: ### Line Breaks If your labels tend to be on the longer side, you might want to introduce line breaks. To make your life easier, you can specify an target aspect ratio for which Jupyter Scatter will then try to find optimial line breaks. ```py{3} scatter.label( by='cat', target_aspect_ratio=5, ) ``` ## Point Labels By default, all points with the same value in a column are grouped and given a single label. However, sometimes you may want to label each individual data point instead. Jupyter Scatter supports this through a special syntax - simply append an exclamation mark to the column name: For example, with the following DataFrame, using `scatter.label(by='city!')` will label each individual city. Hence, even though there are two cities called "Berlin", they each get their own label. | | x | y | city | |---|------|------|----------| | 0 | 0.13 | 0.27 | Paris | | 1 | 0.87 | 0.93 | New York | | 2 | 0.10 | 0.25 | Berlin | | 3 | 0.03 | 0.90 | Rome | | 4 | 0.19 | 0.78 | Tokyo | | 4 | 0.99 | 0.81 | Berlin | ```py scatter.label(by='city!') ``` This is useful when each data point represents a unique entity but their labels are not unique (like cities on a map) ::: info Currently, only one column can be marked as a point label. If multiple columns are marked with exclamation marks, only the first one will be used as a point label. ::: ## Multiple and Hierarchical Label Types Jupyer Scatter also supports multiple and even hierarchical label types. For instance, let's assume the data frame contains another categorical or string column. | | x | y | cat | sub | pval | |---|------|------|-----|-----|------| | 0 | 0.13 | 0.27 | A | A1 | 0.51 | | 1 | 0.87 | 0.93 | B | B1 | 0.80 | | 2 | 0.10 | 0.25 | A | A1 | 0.25 | | 3 | 0.03 | 0.90 | A | A2 | 0.01 | | 4 | 0.19 | 0.78 | B | B1 | 0.65 | We can render out labels for both `"cat"` and `"sub"` as follows: ```py scatter.label(by=["cat", "sub"]) ``` You can customize multiple labels in two ways. You can provide a list of values. For instance, to draw `cat` labels in black bold at 24px and `sub` labels in red italics at 18px, you can do: ```py{3-5} scatter.label( by=['cat', 'sub'] font=['bold', 'italic'], color=['black', '#ff0000'], size=[24, 18], ) ``` If you want to be even more specific with settings, you can also pass a dictionary of `:` pairs. For instance, if you want all `cat` to be black but `B` to be green, you can do the following: ```py{4} scatter.label( by=['cat', 'sub'], font=['bold', 'italic'], color={'cat': 'black', 'cat:B': 'green', 'sub': '#ff0000'}, size=[24, 18], ) ``` When working with multiple label types, collisions are still resolved in order of importance such that the colliding labels with lower priorities appear only at higher zoom levels when they no longer collide with labels of higher importance. To adjust this behavior you can specify type-specific zoom ranges. Zoom ranges are declared as zoom levels where `zoom_scale = 2 ^ zoom_level`. ```py{3} scatter.label( by=['cat', 'sub'], zoom_ranges={'cat': (-math.inf, 2), 'sub': (2, 10)} ) ``` In the above example, `cat` labels are allowed to appear up until zoom level `2` and `sub` labels are allowed to appear from zoom level `2` onward. ::: info Note, zoom ranges do not enforce that labels are shown in that given range but rather they specify the allowed zoom range at which they can appear given the labels' importance and overlap with other labels. ::: If your label types describe a strict hierarchy, as is the case for the example specified above, then you can set `hierarchical` to `True`. For hierarchical label types, Jupyter Scatter automatically enforces that labels in at a lower hierarchical level are shown before labels with a higher hierarchical level. ```py{3} scatter.label( by=['cat', 'sub'], hierarchical=True ) ``` For instance, in the example above, irrespective of the labels' importance, `cat` labels will be shown before `sub` labels if they collide. Non-colliding labels might be shown at the same time. ## Exclude Labels Sometimes you do not want to show all labels. For instance, when clustering labels it's common to label unclear points as *noise*. You can exclude unwanted labels using the `exclude` parameter. For instance, in the following we exclude the label `B`. ```py scatter.label(by=['cat'], exclude=['B']) ``` When using multiple label types, you need to specify excluded labels via `: