pluot_zarr 0.1.4

Format-specific Pluot layers for rendering Zarr data
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
use std::sync::Arc;
use serde::{Deserialize, Serialize};
use pluot_core::{maybe_timeout, FutureExt, Duration};

use pluot_core::log;
use pluot_core::wgpu;
use pluot_core::cache::{use_memo_vec_f32, use_memo_vec_i32, use_memo_numeric_data};
use zarrs::storage::AsyncReadableStorageTraits;
use pluot_core::compute::reduce::reduce_extent;
use pluot_core::zarr::is_timed_out_zarrs_error;
use pluot_core::two::svg::{update_svg, SvgContext};
use pluot_core::render_traits::{CategoricalColormap, CategoricalParams, ColorMode, DrawToRasterGpu, DrawToRasterCpu, DrawToSvg, OpacityMode, PickableLayer, PreparedLayer, SizeMode, ViewParams, AspectRatioMode, UnitsMode, MarginParams, resolve_store_name};
use pluot_core::layers::point_layer::{PointLayer, PointShapeMode, PointLayerParams};
use pluot_core::numeric_data::NumericData;
use pluot_core::render_types::{CpuContext, CpuRenderPass, PrepareResult, RenderResult};
use pluot_core::render_types::GpuContext;
use pluot_core::LayerPickingResult;
use pluot_core::viewport::DataCoord;
use pluot_core::viewport::ScreenCoord;
use pluot_core::viewport::get_bounds;

use crate::zarr_numeric_data::load_arr_as_numeric_data;


#[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(default)]
pub struct ZarrPointLayerParams {
    pub layer_id: String,
    // If None, assume margin: 0 in all directions.
    pub bounds: Option<MarginParams>,
    pub data_unit_mode_x: UnitsMode,
    pub data_unit_mode_y: UnitsMode,

    pub point_radius_unit_mode_x: UnitsMode,
    pub point_radius_unit_mode_y: UnitsMode,
    pub point_shape_mode: PointShapeMode,
    pub model_matrix: Option<[f32; 16]>, // Column-major 4x4 matrix

    pub point_radius: Option<f32>, // None means automatically-determine
    pub point_opacity: Option<f32>, // None means automatically-determine

    // Data keys
    pub store_name: Option<String>,
    pub x_key: String,
    pub y_key: String,

    // TODO: need equivalents to the non-Zarr layers' uniform+instanced support for colors, opacities, stroke_widths, etc.
    // In the zarr case, the instanced mode's values/codes arrays will be a string pointing to an array path, rather than an inlined array/NumericData itself.
    // We still need the sibling params however, to know things like categorical vs. quantitative, interleaved vs not, and the specified colormap, etc.
    // We should also make the parameter names here in the zarr layers more consistent with the non-zarr layers', including removing the _key suffices, for consistency.
    pub color_key: Option<String>,
}

impl Default for ZarrPointLayerParams {
    fn default() -> Self {
        Self {
            layer_id: "".to_string(),
            bounds: None,
            data_unit_mode_x: UnitsMode::Data,
            data_unit_mode_y: UnitsMode::Data,
            point_radius: Some(1.0),
            point_radius_unit_mode_x: UnitsMode::Pixels,
            point_radius_unit_mode_y: UnitsMode::Pixels,
            point_shape_mode: PointShapeMode::Circle,
            model_matrix: None,
            point_opacity: Some(1.0),
            store_name: None,
            x_key: "".to_string(),
            y_key: "".to_string(),
            color_key: None,
        }
    }
}

pub struct ZarrPointLayerData {
    pub x_arr: Arc<Vec<f32>>,
    pub y_arr: Arc<Vec<f32>>,
    pub labels_arr: Arc<Vec<i32>>,
}

pub struct ZarrPointLayer {
    view_params: ViewParams,
    layer_params: ZarrPointLayerParams,
    // TODO: do we want the store or just the store_name here?
    store: Arc<dyn AsyncReadableStorageTraits>,
    store_name: String,

    /// The inner BarPlotLayer, constructed during `prepare()`.
    inner: Option<PointLayer>,
}

impl ZarrPointLayer {
    pub fn new(
        view_params: ViewParams,
        layer_params: ZarrPointLayerParams,
    ) -> Self {
        // Error if point_radius_unit_mode is "data" when data_unit_mode is "pixels".
        if layer_params.point_radius_unit_mode_x == UnitsMode::Data && layer_params.data_unit_mode_x == UnitsMode::Pixels {
            panic!("point_radius_unit_mode cannot be 'data' when data_unit_mode is 'pixels'");
        }
        if layer_params.point_radius_unit_mode_y == UnitsMode::Data && layer_params.data_unit_mode_y == UnitsMode::Pixels {
            panic!("point_radius_unit_mode cannot be 'data' when data_unit_mode is 'pixels'");
        }
        let store_name = resolve_store_name(&layer_params.store_name, &view_params);

        let store = view_params.get_store(&store_name);
        Self {
            view_params,
            layer_params,
            store,
            store_name,
            inner: None,
        }
    }
}

// Port of the dynamic point-size / opacity heuristics from vitessce:
// https://github.com/vitessce/vitessce/blob/main/packages/view-types/scatterplot/src/shared-spatial-scatterplot/dynamic-opacity.js
const BASE_POINT_SIZE: f32 = 5.0;
const LARGE_DATASET_COUNT: f32 = 10000.0;
const SMALL_DATASET_COUNT: f32 = 100.0;

/// Port of `getInitialPointSize`: the point size (in data/axis units) decreases
/// as the number of points grows, to mitigate overplotting. Ranges from 0.05
/// (<= 100 points) down to 0.0005 (>= 10000 points).
fn get_initial_point_size(num_points: usize) -> f32 {
    BASE_POINT_SIZE / (num_points as f32).clamp(SMALL_DATASET_COUNT, LARGE_DATASET_COUNT)
}

/// Port of `getPointSizeDevicePixels`: converts the axis-space initial point size
/// into device pixels, given the data extent (`x_range`/`y_range`) and the
/// currently-visible extent (`visible_x`/`visible_y`, from `get_bounds`).
///
/// deck.gl computes `(xRange * 2**zoom) / width`, the fraction of the viewport
/// the data spans. In pluot that fraction is `x_range / visible_x`.
fn get_point_size_device_pixels(
    device_pixel_ratio: f32,
    x_range: f32,
    y_range: f32,
    visible_x: f32,
    visible_y: f32,
    width: f32,
    height: f32,
    num_points: usize,
) -> f32 {
    let point_size = get_initial_point_size(num_points);

    // Point size bounds, in screen pixels.
    let point_screen_size_max = 10.0;
    let point_screen_size_min = 2.0 / device_pixel_ratio;

    let x_axis_range = 2.0 / (x_range / visible_x.max(f32::EPSILON));
    let y_axis_range = 2.0 / (y_range / visible_y.max(f32::EPSILON));

    // The diagonal screen size as a fraction of the current diagonal axis range,
    // then converted to device pixels.
    let diagonal_screen_size = (width * width + height * height).sqrt();
    let diagonal_axis_range = (x_axis_range * x_axis_range + y_axis_range * y_axis_range).sqrt();
    let diagonal_fraction = point_size / diagonal_axis_range.max(f32::EPSILON);
    let device_size = diagonal_fraction * diagonal_screen_size;

    device_size.clamp(point_screen_size_min, point_screen_size_max)
}

/// Port of `getPointOpacity`: lowers opacity for dense point clouds to avoid
/// overplotting. `x_range`/`y_range` are the data extent and `visible_x`/
/// `visible_y` are the visible extent (from `get_bounds`). `width`/`height` are
/// the plot area in pixels.
fn get_point_opacity(
    x_range: f32,
    y_range: f32,
    visible_x: f32,
    visible_y: f32,
    width: f32,
    height: f32,
    num_points: usize,
) -> f32 {
    let n = num_points as f32;

    // deck.gl: X = maxY - minY (visible y span), Y = maxX - minX (visible x span).
    let x = visible_y.max(f32::EPSILON);
    let y = visible_x.max(f32::EPSILON);
    let x0 = x_range;
    let y0 = y_range;
    let w = width;
    let h = height;

    // Average fill density (deck.gl default when none is provided).
    let rho = (1.0 / 10.0_f32.powf(n.log10() - 3.0)).min(1.0);

    // p (the pixel length/width of a point) is 1 for us, so it drops out.
    let alpha = ((rho * w * h) / n) * (y0 / y) * (x0 / x);
    alpha.clamp(2.01 / 255.0, 1.0)
}



#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)]
impl PreparedLayer for ZarrPointLayer {
    async fn prepare(&mut self, gpu_context: Option<&GpuContext<'_>>) -> PrepareResult {
        let store = self.store.clone();

        // TODO: include the layer type in the memoization dependencies?
        // But what if we want multiple layers to be able to reuse the same cached data?
        // Then we should also avoid including the layer_id...
        let l_i32_future_deps = vec!["l_bytes".to_string(), self.store_name.clone(), self.layer_params.layer_id.to_string()];
        let l_i32_future = use_memo_vec_i32(async || {
            let labels_array_path = &self.layer_params.color_key.as_ref().expect("Color key");
            let labels_array_future = zarrs::array::Array::async_open(store.clone(), labels_array_path);
            let labels_array = labels_array_future.await.unwrap();
            let labels_subset = labels_array.subset_all();
            let labels_vec = labels_array.async_retrieve_array_subset::<Vec<i64>>(&labels_subset).await?;
            // Convert to i32
            let labels_i32: Vec<i32> = labels_vec.iter().map(|&c| c as i32).collect();
            Ok(labels_i32)
        }, &l_i32_future_deps, self.view_params.cache_enabled);

        // TODO: improve the keys / memoization dependencies to at least include the plot_id and store_name.
        // Load the X and Y coordinate arrays in their native dtype (any dtype
        // supported by NumericData); PointLayer uploads each to the GPU at its
        // native width, so there is no per-element cast here.
        let x_data_future_deps = vec!["x_bytes".to_string(), self.store_name.clone(), self.layer_params.layer_id.to_string()];
        let x_data_future = use_memo_numeric_data(async || {
            load_arr_as_numeric_data(store.clone(), &self.layer_params.x_key).await
        }, &x_data_future_deps, self.view_params.cache_enabled);

        let y_data_future_deps = vec!["y_bytes".to_string(), self.store_name.clone(), self.layer_params.layer_id.to_string()];
        let y_data_future = use_memo_numeric_data(async || {
            load_arr_as_numeric_data(store.clone(), &self.layer_params.y_key).await
        }, &y_data_future_deps, self.view_params.cache_enabled);

        // Await in parallel: Use futures::join, similar to Promise.all in JS.
        //let (x_data, y_data, l_i32) = futures::join!(x_data_future, y_data_future, l_i32_future);

        let futures_try_join_result = futures::try_join!(
            maybe_timeout!(x_data_future, self.view_params.timeout),
            maybe_timeout!(y_data_future, self.view_params.timeout),
            maybe_timeout!(l_i32_future, self.view_params.timeout),
        );

        // TODO: load image data as vec of individual chunks (rather than requesting the full slice)
        // to allow for progressive rendering of large images as the chunks load.
        // We want to render the chunks that have loaded prior to the timeout (if there was a timeout specified).
        // First convert the requested slice to the chunk keys?

        let (x_data, y_data, l_i32) = match futures_try_join_result {
            Ok((x_data_result, y_data_result, l_i32_result)) => {
                // x/y are Result<Arc<NumericData>, ArrayError>; labels are Result<Arc<Vec<i32>>, ArrayError>.
                match (x_data_result, y_data_result, l_i32_result) {
                    (Ok(x), Ok(y), Ok(l)) => (x, y, l),
                    (Err(e), _, _) | (_, Err(e), _) | (_, _, Err(e)) => {
                        if is_timed_out_zarrs_error(&e) {
                            // TODO: still render something in this case?
                            return PrepareResult { bailed_early: true };
                        } else {
                            panic!("Zarrs error during ZarrPointLayer prepare: {:?}", e);
                        }
                    }
                }
            }
            Err(_) => {
                // Wall-clock timeout from maybe_timeout!
                return PrepareResult { bailed_early: true };
            }
        };

        // Resolve automatically-determined (None) point_radius / point_opacity values.
        // When either is None, compute the extent (min/max) of the X and Y positions so we
        // can derive "good" defaults from the data range and the number of points.
        let (point_radius, point_opacity) = {
            let auto_radius = self.layer_params.point_radius.is_none();
            let auto_opacity = self.layer_params.point_opacity.is_none();

            if !auto_radius && !auto_opacity {
                // Both provided: no need to compute the extent.
                (self.layer_params.point_radius.unwrap(), self.layer_params.point_opacity.unwrap())
            } else {
                // reduce_extent accepts NumericData directly and reduces it on
                // the GPU in its native dtype — no CPU-side cast to f32.
                let x_for_extent = x_data.as_ref().clone();
                let y_for_extent = y_data.as_ref().clone();

                // Cache the extent so repeated prepares (e.g. on pan/zoom) reuse it.
                // Returns [x_min, x_max, y_min, y_max].
                let extent_future_deps = vec![
                    "point_extent".to_string(),
                    self.store_name.clone(),
                    self.layer_params.layer_id.clone(),
                    self.layer_params.x_key.clone(),
                    self.layer_params.y_key.clone(),
                ];
                let extent = use_memo_vec_f32(async || {
                    let (x_min, x_max) = reduce_extent(gpu_context, x_for_extent).await;
                    let (y_min, y_max) = reduce_extent(gpu_context, y_for_extent).await;
                    Ok::<Vec<f32>, std::convert::Infallible>(vec![x_min, x_max, y_min, y_max])
                }, &extent_future_deps, self.view_params.cache_enabled)
                    .await
                    .expect("Extent computation failed in ZarrPointLayer.prepare");

                let (x_min, x_max, y_min, y_max) = (extent[0], extent[1], extent[2], extent[3]);
                let num_points = x_data.len();

                // Data extent (in pluot's (0,1) data space for Data unit mode).
                let x_range = (x_max - x_min).abs();
                let y_range = (y_max - y_min).abs();

                // Currently-visible extent (camera + aspect ratio + margins applied),
                // the pluot equivalent of deck.gl's OrthographicView.getBounds().
                let visible = get_bounds(&self.view_params);
                let visible_x = (visible.x_max - visible.x_min).abs();
                let visible_y = (visible.y_max - visible.y_min).abs();

                // Plot area in pixels (viewport minus margins), matching get_bounds.
                let (margin_top, margin_right, margin_bottom, margin_left) = match &self.view_params.margins {
                    Some(m) => (
                        m.margin_top.unwrap_or(0.0),
                        m.margin_right.unwrap_or(0.0),
                        m.margin_bottom.unwrap_or(0.0),
                        m.margin_left.unwrap_or(0.0),
                    ),
                    None => (0.0, 0.0, 0.0, 0.0),
                };
                let layer_w = (self.view_params.width as f32 - (margin_left + margin_right)).max(1.0);
                let layer_h = (self.view_params.height as f32 - (margin_top + margin_bottom)).max(1.0);

                let point_radius = match self.layer_params.point_radius {
                    Some(radius) => radius,
                    None => get_point_size_device_pixels(
                        self.view_params.device_pixel_ratio,
                        x_range,
                        y_range,
                        visible_x,
                        visible_y,
                        layer_w,
                        layer_h,
                        num_points,
                    ),
                };
                let point_opacity = match self.layer_params.point_opacity {
                    Some(opacity) => opacity,
                    None => get_point_opacity(
                        x_range,
                        y_range,
                        visible_x,
                        visible_y,
                        layer_w,
                        layer_h,
                        num_points,
                    ),
                };
                (point_radius, point_opacity)
            }
        };

        let mut sublayer = PointLayer::new(
            self.view_params.clone(),
            PointLayerParams {
                layer_id: self.layer_params.layer_id.clone(),
                bounds: self.layer_params.bounds.clone(),
                data_unit_mode_x: self.layer_params.data_unit_mode_x,
                data_unit_mode_y: self.layer_params.data_unit_mode_y,
                point_radius: Some(SizeMode::UniformSize(point_radius)),
                // TODO: if point_radius is None, override the point_radius_unit_mode values to always be UnitsMode::Pixels.
                point_radius_unit_mode_x: self.layer_params.point_radius_unit_mode_x,
                point_radius_unit_mode_y: self.layer_params.point_radius_unit_mode_y,
                point_shape_mode: self.layer_params.point_shape_mode,
                fill_opacity: Some(OpacityMode::UniformOpacity(point_opacity)),
                model_matrix: self.layer_params.model_matrix,
                fill_color: Some(ColorMode::Categorical(CategoricalParams {
                    codes: NumericData::Int32(l_i32.clone()),
                    colormap: CategoricalColormap::Category10,
                })),
                position_x: x_data.as_ref().clone(),
                position_y: y_data.as_ref().clone(),
                ..Default::default()
            }
        );
        sublayer.prepare(gpu_context).await;
        self.inner = Some(sublayer);

        return PrepareResult {
            bailed_early: false,
        };
    }
}

#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)]
impl DrawToRasterGpu for ZarrPointLayer {
    async fn draw(&self, gpu_context: &GpuContext<'_>, pass: &mut wgpu::RenderPass) {
        if let Some(inner) = &self.inner {
            DrawToRasterGpu::draw(inner, gpu_context, pass).await;
        }
    }
}

#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)]
impl DrawToRasterCpu for ZarrPointLayer {
    async fn draw(&self, _cpu_context: &CpuContext<'_>, _pass: &mut CpuRenderPass) {}
}


#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)]
impl DrawToSvg for ZarrPointLayer {
    async fn draw(&self, ctx: &mut SvgContext) {
        if let Some(inner) = &self.inner {
            DrawToSvg::draw(inner, ctx).await
        }
    }
}

impl PickableLayer for ZarrPointLayer {
    fn pick(&self, screen_coord: ScreenCoord, data_coord: Option<DataCoord>) -> Option<LayerPickingResult> {
        let DataCoord::TwoD { x: cx, y: cy } = data_coord? else {
            return None;
        };

        if let Some(inner) = &self.inner {
            return PickableLayer::pick(inner, screen_coord, data_coord);
        }
        return None;
    }
}