diff --git a/ooui/graph/chart.py b/ooui/graph/chart.py
index ba8ae0f..4f790bd 100644
--- a/ooui/graph/chart.py
+++ b/ooui/graph/chart.py
@@ -5,7 +5,8 @@
from ooui.graph.axis import get_y_axis_fieldname
from ooui.graph.timerange import process_timerange_data
from ooui.graph.processor import (
- get_values_grouped_by_field, get_values_for_y_field, get_min_max
+ get_values_grouped_by_field, get_values_grouped_by_fields,
+ get_values_for_y_field, get_min_max
)
@@ -41,6 +42,9 @@ def fields(self):
if y.label and y.label not in fields:
fields.append(y.label)
+ if y.stacked and y.stacked not in fields:
+ fields.append(y.stacked)
+
return fields
def process(self, values, fields, options=None):
@@ -88,14 +92,26 @@ def process(self, values, fields, options=None):
'stacked': y_field.stacked
})
else:
- values_grouped_by_y_label = get_values_grouped_by_field(
- y_field.label, fields, objects_for_x_value
- )
+ if y_field.stacked in fields:
+ values_grouped_by_y_label = get_values_grouped_by_fields(
+ [y_field.label, y_field.stacked],
+ fields,
+ objects_for_x_value
+ )
+ else:
+ values_grouped_by_y_label = get_values_grouped_by_field(
+ y_field.label, fields, objects_for_x_value
+ )
for y_unique_value, grouped_entries in values_grouped_by_y_label.items():
entries = grouped_entries['entries']
label = grouped_entries['label']
+ if isinstance(y_unique_value, tuple):
+ y_label, stacked = y_unique_value
+ else:
+ stacked = y_field.stacked
+
values_for_y_field = get_values_for_y_field(
entries, y_field.name, fields
)
@@ -108,7 +124,7 @@ def process(self, values, fields, options=None):
'value': final_value,
'type': label,
'operator': y_field.operator,
- 'stacked': y_field.stacked
+ 'stacked': stacked
})
# Check if data should be flagged as grouped or stacked
diff --git a/ooui/graph/processor.py b/ooui/graph/processor.py
index 8d7e4d0..77457a6 100644
--- a/ooui/graph/processor.py
+++ b/ooui/graph/processor.py
@@ -42,6 +42,39 @@ def get_values_for_y_field(entries, field_name, fields):
]
+def get_values_grouped_by_fields(fields_names, fields, values):
+ """
+ Group values by multiple fields.
+
+ :param list fields_names: A list of field names by which to group values.
+ :param dict fields: A dictionary containing field definitions.
+ :param list values: A list of dictionaries representing the values to be
+ grouped.
+
+ :rtype: dict
+ :returns: A dictionary where keys are tuples of field values and values are
+ dictionaries containing a label and an "entries" list.
+ """
+ grouped_values = {}
+
+ for entry in values:
+ field_values = [
+ get_value_and_label_for_field(fields, entry, field_name)
+ for field_name in fields_names
+ ]
+ key = tuple(field_value['value'] for field_value in field_values)
+ label = ' - '.join(
+ field_value['label'] or '' for field_value in field_values
+ )
+
+ if key not in grouped_values:
+ grouped_values[key] = {'label': label, 'entries': []}
+
+ grouped_values[key]['entries'].append(entry)
+
+ return grouped_values
+
+
def get_values_grouped_by_field(field_name, fields, values):
"""
Group values by a specific field.
diff --git a/spec/graph/graph_spec.py b/spec/graph/graph_spec.py
index 253b1c5..1c1813a 100644
--- a/spec/graph/graph_spec.py
+++ b/spec/graph/graph_spec.py
@@ -37,6 +37,15 @@
expect(graph.progressbar).to(be_false)
expect(graph.suffix).to(equal('kW'))
+ with it('stacked attribute should be considered a field'):
+ xml = """
+
+
+
+ """
+ graph = parse_graph(xml)
+ expect(graph.fields).to(contain('create_date', 'reclamacio', 'import_phase'))
+
with it('should support progressbar attribute'):
xml = """
@@ -196,3 +205,141 @@
expect(result['yAxisProps']).to(equal({
'mode': 'full',
}))
+ with description("A bar graph with stacked field"):
+ with it("should read the stacked field"):
+ xml = """
+
+
+
+ """
+ graph = parse_graph(xml)
+ fields = {
+ 'create_date': {'type': 'datetime', 'string': 'Create date'},
+ 'reclamacio': {'type': 'char', 'string': 'Reclamacio', 'size': 256},
+ 'import_phase': {'type': 'selection', 'selection': [('10', 'Phase 1'), ('20', 'Phase 2')], 'string': 'Import Phase'}
+ }
+ values = [
+ {
+ "create_date": "2025-03-03 21:00:57",
+ "id": 504776,
+ "reclamacio": "",
+ "import_phase": "10",
+ },
+ {
+ "create_date": "2025-02-28 21:01:07",
+ "id": 504257,
+ "reclamacio": "48-a2",
+ "import_phase": "20",
+ },
+ {
+ "create_date": "2025-02-28 21:00:41",
+ "id": 503955,
+ "reclamacio": "48-a2",
+ "import_phase": "10",
+ },
+ {
+ "create_date": "2025-02-27 21:01:59",
+ "id": 503688,
+ "reclamacio": "48-a3",
+ "import_phase": "10",
+ },
+ {
+ "create_date": "2025-02-07 21:00:39",
+ "id": 488390,
+ "reclamacio": "",
+ "import_phase": "10",
+ },
+ {
+ "create_date": "2025-01-27 21:01:02",
+ "id": 478886,
+ "reclamacio": "",
+ "import_phase": "10",
+ },
+ {
+ "create_date": "2025-01-24 21:01:20",
+ "id": 478167,
+ "reclamacio": "48-a3",
+ "import_phase": "10",
+ },
+ {
+ "create_date": "2025-01-23 21:00:45",
+ "id": 477151,
+ "reclamacio": "",
+ "import_phase": "10",
+ },
+ {
+ "create_date": "2025-01-23 21:00:43",
+ "id": 477130,
+ "reclamacio": "",
+ "import_phase": "20",
+ },
+ {
+ "create_date": "2025-01-08 21:00:58",
+ "id": 467715,
+ "reclamacio": "",
+ "import_phase": "20",
+ }
+ ]
+ result = graph.process(values, fields)
+ expect(result).to(equal({
+ "num_items": 10,
+ "type": "bar",
+ "isGroup": True,
+ "isStack": True,
+ "data": [{
+ "stacked": "10",
+ "operator": "count",
+ "x": "2025-01",
+ "type": " - Phase 1",
+ "value": 2.0
+ },
+ {
+ "stacked": "20",
+ "operator": "count",
+ "x": "2025-01",
+ "type": " - Phase 2",
+ "value": 2.0
+ },
+ {
+ "stacked": "10",
+ "operator": "count",
+ "x": "2025-01",
+ "type": "48-a3 - Phase 1",
+ "value": 1.0
+ },
+ {
+ "stacked": "10",
+ "operator": "count",
+ "x": "2025-02",
+ "type": " - Phase 1",
+ "value": 1.0
+ },
+ {
+ "stacked": "10",
+ "operator": "count",
+ "x": "2025-02",
+ "type": "48-a2 - Phase 1",
+ "value": 1.0
+ },
+ {
+ "stacked": "20",
+ "operator": "count",
+ "x": "2025-02",
+ "type": "48-a2 - Phase 2",
+ "value": 1.0
+ },
+ {
+ "stacked": "10",
+ "operator": "count",
+ "x": "2025-02",
+ "type": "48-a3 - Phase 1",
+ "value": 1.0
+ },
+ {
+ "stacked": "10",
+ "operator": "count",
+ "x": "2025-03",
+ "type": " - Phase 1",
+ "value": 1.0
+ }]
+ }))
diff --git a/spec/graph/processor_spec.py b/spec/graph/processor_spec.py
index b28f6e6..ebda3ea 100644
--- a/spec/graph/processor_spec.py
+++ b/spec/graph/processor_spec.py
@@ -7,7 +7,8 @@
from ooui.graph import parse_graph
from ooui.graph.processor import (
get_values_grouped_by_field, get_all_objects_in_grouped_values,
- get_values_for_y_field, process_graph_data, get_min_max
+ get_values_for_y_field, process_graph_data, get_min_max,
+ get_values_grouped_by_fields
)
current_dir = os.path.dirname(os.path.abspath(__file__))
@@ -471,6 +472,31 @@ def get_graph_data(xml, model):
}))
+with description('Testing get_values_grouped_by_fields') as self:
+ with context('when a selection field has no value'):
+ with it('should preserve the group key and use an empty label'):
+ fields_data = {
+ 'category': {'type': 'string'},
+ 'phase': {'type': 'selection', 'selection': [
+ ('10', 'Phase 1')
+ ]}
+ }
+ values_data = [
+ {'category': 'Fruit', 'phase': False}
+ ]
+
+ grouped_values = get_values_grouped_by_fields(
+ ['category', 'phase'], fields_data, values_data
+ )
+
+ expect(grouped_values).to(equal({
+ ('Fruit', False): {
+ 'label': 'Fruit - ',
+ 'entries': values_data
+ }
+ }))
+
+
with description('Testing get_all_objects_in_grouped_values') as self:
with context('when retrieving all objects from grouped values'):
with it('should return a single list containing all objects'):