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'}))