mirror of https://github.com/terhechte/postsack
You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
207 lines
6.7 KiB
Rust
207 lines
6.7 KiB
Rust
//! Operations on `Segmentations`
|
|
//!
|
|
//! `Segmentations` are collections of `Segments` based on an aggregation of `Items`.
|
|
//!
|
|
//! A `Segmentation` can be changed to be aggregated on a different `Field.
|
|
//! - [`aggregations`]
|
|
//! - [`aggregated_by`]
|
|
//! - [`set_aggregation`]
|
|
//! A `Segmentation` can be changed to only return a `Range` of segments.
|
|
//! - [`current_range`]
|
|
//! - [`set_current_range`]
|
|
//! A `Segmentation` has multiple `Segments` which each can be layouted
|
|
//! to fit into a rectangle.
|
|
//! - [`layouted_segments]
|
|
|
|
use eyre::{eyre, Result};
|
|
|
|
use super::engine::Action;
|
|
use super::{
|
|
types::{Aggregation, Segment},
|
|
Engine,
|
|
};
|
|
use crate::database::query::{Field, Filter, Query};
|
|
use std::ops::RangeInclusive;
|
|
|
|
/// Filter the `Range` of segments of the current `Segmentation`
|
|
///
|
|
/// Returns the `Range` and the total number of segments.
|
|
/// If no custom range has been set with [`set_segments_range`], returns
|
|
/// the full range of items, otherwise the custom range.
|
|
///
|
|
/// Returns `None` if no current `Segmentation` exists.
|
|
///
|
|
/// # Arguments
|
|
///
|
|
/// * `engine` - The engine to use for retrieving data
|
|
/// * `aggregation` - The aggregation to return the fields for. Required to also return the current aggregation field.
|
|
pub fn segments_range(engine: &Engine) -> Option<(RangeInclusive<usize>, usize)> {
|
|
let segmentation = engine.segmentations.last()?;
|
|
let len = segmentation.len();
|
|
Some(match &segmentation.range {
|
|
Some(n) => (0..=len, *n.end()),
|
|
None => (0..=len, len),
|
|
})
|
|
}
|
|
|
|
/// Set the `Range` of segments of the current `Segmentation`
|
|
///
|
|
/// # Arguments
|
|
///
|
|
/// * `engine` - The engine to use for setting data
|
|
/// * `range` - The range to apply. `None` to reset it to all `Segments`
|
|
pub fn set_segments_range(engine: &mut Engine, range: Option<RangeInclusive<usize>>) {
|
|
if let Some(n) = engine.segmentations.last_mut() {
|
|
// Make sure the range does not go beyond the current semgents count
|
|
if let Some(r) = range {
|
|
let len = n.len();
|
|
if len > *r.start() && *r.end() < len {
|
|
n.range = Some(r);
|
|
}
|
|
} else {
|
|
n.range = None;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Additional filters to use in the query
|
|
///
|
|
/// These filters will be evaluated in addition to the `segmentation` conditions
|
|
/// in the query.
|
|
/// Setting this value will recalculate the current segmentations.
|
|
pub fn set_filters(engine: &mut Engine, filters: &[Filter]) -> Result<()> {
|
|
engine.filters = filters.to_vec();
|
|
|
|
// Remove any rows that were cached for this Segmentation
|
|
engine.item_cache.clear();
|
|
engine
|
|
.link
|
|
.request(&make_query(engine)?, Action::RecalculateSegmentation)
|
|
}
|
|
|
|
/// The fields available for the given aggregation
|
|
///
|
|
/// As the user `pushes` Segmentations and dives into the data,
|
|
/// less fields become available to aggregate by. It is inconsequential
|
|
/// to aggregate, say, by year, then by month, and then again by year.
|
|
/// This method returns the possible fields still available for aggregation.
|
|
///
|
|
/// # Arguments
|
|
///
|
|
/// * `engine` - The engine to use for retrieving data
|
|
/// * `aggregation` - The aggregation to return the fields for. Required to also return the current aggregation field.
|
|
pub fn aggregation_fields(engine: &Engine, aggregation: &Aggregation) -> Vec<Field> {
|
|
#[allow(clippy::unnecessary_filter_map)]
|
|
Field::all_cases()
|
|
.filter_map(|f| {
|
|
if f == aggregation.field {
|
|
return Some(f);
|
|
}
|
|
if engine.group_by_stack.contains(&f) {
|
|
None
|
|
} else {
|
|
Some(f)
|
|
}
|
|
})
|
|
.collect()
|
|
}
|
|
|
|
/// Return all `Aggregation`s applied for the current `Segmentation`
|
|
///
|
|
/// E.g. if we're first aggregating by Year, and then by Month, this
|
|
/// will return a `Vec` of `[Year, Month]`.
|
|
///
|
|
/// # Arguments
|
|
///
|
|
/// * `engine` - The engine to use for retrieving data
|
|
pub fn aggregated_by(engine: &Engine) -> Vec<Aggregation> {
|
|
let mut result = Vec::new();
|
|
// for everything in the current stack
|
|
let len = engine.group_by_stack.len();
|
|
for (index, field) in engine.group_by_stack.iter().enumerate() {
|
|
let value = match (
|
|
len,
|
|
engine.segmentations.get(index).map(|e| e.selected.as_ref()),
|
|
) {
|
|
(n, Some(Some(segment))) if len == n => Some(segment.field.clone()),
|
|
_ => None,
|
|
};
|
|
result.push(Aggregation {
|
|
value,
|
|
field: *field,
|
|
index,
|
|
});
|
|
}
|
|
result
|
|
}
|
|
|
|
/// Change the `Field` in the given `Aggregation` to the new one.
|
|
///
|
|
/// The `Aggregation` will identify the `Segmentation` to use. So this function
|
|
/// can be used to change the way a `Segmentation` is the aggregated.
|
|
///
|
|
/// Retrieve the available aggregations with [`segmentation::aggregated_by`].
|
|
///
|
|
/// # Arguments
|
|
///
|
|
/// * `engine` - The engine to use for retrieving data
|
|
/// * `aggregation` - The aggregation to change
|
|
/// * `field` - The field to aggregate the `aggregation` by.
|
|
pub fn set_aggregation(
|
|
engine: &mut Engine,
|
|
aggregation: &Aggregation,
|
|
field: &Field,
|
|
) -> Result<()> {
|
|
if let Some(e) = engine.group_by_stack.get_mut(aggregation.index) {
|
|
*e = *field;
|
|
}
|
|
// Remove any rows that were cached for this Segmentation
|
|
engine.item_cache.clear();
|
|
engine
|
|
.link
|
|
.request(&make_query(engine)?, Action::RecalculateSegmentation)
|
|
}
|
|
|
|
/// Return the `Segment`s in the current `Segmentation`. Apply layout based on `Rect`.
|
|
///
|
|
/// It will perform the calculations so that all segments fit into bounds.
|
|
/// The results will be applied to each `Segment`.
|
|
///
|
|
/// Returns the layouted segments.
|
|
///
|
|
/// # Arguments
|
|
///
|
|
/// * `engine` - The engine to use for retrieving data
|
|
/// * `Rect` - The bounds into which the segments have to fit.
|
|
pub fn layouted_segments(engine: &mut Engine, bounds: eframe::egui::Rect) -> Option<&[Segment]> {
|
|
let segmentation = engine.segmentations.last_mut()?;
|
|
segmentation.update_layout(bounds);
|
|
Some(segmentation.items())
|
|
}
|
|
|
|
/// Can another level of aggregation be performed? Based on
|
|
/// [`Engine::default_group_by_stack`]
|
|
pub fn can_aggregate_more(engine: &Engine) -> bool {
|
|
let index = engine.group_by_stack.len();
|
|
super::engine::default_group_by_stack(index).is_some()
|
|
}
|
|
|
|
/// Perform the query that returns an aggregated `Segmentation`
|
|
pub(super) fn make_query(engine: &Engine) -> Result<Query> {
|
|
let mut filters = Vec::new();
|
|
for entry in &engine.search_stack {
|
|
filters.push(Filter::Like(entry.clone()));
|
|
}
|
|
for entry in &engine.filters {
|
|
filters.push(entry.clone());
|
|
}
|
|
let last = engine
|
|
.group_by_stack
|
|
.last()
|
|
.ok_or_else(|| eyre!("Invalid Segmentation state"))?;
|
|
Ok(Query::Grouped {
|
|
filters,
|
|
group_by: *last,
|
|
})
|
|
}
|