use std::collections::HashMap;
use chrono::{DateTime, FixedOffset};
use itertools::Itertools;
use macros_process_mining::register_binding;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use crate::{
core::event_data::case_centric::{Event, Trace, XESEditableAttribute},
EventLog,
};
const DEFAULT_TIMESTAMP_KEY: &str = "time:timestamp";
fn get_event_time<'a>(event: &'a Event, timestamp_key: &str) -> Option<&'a DateTime<FixedOffset>> {
event
.attributes
.get_by_key(timestamp_key)
.and_then(|a| a.value.try_as_date())
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub enum DottedChartXAxis {
Time,
TimeSinceCaseStart,
TimeRelativeToCaseDuration,
StepNumberSinceCaseStart,
}
impl DottedChartXAxis {
pub fn get_value(
&self,
trace: &Trace,
event: &Event,
event_index: usize,
timestamp_key: &str,
) -> f64 {
match self {
DottedChartXAxis::Time => get_event_time(event, timestamp_key)
.map(|t| t.timestamp_millis() as f64)
.unwrap_or_default(),
DottedChartXAxis::TimeSinceCaseStart => {
let first_time = trace
.events
.first()
.and_then(|e| get_event_time(e, timestamp_key));
let event_time = get_event_time(event, timestamp_key);
match (first_time, event_time) {
(Some(first), Some(current)) => (*current - first).num_milliseconds() as f64,
_ => 0.0,
}
}
DottedChartXAxis::StepNumberSinceCaseStart => event_index as f64,
DottedChartXAxis::TimeRelativeToCaseDuration => {
let first_time = trace
.events
.first()
.and_then(|e| get_event_time(e, timestamp_key));
let last_time = trace
.events
.last()
.and_then(|e| get_event_time(e, timestamp_key));
let event_time = get_event_time(event, timestamp_key);
match (first_time, last_time, event_time) {
(Some(first), Some(last), Some(current)) => {
let case_duration = (*last - *first).num_milliseconds() as f64;
if case_duration.abs() < f64::EPSILON {
0.0
} else {
(*current - *first).num_milliseconds() as f64 / case_duration
}
}
_ => 0.0,
}
}
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub enum DottedChartYAxis {
Case,
Resource,
EventAttribute(String),
CaseAttribute(String),
}
impl DottedChartYAxis {
pub fn get_value(&self, trace: &Trace, event: &Event) -> String {
let attr = match self {
DottedChartYAxis::Case => trace.attributes.get_by_key("concept:name"),
DottedChartYAxis::Resource => event.attributes.get_by_key("org:resource"),
DottedChartYAxis::EventAttribute(attr_name) => event.attributes.get_by_key(attr_name),
DottedChartYAxis::CaseAttribute(attr_name) => trace.attributes.get_by_key(attr_name),
};
attr.and_then(|a| a.value.try_as_string())
.cloned()
.unwrap_or_else(|| "UNKNOWN".to_string())
}
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub enum DottedChartColorAxis {
Activity,
Resource,
Case,
EventAttribute(String),
CaseAttribute(String),
}
impl DottedChartColorAxis {
pub fn get_value(&self, trace: &Trace, event: &Event) -> String {
let attr = match self {
DottedChartColorAxis::Activity => event.attributes.get_by_key("concept:name"),
DottedChartColorAxis::Resource => event.attributes.get_by_key("org:resource"),
DottedChartColorAxis::Case => trace.attributes.get_by_key("concept:name"),
DottedChartColorAxis::EventAttribute(attr_name) => {
event.attributes.get_by_key(attr_name)
}
DottedChartColorAxis::CaseAttribute(attr_name) => {
trace.attributes.get_by_key(attr_name)
}
};
attr.and_then(|a| a.value.try_as_string())
.cloned()
.unwrap_or_else(|| "UNKNOWN".to_string())
}
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct DottedChartData {
pub dots_per_color: HashMap<String, DottedChartPoints>,
pub y_values: Vec<String>,
}
#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema)]
pub struct DottedChartPoints {
pub x: Vec<f64>,
pub y: Vec<usize>,
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct DottedChartOptions {
pub x_axis: DottedChartXAxis,
pub y_axis: DottedChartYAxis,
pub color_axis: DottedChartColorAxis,
pub timestamp_key: String,
}
impl Default for DottedChartOptions {
fn default() -> Self {
Self {
x_axis: DottedChartXAxis::Time,
y_axis: DottedChartYAxis::Case,
color_axis: DottedChartColorAxis::Activity,
timestamp_key: DEFAULT_TIMESTAMP_KEY.to_string(),
}
}
}
#[register_binding(stringify_error)]
pub fn get_dotted_chart(
xes: &EventLog,
#[bind(default)] options: &DottedChartOptions,
) -> Result<DottedChartData, String> {
let DottedChartOptions {
x_axis,
y_axis,
color_axis,
timestamp_key,
} = options;
let mut y_values: HashMap<String, usize> = HashMap::default();
let mut data_per_color: HashMap<String, DottedChartPoints> = HashMap::default();
xes.traces
.iter()
.sorted_by_cached_key(|t: &&Trace| {
t.events
.first()
.and_then(|e| get_event_time(e, timestamp_key))
.cloned()
})
.for_each(|t| {
t.events.iter().enumerate().for_each(|(e_index, e)| {
let color_value = color_axis.get_value(t, e);
let points = data_per_color.entry(color_value).or_default();
let y_key = y_axis.get_value(t, e);
let y_index = match y_values.get(&y_key) {
Some(&idx) => idx,
None => {
let next = y_values.len();
y_values.insert(y_key, next);
next
}
};
points.y.push(y_index);
points
.x
.push(x_axis.get_value(t, e, e_index, timestamp_key));
})
});
Ok(DottedChartData {
dots_per_color: data_per_color,
y_values: y_values
.into_iter()
.sorted_by_key(|(_, i)| *i)
.map(|(v, _)| v)
.collect(),
})
}