• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

geo-engine / geoengine / 3929938005

pending completion
3929938005

push

github

GitHub
Merge #713

84930 of 96741 relevant lines covered (87.79%)

79640.1 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

97.56
/operators/src/plot/scatter_plot.rs
1
use async_trait::async_trait;
2
use futures::StreamExt;
3
use serde::{Deserialize, Serialize};
4

5
use geoengine_datatypes::collections::FeatureCollectionInfos;
6
use geoengine_datatypes::plots::{Histogram2D, HistogramDimension, Plot, PlotData};
7

8
use crate::engine::{
9
    CreateSpan, ExecutionContext, InitializedPlotOperator, InitializedVectorOperator, Operator,
10
    OperatorName, PlotOperator, PlotQueryProcessor, PlotResultDescriptor, QueryContext,
11
    QueryProcessor, SingleVectorSource, TypedPlotQueryProcessor, TypedVectorQueryProcessor,
12
};
13
use crate::error::Error;
14
use crate::util::Result;
15
use geoengine_datatypes::primitives::{Coordinate2D, VectorQueryRectangle};
16
use tracing::{span, Level};
17

18
pub const SCATTERPLOT_OPERATOR_NAME: &str = "ScatterPlot";
19

20
/// The maximum number of elements for a scatter plot
21
const SCATTER_PLOT_THRESHOLD: usize = 500;
22

23
/// The number of elements to process at once (i.e., without switching from scatter-plot to histogram)
24
const BATCH_SIZE: usize = 1000;
25

26
/// The maximum number of elements before we turn the collector into a histogram.
27
/// At this point, the bounds of the histogram are fixed (i.e., further values exceeding
28
/// the min/max seen so far are ignored)
29
const COLLECTOR_TO_HISTOGRAM_THRESHOLD: usize = BATCH_SIZE * 10;
30

31
/// A scatter plot about two attributes of a vector dataset. If the
32
/// dataset contains more then `SCATTER_PLOT_THRESHOLD` elements, this
33
/// operator creates a 2D histogram.
34
pub type ScatterPlot = Operator<ScatterPlotParams, SingleVectorSource>;
35

36
impl OperatorName for ScatterPlot {
37
    const TYPE_NAME: &'static str = "ScatterPlot";
38
}
39

40
/// The parameter spec for `ScatterPlot`
41
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
5✔
42
#[serde(rename_all = "camelCase")]
43
pub struct ScatterPlotParams {
44
    /// Name of the (numeric) attribute for the x-axis.
45
    pub column_x: String,
46
    /// Name of the (numeric) attribute for the y-axis.
47
    pub column_y: String,
48
}
49

50
#[typetag::serde]
×
51
#[async_trait]
52
impl PlotOperator for ScatterPlot {
53
    async fn _initialize(
10✔
54
        self: Box<Self>,
10✔
55
        context: &dyn ExecutionContext,
10✔
56
    ) -> Result<Box<dyn InitializedPlotOperator>> {
10✔
57
        let source = self.sources.vector.initialize(context).await?;
10✔
58
        for cn in [&self.params.column_x, &self.params.column_y] {
17✔
59
            match source.result_descriptor().column_data_type(cn.as_str()) {
17✔
60
                Some(column) if !column.is_numeric() => {
16✔
61
                    return Err(Error::InvalidOperatorSpec {
3✔
62
                        reason: format!("Column '{cn}' is not numeric."),
3✔
63
                    });
3✔
64
                }
65
                Some(_) => {
13✔
66
                    // OK
13✔
67
                }
13✔
68
                None => {
69
                    return Err(Error::ColumnDoesNotExist {
1✔
70
                        column: cn.to_string(),
1✔
71
                    });
1✔
72
                }
73
            }
74
        }
75

76
        let in_desc = source.result_descriptor().clone();
6✔
77

6✔
78
        Ok(InitializedScatterPlot::new(in_desc.into(), self.params, source).boxed())
6✔
79
    }
20✔
80

81
    span_fn!(ScatterPlot);
×
82
}
83

84
/// The initialization of `Histogram`
85
pub struct InitializedScatterPlot<Op> {
86
    result_descriptor: PlotResultDescriptor,
87
    column_x: String,
88
    column_y: String,
89
    source: Op,
90
}
91

92
impl<Op> InitializedScatterPlot<Op> {
93
    pub fn new(
6✔
94
        result_descriptor: PlotResultDescriptor,
6✔
95
        params: ScatterPlotParams,
6✔
96
        source: Op,
6✔
97
    ) -> Self {
6✔
98
        Self {
6✔
99
            result_descriptor,
6✔
100
            column_x: params.column_x,
6✔
101
            column_y: params.column_y,
6✔
102
            source,
6✔
103
        }
6✔
104
    }
6✔
105
}
106
impl InitializedPlotOperator for InitializedScatterPlot<Box<dyn InitializedVectorOperator>> {
107
    fn result_descriptor(&self) -> &PlotResultDescriptor {
×
108
        &self.result_descriptor
×
109
    }
×
110

111
    fn query_processor(&self) -> Result<TypedPlotQueryProcessor> {
6✔
112
        let processor = ScatterPlotQueryProcessor {
6✔
113
            input: self.source.query_processor()?,
6✔
114
            column_x: self.column_x.clone(),
6✔
115
            column_y: self.column_y.clone(),
6✔
116
        };
6✔
117

6✔
118
        Ok(TypedPlotQueryProcessor::JsonVega(processor.boxed()))
6✔
119
    }
6✔
120
}
121

122
/// A query processor that calculates the scatter plot about its vector input.
123
pub struct ScatterPlotQueryProcessor {
124
    input: TypedVectorQueryProcessor,
125
    column_x: String,
126
    column_y: String,
127
}
128

129
#[async_trait]
130
impl PlotQueryProcessor for ScatterPlotQueryProcessor {
131
    type OutputFormat = PlotData;
132

133
    fn plot_type(&self) -> &'static str {
×
134
        SCATTERPLOT_OPERATOR_NAME
×
135
    }
×
136

137
    async fn plot_query<'p>(
6✔
138
        &'p self,
6✔
139
        query: VectorQueryRectangle,
6✔
140
        ctx: &'p dyn QueryContext,
6✔
141
    ) -> Result<Self::OutputFormat> {
6✔
142
        let mut collector =
6✔
143
            CollectorKind::Values(Collector::new(self.column_x.clone(), self.column_y.clone()));
6✔
144

145
        call_on_generic_vector_processor!(&self.input, processor => {
6✔
146
            let mut query = processor.query(query, ctx).await?;
6✔
147
            while let Some(collection) = query.next().await {
15✔
148
                let collection = collection?;
9✔
149

150
                let data_x = collection.data(&self.column_x).expect("checked in param");
9✔
151
                let data_y = collection.data(&self.column_y).expect("checked in param");
9✔
152

153
                let valid_points = data_x.float_options_iter().zip(data_y.float_options_iter()).filter_map(|(a,b)| match (a,b) {
18,717✔
154
                    (Some(x),Some(y)) if x.is_finite() && y.is_finite() => Some(Coordinate2D::new(x,y)),
18,714✔
155
                    _ => None
6✔
156
                });
18,717✔
157

158
                for chunk in &itertools::Itertools::chunks(valid_points, BATCH_SIZE) {
23✔
159
                    collector.add_batch( chunk )?;
23✔
160
                }
161
            }
162
        });
163
        Ok(collector.into_plot()?.to_vega_embeddable(false)?)
6✔
164
    }
12✔
165
}
166

167
struct Collector {
168
    elements: Vec<Coordinate2D>,
169
    column_x: String,
170
    column_y: String,
171
    bounds_x: (f64, f64),
172
    bounds_y: (f64, f64),
173
}
174

175
impl Collector {
176
    fn new(column_x: String, column_y: String) -> Self {
12✔
177
        Collector {
12✔
178
            column_x,
12✔
179
            column_y,
12✔
180
            elements: Vec::with_capacity(SCATTER_PLOT_THRESHOLD),
12✔
181
            bounds_x: (f64::INFINITY, f64::NEG_INFINITY),
12✔
182
            bounds_y: (f64::INFINITY, f64::NEG_INFINITY),
12✔
183
        }
12✔
184
    }
12✔
185

186
    fn element_count(&self) -> usize {
35✔
187
        self.elements.len()
35✔
188
    }
35✔
189

190
    fn add_batch(&mut self, values: impl Iterator<Item = Coordinate2D>) {
21✔
191
        for v in values {
42,985✔
192
            self.add(v);
42,964✔
193
        }
42,964✔
194
    }
21✔
195

196
    fn add(&mut self, value: Coordinate2D) {
42,964✔
197
        if value.x.is_finite() && value.y.is_finite() {
42,964✔
198
            self.bounds_x.0 = std::cmp::min_by(self.bounds_x.0, value.x, |a, b| {
42,964✔
199
                a.partial_cmp(b).expect("checked")
42,964✔
200
            });
42,964✔
201
            self.bounds_x.1 = std::cmp::max_by(self.bounds_x.1, value.x, |a, b| {
42,964✔
202
                a.partial_cmp(b).expect("checked")
42,964✔
203
            });
42,964✔
204
            self.bounds_y.0 = std::cmp::min_by(self.bounds_y.0, value.y, |a, b| {
42,964✔
205
                a.partial_cmp(b).expect("checked")
42,964✔
206
            });
42,964✔
207
            self.bounds_y.1 = std::cmp::max_by(self.bounds_y.1, value.y, |a, b| {
42,964✔
208
                a.partial_cmp(b).expect("checked")
42,964✔
209
            });
42,964✔
210
            self.elements.push(value);
42,964✔
211
        }
42,964✔
212
    }
42,964✔
213
}
214

215
enum CollectorKind {
216
    Values(Collector),
217
    Histogram(Histogram2D),
218
}
219

220
impl CollectorKind {
221
    fn histogram_from_collector(value: &Collector) -> Result<Histogram2D> {
6✔
222
        let bucket_count = std::cmp::min(100, f64::sqrt(value.element_count() as f64) as usize);
6✔
223

224
        let dim_x = HistogramDimension::new(
6✔
225
            value.column_x.clone(),
6✔
226
            value.bounds_x.0,
6✔
227
            value.bounds_x.1,
6✔
228
            bucket_count,
6✔
229
        )?;
6✔
230
        let dim_y = HistogramDimension::new(
6✔
231
            value.column_y.clone(),
6✔
232
            value.bounds_y.0,
6✔
233
            value.bounds_y.1,
6✔
234
            bucket_count,
6✔
235
        )?;
6✔
236

237
        let mut result = Histogram2D::new(dim_x, dim_y);
6✔
238
        result.update_batch(value.elements.iter().copied());
6✔
239
        Ok(result)
6✔
240
    }
6✔
241

242
    fn add_batch(&mut self, values: impl Iterator<Item = Coordinate2D>) -> Result<()> {
30✔
243
        match self {
30✔
244
            Self::Values(ref mut c) => {
21✔
245
                c.add_batch(values);
21✔
246
                if c.element_count() > COLLECTOR_TO_HISTOGRAM_THRESHOLD {
21✔
247
                    *self = Self::Histogram(Self::histogram_from_collector(c)?);
4✔
248
                }
17✔
249
            }
250
            Self::Histogram(ref mut h) => {
9✔
251
                h.update_batch(values);
9✔
252
            }
9✔
253
        }
254
        Ok(())
30✔
255
    }
30✔
256

257
    fn into_plot(self) -> Result<Box<dyn Plot>> {
258
        match self {
8✔
259
            Self::Histogram(h) => Ok(Box::new(h)),
4✔
260
            Self::Values(v) if v.element_count() <= SCATTER_PLOT_THRESHOLD => Ok(Box::new(
8✔
261
                geoengine_datatypes::plots::ScatterPlot::new_with_data(
6✔
262
                    v.column_x, v.column_y, v.elements,
6✔
263
                ),
6✔
264
            )),
6✔
265
            Self::Values(v) => Ok(Box::new(Self::histogram_from_collector(&v)?)),
2✔
266
        }
267
    }
12✔
268
}
269

270
#[cfg(test)]
271
mod tests {
272
    use geoengine_datatypes::util::test::TestDefault;
273
    use serde_json::json;
274

275
    use geoengine_datatypes::primitives::{
276
        BoundingBox2D, FeatureData, NoGeometry, SpatialResolution, TimeInterval,
277
    };
278
    use geoengine_datatypes::{collections::DataCollection, primitives::MultiPoint};
279

280
    use crate::engine::{ChunkByteSize, MockExecutionContext, MockQueryContext, VectorOperator};
281
    use crate::mock::MockFeatureCollectionSource;
282

283
    use super::*;
284

285
    #[test]
1✔
286
    fn serialization() {
1✔
287
        let scatter_plot = ScatterPlot {
1✔
288
            params: ScatterPlotParams {
1✔
289
                column_x: "foo".to_owned(),
1✔
290
                column_y: "bar".to_owned(),
1✔
291
            },
1✔
292
            sources: MockFeatureCollectionSource::<MultiPoint>::multiple(vec![])
1✔
293
                .boxed()
1✔
294
                .into(),
1✔
295
        };
1✔
296

1✔
297
        let serialized = json!({
1✔
298
            "type": "ScatterPlot",
1✔
299
            "params": {
1✔
300
                "columnX": "foo",
1✔
301
                "columnY": "bar",
1✔
302
            },
1✔
303
            "sources": {
1✔
304
                "vector": {
1✔
305
                    "type": "MockFeatureCollectionSourceMultiPoint",
1✔
306
                    "params": {
1✔
307
                        "collections": [],
1✔
308
                        "spatialReference": "EPSG:4326",
1✔
309
                        "measurements": {},
1✔
310
                    }
1✔
311
                }
1✔
312
            }
1✔
313
        })
1✔
314
        .to_string();
1✔
315

1✔
316
        let deserialized: ScatterPlot = serde_json::from_str(&serialized).unwrap();
1✔
317

1✔
318
        assert_eq!(deserialized.params, scatter_plot.params);
1✔
319
    }
1✔
320

321
    #[tokio::test]
1✔
322
    async fn vector_data() {
1✔
323
        let vector_source = MockFeatureCollectionSource::multiple(vec![
1✔
324
            DataCollection::from_slices(
1✔
325
                &[] as &[NoGeometry],
1✔
326
                &[TimeInterval::default(); 4],
1✔
327
                &[
1✔
328
                    ("foo", FeatureData::Int(vec![1, 2, 3, 4])),
1✔
329
                    ("bar", FeatureData::Int(vec![1, 2, 3, 4])),
1✔
330
                ],
1✔
331
            )
1✔
332
            .unwrap(),
1✔
333
            DataCollection::from_slices(
1✔
334
                &[] as &[NoGeometry],
1✔
335
                &[TimeInterval::default(); 4],
1✔
336
                &[
1✔
337
                    ("foo", FeatureData::Int(vec![5, 6, 7, 8])),
1✔
338
                    ("bar", FeatureData::Int(vec![5, 6, 7, 8])),
1✔
339
                ],
1✔
340
            )
1✔
341
            .unwrap(),
1✔
342
        ])
1✔
343
        .boxed();
1✔
344

1✔
345
        let box_plot = ScatterPlot {
1✔
346
            params: ScatterPlotParams {
1✔
347
                column_x: "foo".to_string(),
1✔
348
                column_y: "bar".to_string(),
1✔
349
            },
1✔
350
            sources: vector_source.into(),
1✔
351
        };
1✔
352

1✔
353
        let execution_context = MockExecutionContext::test_default();
1✔
354

355
        let query_processor = box_plot
1✔
356
            .boxed()
1✔
357
            .initialize(&execution_context)
1✔
358
            .await
×
359
            .unwrap()
1✔
360
            .query_processor()
1✔
361
            .unwrap()
1✔
362
            .json_vega()
1✔
363
            .unwrap();
1✔
364

365
        let result = query_processor
1✔
366
            .plot_query(
1✔
367
                VectorQueryRectangle {
1✔
368
                    spatial_bounds: BoundingBox2D::new((-180., -90.).into(), (180., 90.).into())
1✔
369
                        .unwrap(),
1✔
370
                    time_interval: TimeInterval::default(),
1✔
371
                    spatial_resolution: SpatialResolution::one(),
1✔
372
                },
1✔
373
                &MockQueryContext::new(ChunkByteSize::MIN),
1✔
374
            )
1✔
375
            .await
×
376
            .unwrap();
1✔
377

1✔
378
        let mut expected =
1✔
379
            geoengine_datatypes::plots::ScatterPlot::new("foo".to_string(), "bar".to_string());
1✔
380
        for i in 1..=8 {
9✔
381
            expected.update(Coordinate2D::new(f64::from(i), f64::from(i)));
8✔
382
        }
8✔
383
        assert_eq!(expected.to_vega_embeddable(false).unwrap(), result);
1✔
384
    }
385

386
    #[tokio::test]
1✔
387
    async fn vector_data_with_nulls_and_nan() {
1✔
388
        let vector_source =
1✔
389
            MockFeatureCollectionSource::multiple(vec![DataCollection::from_slices(
1✔
390
                &[] as &[NoGeometry],
1✔
391
                &[TimeInterval::default(); 7],
1✔
392
                &[
1✔
393
                    (
1✔
394
                        "foo",
1✔
395
                        FeatureData::NullableFloat(vec![
1✔
396
                            Some(1.0),
1✔
397
                            None,
1✔
398
                            Some(3.0),
1✔
399
                            None,
1✔
400
                            Some(f64::NAN),
1✔
401
                            Some(6.0),
1✔
402
                            Some(f64::NAN),
1✔
403
                        ]),
1✔
404
                    ),
1✔
405
                    (
1✔
406
                        "bar",
1✔
407
                        FeatureData::NullableFloat(vec![
1✔
408
                            Some(1.0),
1✔
409
                            Some(2.0),
1✔
410
                            None,
1✔
411
                            None,
1✔
412
                            Some(5.0),
1✔
413
                            Some(f64::NAN),
1✔
414
                            Some(f64::NAN),
1✔
415
                        ]),
1✔
416
                    ),
1✔
417
                ],
1✔
418
            )
1✔
419
            .unwrap()])
1✔
420
            .boxed();
1✔
421

1✔
422
        let box_plot = ScatterPlot {
1✔
423
            params: ScatterPlotParams {
1✔
424
                column_x: "foo".to_string(),
1✔
425
                column_y: "bar".to_string(),
1✔
426
            },
1✔
427
            sources: vector_source.into(),
1✔
428
        };
1✔
429

1✔
430
        let execution_context = MockExecutionContext::test_default();
1✔
431

432
        let query_processor = box_plot
1✔
433
            .boxed()
1✔
434
            .initialize(&execution_context)
1✔
435
            .await
×
436
            .unwrap()
1✔
437
            .query_processor()
1✔
438
            .unwrap()
1✔
439
            .json_vega()
1✔
440
            .unwrap();
1✔
441

442
        let result = query_processor
1✔
443
            .plot_query(
1✔
444
                VectorQueryRectangle {
1✔
445
                    spatial_bounds: BoundingBox2D::new((-180., -90.).into(), (180., 90.).into())
1✔
446
                        .unwrap(),
1✔
447
                    time_interval: TimeInterval::default(),
1✔
448
                    spatial_resolution: SpatialResolution::one(),
1✔
449
                },
1✔
450
                &MockQueryContext::new(ChunkByteSize::MIN),
1✔
451
            )
1✔
452
            .await
×
453
            .unwrap();
1✔
454

1✔
455
        let mut expected =
1✔
456
            geoengine_datatypes::plots::ScatterPlot::new("foo".to_string(), "bar".to_string());
1✔
457
        expected.update(Coordinate2D::new(1.0, 1.0));
1✔
458
        assert_eq!(expected.to_vega_embeddable(false).unwrap(), result);
1✔
459
    }
460

461
    #[tokio::test]
1✔
462
    async fn vector_data_text_column_x() {
1✔
463
        let vector_source = MockFeatureCollectionSource::single(
1✔
464
            DataCollection::from_slices(
1✔
465
                &[] as &[NoGeometry],
1✔
466
                &[TimeInterval::default(); 1],
1✔
467
                &[
1✔
468
                    ("foo", FeatureData::Text(vec!["test".to_string()])),
1✔
469
                    ("bar", FeatureData::Int(vec![64])),
1✔
470
                ],
1✔
471
            )
1✔
472
            .unwrap(),
1✔
473
        )
1✔
474
        .boxed();
1✔
475

1✔
476
        let box_plot = ScatterPlot {
1✔
477
            params: ScatterPlotParams {
1✔
478
                column_x: "foo".to_string(),
1✔
479
                column_y: "bar".to_string(),
1✔
480
            },
1✔
481
            sources: vector_source.into(),
1✔
482
        };
1✔
483

1✔
484
        let execution_context = MockExecutionContext::test_default();
1✔
485

486
        let init = box_plot.boxed().initialize(&execution_context).await;
1✔
487

488
        assert!(init.is_err());
1✔
489
    }
490

491
    #[tokio::test]
1✔
492
    async fn vector_data_text_column_y() {
1✔
493
        let vector_source = MockFeatureCollectionSource::single(
1✔
494
            DataCollection::from_slices(
1✔
495
                &[] as &[NoGeometry],
1✔
496
                &[TimeInterval::default(); 1],
1✔
497
                &[
1✔
498
                    ("foo", FeatureData::Text(vec!["test".to_string()])),
1✔
499
                    ("bar", FeatureData::Int(vec![64])),
1✔
500
                ],
1✔
501
            )
1✔
502
            .unwrap(),
1✔
503
        )
1✔
504
        .boxed();
1✔
505

1✔
506
        let box_plot = ScatterPlot {
1✔
507
            params: ScatterPlotParams {
1✔
508
                column_x: "bar".to_string(),
1✔
509
                column_y: "foo".to_string(),
1✔
510
            },
1✔
511
            sources: vector_source.into(),
1✔
512
        };
1✔
513

1✔
514
        let execution_context = MockExecutionContext::test_default();
1✔
515

516
        let init = box_plot.boxed().initialize(&execution_context).await;
1✔
517

518
        assert!(init.is_err());
1✔
519
    }
520

521
    #[tokio::test]
1✔
522
    async fn vector_data_missing_column_x() {
1✔
523
        let vector_source = MockFeatureCollectionSource::single(
1✔
524
            DataCollection::from_slices(
1✔
525
                &[] as &[NoGeometry],
1✔
526
                &[TimeInterval::default(); 1],
1✔
527
                &[
1✔
528
                    ("foo", FeatureData::Text(vec!["test".to_string()])),
1✔
529
                    ("bar", FeatureData::Int(vec![64])),
1✔
530
                ],
1✔
531
            )
1✔
532
            .unwrap(),
1✔
533
        )
1✔
534
        .boxed();
1✔
535

1✔
536
        let box_plot = ScatterPlot {
1✔
537
            params: ScatterPlotParams {
1✔
538
                column_x: "fo".to_string(),
1✔
539
                column_y: "bar".to_string(),
1✔
540
            },
1✔
541
            sources: vector_source.into(),
1✔
542
        };
1✔
543

1✔
544
        let execution_context = MockExecutionContext::test_default();
1✔
545

546
        let init = box_plot.boxed().initialize(&execution_context).await;
1✔
547

548
        assert!(init.is_err());
1✔
549
    }
550

551
    #[tokio::test]
1✔
552
    async fn vector_data_missing_column_y() {
1✔
553
        let vector_source = MockFeatureCollectionSource::single(
1✔
554
            DataCollection::from_slices(
1✔
555
                &[] as &[NoGeometry],
1✔
556
                &[TimeInterval::default(); 1],
1✔
557
                &[
1✔
558
                    ("foo", FeatureData::Text(vec!["test".to_string()])),
1✔
559
                    ("bar", FeatureData::Int(vec![64])),
1✔
560
                ],
1✔
561
            )
1✔
562
            .unwrap(),
1✔
563
        )
1✔
564
        .boxed();
1✔
565

1✔
566
        let box_plot = ScatterPlot {
1✔
567
            params: ScatterPlotParams {
1✔
568
                column_x: "foo".to_string(),
1✔
569
                column_y: "ba".to_string(),
1✔
570
            },
1✔
571
            sources: vector_source.into(),
1✔
572
        };
1✔
573

1✔
574
        let execution_context = MockExecutionContext::test_default();
1✔
575

576
        let init = box_plot.boxed().initialize(&execution_context).await;
1✔
577

578
        assert!(init.is_err());
1✔
579
    }
580

581
    #[tokio::test]
1✔
582
    async fn vector_data_single_feature() {
1✔
583
        let vector_source =
1✔
584
            MockFeatureCollectionSource::multiple(vec![DataCollection::from_slices(
1✔
585
                &[] as &[NoGeometry],
1✔
586
                &[TimeInterval::default(); 1],
1✔
587
                &[
1✔
588
                    ("foo", FeatureData::Int(vec![1])),
1✔
589
                    ("bar", FeatureData::Int(vec![1])),
1✔
590
                ],
1✔
591
            )
1✔
592
            .unwrap()])
1✔
593
            .boxed();
1✔
594

1✔
595
        let box_plot = ScatterPlot {
1✔
596
            params: ScatterPlotParams {
1✔
597
                column_x: "foo".to_string(),
1✔
598
                column_y: "bar".to_string(),
1✔
599
            },
1✔
600
            sources: vector_source.into(),
1✔
601
        };
1✔
602

1✔
603
        let execution_context = MockExecutionContext::test_default();
1✔
604

605
        let query_processor = box_plot
1✔
606
            .boxed()
1✔
607
            .initialize(&execution_context)
1✔
608
            .await
×
609
            .unwrap()
1✔
610
            .query_processor()
1✔
611
            .unwrap()
1✔
612
            .json_vega()
1✔
613
            .unwrap();
1✔
614

615
        let result = query_processor
1✔
616
            .plot_query(
1✔
617
                VectorQueryRectangle {
1✔
618
                    spatial_bounds: BoundingBox2D::new((-180., -90.).into(), (180., 90.).into())
1✔
619
                        .unwrap(),
1✔
620
                    time_interval: TimeInterval::default(),
1✔
621
                    spatial_resolution: SpatialResolution::one(),
1✔
622
                },
1✔
623
                &MockQueryContext::new(ChunkByteSize::MIN),
1✔
624
            )
1✔
625
            .await
×
626
            .unwrap();
1✔
627

1✔
628
        let mut expected =
1✔
629
            geoengine_datatypes::plots::ScatterPlot::new("foo".to_string(), "bar".to_string());
1✔
630
        expected.update(Coordinate2D::new(1.0, 1.0));
1✔
631
        assert_eq!(expected.to_vega_embeddable(false).unwrap(), result);
1✔
632
    }
633

634
    #[tokio::test]
1✔
635
    async fn vector_data_empty() {
1✔
636
        let vector_source =
1✔
637
            MockFeatureCollectionSource::multiple(vec![DataCollection::from_slices(
1✔
638
                &[] as &[NoGeometry],
1✔
639
                &[] as &[TimeInterval],
1✔
640
                &[
1✔
641
                    ("foo", FeatureData::Int(vec![])),
1✔
642
                    ("bar", FeatureData::Int(vec![])),
1✔
643
                ],
1✔
644
            )
1✔
645
            .unwrap()])
1✔
646
            .boxed();
1✔
647

1✔
648
        let box_plot = ScatterPlot {
1✔
649
            params: ScatterPlotParams {
1✔
650
                column_x: "foo".to_string(),
1✔
651
                column_y: "bar".to_string(),
1✔
652
            },
1✔
653
            sources: vector_source.into(),
1✔
654
        };
1✔
655

1✔
656
        let execution_context = MockExecutionContext::test_default();
1✔
657

658
        let query_processor = box_plot
1✔
659
            .boxed()
1✔
660
            .initialize(&execution_context)
1✔
661
            .await
×
662
            .unwrap()
1✔
663
            .query_processor()
1✔
664
            .unwrap()
1✔
665
            .json_vega()
1✔
666
            .unwrap();
1✔
667

668
        let result = query_processor
1✔
669
            .plot_query(
1✔
670
                VectorQueryRectangle {
1✔
671
                    spatial_bounds: BoundingBox2D::new((-180., -90.).into(), (180., 90.).into())
1✔
672
                        .unwrap(),
1✔
673
                    time_interval: TimeInterval::default(),
1✔
674
                    spatial_resolution: SpatialResolution::one(),
1✔
675
                },
1✔
676
                &MockQueryContext::new(ChunkByteSize::MIN),
1✔
677
            )
1✔
678
            .await
×
679
            .unwrap();
1✔
680

1✔
681
        let expected =
1✔
682
            geoengine_datatypes::plots::ScatterPlot::new("foo".to_string(), "bar".to_string());
1✔
683
        assert_eq!(expected.to_vega_embeddable(false).unwrap(), result);
1✔
684
    }
685

686
    #[tokio::test]
1✔
687
    async fn to_histogram_at_end() {
1✔
688
        let mut values = vec![1; 700];
1✔
689
        values.push(2);
1✔
690

1✔
691
        let vector_source =
1✔
692
            MockFeatureCollectionSource::multiple(vec![DataCollection::from_slices(
1✔
693
                &[] as &[NoGeometry],
1✔
694
                &[TimeInterval::default(); 701],
1✔
695
                &[
1✔
696
                    ("foo", FeatureData::Int(values.clone())),
1✔
697
                    ("bar", FeatureData::Int(values.clone())),
1✔
698
                ],
1✔
699
            )
1✔
700
            .unwrap()])
1✔
701
            .boxed();
1✔
702

1✔
703
        let box_plot = ScatterPlot {
1✔
704
            params: ScatterPlotParams {
1✔
705
                column_x: "foo".to_string(),
1✔
706
                column_y: "bar".to_string(),
1✔
707
            },
1✔
708
            sources: vector_source.into(),
1✔
709
        };
1✔
710

1✔
711
        let execution_context = MockExecutionContext::test_default();
1✔
712

713
        let query_processor = box_plot
1✔
714
            .boxed()
1✔
715
            .initialize(&execution_context)
1✔
716
            .await
×
717
            .unwrap()
1✔
718
            .query_processor()
1✔
719
            .unwrap()
1✔
720
            .json_vega()
1✔
721
            .unwrap();
1✔
722

723
        let result = query_processor
1✔
724
            .plot_query(
1✔
725
                VectorQueryRectangle {
1✔
726
                    spatial_bounds: BoundingBox2D::new((-180., -90.).into(), (180., 90.).into())
1✔
727
                        .unwrap(),
1✔
728
                    time_interval: TimeInterval::default(),
1✔
729
                    spatial_resolution: SpatialResolution::one(),
1✔
730
                },
1✔
731
                &MockQueryContext::new(ChunkByteSize::MIN),
1✔
732
            )
1✔
733
            .await
×
734
            .unwrap();
1✔
735

1✔
736
        let dim_x = HistogramDimension::new("foo".to_string(), 1.0, 2.0, 26).unwrap();
1✔
737
        let dim_y = HistogramDimension::new("bar".to_string(), 1.0, 2.0, 26).unwrap();
1✔
738

1✔
739
        let mut expected = geoengine_datatypes::plots::Histogram2D::new(dim_x, dim_y);
1✔
740
        expected.update_batch(
1✔
741
            values
1✔
742
                .into_iter()
1✔
743
                .map(|x| Coordinate2D::new(x as f64, x as f64)),
701✔
744
        );
1✔
745
        assert_eq!(expected.to_vega_embeddable(false).unwrap(), result);
1✔
746
    }
747

748
    #[tokio::test]
1✔
749
    async fn to_histogram_while_iterating() {
1✔
750
        let mut values = vec![1; 5999];
1✔
751
        values.push(2);
1✔
752

1✔
753
        let vector_source = MockFeatureCollectionSource::multiple(vec![
1✔
754
            DataCollection::from_slices(
1✔
755
                &[] as &[NoGeometry],
1✔
756
                &[TimeInterval::default(); 6000],
1✔
757
                &[
1✔
758
                    ("foo", FeatureData::Int(values.clone())),
1✔
759
                    ("bar", FeatureData::Int(values.clone())),
1✔
760
                ],
1✔
761
            )
1✔
762
            .unwrap(),
1✔
763
            DataCollection::from_slices(
1✔
764
                &[] as &[NoGeometry],
1✔
765
                &[TimeInterval::default(); 6000],
1✔
766
                &[
1✔
767
                    ("foo", FeatureData::Int(values.clone())),
1✔
768
                    ("bar", FeatureData::Int(values.clone())),
1✔
769
                ],
1✔
770
            )
1✔
771
            .unwrap(),
1✔
772
            DataCollection::from_slices(
1✔
773
                &[] as &[NoGeometry],
1✔
774
                &[TimeInterval::default(); 6000],
1✔
775
                &[
1✔
776
                    ("foo", FeatureData::Int(values.clone())),
1✔
777
                    ("bar", FeatureData::Int(values.clone())),
1✔
778
                ],
1✔
779
            )
1✔
780
            .unwrap(),
1✔
781
        ])
1✔
782
        .boxed();
1✔
783

1✔
784
        let box_plot = ScatterPlot {
1✔
785
            params: ScatterPlotParams {
1✔
786
                column_x: "foo".to_string(),
1✔
787
                column_y: "bar".to_string(),
1✔
788
            },
1✔
789
            sources: vector_source.into(),
1✔
790
        };
1✔
791

1✔
792
        let execution_context = MockExecutionContext::test_default();
1✔
793

794
        let query_processor = box_plot
1✔
795
            .boxed()
1✔
796
            .initialize(&execution_context)
1✔
797
            .await
×
798
            .unwrap()
1✔
799
            .query_processor()
1✔
800
            .unwrap()
1✔
801
            .json_vega()
1✔
802
            .unwrap();
1✔
803

804
        let result = query_processor
1✔
805
            .plot_query(
1✔
806
                VectorQueryRectangle {
1✔
807
                    spatial_bounds: BoundingBox2D::new((-180., -90.).into(), (180., 90.).into())
1✔
808
                        .unwrap(),
1✔
809
                    time_interval: TimeInterval::default(),
1✔
810
                    spatial_resolution: SpatialResolution::one(),
1✔
811
                },
1✔
812
                &MockQueryContext::new(ChunkByteSize::MIN),
1✔
813
            )
1✔
814
            .await
×
815
            .unwrap();
1✔
816

1✔
817
        let dim_x = HistogramDimension::new("foo".to_string(), 1.0, 2.0, 100).unwrap();
1✔
818
        let dim_y = HistogramDimension::new("bar".to_string(), 1.0, 2.0, 100).unwrap();
1✔
819

1✔
820
        let mut expected = geoengine_datatypes::plots::Histogram2D::new(dim_x, dim_y);
1✔
821
        expected.update_batch(
1✔
822
            values
1✔
823
                .iter()
1✔
824
                .map(|x| Coordinate2D::new(*x as f64, *x as f64)),
6,000✔
825
        );
1✔
826
        expected.update_batch(
1✔
827
            values
1✔
828
                .iter()
1✔
829
                .map(|x| Coordinate2D::new(*x as f64, *x as f64)),
6,000✔
830
        );
1✔
831
        expected.update_batch(
1✔
832
            values
1✔
833
                .iter()
1✔
834
                .map(|x| Coordinate2D::new(*x as f64, *x as f64)),
6,000✔
835
        );
1✔
836
        assert_eq!(expected.to_vega_embeddable(false).unwrap(), result);
1✔
837
    }
838

839
    #[test]
1✔
840
    fn test_collector_kind_empty() {
1✔
841
        let cx = "x".to_string();
1✔
842
        let cy = "y".to_string();
1✔
843

1✔
844
        let c = CollectorKind::Values(Collector::new(cx.clone(), cy.clone()));
1✔
845
        let res = c.into_plot().unwrap().to_vega_embeddable(false).unwrap();
1✔
846

1✔
847
        let expected = geoengine_datatypes::plots::ScatterPlot::new(cx, cy)
1✔
848
            .to_vega_embeddable(false)
1✔
849
            .unwrap();
1✔
850

1✔
851
        assert_eq!(expected, res);
1✔
852
    }
1✔
853

854
    #[test]
1✔
855
    fn test_collector_kind_scatter_plot() {
1✔
856
        let cx = "x".to_string();
1✔
857
        let cy = "y".to_string();
1✔
858

1✔
859
        let mut values = Vec::with_capacity(200);
1✔
860
        for i in 0..SCATTER_PLOT_THRESHOLD / 2 {
250✔
861
            values.push(Coordinate2D::new(i as f64, i as f64));
250✔
862
        }
250✔
863

864
        let mut c = CollectorKind::Values(Collector::new(cx.clone(), cy.clone()));
1✔
865

1✔
866
        c.add_batch(values.clone().into_iter()).unwrap();
1✔
867

868
        assert!(matches!(c, CollectorKind::Values(_)));
1✔
869

870
        let res = c.into_plot().unwrap().to_vega_embeddable(false).unwrap();
1✔
871

1✔
872
        let mut expected = geoengine_datatypes::plots::ScatterPlot::new(cx, cy);
1✔
873
        expected.update_batch(values.into_iter());
1✔
874

1✔
875
        assert_eq!(expected.to_vega_embeddable(false).unwrap(), res);
1✔
876
    }
1✔
877

878
    #[test]
1✔
879
    fn test_collector_kind_histogram_end() {
1✔
880
        let cx = "x".to_string();
1✔
881
        let cy = "y".to_string();
1✔
882

1✔
883
        let element_count = SCATTER_PLOT_THRESHOLD * 2;
1✔
884

1✔
885
        let mut values = Vec::with_capacity(element_count);
1✔
886
        for i in 0..element_count {
1,000✔
887
            values.push(Coordinate2D::new(i as f64, i as f64));
1,000✔
888
        }
1,000✔
889

890
        let mut c = CollectorKind::Values(Collector::new(cx.clone(), cy.clone()));
1✔
891
        c.add_batch(values.clone().into_iter()).unwrap();
1✔
892

893
        assert!(matches!(c, CollectorKind::Values(_)));
1✔
894

895
        let res = c.into_plot().unwrap().to_vega_embeddable(false).unwrap();
1✔
896

1✔
897
        // expected
1✔
898
        let bucket_count = std::cmp::min(100, f64::sqrt(element_count as f64) as usize);
1✔
899
        let dimx =
1✔
900
            HistogramDimension::new(cx, 0.0, (element_count - 1) as f64, bucket_count).unwrap();
1✔
901

1✔
902
        let dimy =
1✔
903
            HistogramDimension::new(cy, 0.0, (element_count - 1) as f64, bucket_count).unwrap();
1✔
904

1✔
905
        let mut expected = geoengine_datatypes::plots::Histogram2D::new(dimx, dimy);
1✔
906
        expected.update_batch(values.into_iter());
1✔
907

1✔
908
        assert_eq!(expected.to_vega_embeddable(false).unwrap(), res);
1✔
909
    }
1✔
910
    #[test]
1✔
911
    fn test_collector_kind_histogram_in_flight() {
1✔
912
        let cx = "x".to_string();
1✔
913
        let cy = "y".to_string();
1✔
914

1✔
915
        let element_count = COLLECTOR_TO_HISTOGRAM_THRESHOLD + 1;
1✔
916

1✔
917
        let mut values = Vec::with_capacity(element_count);
1✔
918
        for i in 0..element_count {
10,001✔
919
            values.push(Coordinate2D::new(i as f64, i as f64));
10,001✔
920
        }
10,001✔
921

922
        let mut c = CollectorKind::Values(Collector::new(cx.clone(), cy.clone()));
1✔
923
        c.add_batch(values.clone().into_iter()).unwrap();
1✔
924

925
        assert!(matches!(c, CollectorKind::Histogram(_)));
1✔
926

927
        let res = c.into_plot().unwrap().to_vega_embeddable(false).unwrap();
1✔
928

1✔
929
        // expected
1✔
930
        let bucket_count = std::cmp::min(100, f64::sqrt(element_count as f64) as usize);
1✔
931
        let dimx =
1✔
932
            HistogramDimension::new(cx, 0.0, (element_count - 1) as f64, bucket_count).unwrap();
1✔
933

1✔
934
        let dimy =
1✔
935
            HistogramDimension::new(cy, 0.0, (element_count - 1) as f64, bucket_count).unwrap();
1✔
936

1✔
937
        let mut expected = geoengine_datatypes::plots::Histogram2D::new(dimx, dimy);
1✔
938
        expected.update_batch(values.into_iter());
1✔
939

1✔
940
        assert_eq!(expected.to_vega_embeddable(false).unwrap(), res);
1✔
941
    }
1✔
942

943
    #[test]
1✔
944
    fn test_collector_kind_histogram_out_of_range() {
1✔
945
        let cx = "x".to_string();
1✔
946
        let cy = "y".to_string();
1✔
947

1✔
948
        let element_count = COLLECTOR_TO_HISTOGRAM_THRESHOLD + 1;
1✔
949

1✔
950
        let mut values = Vec::with_capacity(element_count);
1✔
951
        for i in 0..element_count {
10,001✔
952
            values.push(Coordinate2D::new(i as f64, i as f64));
10,001✔
953
        }
10,001✔
954

955
        let mut c = CollectorKind::Values(Collector::new(cx.clone(), cy.clone()));
1✔
956
        c.add_batch(values.clone().into_iter()).unwrap();
1✔
957

958
        assert!(matches!(c, CollectorKind::Histogram(_)));
1✔
959

960
        // This value should be skipped
961
        c.add_batch(
1✔
962
            [Coordinate2D::new(
1✔
963
                element_count as f64,
1✔
964
                element_count as f64,
1✔
965
            )]
1✔
966
            .into_iter(),
1✔
967
        )
1✔
968
        .unwrap();
1✔
969

1✔
970
        let res = c.into_plot().unwrap().to_vega_embeddable(false).unwrap();
1✔
971

1✔
972
        // expected
1✔
973
        let bucket_count = std::cmp::min(100, f64::sqrt(element_count as f64) as usize);
1✔
974
        let dimx =
1✔
975
            HistogramDimension::new(cx, 0.0, (element_count - 1) as f64, bucket_count).unwrap();
1✔
976

1✔
977
        let dimy =
1✔
978
            HistogramDimension::new(cy, 0.0, (element_count - 1) as f64, bucket_count).unwrap();
1✔
979

1✔
980
        let mut expected = geoengine_datatypes::plots::Histogram2D::new(dimx, dimy);
1✔
981
        expected.update_batch(values.into_iter());
1✔
982

1✔
983
        assert_eq!(expected.to_vega_embeddable(false).unwrap(), res);
1✔
984
    }
1✔
985

986
    #[test]
1✔
987
    fn test_collector_kind_histogram_infinite() {
1✔
988
        let cx = "x".to_string();
1✔
989
        let cy = "y".to_string();
1✔
990

1✔
991
        let element_count = COLLECTOR_TO_HISTOGRAM_THRESHOLD + 1;
1✔
992

1✔
993
        let mut values = Vec::with_capacity(element_count);
1✔
994
        for i in 0..element_count {
10,001✔
995
            values.push(Coordinate2D::new(i as f64, i as f64));
10,001✔
996
        }
10,001✔
997

998
        let mut c = CollectorKind::Values(Collector::new(cx.clone(), cy.clone()));
1✔
999
        c.add_batch(values.clone().into_iter()).unwrap();
1✔
1000

1001
        assert!(matches!(c, CollectorKind::Histogram(_)));
1✔
1002

1003
        // This value should be skipped
1004
        c.add_batch([Coordinate2D::new(f64::NAN, f64::NAN)].into_iter())
1✔
1005
            .unwrap();
1✔
1006

1✔
1007
        let res = c.into_plot().unwrap().to_vega_embeddable(false).unwrap();
1✔
1008

1✔
1009
        // expected
1✔
1010
        let bucket_count = std::cmp::min(100, f64::sqrt(element_count as f64) as usize);
1✔
1011
        let dimx =
1✔
1012
            HistogramDimension::new(cx, 0.0, (element_count - 1) as f64, bucket_count).unwrap();
1✔
1013

1✔
1014
        let dimy =
1✔
1015
            HistogramDimension::new(cy, 0.0, (element_count - 1) as f64, bucket_count).unwrap();
1✔
1016

1✔
1017
        let mut expected = geoengine_datatypes::plots::Histogram2D::new(dimx, dimy);
1✔
1018
        expected.update_batch(values.into_iter());
1✔
1019

1✔
1020
        assert_eq!(expected.to_vega_embeddable(false).unwrap(), res);
1✔
1021
    }
1✔
1022
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc