chore: start testing the grid
This commit is contained in:
310
src/ui/grid.rs
310
src/ui/grid.rs
@ -37,35 +37,15 @@ impl<'a> GridWidget<'a> {
|
|||||||
let col_cats: Vec<&str> = view.categories_on(Axis::Column);
|
let col_cats: Vec<&str> = view.categories_on(Axis::Column);
|
||||||
let page_cats: Vec<&str> = view.categories_on(Axis::Page);
|
let page_cats: Vec<&str> = view.categories_on(Axis::Page);
|
||||||
|
|
||||||
// Gather row items
|
// Gather row items — cross-product of all row-axis categories
|
||||||
let row_items: Vec<Vec<String>> = if row_cats.is_empty() {
|
let row_items: Vec<Vec<String>> = cross_product_items(
|
||||||
vec![vec![]]
|
&row_cats, self.model, view,
|
||||||
} else {
|
);
|
||||||
let cat_name = row_cats[0];
|
|
||||||
let cat = match self.model.category(cat_name) {
|
|
||||||
Some(c) => c,
|
|
||||||
None => return,
|
|
||||||
};
|
|
||||||
cat.ordered_item_names().into_iter()
|
|
||||||
.filter(|item| !view.is_hidden(cat_name, item))
|
|
||||||
.map(|item| vec![item.to_string()])
|
|
||||||
.collect()
|
|
||||||
};
|
|
||||||
|
|
||||||
// Gather col items
|
// Gather col items — cross-product of all col-axis categories
|
||||||
let col_items: Vec<Vec<String>> = if col_cats.is_empty() {
|
let col_items: Vec<Vec<String>> = cross_product_items(
|
||||||
vec![vec![]]
|
&col_cats, self.model, view,
|
||||||
} else {
|
);
|
||||||
let cat_name = col_cats[0];
|
|
||||||
let cat = match self.model.category(cat_name) {
|
|
||||||
Some(c) => c,
|
|
||||||
None => return,
|
|
||||||
};
|
|
||||||
cat.ordered_item_names().into_iter()
|
|
||||||
.filter(|item| !view.is_hidden(cat_name, item))
|
|
||||||
.map(|item| vec![item.to_string()])
|
|
||||||
.collect()
|
|
||||||
};
|
|
||||||
|
|
||||||
// Page filter coords
|
// Page filter coords
|
||||||
let page_coords: Vec<(String, String)> = page_cats.iter().map(|cat_name| {
|
let page_coords: Vec<(String, String)> = page_cats.iter().map(|cat_name| {
|
||||||
@ -227,6 +207,38 @@ impl<'a> GridWidget<'a> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns the cross-product of items across `cats`, filtered by hidden state.
|
||||||
|
/// Each element is a Vec of item names, one per category in `cats` order.
|
||||||
|
fn cross_product_items(cats: &[&str], model: &Model, view: &crate::view::View) -> Vec<Vec<String>> {
|
||||||
|
if cats.is_empty() {
|
||||||
|
return vec![vec![]];
|
||||||
|
}
|
||||||
|
let mut result: Vec<Vec<String>> = vec![vec![]];
|
||||||
|
for &cat_name in cats {
|
||||||
|
let items: Vec<String> = model
|
||||||
|
.category(cat_name)
|
||||||
|
.map(|c| {
|
||||||
|
c.ordered_item_names()
|
||||||
|
.into_iter()
|
||||||
|
.filter(|item| !view.is_hidden(cat_name, item))
|
||||||
|
.map(String::from)
|
||||||
|
.collect()
|
||||||
|
})
|
||||||
|
.unwrap_or_default();
|
||||||
|
result = result
|
||||||
|
.into_iter()
|
||||||
|
.flat_map(|prefix| {
|
||||||
|
items.iter().map(move |item| {
|
||||||
|
let mut row = prefix.clone();
|
||||||
|
row.push(item.clone());
|
||||||
|
row
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
}
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
impl<'a> Widget for GridWidget<'a> {
|
impl<'a> Widget for GridWidget<'a> {
|
||||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||||
let view_name = self.model.active_view
|
let view_name = self.model.active_view
|
||||||
@ -319,3 +331,245 @@ fn truncate(s: &str, max_width: usize) -> String {
|
|||||||
s.chars().take(max_width).collect()
|
s.chars().take(max_width).collect()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use ratatui::{buffer::Buffer, layout::Rect, widgets::Widget};
|
||||||
|
|
||||||
|
use crate::model::Model;
|
||||||
|
use crate::model::cell::{CellKey, CellValue};
|
||||||
|
use crate::formula::parse_formula;
|
||||||
|
use crate::ui::app::AppMode;
|
||||||
|
use super::GridWidget;
|
||||||
|
|
||||||
|
// ── Helpers ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/// Render a GridWidget into a fresh buffer of the given size.
|
||||||
|
fn render(model: &Model, width: u16, height: u16) -> Buffer {
|
||||||
|
let area = Rect::new(0, 0, width, height);
|
||||||
|
let mut buf = Buffer::empty(area);
|
||||||
|
GridWidget::new(model, &AppMode::Normal, "").render(area, &mut buf);
|
||||||
|
buf
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Flatten the buffer to a multiline string, trailing spaces trimmed per row.
|
||||||
|
fn buf_text(buf: &Buffer) -> String {
|
||||||
|
let w = buf.area().width as usize;
|
||||||
|
buf.content()
|
||||||
|
.chunks(w)
|
||||||
|
.map(|row| row.iter().map(|c| c.symbol()).collect::<String>().trim_end().to_string())
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join("\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn coord(pairs: &[(&str, &str)]) -> CellKey {
|
||||||
|
CellKey::new(pairs.iter().map(|(c, i)| (c.to_string(), i.to_string())).collect())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Minimal model: Type on Row, Month on Column.
|
||||||
|
fn two_cat_model() -> Model {
|
||||||
|
let mut m = Model::new("Test");
|
||||||
|
m.add_category("Type").unwrap(); // → Row
|
||||||
|
m.add_category("Month").unwrap(); // → Column
|
||||||
|
if let Some(c) = m.category_mut("Type") {
|
||||||
|
c.add_item("Food");
|
||||||
|
c.add_item("Clothing");
|
||||||
|
}
|
||||||
|
if let Some(c) = m.category_mut("Month") {
|
||||||
|
c.add_item("Jan");
|
||||||
|
c.add_item("Feb");
|
||||||
|
}
|
||||||
|
m
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Column headers ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn column_headers_appear() {
|
||||||
|
let m = two_cat_model();
|
||||||
|
let text = buf_text(&render(&m, 80, 24));
|
||||||
|
assert!(text.contains("Jan"), "expected 'Jan' in:\n{text}");
|
||||||
|
assert!(text.contains("Feb"), "expected 'Feb' in:\n{text}");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Row headers ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn row_headers_appear() {
|
||||||
|
let m = two_cat_model();
|
||||||
|
let text = buf_text(&render(&m, 80, 24));
|
||||||
|
assert!(text.contains("Food"), "expected 'Food' in:\n{text}");
|
||||||
|
assert!(text.contains("Clothing"), "expected 'Clothing' in:\n{text}");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Cell values ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn cell_value_appears_in_correct_position() {
|
||||||
|
let mut m = two_cat_model();
|
||||||
|
m.set_cell(coord(&[("Type", "Food"), ("Month", "Jan")]), CellValue::Number(123.0));
|
||||||
|
let text = buf_text(&render(&m, 80, 24));
|
||||||
|
assert!(text.contains("123"), "expected '123' in:\n{text}");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn multiple_cell_values_all_appear() {
|
||||||
|
let mut m = two_cat_model();
|
||||||
|
m.set_cell(coord(&[("Type", "Food"), ("Month", "Jan")]), CellValue::Number(100.0));
|
||||||
|
m.set_cell(coord(&[("Type", "Food"), ("Month", "Feb")]), CellValue::Number(200.0));
|
||||||
|
m.set_cell(coord(&[("Type", "Clothing"), ("Month", "Jan")]), CellValue::Number(50.0));
|
||||||
|
let text = buf_text(&render(&m, 80, 24));
|
||||||
|
assert!(text.contains("100"), "expected '100' in:\n{text}");
|
||||||
|
assert!(text.contains("200"), "expected '200' in:\n{text}");
|
||||||
|
assert!(text.contains("50"), "expected '50' in:\n{text}");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn unset_cells_show_no_value() {
|
||||||
|
let m = two_cat_model();
|
||||||
|
let text = buf_text(&render(&m, 80, 24));
|
||||||
|
// No digits should appear in the data area if nothing is set
|
||||||
|
// (Total row shows "0" — exclude that from this check by looking for non-zero)
|
||||||
|
assert!(!text.contains("100"), "unexpected '100' in:\n{text}");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Total row ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn total_row_label_appears() {
|
||||||
|
let m = two_cat_model();
|
||||||
|
let text = buf_text(&render(&m, 80, 24));
|
||||||
|
assert!(text.contains("Total"), "expected 'Total' in:\n{text}");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn total_row_sums_column_correctly() {
|
||||||
|
let mut m = two_cat_model();
|
||||||
|
m.set_cell(coord(&[("Type", "Food"), ("Month", "Jan")]), CellValue::Number(100.0));
|
||||||
|
m.set_cell(coord(&[("Type", "Clothing"), ("Month", "Jan")]), CellValue::Number(50.0));
|
||||||
|
let text = buf_text(&render(&m, 80, 24));
|
||||||
|
// Food(100) + Clothing(50) = 150 for Jan
|
||||||
|
assert!(text.contains("150"), "expected '150' (total for Jan) in:\n{text}");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Page filter bar ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn page_filter_bar_shows_category_and_selection() {
|
||||||
|
let mut m = Model::new("Test");
|
||||||
|
m.add_category("Type").unwrap(); // → Row
|
||||||
|
m.add_category("Month").unwrap(); // → Column
|
||||||
|
m.add_category("Payer").unwrap(); // → Page
|
||||||
|
if let Some(c) = m.category_mut("Type") { c.add_item("Food"); }
|
||||||
|
if let Some(c) = m.category_mut("Month") { c.add_item("Jan"); }
|
||||||
|
if let Some(c) = m.category_mut("Payer") {
|
||||||
|
c.add_item("Alice");
|
||||||
|
c.add_item("Bob");
|
||||||
|
}
|
||||||
|
if let Some(v) = m.active_view_mut() {
|
||||||
|
v.set_page_selection("Payer", "Bob");
|
||||||
|
}
|
||||||
|
let text = buf_text(&render(&m, 80, 24));
|
||||||
|
assert!(text.contains("Payer = Bob"), "expected 'Payer = Bob' in:\n{text}");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn page_filter_defaults_to_first_item() {
|
||||||
|
let mut m = Model::new("Test");
|
||||||
|
m.add_category("Type").unwrap();
|
||||||
|
m.add_category("Month").unwrap();
|
||||||
|
m.add_category("Payer").unwrap();
|
||||||
|
if let Some(c) = m.category_mut("Type") { c.add_item("Food"); }
|
||||||
|
if let Some(c) = m.category_mut("Month") { c.add_item("Jan"); }
|
||||||
|
if let Some(c) = m.category_mut("Payer") {
|
||||||
|
c.add_item("Alice");
|
||||||
|
c.add_item("Bob");
|
||||||
|
}
|
||||||
|
// No explicit selection — should default to first item
|
||||||
|
let text = buf_text(&render(&m, 80, 24));
|
||||||
|
assert!(text.contains("Payer = Alice"), "expected 'Payer = Alice' in:\n{text}");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Formula evaluation ────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn formula_cell_renders_computed_value() {
|
||||||
|
let mut m = Model::new("Test");
|
||||||
|
m.add_category("Measure").unwrap(); // → Row
|
||||||
|
m.add_category("Region").unwrap(); // → Column
|
||||||
|
if let Some(c) = m.category_mut("Measure") {
|
||||||
|
c.add_item("Revenue");
|
||||||
|
c.add_item("Cost");
|
||||||
|
c.add_item("Profit");
|
||||||
|
}
|
||||||
|
if let Some(c) = m.category_mut("Region") { c.add_item("East"); }
|
||||||
|
m.set_cell(coord(&[("Measure", "Revenue"), ("Region", "East")]), CellValue::Number(1000.0));
|
||||||
|
m.set_cell(coord(&[("Measure", "Cost"), ("Region", "East")]), CellValue::Number(600.0));
|
||||||
|
m.add_formula(parse_formula("Profit = Revenue - Cost", "Measure").unwrap());
|
||||||
|
|
||||||
|
let text = buf_text(&render(&m, 80, 24));
|
||||||
|
assert!(text.contains("400"), "expected '400' (Profit) in:\n{text}");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Multiple categories on same axis (cross-product) ─────────────────────
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn two_row_categories_produce_cross_product_labels() {
|
||||||
|
let mut m = Model::new("Test");
|
||||||
|
m.add_category("Type").unwrap(); // → Row
|
||||||
|
m.add_category("Month").unwrap(); // → Column
|
||||||
|
m.add_category("Recipient").unwrap(); // → Page by default; move to Row
|
||||||
|
if let Some(c) = m.category_mut("Type") { c.add_item("Food"); c.add_item("Clothing"); }
|
||||||
|
if let Some(c) = m.category_mut("Month") { c.add_item("Jan"); }
|
||||||
|
if let Some(c) = m.category_mut("Recipient") { c.add_item("Alice"); c.add_item("Bob"); }
|
||||||
|
if let Some(v) = m.active_view_mut() {
|
||||||
|
v.set_axis("Recipient", crate::view::Axis::Row);
|
||||||
|
}
|
||||||
|
|
||||||
|
let text = buf_text(&render(&m, 80, 24));
|
||||||
|
// Cross-product rows: Food/Alice, Food/Bob, Clothing/Alice, Clothing/Bob
|
||||||
|
assert!(text.contains("Food/Alice"), "expected 'Food/Alice' in:\n{text}");
|
||||||
|
assert!(text.contains("Food/Bob"), "expected 'Food/Bob' in:\n{text}");
|
||||||
|
assert!(text.contains("Clothing/Alice"), "expected 'Clothing/Alice' in:\n{text}");
|
||||||
|
assert!(text.contains("Clothing/Bob"), "expected 'Clothing/Bob' in:\n{text}");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn two_row_categories_include_all_coords_in_cell_lookup() {
|
||||||
|
let mut m = Model::new("Test");
|
||||||
|
m.add_category("Type").unwrap();
|
||||||
|
m.add_category("Month").unwrap();
|
||||||
|
m.add_category("Recipient").unwrap();
|
||||||
|
if let Some(c) = m.category_mut("Type") { c.add_item("Food"); }
|
||||||
|
if let Some(c) = m.category_mut("Month") { c.add_item("Jan"); }
|
||||||
|
if let Some(c) = m.category_mut("Recipient") { c.add_item("Alice"); c.add_item("Bob"); }
|
||||||
|
if let Some(v) = m.active_view_mut() {
|
||||||
|
v.set_axis("Recipient", crate::view::Axis::Row);
|
||||||
|
}
|
||||||
|
// Set data at the full 3-coordinate key
|
||||||
|
m.set_cell(
|
||||||
|
coord(&[("Month", "Jan"), ("Recipient", "Alice"), ("Type", "Food")]),
|
||||||
|
CellValue::Number(77.0),
|
||||||
|
);
|
||||||
|
let text = buf_text(&render(&m, 80, 24));
|
||||||
|
assert!(text.contains("77"), "expected '77' in:\n{text}");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn two_column_categories_produce_cross_product_headers() {
|
||||||
|
let mut m = Model::new("Test");
|
||||||
|
m.add_category("Type").unwrap(); // → Row
|
||||||
|
m.add_category("Month").unwrap(); // → Column
|
||||||
|
m.add_category("Year").unwrap(); // → Page by default; move to Column
|
||||||
|
if let Some(c) = m.category_mut("Type") { c.add_item("Food"); }
|
||||||
|
if let Some(c) = m.category_mut("Month") { c.add_item("Jan"); }
|
||||||
|
if let Some(c) = m.category_mut("Year") { c.add_item("2024"); c.add_item("2025"); }
|
||||||
|
if let Some(v) = m.active_view_mut() {
|
||||||
|
v.set_axis("Year", crate::view::Axis::Column);
|
||||||
|
}
|
||||||
|
|
||||||
|
let text = buf_text(&render(&m, 80, 24));
|
||||||
|
assert!(text.contains("Jan/2024"), "expected 'Jan/2024' in:\n{text}");
|
||||||
|
assert!(text.contains("Jan/2025"), "expected 'Jan/2025' in:\n{text}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
319
whatever.improv
319
whatever.improv
@ -1,319 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "New Model",
|
|
||||||
"categories": {
|
|
||||||
"Type": {
|
|
||||||
"id": 0,
|
|
||||||
"name": "Type",
|
|
||||||
"items": {
|
|
||||||
"Food": {
|
|
||||||
"id": 0,
|
|
||||||
"name": "Food",
|
|
||||||
"group": null
|
|
||||||
},
|
|
||||||
"Clothing": {
|
|
||||||
"id": 1,
|
|
||||||
"name": "Clothing",
|
|
||||||
"group": null
|
|
||||||
},
|
|
||||||
"Gas": {
|
|
||||||
"id": 2,
|
|
||||||
"name": "Gas",
|
|
||||||
"group": null
|
|
||||||
},
|
|
||||||
"Medical": {
|
|
||||||
"id": 3,
|
|
||||||
"name": "Medical",
|
|
||||||
"group": null
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"groups": [],
|
|
||||||
"next_item_id": 4
|
|
||||||
},
|
|
||||||
"Month": {
|
|
||||||
"id": 1,
|
|
||||||
"name": "Month",
|
|
||||||
"items": {
|
|
||||||
"Janury": {
|
|
||||||
"id": 0,
|
|
||||||
"name": "Janury",
|
|
||||||
"group": null
|
|
||||||
},
|
|
||||||
"February": {
|
|
||||||
"id": 1,
|
|
||||||
"name": "February",
|
|
||||||
"group": null
|
|
||||||
},
|
|
||||||
"March": {
|
|
||||||
"id": 2,
|
|
||||||
"name": "March",
|
|
||||||
"group": null
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"groups": [],
|
|
||||||
"next_item_id": 3
|
|
||||||
},
|
|
||||||
"Recipient": {
|
|
||||||
"id": 2,
|
|
||||||
"name": "Recipient",
|
|
||||||
"items": {
|
|
||||||
"Bob": {
|
|
||||||
"id": 0,
|
|
||||||
"name": "Bob",
|
|
||||||
"group": null
|
|
||||||
},
|
|
||||||
"Joe": {
|
|
||||||
"id": 1,
|
|
||||||
"name": "Joe",
|
|
||||||
"group": null
|
|
||||||
},
|
|
||||||
"Mary": {
|
|
||||||
"id": 2,
|
|
||||||
"name": "Mary",
|
|
||||||
"group": null
|
|
||||||
},
|
|
||||||
"Jane": {
|
|
||||||
"id": 3,
|
|
||||||
"name": "Jane",
|
|
||||||
"group": null
|
|
||||||
},
|
|
||||||
"Will": {
|
|
||||||
"id": 4,
|
|
||||||
"name": "Will",
|
|
||||||
"group": null
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"groups": [],
|
|
||||||
"next_item_id": 5
|
|
||||||
},
|
|
||||||
"Payer": {
|
|
||||||
"id": 3,
|
|
||||||
"name": "Payer",
|
|
||||||
"items": {
|
|
||||||
"Bernadette": {
|
|
||||||
"id": 0,
|
|
||||||
"name": "Bernadette",
|
|
||||||
"group": null
|
|
||||||
},
|
|
||||||
"Ed": {
|
|
||||||
"id": 1,
|
|
||||||
"name": "Ed",
|
|
||||||
"group": null
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"groups": [],
|
|
||||||
"next_item_id": 2
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"data": [
|
|
||||||
[
|
|
||||||
[
|
|
||||||
[
|
|
||||||
"Month",
|
|
||||||
"Janury"
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"Payer",
|
|
||||||
"Bernadette"
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"Recipient",
|
|
||||||
"Bob"
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"Type",
|
|
||||||
"Clothing"
|
|
||||||
]
|
|
||||||
],
|
|
||||||
{
|
|
||||||
"Number": 3.0
|
|
||||||
}
|
|
||||||
],
|
|
||||||
[
|
|
||||||
[
|
|
||||||
[
|
|
||||||
"Month",
|
|
||||||
"Janury"
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"Payer",
|
|
||||||
"Bernadette"
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"Recipient",
|
|
||||||
"Bob"
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"Type",
|
|
||||||
"Gas"
|
|
||||||
]
|
|
||||||
],
|
|
||||||
{
|
|
||||||
"Number": 4.0
|
|
||||||
}
|
|
||||||
],
|
|
||||||
[
|
|
||||||
[
|
|
||||||
[
|
|
||||||
"Month",
|
|
||||||
"Janury"
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"Payer",
|
|
||||||
"Bernadette"
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"Recipient",
|
|
||||||
"Bob"
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"Type",
|
|
||||||
"Medical"
|
|
||||||
]
|
|
||||||
],
|
|
||||||
{
|
|
||||||
"Number": 5.0
|
|
||||||
}
|
|
||||||
],
|
|
||||||
[
|
|
||||||
[
|
|
||||||
[
|
|
||||||
"Month",
|
|
||||||
"February"
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"Payer",
|
|
||||||
"Bernadette"
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"Recipient",
|
|
||||||
"Bob"
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"Type",
|
|
||||||
"Medical"
|
|
||||||
]
|
|
||||||
],
|
|
||||||
{
|
|
||||||
"Number": 33.0
|
|
||||||
}
|
|
||||||
],
|
|
||||||
[
|
|
||||||
[
|
|
||||||
[
|
|
||||||
"Month",
|
|
||||||
"February"
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"Payer",
|
|
||||||
"Bernadette"
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"Recipient",
|
|
||||||
"Bob"
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"Type",
|
|
||||||
"Gas"
|
|
||||||
]
|
|
||||||
],
|
|
||||||
{
|
|
||||||
"Number": 55.0
|
|
||||||
}
|
|
||||||
],
|
|
||||||
[
|
|
||||||
[
|
|
||||||
[
|
|
||||||
"Month",
|
|
||||||
"February"
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"Payer",
|
|
||||||
"Bernadette"
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"Recipient",
|
|
||||||
"Bob"
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"Type",
|
|
||||||
"Food"
|
|
||||||
]
|
|
||||||
],
|
|
||||||
{
|
|
||||||
"Number": 4.0
|
|
||||||
}
|
|
||||||
],
|
|
||||||
[
|
|
||||||
[
|
|
||||||
[
|
|
||||||
"Month",
|
|
||||||
"February"
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"Payer",
|
|
||||||
"Bernadette"
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"Recipient",
|
|
||||||
"Bob"
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"Type",
|
|
||||||
"Clothing"
|
|
||||||
]
|
|
||||||
],
|
|
||||||
{
|
|
||||||
"Text": "i5"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
[
|
|
||||||
[
|
|
||||||
[
|
|
||||||
"Month",
|
|
||||||
"Janury"
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"Payer",
|
|
||||||
"Bernadette"
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"Recipient",
|
|
||||||
"Bob"
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"Type",
|
|
||||||
"Food"
|
|
||||||
]
|
|
||||||
],
|
|
||||||
{
|
|
||||||
"Number": 12.0
|
|
||||||
}
|
|
||||||
]
|
|
||||||
],
|
|
||||||
"formulas": [],
|
|
||||||
"views": {
|
|
||||||
"Default": {
|
|
||||||
"name": "Default",
|
|
||||||
"category_axes": {
|
|
||||||
"Type": "Row",
|
|
||||||
"Month": "Column",
|
|
||||||
"Recipient": "Page",
|
|
||||||
"Payer": "Page"
|
|
||||||
},
|
|
||||||
"page_selections": {
|
|
||||||
"Recipient": "Bob"
|
|
||||||
},
|
|
||||||
"hidden_items": {},
|
|
||||||
"collapsed_groups": {},
|
|
||||||
"number_format": ",.0",
|
|
||||||
"row_offset": 0,
|
|
||||||
"col_offset": 0,
|
|
||||||
"selected": [
|
|
||||||
6,
|
|
||||||
2
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"active_view": "Default",
|
|
||||||
"next_category_id": 4
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user