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

geo-engine / geoengine / 19065440894

04 Nov 2025 10:21AM UTC coverage: 88.783%. First build
19065440894

Pull #1083

github

web-flow
Merge 577088df6 into 3d9be4869
Pull Request #1083: feat: new gdal source workflow optimization

5714 of 6540 new or added lines in 71 files covered. (87.37%)

115349 of 129923 relevant lines covered (88.78%)

499710.35 hits per line

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

94.87
/operators/src/plot/pie_chart.rs
1
use crate::engine::{
2
    CanonicOperatorName, ExecutionContext, InitializedPlotOperator, InitializedSources,
3
    InitializedVectorOperator, Operator, OperatorName, PlotOperator, PlotQueryProcessor,
4
    PlotResultDescriptor, QueryContext, TypedPlotQueryProcessor, TypedVectorQueryProcessor,
5
    WorkflowOperatorPath,
6
};
7
use crate::engine::{QueryProcessor, SingleVectorSource};
8
use crate::error::Error;
9
use crate::optimization::OptimizationError;
10
use crate::util::Result;
11
use async_trait::async_trait;
12
use futures::StreamExt;
13
use geoengine_datatypes::collections::FeatureCollectionInfos;
14
use geoengine_datatypes::plots::{Plot, PlotData};
15
use geoengine_datatypes::primitives::{
16
    ColumnSelection, FeatureDataRef, Measurement, PlotQueryRectangle, SpatialResolution,
17
    VectorQueryRectangle,
18
};
19
use serde::{Deserialize, Serialize};
20
use snafu::Snafu;
21
use std::collections::{BTreeMap, HashMap};
22

23
pub const PIE_CHART_OPERATOR_NAME: &str = "PieChart";
24

25
/// If the number of slices in the result is greater than this, the operator will fail.
26
pub const MAX_NUMBER_OF_SLICES: usize = 32;
27

28
/// A pie chart plot about a column of a vector input.
29
pub type PieChart = Operator<PieChartParams, SingleVectorSource>;
30

31
impl OperatorName for PieChart {
32
    const TYPE_NAME: &'static str = PIE_CHART_OPERATOR_NAME;
33
}
34

35
/// The parameter spec for `PieChart`
36
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
37
#[serde(tag = "type", rename_all = "camelCase")]
38
pub enum PieChartParams {
39
    /// Count the distinct values of a column
40
    #[serde(rename_all = "camelCase")]
41
    Count {
42
        /// Name of the (numeric) attribute to compute the histogram on. Fails if set for rasters.
43
        column_name: String,
44
        /// Whether to display the pie chart as a normal pie chart or as a donut chart.
45
        /// Defaults to `false`.
46
        #[serde(default)]
47
        donut: bool,
48
    },
49
    // TODO: another useful method would be `Sum` which sums up all values of a column A for a group column B
50
}
51

52
#[typetag::serde]
×
53
#[async_trait]
54
impl PlotOperator for PieChart {
55
    async fn _initialize(
56
        self: Box<Self>,
57
        path: WorkflowOperatorPath,
58
        context: &dyn ExecutionContext,
59
    ) -> Result<Box<dyn InitializedPlotOperator>> {
6✔
60
        let name = CanonicOperatorName::from(&self);
61

62
        let initialized_sources = self
63
            .sources
64
            .initialize_sources(path.clone(), context)
65
            .await?;
66
        let vector_source = initialized_sources.vector;
67

68
        let in_desc = vector_source.result_descriptor().clone();
69

70
        match self.params {
71
            PieChartParams::Count { column_name, donut } => {
72
                let Some(column_measurement) = in_desc.column_measurement(&column_name) else {
73
                    return Err(Error::ColumnDoesNotExist {
74
                        column: column_name,
75
                    });
76
                };
77

78
                let mut column_label = column_measurement.to_string();
79
                if column_label.is_empty() {
80
                    // in case of `Measurement::Unitless`
81
                    column_label.clone_from(&column_name);
82
                }
83

84
                let class_mapping =
85
                    if let Measurement::Classification(measurement) = &column_measurement {
86
                        Some(measurement.classes.clone())
87
                    } else {
88
                        None
89
                    };
90

91
                Ok(InitializedCountPieChart::new(
92
                    name,
93
                    vector_source,
94
                    in_desc.into(),
95
                    column_name.clone(),
96
                    column_label,
97
                    class_mapping,
98
                    donut,
99
                )
100
                .boxed())
101
            }
102
        }
103
    }
6✔
104

105
    span_fn!(PieChart);
106
}
107

108
/// The initialization of `Histogram`
109
pub struct InitializedCountPieChart<Op> {
110
    name: CanonicOperatorName,
111
    source: Op,
112
    result_descriptor: PlotResultDescriptor,
113
    column_name: String,
114
    column_label: String,
115
    class_mapping: Option<BTreeMap<u8, String>>,
116
    donut: bool,
117
}
118

119
impl<Op> InitializedCountPieChart<Op> {
120
    pub fn new(
6✔
121
        name: CanonicOperatorName,
6✔
122
        source: Op,
6✔
123
        result_descriptor: PlotResultDescriptor,
6✔
124
        column_name: String,
6✔
125
        column_label: String,
6✔
126
        class_mapping: Option<BTreeMap<u8, String>>,
6✔
127
        donut: bool,
6✔
128
    ) -> Self {
6✔
129
        Self {
6✔
130
            name,
6✔
131
            source,
6✔
132
            result_descriptor,
6✔
133
            column_name,
6✔
134
            column_label,
6✔
135
            class_mapping,
6✔
136
            donut,
6✔
137
        }
6✔
138
    }
6✔
139
}
140

141
impl InitializedPlotOperator for InitializedCountPieChart<Box<dyn InitializedVectorOperator>> {
142
    fn query_processor(&self) -> Result<TypedPlotQueryProcessor> {
6✔
143
        let processor = CountPieChartVectorQueryProcessor {
6✔
144
            input: self.source.query_processor()?,
6✔
145
            column_label: self.column_label.clone(),
6✔
146
            column_name: self.column_name.clone(),
6✔
147
            class_mapping: self.class_mapping.clone(),
6✔
148
            donut: self.donut,
6✔
149
        };
150

151
        Ok(TypedPlotQueryProcessor::JsonVega(processor.boxed()))
6✔
152
    }
6✔
153

154
    fn result_descriptor(&self) -> &PlotResultDescriptor {
×
155
        &self.result_descriptor
×
156
    }
×
157

158
    fn canonic_name(&self) -> CanonicOperatorName {
×
159
        self.name.clone()
×
160
    }
×
161

NEW
162
    fn optimize(
×
NEW
163
        &self,
×
NEW
164
        target_resolution: SpatialResolution,
×
NEW
165
    ) -> Result<Box<dyn PlotOperator>, OptimizationError> {
×
166
        Ok(PieChart {
NEW
167
            params: PieChartParams::Count {
×
NEW
168
                column_name: self.column_name.clone(),
×
NEW
169
                donut: self.donut,
×
NEW
170
            },
×
171
            sources: SingleVectorSource {
NEW
172
                vector: self.source.optimize(target_resolution)?,
×
173
            },
174
        }
NEW
175
        .boxed())
×
NEW
176
    }
×
177
}
178

179
/// A query processor that calculates the Histogram about its vector inputs.
180
pub struct CountPieChartVectorQueryProcessor {
181
    input: TypedVectorQueryProcessor,
182
    column_label: String,
183
    column_name: String,
184
    class_mapping: Option<BTreeMap<u8, String>>,
185
    donut: bool,
186
}
187

188
#[async_trait]
189
impl PlotQueryProcessor for CountPieChartVectorQueryProcessor {
190
    type OutputFormat = PlotData;
191

192
    fn plot_type(&self) -> &'static str {
×
193
        PIE_CHART_OPERATOR_NAME
×
194
    }
×
195

196
    async fn plot_query<'p>(
197
        &'p self,
198
        query: PlotQueryRectangle,
199
        ctx: &'p dyn QueryContext,
200
    ) -> Result<Self::OutputFormat> {
6✔
201
        self.process(query, ctx).await
202
    }
6✔
203
}
204

205
/// Creates an iterator over all values as string
206
/// Null-values are empty strings.
207
pub fn feature_data_strings_iter<'f>(
52✔
208
    feature_data: &'f FeatureDataRef,
52✔
209
    class_mapping: Option<&'f BTreeMap<u8, String>>,
52✔
210
) -> Box<dyn Iterator<Item = String> + 'f> {
52✔
211
    match (feature_data, class_mapping) {
52✔
212
        (FeatureDataRef::Category(feature_data_ref), Some(class_mapping)) => {
×
213
            return Box::new(feature_data_ref.as_ref().iter().map(|v| {
×
214
                class_mapping
×
215
                    .get(v)
×
216
                    .map_or_else(String::new, ToString::to_string)
×
217
            }));
×
218
        }
219
        (FeatureDataRef::Int(feature_data_ref), Some(class_mapping)) => {
2✔
220
            return Box::new(feature_data_ref.as_ref().iter().map(|v| {
12✔
221
                class_mapping
12✔
222
                    .get(&(*v as u8))
12✔
223
                    .map_or_else(String::new, ToString::to_string)
12✔
224
            }));
12✔
225
        }
226
        (FeatureDataRef::Float(feature_data_ref), Some(class_mapping)) => {
2✔
227
            return Box::new(feature_data_ref.as_ref().iter().map(|v| {
2✔
228
                class_mapping
1✔
229
                    .get(&(*v as u8))
1✔
230
                    .map_or_else(String::new, ToString::to_string)
1✔
231
            }));
1✔
232
        }
233
        _ => {
48✔
234
            // no special treatment for other types
48✔
235
        }
48✔
236
    }
237

238
    feature_data.strings_iter()
48✔
239
}
52✔
240

241
impl CountPieChartVectorQueryProcessor {
242
    async fn process<'p>(
6✔
243
        &'p self,
6✔
244
        query: PlotQueryRectangle,
6✔
245
        ctx: &'p dyn QueryContext,
6✔
246
    ) -> Result<<CountPieChartVectorQueryProcessor as PlotQueryProcessor>::OutputFormat> {
6✔
247
        let mut slices: HashMap<String, f64> = HashMap::new();
6✔
248

249
        // TODO: parallelize
250
        let query: VectorQueryRectangle = query.select_attributes(ColumnSelection::all());
6✔
251

252
        call_on_generic_vector_processor!(&self.input, processor => {
6✔
253

254

255
            let mut query = processor.query(query, ctx).await?;
4✔
256

257
            while let Some(collection) = query.next().await {
9✔
258
                let collection = collection?;
5✔
259

260
                let feature_data = collection.data(&self.column_name)?;
5✔
261

262
                let feature_data_strings = feature_data_strings_iter(&feature_data, self.class_mapping.as_ref());
5✔
263

264
                for v in feature_data_strings {
24✔
265
                    if v.is_empty() {
19✔
266
                        continue; // ignore no data
2✔
267
                    }
17✔
268

269
                    *slices.entry(v).or_insert(0.0) += 1.0;
17✔
270
                }
271

272
                if slices.len() > MAX_NUMBER_OF_SLICES {
5✔
273
                    return Err(PieChartError::TooManySlices.into());
×
274
                }
5✔
275
            }
276
        });
277

278
        // TODO: display NO-DATA count?
279

280
        let bar_chart = geoengine_datatypes::plots::PieChart::new(
5✔
281
            slices.into_iter().collect(),
5✔
282
            self.column_label.clone(),
5✔
283
            self.donut,
5✔
284
        )?;
×
285
        let chart = bar_chart.to_vega_embeddable(false)?;
5✔
286

287
        Ok(chart)
5✔
288
    }
6✔
289
}
290

291
#[derive(Debug, Snafu, Clone, PartialEq, Eq)]
292
#[snafu(
293
    visibility(pub(crate)),
294
    context(suffix(false)), // disables default `Snafu` suffix
295
    module(error),
296
)]
297
pub enum PieChartError {
298
    #[snafu(display(
299
        "The number of slices is too high. Maximum is {}.",
300
        MAX_NUMBER_OF_SLICES
301
    ))]
302
    TooManySlices,
303
}
304

305
#[cfg(test)]
306
mod tests {
307

308
    use super::*;
309

310
    use crate::engine::{
311
        ChunkByteSize, MockExecutionContext, StaticMetaData, VectorColumnInfo, VectorOperator,
312
        VectorResultDescriptor,
313
    };
314
    use crate::mock::MockFeatureCollectionSource;
315
    use crate::source::{
316
        AttributeFilter, OgrSource, OgrSourceColumnSpec, OgrSourceDataset,
317
        OgrSourceDatasetTimeType, OgrSourceErrorSpec, OgrSourceParameters,
318
    };
319
    use crate::test_data;
320
    use geoengine_datatypes::dataset::{DataId, DatasetId, NamedData};
321
    use geoengine_datatypes::primitives::{
322
        BoundingBox2D, FeatureData, FeatureDataType, NoGeometry, PlotQueryRectangle,
323
        PlotSeriesSelection, TimeInterval,
324
    };
325
    use geoengine_datatypes::primitives::{CacheTtlSeconds, VectorQueryRectangle};
326
    use geoengine_datatypes::spatial_reference::SpatialReference;
327
    use geoengine_datatypes::util::Identifier;
328
    use geoengine_datatypes::util::test::TestDefault;
329
    use geoengine_datatypes::{
330
        collections::{DataCollection, VectorDataType},
331
        primitives::MultiPoint,
332
    };
333
    use serde_json::json;
334

335
    #[test]
336
    fn serialization() {
1✔
337
        let pie_chart = PieChart {
1✔
338
            params: PieChartParams::Count {
1✔
339
                column_name: "my_column".to_string(),
1✔
340
                donut: false,
1✔
341
            },
1✔
342
            sources: MockFeatureCollectionSource::<MultiPoint>::multiple(vec![])
1✔
343
                .boxed()
1✔
344
                .into(),
1✔
345
        };
1✔
346

347
        let serialized = json!({
1✔
348
            "type": "PieChart",
1✔
349
            "params": {
1✔
350
                "type": "count",
1✔
351
                "columnName": "my_column",
1✔
352
            },
353
            "sources": {
1✔
354
                "vector": {
1✔
355
                    "type": "MockFeatureCollectionSourceMultiPoint",
1✔
356
                    "params": {
1✔
357
                        "collections": [],
1✔
358
                        "spatialReference": "EPSG:4326",
1✔
359
                        "measurements": {},
1✔
360
                    }
361
                }
362
            }
363
        })
364
        .to_string();
1✔
365

366
        let deserialized: PieChart = serde_json::from_str(&serialized).unwrap();
1✔
367

368
        assert_eq!(deserialized.params, pie_chart.params);
1✔
369
    }
1✔
370

371
    #[tokio::test]
372
    async fn vector_data_with_classification() {
1✔
373
        let measurement = Measurement::classification(
1✔
374
            "foo".to_string(),
1✔
375
            [
1✔
376
                (1, "A".to_string()),
1✔
377
                (2, "B".to_string()),
1✔
378
                (3, "C".to_string()),
1✔
379
            ]
1✔
380
            .into_iter()
1✔
381
            .collect(),
1✔
382
        );
383

384
        let vector_source = MockFeatureCollectionSource::with_collections_and_measurements(
1✔
385
            vec![
1✔
386
                DataCollection::from_slices(
1✔
387
                    &[] as &[NoGeometry],
1✔
388
                    &[TimeInterval::default(); 8],
1✔
389
                    &[("foo", FeatureData::Int(vec![1, 1, 2, 2, 3, 3, 1, 2]))],
1✔
390
                )
391
                .unwrap(),
1✔
392
                DataCollection::from_slices(
1✔
393
                    &[] as &[NoGeometry],
1✔
394
                    &[TimeInterval::default(); 4],
1✔
395
                    &[("foo", FeatureData::Int(vec![1, 1, 2, 3]))],
1✔
396
                )
397
                .unwrap(),
1✔
398
            ],
399
            [("foo".to_string(), measurement)].into_iter().collect(),
1✔
400
        )
401
        .boxed();
1✔
402

403
        let pie_chart = PieChart {
1✔
404
            params: PieChartParams::Count {
1✔
405
                column_name: "foo".to_string(),
1✔
406
                donut: false,
1✔
407
            },
1✔
408
            sources: vector_source.into(),
1✔
409
        };
1✔
410

411
        let execution_context = MockExecutionContext::test_default();
1✔
412

413
        let query_processor = pie_chart
1✔
414
            .boxed()
1✔
415
            .initialize(WorkflowOperatorPath::initialize_root(), &execution_context)
1✔
416
            .await
1✔
417
            .unwrap()
1✔
418
            .query_processor()
1✔
419
            .unwrap()
1✔
420
            .json_vega()
1✔
421
            .unwrap();
1✔
422

423
        let result = query_processor
1✔
424
            .plot_query(
1✔
425
                PlotQueryRectangle::new(
1✔
426
                    BoundingBox2D::new((-180., -90.).into(), (180., 90.).into()).unwrap(),
1✔
427
                    TimeInterval::default(),
1✔
428
                    PlotSeriesSelection::all(),
1✔
429
                ),
1✔
430
                &execution_context.mock_query_context(ChunkByteSize::MIN),
1✔
431
            )
1✔
432
            .await
1✔
433
            .unwrap();
1✔
434

435
        assert_eq!(
1✔
436
            result,
1✔
437
            geoengine_datatypes::plots::PieChart::new(
1✔
438
                [
1✔
439
                    ("A".to_string(), 5.),
1✔
440
                    ("B".to_string(), 4.),
1✔
441
                    ("C".to_string(), 3.),
1✔
442
                ]
1✔
443
                .into(),
1✔
444
                "foo".to_string(),
1✔
445
                false,
1✔
446
            )
1✔
447
            .unwrap()
1✔
448
            .to_vega_embeddable(false)
1✔
449
            .unwrap()
1✔
450
        );
1✔
451
    }
1✔
452

453
    #[tokio::test]
454
    async fn vector_data_with_nulls() {
1✔
455
        let measurement = Measurement::continuous("foo".to_string(), None);
1✔
456

457
        let vector_source = MockFeatureCollectionSource::with_collections_and_measurements(
1✔
458
            vec![
1✔
459
                DataCollection::from_slices(
1✔
460
                    &[] as &[NoGeometry],
1✔
461
                    &[TimeInterval::default(); 6],
1✔
462
                    &[(
1✔
463
                        "foo",
1✔
464
                        FeatureData::NullableFloat(vec![
1✔
465
                            Some(1.),
1✔
466
                            Some(2.),
1✔
467
                            None,
1✔
468
                            Some(1.),
1✔
469
                            None,
1✔
470
                            Some(3.),
1✔
471
                        ]),
1✔
472
                    )],
1✔
473
                )
474
                .unwrap(),
1✔
475
            ],
476
            [("foo".to_string(), measurement)].into_iter().collect(),
1✔
477
        )
478
        .boxed();
1✔
479

480
        let pie_chart = PieChart {
1✔
481
            params: PieChartParams::Count {
1✔
482
                column_name: "foo".to_string(),
1✔
483
                donut: false,
1✔
484
            },
1✔
485
            sources: vector_source.into(),
1✔
486
        };
1✔
487

488
        let execution_context = MockExecutionContext::test_default();
1✔
489

490
        let query_processor = pie_chart
1✔
491
            .boxed()
1✔
492
            .initialize(WorkflowOperatorPath::initialize_root(), &execution_context)
1✔
493
            .await
1✔
494
            .unwrap()
1✔
495
            .query_processor()
1✔
496
            .unwrap()
1✔
497
            .json_vega()
1✔
498
            .unwrap();
1✔
499

500
        let result = query_processor
1✔
501
            .plot_query(
1✔
502
                PlotQueryRectangle::new(
1✔
503
                    BoundingBox2D::new((-180., -90.).into(), (180., 90.).into()).unwrap(),
1✔
504
                    TimeInterval::default(),
1✔
505
                    PlotSeriesSelection::all(),
1✔
506
                ),
1✔
507
                &execution_context.mock_query_context(ChunkByteSize::MIN),
1✔
508
            )
1✔
509
            .await
1✔
510
            .unwrap();
1✔
511

512
        assert_eq!(
1✔
513
            result,
1✔
514
            geoengine_datatypes::plots::PieChart::new(
1✔
515
                [
1✔
516
                    ("1".to_string(), 2.),
1✔
517
                    ("2".to_string(), 1.),
1✔
518
                    ("3".to_string(), 1.),
1✔
519
                ]
1✔
520
                .into(),
1✔
521
                "foo".to_string(),
1✔
522
                false,
1✔
523
            )
1✔
524
            .unwrap()
1✔
525
            .to_vega_embeddable(false)
1✔
526
            .unwrap()
1✔
527
        );
1✔
528
    }
1✔
529

530
    #[tokio::test]
531
    #[allow(clippy::too_many_lines)]
532
    async fn text_attribute() {
1✔
533
        let dataset_id = DatasetId::new();
1✔
534
        let dataset_name = NamedData::with_system_name("ne_10m_ports");
1✔
535

536
        let mut execution_context = MockExecutionContext::test_default();
1✔
537
        execution_context.add_meta_data::<_, _, VectorQueryRectangle>(
1✔
538
            DataId::Internal { dataset_id },
1✔
539
            dataset_name.clone(),
1✔
540
            Box::new(StaticMetaData {
1✔
541
                loading_info: OgrSourceDataset {
1✔
542
                    file_name: test_data!("vector/data/ne_10m_ports/ne_10m_ports.shp").into(),
1✔
543
                    layer_name: "ne_10m_ports".to_string(),
1✔
544
                    data_type: Some(VectorDataType::MultiPoint),
1✔
545
                    time: OgrSourceDatasetTimeType::None,
1✔
546
                    default_geometry: None,
1✔
547
                    columns: Some(OgrSourceColumnSpec {
1✔
548
                        format_specifics: None,
1✔
549
                        x: String::new(),
1✔
550
                        y: None,
1✔
551
                        int: vec!["natlscale".to_string()],
1✔
552
                        float: vec!["scalerank".to_string()],
1✔
553
                        text: vec![
1✔
554
                            "featurecla".to_string(),
1✔
555
                            "name".to_string(),
1✔
556
                            "website".to_string(),
1✔
557
                        ],
1✔
558
                        bool: vec![],
1✔
559
                        datetime: vec![],
1✔
560
                        rename: None,
1✔
561
                    }),
1✔
562
                    force_ogr_time_filter: false,
1✔
563
                    force_ogr_spatial_filter: false,
1✔
564
                    on_error: OgrSourceErrorSpec::Ignore,
1✔
565
                    sql_query: None,
1✔
566
                    attribute_query: None,
1✔
567
                    cache_ttl: CacheTtlSeconds::default(),
1✔
568
                },
1✔
569
                result_descriptor: VectorResultDescriptor {
1✔
570
                    data_type: VectorDataType::MultiPoint,
1✔
571
                    spatial_reference: SpatialReference::epsg_4326().into(),
1✔
572
                    columns: [
1✔
573
                        (
1✔
574
                            "natlscale".to_string(),
1✔
575
                            VectorColumnInfo {
1✔
576
                                data_type: FeatureDataType::Float,
1✔
577
                                measurement: Measurement::Unitless,
1✔
578
                            },
1✔
579
                        ),
1✔
580
                        (
1✔
581
                            "scalerank".to_string(),
1✔
582
                            VectorColumnInfo {
1✔
583
                                data_type: FeatureDataType::Int,
1✔
584
                                measurement: Measurement::Unitless,
1✔
585
                            },
1✔
586
                        ),
1✔
587
                        (
1✔
588
                            "featurecla".to_string(),
1✔
589
                            VectorColumnInfo {
1✔
590
                                data_type: FeatureDataType::Text,
1✔
591
                                measurement: Measurement::Unitless,
1✔
592
                            },
1✔
593
                        ),
1✔
594
                        (
1✔
595
                            "name".to_string(),
1✔
596
                            VectorColumnInfo {
1✔
597
                                data_type: FeatureDataType::Text,
1✔
598
                                measurement: Measurement::Unitless,
1✔
599
                            },
1✔
600
                        ),
1✔
601
                        (
1✔
602
                            "website".to_string(),
1✔
603
                            VectorColumnInfo {
1✔
604
                                data_type: FeatureDataType::Text,
1✔
605
                                measurement: Measurement::Unitless,
1✔
606
                            },
1✔
607
                        ),
1✔
608
                    ]
1✔
609
                    .iter()
1✔
610
                    .cloned()
1✔
611
                    .collect(),
1✔
612
                    time: None,
1✔
613
                    bbox: None,
1✔
614
                },
1✔
615
                phantom: Default::default(),
1✔
616
            }),
1✔
617
        );
618

619
        let pie_chart = PieChart {
1✔
620
            params: PieChartParams::Count {
1✔
621
                column_name: "name".to_string(),
1✔
622
                donut: false,
1✔
623
            },
1✔
624
            sources: OgrSource {
1✔
625
                params: OgrSourceParameters {
1✔
626
                    data: dataset_name.clone(),
1✔
627
                    attribute_projection: None,
1✔
628
                    attribute_filters: None,
1✔
629
                },
1✔
630
            }
1✔
631
            .boxed()
1✔
632
            .into(),
1✔
633
        };
1✔
634

635
        let query_processor = pie_chart
1✔
636
            .boxed()
1✔
637
            .initialize(WorkflowOperatorPath::initialize_root(), &execution_context)
1✔
638
            .await
1✔
639
            .unwrap()
1✔
640
            .query_processor()
1✔
641
            .unwrap()
1✔
642
            .json_vega()
1✔
643
            .unwrap();
1✔
644

645
        let result = query_processor
1✔
646
            .plot_query(
1✔
647
                PlotQueryRectangle::new(
1✔
648
                    BoundingBox2D::new((-180., -90.).into(), (180., 90.).into()).unwrap(),
1✔
649
                    TimeInterval::default(),
1✔
650
                    PlotSeriesSelection::all(),
1✔
651
                ),
1✔
652
                &execution_context.mock_query_context(ChunkByteSize::MIN),
1✔
653
            )
1✔
654
            .await
1✔
655
            .unwrap_err();
1✔
656

657
        assert_eq!(
1✔
658
            result.to_string(),
1✔
659
            "PieChart error: The number of slices is too high. Maximum is 32."
660
        );
661

662
        let pie_chart = PieChart {
1✔
663
            params: PieChartParams::Count {
1✔
664
                column_name: "name".to_string(),
1✔
665
                donut: false,
1✔
666
            },
1✔
667
            sources: OgrSource {
1✔
668
                params: OgrSourceParameters {
1✔
669
                    data: dataset_name,
1✔
670
                    attribute_projection: None,
1✔
671
                    attribute_filters: Some(vec![AttributeFilter {
1✔
672
                        attribute: "name".to_string(),
1✔
673
                        ranges: vec![("E".to_string()..="F".to_string()).into()],
1✔
674
                        keep_nulls: false,
1✔
675
                    }]),
1✔
676
                },
1✔
677
            }
1✔
678
            .boxed()
1✔
679
            .into(),
1✔
680
        };
1✔
681

682
        let query_processor = pie_chart
1✔
683
            .boxed()
1✔
684
            .initialize(WorkflowOperatorPath::initialize_root(), &execution_context)
1✔
685
            .await
1✔
686
            .unwrap()
1✔
687
            .query_processor()
1✔
688
            .unwrap()
1✔
689
            .json_vega()
1✔
690
            .unwrap();
1✔
691

692
        let result = query_processor
1✔
693
            .plot_query(
1✔
694
                PlotQueryRectangle::new(
1✔
695
                    BoundingBox2D::new((-180., -90.).into(), (180., 90.).into()).unwrap(),
1✔
696
                    TimeInterval::default(),
1✔
697
                    PlotSeriesSelection::all(),
1✔
698
                ),
1✔
699
                &execution_context.mock_query_context(ChunkByteSize::MIN),
1✔
700
            )
1✔
701
            .await
1✔
702
            .unwrap();
1✔
703

704
        assert_eq!(
1✔
705
            result,
1✔
706
            geoengine_datatypes::plots::PieChart::new(
1✔
707
                [
1✔
708
                    ("Esquimalt".to_string(), 1.),
1✔
709
                    ("Eckernforde".to_string(), 1.),
1✔
710
                    ("Escanaba".to_string(), 1.),
1✔
711
                    ("Esperance".to_string(), 1.),
1✔
712
                    ("Eden".to_string(), 1.),
1✔
713
                    ("Esmeraldas".to_string(), 1.),
1✔
714
                    ("Europoort".to_string(), 1.),
1✔
715
                    ("Elat".to_string(), 1.),
1✔
716
                    ("Emden".to_string(), 1.),
1✔
717
                    ("Esbjerg".to_string(), 1.),
1✔
718
                    ("Ensenada".to_string(), 1.),
1✔
719
                    ("East London".to_string(), 1.),
1✔
720
                    ("Erie".to_string(), 1.),
1✔
721
                    ("Eureka".to_string(), 1.),
1✔
722
                ]
1✔
723
                .into(),
1✔
724
                "name".to_string(),
1✔
725
                false,
1✔
726
            )
1✔
727
            .unwrap()
1✔
728
            .to_vega_embeddable(false)
1✔
729
            .unwrap()
1✔
730
        );
1✔
731
    }
1✔
732

733
    #[tokio::test]
734
    async fn empty_feature_collection() {
1✔
735
        let measurement = Measurement::classification(
1✔
736
            "foo".to_string(),
1✔
737
            [(1, "A".to_string())].into_iter().collect(),
1✔
738
        );
739

740
        let vector_source = MockFeatureCollectionSource::with_collections_and_measurements(
1✔
741
            vec![
1✔
742
                DataCollection::from_slices(
1✔
743
                    &[] as &[NoGeometry],
1✔
744
                    &[] as &[TimeInterval],
1✔
745
                    &[("foo", FeatureData::Float(vec![]))],
1✔
746
                )
747
                .unwrap(),
1✔
748
            ],
749
            [("foo".to_string(), measurement)].into_iter().collect(),
1✔
750
        )
751
        .boxed();
1✔
752

753
        let pie_chart = PieChart {
1✔
754
            params: PieChartParams::Count {
1✔
755
                column_name: "foo".to_string(),
1✔
756
                donut: false,
1✔
757
            },
1✔
758
            sources: vector_source.into(),
1✔
759
        };
1✔
760

761
        let execution_context = MockExecutionContext::test_default();
1✔
762

763
        let query_processor = pie_chart
1✔
764
            .boxed()
1✔
765
            .initialize(WorkflowOperatorPath::initialize_root(), &execution_context)
1✔
766
            .await
1✔
767
            .unwrap()
1✔
768
            .query_processor()
1✔
769
            .unwrap()
1✔
770
            .json_vega()
1✔
771
            .unwrap();
1✔
772

773
        let result = query_processor
1✔
774
            .plot_query(
1✔
775
                PlotQueryRectangle::new(
1✔
776
                    BoundingBox2D::new((-180., -90.).into(), (180., 90.).into()).unwrap(),
1✔
777
                    TimeInterval::default(),
1✔
778
                    PlotSeriesSelection::all(),
1✔
779
                ),
1✔
780
                &execution_context.mock_query_context(ChunkByteSize::MIN),
1✔
781
            )
1✔
782
            .await
1✔
783
            .unwrap();
1✔
784

785
        assert_eq!(
1✔
786
            result,
1✔
787
            geoengine_datatypes::plots::PieChart::new(
1✔
788
                Default::default(),
1✔
789
                "foo".to_string(),
1✔
790
                false,
1✔
791
            )
1✔
792
            .unwrap()
1✔
793
            .to_vega_embeddable(false)
1✔
794
            .unwrap()
1✔
795
        );
1✔
796
    }
1✔
797

798
    #[tokio::test]
799
    async fn feature_collection_with_one_feature() {
1✔
800
        let measurement = Measurement::classification(
1✔
801
            "foo".to_string(),
1✔
802
            [(5, "A".to_string())].into_iter().collect(),
1✔
803
        );
804

805
        let vector_source = MockFeatureCollectionSource::with_collections_and_measurements(
1✔
806
            vec![
1✔
807
                DataCollection::from_slices(
1✔
808
                    &[] as &[NoGeometry],
1✔
809
                    &[TimeInterval::default()],
1✔
810
                    &[("foo", FeatureData::Float(vec![5.0]))],
1✔
811
                )
812
                .unwrap(),
1✔
813
            ],
814
            [("foo".to_string(), measurement)].into_iter().collect(),
1✔
815
        )
816
        .boxed();
1✔
817

818
        let pie_chart = PieChart {
1✔
819
            params: PieChartParams::Count {
1✔
820
                column_name: "foo".to_string(),
1✔
821
                donut: false,
1✔
822
            },
1✔
823
            sources: vector_source.into(),
1✔
824
        };
1✔
825

826
        let execution_context = MockExecutionContext::test_default();
1✔
827

828
        let query_processor = pie_chart
1✔
829
            .boxed()
1✔
830
            .initialize(WorkflowOperatorPath::initialize_root(), &execution_context)
1✔
831
            .await
1✔
832
            .unwrap()
1✔
833
            .query_processor()
1✔
834
            .unwrap()
1✔
835
            .json_vega()
1✔
836
            .unwrap();
1✔
837

838
        let result = query_processor
1✔
839
            .plot_query(
1✔
840
                PlotQueryRectangle::new(
1✔
841
                    BoundingBox2D::new((-180., -90.).into(), (180., 90.).into()).unwrap(),
1✔
842
                    TimeInterval::default(),
1✔
843
                    PlotSeriesSelection::all(),
1✔
844
                ),
1✔
845
                &execution_context.mock_query_context(ChunkByteSize::MIN),
1✔
846
            )
1✔
847
            .await
1✔
848
            .unwrap();
1✔
849

850
        assert_eq!(
1✔
851
            result,
1✔
852
            geoengine_datatypes::plots::PieChart::new(
1✔
853
                [("A".to_string(), 1.),].into(),
1✔
854
                "foo".to_string(),
1✔
855
                false,
1✔
856
            )
1✔
857
            .unwrap()
1✔
858
            .to_vega_embeddable(false)
1✔
859
            .unwrap()
1✔
860
        );
1✔
861
    }
1✔
862
}
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

© 2026 Coveralls, Inc