diff --git a/docs/usage.md b/docs/usage.md index cf8789b..81bcd72 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -66,12 +66,16 @@ result = graph.process(data, fields) print(result) # Processed graph data ready for visualization ``` -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. +For an X-axis range slider on a line or bar graph, set `x_range="slider"`. +The Y-axis initial range and zoom control can be configured independently with +`y_range="auto|full"` and `y_zoom="none|slider"`. For example, +`y_range="auto" y_zoom="slider"` starts at the data range and lets the user +adjust it. The processed `yAxisOpts` keeps the range in `mode` and exposes the +control as `zoom: 'slider'`. For `auto`, `valueOpts` contains the calculated +minimum and maximum values. The legacy `y_range="slider"` syntax remains +supported and returns `mode: 'slider'`. An explicit `y_zoom="none"` is returned +as `zoom: 'none'`; when `y_zoom` is omitted, `zoom` is omitted too for backward +compatibility. 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: @@ -340,4 +344,4 @@ except Exception as e: - Check out [Examples](examples.md) for more practical use cases - Explore the [API Reference](api-reference.md) for detailed method documentation -- Learn about [Advanced Usage](advanced-usage.md) for complex scenarios \ No newline at end of file +- Learn about [Advanced Usage](advanced-usage.md) for complex scenarios diff --git a/ooui/graph/base.py b/ooui/graph/base.py index d461eaf..3da0224 100644 --- a/ooui/graph/base.py +++ b/ooui/graph/base.py @@ -10,6 +10,8 @@ def __init__(self, element): self._timerange = element.get('timerange', None) self._x_range = element.get('x_range', 'default') self._y_range = element.get('y_range', "default") + self._y_zoom = element.get('y_zoom', 'none') + self._y_zoom_explicit = element.get('y_zoom') is not None interval = element.get('interval', None) self._interval = int(interval) if interval is not None else 1 @@ -32,6 +34,14 @@ def interval(self): def y_range(self): return self._y_range + @property + def y_zoom(self): + return self._y_zoom + + @property + def y_zoom_explicit(self): + return self._y_zoom_explicit + @property def x_range(self): return self._x_range diff --git a/ooui/graph/chart.py b/ooui/graph/chart.py index 8c85172..7babf3a 100644 --- a/ooui/graph/chart.py +++ b/ooui/graph/chart.py @@ -183,10 +183,15 @@ def process(self, values, fields, options=None): } if ((self.type == 'line' and self.y_range) - or (self.type == 'bar' and self.y_range == 'slider')): + or (self.type == 'bar' and ( + self.y_range in ('auto', 'full', 'slider') + or self.y_zoom == 'slider' + ))): y_axis_opts = {'mode': self.y_range} if self.y_range == 'auto' and final_data: y_axis_opts['valueOpts'] = get_min_max(final_data) + if self.y_zoom_explicit: + y_axis_opts['zoom'] = self.y_zoom result['yAxisOpts'] = y_axis_opts if self.type in ('line', 'bar') and self.x_range == 'slider': diff --git a/spec/graph/graph_spec.py b/spec/graph/graph_spec.py index c3c5dcf..ff993d4 100644 --- a/spec/graph/graph_spec.py +++ b/spec/graph/graph_spec.py @@ -162,6 +162,17 @@ expect(graph.y[0].operator).to(equal('+')) expect(graph.y_range).to(equal('auto')) + with it("should parse an independent y-axis zoom mode"): + xml = """ + + + + + """ + graph = parse_graph(xml) + expect(graph.y_range).to(equal('full')) + expect(graph.y_zoom).to(equal('slider')) + with description("Processing a Graph"): with description("A line graph with y_range auto"): with it("should return yAxisOpts to the result with min and max values"): diff --git a/spec/graph/slider_spec.py b/spec/graph/slider_spec.py index 763cd98..868b653 100644 --- a/spec/graph/slider_spec.py +++ b/spec/graph/slider_spec.py @@ -22,6 +22,38 @@ expect(result).not_to(have_key('xAxisOpts')) expect(result).not_to(have_key('yAxisProps')) + with it('returns independent Y range and zoom metadata'): + 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['yAxisOpts']).to(equal({ + 'mode': 'auto', + 'zoom': 'slider', + 'valueOpts': {'min': 10, 'max': 10}, + })) + + with it('returns an explicit Y range for bars without enabling zoom'): + fields = {'name': {'type': 'char'}, 'amount': {'type': 'integer'}} + values = [ + {'name': 'January', 'amount': 10}, + {'name': 'February', 'amount': 20}, + ] + for y_range in ('auto', 'full'): + xml = ('' + '' + '' + '').format(y_range) + result = parse_graph(xml).process(values, fields) + expected = {'mode': y_range, 'zoom': 'none'} + if y_range == 'auto': + expected['valueOpts'] = {'min': 9, 'max': 21} + expect(result['yAxisOpts']).to(equal(expected)) + 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}] @@ -35,6 +67,20 @@ expect(result).not_to(have_key('xAxisOpts')) expect(result).not_to(have_key('yAxisProps')) + with it('preserves an explicitly disabled Y zoom with a legacy range'): + 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['yAxisOpts']).to(equal({ + 'mode': 'slider', + 'zoom': 'none', + })) + 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}]