diff --git a/docs/usage.md b/docs/usage.md index cd2a53f..cf8789b 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -66,9 +66,12 @@ result = graph.process(data, fields) print(result) # Processed graph data ready for visualization ``` -For a Y-axis range slider, set `y_range="slider"` on a line or bar graph. -The processed result includes `yAxisOpts: {'mode': 'slider'}`; line graphs also -retain their existing `yAxisProps` response. +For an X-axis or Y-axis range slider on a line or bar graph, set +`x_range="slider"` or `y_range="slider"`. Both may be used together. The +processed result includes `xAxisOpts` and/or `yAxisOpts` with `mode: 'slider'`. +Line graphs also use `yAxisOpts` for the default, `auto`, and `full` Y-axis +modes. For `auto`, `valueOpts` contains the calculated minimum and maximum +values. Gantt graphs use one X field for the task name and two Y fields with `start` and `end` roles. An optional `label` on either Y field groups intervals: diff --git a/ooui/graph/base.py b/ooui/graph/base.py index 64fa124..d461eaf 100644 --- a/ooui/graph/base.py +++ b/ooui/graph/base.py @@ -8,6 +8,7 @@ def __init__(self, element): """ self._string = element.get('string') self._timerange = element.get('timerange', None) + self._x_range = element.get('x_range', 'default') self._y_range = element.get('y_range', "default") interval = element.get('interval', None) @@ -31,6 +32,10 @@ def interval(self): def y_range(self): return self._y_range + @property + def x_range(self): + return self._x_range + @property def type(self): return self._type diff --git a/ooui/graph/chart.py b/ooui/graph/chart.py index ba8ae0f..0d2b9c6 100644 --- a/ooui/graph/chart.py +++ b/ooui/graph/chart.py @@ -166,13 +166,14 @@ def process(self, values, fields, options=None): 'num_items': len(values), } - if self.type == "line" and self.y_range: - y_axis_props = {'mode': self.y_range} - if self.y_range == "auto": - y_axis_props['valueOpts'] = get_min_max(final_data) - result['yAxisProps'] = y_axis_props - - if self.type in ('line', 'bar') and self.y_range == 'slider': - result['yAxisOpts'] = {'mode': 'slider'} + if ((self.type == 'line' and self.y_range) + or (self.type == 'bar' and self.y_range == 'slider')): + y_axis_opts = {'mode': self.y_range} + if self.y_range == 'auto': + y_axis_opts['valueOpts'] = get_min_max(final_data) + result['yAxisOpts'] = y_axis_opts + + if self.type in ('line', 'bar') and self.x_range == 'slider': + result['xAxisOpts'] = {'mode': 'slider'} return result diff --git a/ooui/graph/gantt.py b/ooui/graph/gantt.py index 41fb301..6b816d0 100644 --- a/ooui/graph/gantt.py +++ b/ooui/graph/gantt.py @@ -47,6 +47,8 @@ def _label(self, record, fields, name, uninformed_string): label for key, label in field.get('selection', []) if key == value and isinstance(key, bool) == isinstance(value, bool) ), None) + if value == 0 and not isinstance(value, bool): + return value return value or uninformed_string def process(self, values, fields, options=None): diff --git a/spec/graph/gantt_spec.py b/spec/graph/gantt_spec.py index 284ff48..4d8dec7 100644 --- a/spec/graph/gantt_spec.py +++ b/spec/graph/gantt_spec.py @@ -57,6 +57,29 @@ expect([item['type'] for item in result['data']]).to(equal(['North', 'South'])) expect([item['id'] for item in result['data']]).to(equal([1, 2])) + with it('keeps numeric zero as a task and group label but not boolean false'): + xml = ('' + '' + '' + '' + '') + graph = parse_graph(xml) + fields = { + 'task': {'type': 'integer'}, + 'start': {'type': 'date'}, + 'end': {'type': 'date'}, + 'group': {'type': 'integer'}, + } + values = [ + {'id': 1, 'task': 0, 'group': 0, + 'start': '2026-09-22', 'end': '2026-09-23'}, + {'id': 2, 'task': False, 'group': False, + 'start': '2026-09-24', 'end': '2026-09-25'}, + ] + result = graph.process(values, fields, {'uninformedString': 'Sense informar'}) + expect([item['x'] for item in result['data']]).to(equal([0, 'Sense informar'])) + expect([item['type'] for item in result['data']]).to(equal([0, 'Sense informar'])) + with it('keeps ungrouped and empty Gantt results valid'): xml = ('' '' diff --git a/spec/graph/graph_spec.py b/spec/graph/graph_spec.py index 253b1c5..dca0b39 100644 --- a/spec/graph/graph_spec.py +++ b/spec/graph/graph_spec.py @@ -155,7 +155,7 @@ with description("Processing a Graph"): with description("A line graph with y_range auto"): - with it("should return yAxisProps to the result with min and max values"): + with it("should return yAxisOpts to the result with min and max values"): xml = """ @@ -170,7 +170,7 @@ ] fields = {'date': {'type': 'date'}, 'v': {'type': 'integer'}} result = graph.process(values, fields) - expect(result['yAxisProps']).to(equal({ + expect(result['yAxisOpts']).to(equal({ 'mode': 'auto', 'valueOpts': { 'min': 8, @@ -178,7 +178,7 @@ } })) with description("A line graph with y_range to full"): - with it("should return yAxisProps to the result with mode full"): + with it("should return yAxisOpts to the result with mode full"): xml = """ @@ -193,6 +193,6 @@ ] fields = {'date': {'type': 'date'}, 'v': {'type': 'integer'}} result = graph.process(values, fields) - expect(result['yAxisProps']).to(equal({ + expect(result['yAxisOpts']).to(equal({ 'mode': 'full', })) diff --git a/spec/graph/slider_spec.py b/spec/graph/slider_spec.py index 086bb9c..763cd98 100644 --- a/spec/graph/slider_spec.py +++ b/spec/graph/slider_spec.py @@ -3,8 +3,8 @@ from ooui.graph import parse_graph -with description('Graph Y-axis slider'): - with it('leaves old line and bar payloads unchanged without a slider'): +with description('Graph axis sliders'): + with it('keeps existing line and bar data without a slider'): fields = {'name': {'type': 'char'}, 'amount': {'type': 'integer'}} values = [{'name': 'January', 'amount': 10}] for graph_type in ('line', 'bar'): @@ -12,9 +12,17 @@ '' '').format(graph_type) result = parse_graph(xml).process(values, fields) - expect(result).not_to(have_key('yAxisOpts')) + expect(result['data'][0]['x']).to(equal('January')) + expect(result['data'][0]['value']).to(equal(10)) + expect(result['num_items']).to(equal(1)) + if graph_type == 'line': + expect(result['yAxisOpts']).to(equal({'mode': 'default'})) + else: + expect(result).not_to(have_key('yAxisOpts')) + expect(result).not_to(have_key('xAxisOpts')) + expect(result).not_to(have_key('yAxisProps')) - with it('returns slider metadata for line and bar without removing legacy axis props'): + with it('returns slider metadata for line and bar under one key'): fields = {'name': {'type': 'char'}, 'amount': {'type': 'integer', 'string': 'Amount'}} values = [{'name': 'January', 'amount': 10}] for graph_type in ('line', 'bar'): @@ -24,5 +32,31 @@ '').format(graph_type) result = parse_graph(xml).process(values, fields) expect(result['yAxisOpts']).to(equal({'mode': 'slider'})) + expect(result).not_to(have_key('xAxisOpts')) + expect(result).not_to(have_key('yAxisProps')) + + with it('returns an X-axis slider for line and bar independently of Y'): + fields = {'name': {'type': 'char'}, 'amount': {'type': 'integer'}} + values = [{'name': 'January', 'amount': 10}] + for graph_type in ('line', 'bar'): + xml = ('' + '' + '' + '').format(graph_type) + result = parse_graph(xml).process(values, fields) + expect(result['xAxisOpts']).to(equal({'mode': 'slider'})) if graph_type == 'line': - expect(result['yAxisProps']).to(equal({'mode': 'slider'})) + expect(result['yAxisOpts']).to(equal({'mode': 'default'})) + else: + expect(result).not_to(have_key('yAxisOpts')) + + with it('returns independent X and Y slider metadata together'): + fields = {'name': {'type': 'char'}, 'amount': {'type': 'integer'}} + values = [{'name': 'January', 'amount': 10}] + xml = ('' + '' + '' + '') + result = parse_graph(xml).process(values, fields) + expect(result['xAxisOpts']).to(equal({'mode': 'slider'})) + expect(result['yAxisOpts']).to(equal({'mode': 'slider'}))