1use crate::{
2 AttributeOverview, AttributeOverviewData, OverviewResult,
3 tabled_modifiers::MergeDuplicatesVerticalByColumn,
4};
5use graphrecords_core::{
6 GraphRecord,
7 errors::GraphRecordError,
8 prelude::{AttributeType, GraphRecordAttribute, GraphRecordValue, Group, GroupSchema},
9};
10use graphrecords_query::{
11 Attribute, Filter, Index, IsIn, IsNull, Maximum, Mean, Minimum, OnErrorOf, QueryEdges,
12 QueryNodes, QueryResult, error::structure::MissingAttribute, operations::policy::Drop,
13};
14use graphrecords_utils::aliases::{GrHashMap, GrHashSet};
15use itertools::Itertools;
16use rayon::iter::{IntoParallelRefIterator, ParallelIterator};
17use std::fmt::{Display, Formatter};
18use tabled::{
19 builder::Builder,
20 settings::{Alignment, Panel, Style, Width, object::Columns, themes::BorderCorrection},
21};
22
23#[derive(Debug, Clone)]
24pub struct NodeGroupOverview {
25 pub count: usize,
26 pub attributes: GrHashMap<GraphRecordAttribute, AttributeOverview>,
27
28 truncate_details: Option<usize>,
29}
30
31impl Display for NodeGroupOverview {
32 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
33 let mut builder = Builder::new();
34
35 builder.push_record([
36 "Node Count",
37 "Attribute",
38 "Attribute Type",
39 "Data Type",
40 "Details",
41 ]);
42
43 for (attribute, overview) in &self.attributes {
44 let details = overview.data.details();
45
46 builder.push_record([
47 &self.count.to_string(),
48 &attribute.to_string(),
49 overview.data.attribute_type_name(),
50 &overview.data_type.to_string(),
51 &details,
52 ]);
53 }
54
55 if self.attributes.is_empty() && self.count > 0 {
56 builder.push_record([&self.count.to_string(), "-", "-", "-", "-"]);
57 }
58
59 let mut table = builder.build();
60 table.with(Style::modern());
61 table.with(Panel::header("Node Overview"));
62 table.with(MergeDuplicatesVerticalByColumn::new(vec![0]));
63 table.with(Alignment::center_vertical());
64 table.with(BorderCorrection {});
65
66 if let Some(truncate_details) = self.truncate_details {
67 table.modify(Columns::last(), Width::truncate(truncate_details));
68 }
69
70 writeln!(f, "{table}")
71 }
72}
73
74impl NodeGroupOverview {
75 fn new(
76 graphrecord: &GraphRecord,
77 group_schema: &GroupSchema,
78 group: Option<&Group>,
79 truncate_details: Option<usize>,
80 ) -> OverviewResult<Self> {
81 let nodes_in_group: GrHashSet<_> = match group {
82 Some(group) => graphrecord.nodes_in_group(group)?.cloned().collect(),
83 None => graphrecord.ungrouped_nodes().cloned().collect(),
84 };
85 let count = nodes_in_group.len();
86
87 let attributes: GrHashMap<_, _> = group_schema
88 .nodes()
89 .par_iter()
90 .map(|(key, attribute_data_type)| {
91 let attribute_type = attribute_data_type.attribute_type();
92 let data_type = attribute_data_type.data_type().clone();
93
94 let attribute_overview = match attribute_type {
95 AttributeType::Categorical => {
96 let selection = graphrecord.query_nodes(|nodes| {
97 let nodes = nodes.filter(nodes.index().is_in(nodes_in_group.clone()));
98
99 nodes
100 .attribute(key.clone())
101 .on_error_of::<MissingAttribute>(Drop)
102 });
103
104 let values: Vec<_> = selection
105 .evaluate()?
106 .map(|(_, value)| value)
107 .collect::<QueryResult<_>>()?;
108
109 AttributeOverview {
110 data_type,
111 data: AttributeOverviewData::Categorical {
112 distinct_values: values
113 .into_iter()
114 .sorted_by(GraphRecordValue::total_cmp)
115 .dedup()
116 .collect(),
117 },
118 }
119 }
120 AttributeType::Continuous => {
121 let selection = graphrecord.query_nodes(|nodes| {
122 let nodes = nodes.filter(nodes.index().is_in(nodes_in_group.clone()));
123 let values = nodes
124 .attribute(key.clone())
125 .on_error_of::<MissingAttribute>(Drop);
126 let values = values.filter(!values.is_null());
127
128 (values.min(), values.mean(), values.max())
129 });
130
131 let (minimum, mean, maximum) = selection.evaluate()?;
132
133 AttributeOverview {
134 data_type,
135 data: AttributeOverviewData::Continuous {
136 min: minimum.transpose()?.unwrap_or(GraphRecordValue::Null),
137 mean: mean.transpose()?.unwrap_or(GraphRecordValue::Null),
138 max: maximum.transpose()?.unwrap_or(GraphRecordValue::Null),
139 },
140 }
141 }
142 AttributeType::Temporal => {
143 let selection = graphrecord.query_nodes(|nodes| {
144 let nodes = nodes.filter(nodes.index().is_in(nodes_in_group.clone()));
145 let values = nodes
146 .attribute(key.clone())
147 .on_error_of::<MissingAttribute>(Drop);
148 let values = values.filter(!values.is_null());
149
150 (values.min(), values.max())
151 });
152
153 let (minimum, maximum) = selection.evaluate()?;
154
155 AttributeOverview {
156 data_type,
157 data: AttributeOverviewData::Temporal {
158 min: minimum.transpose()?.unwrap_or(GraphRecordValue::Null),
159 max: maximum.transpose()?.unwrap_or(GraphRecordValue::Null),
160 },
161 }
162 }
163 AttributeType::Unstructured => {
164 let selection = graphrecord.query_nodes(|nodes| {
165 let nodes = nodes.filter(nodes.index().is_in(nodes_in_group.clone()));
166
167 nodes
168 .attribute(key.clone())
169 .on_error_of::<MissingAttribute>(Drop)
170 });
171
172 let values: Vec<_> = selection
173 .evaluate()?
174 .map(|(_, value)| value)
175 .collect::<QueryResult<_>>()?;
176
177 AttributeOverview {
178 data_type,
179 data: AttributeOverviewData::Unstructured {
180 distinct_count: values
181 .into_iter()
182 .sorted_by(GraphRecordValue::total_cmp)
183 .dedup()
184 .count(),
185 },
186 }
187 }
188 };
189
190 Ok((key.clone(), attribute_overview))
191 })
192 .collect::<QueryResult<_>>()?;
193
194 Ok(Self {
195 count,
196 attributes,
197 truncate_details,
198 })
199 }
200}
201
202#[derive(Debug, Clone)]
203pub struct EdgeGroupOverview {
204 pub count: usize,
205 pub attributes: GrHashMap<GraphRecordAttribute, AttributeOverview>,
206
207 truncate_details: Option<usize>,
208}
209
210impl Display for EdgeGroupOverview {
211 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
212 let mut builder = Builder::new();
213
214 builder.push_record([
215 "Edge Count",
216 "Attribute",
217 "Attribute Type",
218 "Data Type",
219 "Details",
220 ]);
221
222 for (attribute, overview) in &self.attributes {
223 let details = overview.data.details();
224
225 builder.push_record([
226 &self.count.to_string(),
227 &attribute.to_string(),
228 overview.data.attribute_type_name(),
229 &overview.data_type.to_string(),
230 &details,
231 ]);
232 }
233
234 let mut table = builder.build();
235 table.with(Style::modern());
236 table.with(Panel::header("Edge Overview"));
237 table.with(MergeDuplicatesVerticalByColumn::new(vec![0]));
238 table.with(Alignment::center_vertical());
239 table.with(BorderCorrection {});
240
241 if let Some(truncate_details) = self.truncate_details {
242 table.modify(Columns::last(), Width::truncate(truncate_details));
243 }
244
245 writeln!(f, "{table}")
246 }
247}
248
249impl EdgeGroupOverview {
250 fn new(
251 graphrecord: &GraphRecord,
252 group_schema: &GroupSchema,
253 group: Option<&Group>,
254 truncate_details: Option<usize>,
255 ) -> OverviewResult<Self> {
256 let edges_in_group: GrHashSet<_> = match group {
257 Some(group) => graphrecord.edges_in_group(group)?.copied().collect(),
258 None => graphrecord.ungrouped_edges().copied().collect(),
259 };
260 let count = edges_in_group.len();
261
262 let attributes: GrHashMap<_, _> = group_schema
263 .edges()
264 .par_iter()
265 .map(|(key, attribute_data_type)| {
266 let attribute_type = attribute_data_type.attribute_type();
267 let data_type = attribute_data_type.data_type().clone();
268
269 let attribute_overview = match attribute_type {
270 AttributeType::Categorical => {
271 let selection = graphrecord.query_edges(|edges| {
272 let edges = edges.filter(edges.index().is_in(edges_in_group.clone()));
273
274 edges
275 .attribute(key.clone())
276 .on_error_of::<MissingAttribute>(Drop)
277 });
278
279 let values: Vec<_> = selection
280 .evaluate()?
281 .map(|(_, value)| value)
282 .collect::<QueryResult<_>>()?;
283
284 AttributeOverview {
285 data_type,
286 data: AttributeOverviewData::Categorical {
287 distinct_values: values
288 .into_iter()
289 .sorted_by(GraphRecordValue::total_cmp)
290 .dedup()
291 .collect(),
292 },
293 }
294 }
295 AttributeType::Continuous => {
296 let selection = graphrecord.query_edges(|edges| {
297 let edges = edges.filter(edges.index().is_in(edges_in_group.clone()));
298 let values = edges
299 .attribute(key.clone())
300 .on_error_of::<MissingAttribute>(Drop);
301 let values = values.filter(!values.is_null());
302
303 (values.min(), values.mean(), values.max())
304 });
305
306 let (minimum, mean, maximum) = selection.evaluate()?;
307
308 AttributeOverview {
309 data_type,
310 data: AttributeOverviewData::Continuous {
311 min: minimum.transpose()?.unwrap_or(GraphRecordValue::Null),
312 mean: mean.transpose()?.unwrap_or(GraphRecordValue::Null),
313 max: maximum.transpose()?.unwrap_or(GraphRecordValue::Null),
314 },
315 }
316 }
317 AttributeType::Temporal => {
318 let selection = graphrecord.query_edges(|edges| {
319 let edges = edges.filter(edges.index().is_in(edges_in_group.clone()));
320 let values = edges
321 .attribute(key.clone())
322 .on_error_of::<MissingAttribute>(Drop);
323 let values = values.filter(!values.is_null());
324
325 (values.min(), values.max())
326 });
327
328 let (minimum, maximum) = selection.evaluate()?;
329
330 AttributeOverview {
331 data_type,
332 data: AttributeOverviewData::Temporal {
333 min: minimum.transpose()?.unwrap_or(GraphRecordValue::Null),
334 max: maximum.transpose()?.unwrap_or(GraphRecordValue::Null),
335 },
336 }
337 }
338 AttributeType::Unstructured => {
339 let selection = graphrecord.query_edges(|edges| {
340 let edges = edges.filter(edges.index().is_in(edges_in_group.clone()));
341
342 edges
343 .attribute(key.clone())
344 .on_error_of::<MissingAttribute>(Drop)
345 });
346
347 let values: Vec<_> = selection
348 .evaluate()?
349 .map(|(_, value)| value)
350 .collect::<QueryResult<_>>()?;
351
352 AttributeOverview {
353 data_type,
354 data: AttributeOverviewData::Unstructured {
355 distinct_count: values
356 .into_iter()
357 .sorted_by(GraphRecordValue::total_cmp)
358 .dedup()
359 .count(),
360 },
361 }
362 }
363 };
364
365 Ok((key.clone(), attribute_overview))
366 })
367 .collect::<QueryResult<_>>()?;
368
369 Ok(Self {
370 count,
371 attributes,
372 truncate_details,
373 })
374 }
375}
376
377#[derive(Debug, Clone)]
378pub struct GroupOverview {
379 pub node_overview: NodeGroupOverview,
380 pub edge_overview: EdgeGroupOverview,
381}
382
383impl Display for GroupOverview {
384 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
385 writeln!(f, "{}", self.node_overview)?;
386 writeln!(f, "{}", self.edge_overview)
387 }
388}
389
390impl GroupOverview {
391 pub(crate) fn new(
392 graphrecord: &GraphRecord,
393 group: Option<&Group>,
394 truncate_details: Option<usize>,
395 ) -> OverviewResult<Self> {
396 let schema = graphrecord.get_schema();
397
398 let group_schema = match group {
399 Some(group) => schema.group(group).map_err(GraphRecordError::from)?,
400 None => schema.ungrouped(),
401 };
402
403 Ok(Self {
404 node_overview: NodeGroupOverview::new(
405 graphrecord,
406 group_schema,
407 group,
408 truncate_details,
409 )?,
410 edge_overview: EdgeGroupOverview::new(
411 graphrecord,
412 group_schema,
413 group,
414 truncate_details,
415 )?,
416 })
417 }
418}
419
420pub trait GroupOverviewable {
421 fn group_overview(
422 &self,
423 group: &Group,
424 truncate_details: Option<usize>,
425 ) -> OverviewResult<GroupOverview>;
426}
427
428impl GroupOverviewable for GraphRecord {
429 fn group_overview(
430 &self,
431 group: &Group,
432 truncate_details: Option<usize>,
433 ) -> OverviewResult<GroupOverview> {
434 GroupOverview::new(self, Some(group), truncate_details)
435 }
436}