Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 6 additions & 3 deletions docs/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
5 changes: 5 additions & 0 deletions ooui/graph/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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
Expand Down
17 changes: 9 additions & 8 deletions ooui/graph/chart.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
2 changes: 2 additions & 0 deletions ooui/graph/gantt.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
23 changes: 23 additions & 0 deletions spec/graph/gantt_spec.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 type="gantt">'
'<field name="task" axis="x"/>'
'<field name="start" axis="y" role="start" label="group"/>'
'<field name="end" axis="y" role="end"/>'
'</graph>')
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 = ('<graph type="gantt">'
'<field name="name" axis="x"/>'
Expand Down
8 changes: 4 additions & 4 deletions spec/graph/graph_spec.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 = """<?xml version="1.0"?>
<graph type="line" y_range="auto" timerange="day">
<field name="date" axis="x"/>
Expand All @@ -170,15 +170,15 @@
]
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,
'max': 32
}
}))
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 = """<?xml version="1.0"?>
<graph type="line" y_range="full" timerange="day">
<field name="date" axis="x"/>
Expand All @@ -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',
}))
44 changes: 39 additions & 5 deletions spec/graph/slider_spec.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,26 @@
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'):
xml = ('<graph type="{}"><field name="name" axis="x"/>'
'<field name="amount" axis="y" operator="+"/>'
'</graph>').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'):
Expand All @@ -24,5 +32,31 @@
'</graph>').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 = ('<graph type="{}" x_range="slider">'
'<field name="name" axis="x"/>'
'<field name="amount" axis="y" operator="+"/>'
'</graph>').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 = ('<graph type="line" x_range="slider" y_range="slider">'
'<field name="name" axis="x"/>'
'<field name="amount" axis="y" operator="+"/>'
'</graph>')
result = parse_graph(xml).process(values, fields)
expect(result['xAxisOpts']).to(equal({'mode': 'slider'}))
expect(result['yAxisOpts']).to(equal({'mode': 'slider'}))
Loading