diff --git a/examples/compiled/bar_qq_stack.png b/examples/compiled/bar_qq_stack.png new file mode 100644 index 0000000000..e4f5fdb8a5 Binary files /dev/null and b/examples/compiled/bar_qq_stack.png differ diff --git a/examples/compiled/bar_qq_stack.svg b/examples/compiled/bar_qq_stack.svg new file mode 100644 index 0000000000..6d13253cd9 --- /dev/null +++ b/examples/compiled/bar_qq_stack.svg @@ -0,0 +1 @@ +12345a020406080b \ No newline at end of file diff --git a/examples/compiled/bar_qq_stack.vg.json b/examples/compiled/bar_qq_stack.vg.json new file mode 100644 index 0000000000..c7c5a653e6 --- /dev/null +++ b/examples/compiled/bar_qq_stack.vg.json @@ -0,0 +1,123 @@ +{ + "$schema": "https://vega.github.io/schema/vega/v5.json", + "background": "white", + "padding": 5, + "width": 200, + "height": 200, + "style": "cell", + "data": [ + { + "name": "source_0", + "values": [{"a": 1, "b": 28}, {"a": 1, "b": 55}, {"a": 5, "b": 43}] + }, + { + "name": "data_0", + "source": "source_0", + "transform": [ + { + "type": "stack", + "groupby": ["a"], + "field": "b", + "sort": {"field": [], "order": []}, + "as": ["b_start", "b_end"], + "offset": "zero" + }, + { + "type": "filter", + "expr": "isValid(datum[\"a\"]) && isFinite(+datum[\"a\"]) && isValid(datum[\"b\"]) && isFinite(+datum[\"b\"])" + } + ] + } + ], + "marks": [ + { + "name": "marks", + "type": "rect", + "style": ["bar"], + "from": {"data": "data_0"}, + "encode": { + "update": { + "fill": {"value": "#4c78a8"}, + "ariaRoleDescription": {"value": "bar"}, + "description": { + "signal": "\"a: \" + (format(datum[\"a\"], \"\")) + \"; b: \" + (format(datum[\"b\"], \"\"))" + }, + "xc": {"scale": "x", "field": "a"}, + "width": {"value": 5}, + "y": {"scale": "y", "field": "b_end"}, + "y2": {"scale": "y", "field": "b_start"} + } + } + } + ], + "scales": [ + { + "name": "x", + "type": "linear", + "domain": {"data": "data_0", "field": "a"}, + "range": [0, {"signal": "width"}], + "nice": true, + "zero": false, + "padding": 5 + }, + { + "name": "y", + "type": "linear", + "domain": {"data": "data_0", "fields": ["b_start", "b_end"]}, + "range": [{"signal": "height"}, 0], + "nice": true, + "zero": true + } + ], + "axes": [ + { + "scale": "x", + "orient": "bottom", + "gridScale": "y", + "grid": true, + "tickCount": {"signal": "ceil(width/40)"}, + "domain": false, + "labels": false, + "aria": false, + "maxExtent": 0, + "minExtent": 0, + "ticks": false, + "zindex": 0 + }, + { + "scale": "y", + "orient": "left", + "gridScale": "x", + "grid": true, + "tickCount": {"signal": "ceil(height/40)"}, + "domain": false, + "labels": false, + "aria": false, + "maxExtent": 0, + "minExtent": 0, + "ticks": false, + "zindex": 0 + }, + { + "scale": "x", + "orient": "bottom", + "grid": false, + "title": "a", + "labelAngle": 0, + "labelBaseline": "top", + "labelFlush": true, + "labelOverlap": true, + "tickCount": {"signal": "ceil(width/40)"}, + "zindex": 0 + }, + { + "scale": "y", + "orient": "left", + "grid": false, + "title": "b", + "labelOverlap": true, + "tickCount": {"signal": "ceil(height/40)"}, + "zindex": 0 + } + ] +} diff --git a/examples/compiled/bar_qq_stack_horizontal.png b/examples/compiled/bar_qq_stack_horizontal.png new file mode 100644 index 0000000000..06752eb9db Binary files /dev/null and b/examples/compiled/bar_qq_stack_horizontal.png differ diff --git a/examples/compiled/bar_qq_stack_horizontal.svg b/examples/compiled/bar_qq_stack_horizontal.svg new file mode 100644 index 0000000000..049a2a761e --- /dev/null +++ b/examples/compiled/bar_qq_stack_horizontal.svg @@ -0,0 +1 @@ +020406080b12345a \ No newline at end of file diff --git a/examples/compiled/bar_qq_stack_horizontal.vg.json b/examples/compiled/bar_qq_stack_horizontal.vg.json new file mode 100644 index 0000000000..734c1bd839 --- /dev/null +++ b/examples/compiled/bar_qq_stack_horizontal.vg.json @@ -0,0 +1,124 @@ +{ + "$schema": "https://vega.github.io/schema/vega/v5.json", + "description": "A simple bar chart with embedded data.", + "background": "white", + "padding": 5, + "width": 200, + "height": 200, + "style": "cell", + "data": [ + { + "name": "source_0", + "values": [{"a": 1, "b": 28}, {"a": 1, "b": 55}, {"a": 5, "b": 43}] + }, + { + "name": "data_0", + "source": "source_0", + "transform": [ + { + "type": "stack", + "groupby": ["a"], + "field": "b", + "sort": {"field": [], "order": []}, + "as": ["b_start", "b_end"], + "offset": "zero" + }, + { + "type": "filter", + "expr": "isValid(datum[\"b\"]) && isFinite(+datum[\"b\"]) && isValid(datum[\"a\"]) && isFinite(+datum[\"a\"])" + } + ] + } + ], + "marks": [ + { + "name": "marks", + "type": "rect", + "style": ["bar"], + "from": {"data": "data_0"}, + "encode": { + "update": { + "fill": {"value": "#4c78a8"}, + "ariaRoleDescription": {"value": "bar"}, + "description": { + "signal": "\"b: \" + (format(datum[\"b\"], \"\")) + \"; a: \" + (format(datum[\"a\"], \"\"))" + }, + "x": {"scale": "x", "field": "b_end"}, + "x2": {"scale": "x", "field": "b_start"}, + "yc": {"scale": "y", "field": "a"}, + "height": {"value": 5} + } + } + } + ], + "scales": [ + { + "name": "x", + "type": "linear", + "domain": {"data": "data_0", "fields": ["b_start", "b_end"]}, + "range": [0, {"signal": "width"}], + "nice": true, + "zero": true + }, + { + "name": "y", + "type": "linear", + "domain": {"data": "data_0", "field": "a"}, + "range": [{"signal": "height"}, 0], + "nice": true, + "zero": false, + "padding": 5 + } + ], + "axes": [ + { + "scale": "x", + "orient": "bottom", + "gridScale": "y", + "grid": true, + "tickCount": {"signal": "ceil(width/40)"}, + "domain": false, + "labels": false, + "aria": false, + "maxExtent": 0, + "minExtent": 0, + "ticks": false, + "zindex": 0 + }, + { + "scale": "y", + "orient": "left", + "gridScale": "x", + "grid": true, + "tickCount": {"signal": "ceil(height/40)"}, + "domain": false, + "labels": false, + "aria": false, + "maxExtent": 0, + "minExtent": 0, + "ticks": false, + "zindex": 0 + }, + { + "scale": "x", + "orient": "bottom", + "grid": false, + "title": "b", + "labelFlush": true, + "labelOverlap": true, + "tickCount": {"signal": "ceil(width/40)"}, + "zindex": 0 + }, + { + "scale": "y", + "orient": "left", + "grid": false, + "title": "a", + "labelAngle": 0, + "labelAlign": "right", + "labelOverlap": true, + "tickCount": {"signal": "ceil(height/40)"}, + "zindex": 0 + } + ] +} diff --git a/examples/specs/bar_qq_stack.vl.json b/examples/specs/bar_qq_stack.vl.json new file mode 100644 index 0000000000..64bc4db147 --- /dev/null +++ b/examples/specs/bar_qq_stack.vl.json @@ -0,0 +1,13 @@ +{ + "$schema": "https://vega.github.io/schema/vega-lite/v5.json", + "data": { + "values": [ + {"a": 1, "b": 28}, {"a": 1, "b": 55}, {"a": 5, "b": 43} + ] + }, + "mark": "bar", + "encoding": { + "x": {"field": "a", "type": "quantitative", "axis": {"labelAngle": 0}}, + "y": {"field": "b", "type": "quantitative"} + } +} diff --git a/examples/specs/bar_qq_stack_horizontal.vl.json b/examples/specs/bar_qq_stack_horizontal.vl.json new file mode 100644 index 0000000000..04ba5cd170 --- /dev/null +++ b/examples/specs/bar_qq_stack_horizontal.vl.json @@ -0,0 +1,14 @@ +{ + "$schema": "https://vega.github.io/schema/vega-lite/v5.json", + "description": "A simple bar chart with embedded data.", + "data": { + "values": [ + {"a": 1, "b": 28}, {"a": 1, "b": 55}, {"a": 5, "b": 43} + ] + }, + "mark": {"type": "bar", "orient": "horizontal"}, + "encoding": { + "y": {"field": "a", "type": "quantitative", "axis": {"labelAngle": 0}}, + "x": {"field": "b", "type": "quantitative"} + } +} diff --git a/src/compile/unit.ts b/src/compile/unit.ts index c13ebde5b3..c56a8d4501 100644 --- a/src/compile/unit.ts +++ b/src/compile/unit.ts @@ -113,7 +113,7 @@ export class UnitModel extends ModelWithField { }); // calculate stack properties - this.stack = stack(mark, encoding); + this.stack = stack(this.markDef, encoding); this.specifiedScales = this.initScales(mark, encoding); this.specifiedAxes = this.initAxes(encoding); diff --git a/src/stack.ts b/src/stack.ts index 89035980b2..fb4663b7c0 100644 --- a/src/stack.ts +++ b/src/stack.ts @@ -82,10 +82,13 @@ function isUnbinnedQuantitative(channelDef: PositionDef) { function potentialStackedChannel( encoding: Encoding, - x: 'x' | 'theta' + x: 'x' | 'theta', + {orient, type: mark}: MarkDef ): 'x' | 'y' | 'theta' | 'radius' | undefined { const y = x === 'x' ? 'y' : 'radius'; + const isCartesian = x === 'x'; + const xDef = encoding[x]; const yDef = encoding[y]; @@ -111,6 +114,14 @@ function potentialStackedChannel( return x; } } + + if (isCartesian && mark === 'bar') { + if (orient === 'vertical') { + return y; + } else if (orient === 'horizontal') { + return x; + } + } } else if (isUnbinnedQuantitative(xDef)) { return x; } else if (isUnbinnedQuantitative(yDef)) { @@ -138,7 +149,9 @@ function getDimensionChannel(channel: 'x' | 'y' | 'theta' | 'radius') { } export function stack(m: Mark | MarkDef, encoding: Encoding): StackProperties { - const mark = isMarkDef(m) ? m.type : m; + const markDef = isMarkDef(m) ? m : {type: m}; + const mark = markDef.type; + // Should have stackable mark if (!STACKABLE_MARKS.has(mark)) { return null; @@ -149,7 +162,8 @@ export function stack(m: Mark | MarkDef, encoding: Encoding): StackPrope // Note: The logic here is not perfectly correct. If we want to support stacked dot plots where each dot is a pie chart with label, we have to change the stack logic here to separate Cartesian stacking for polar stacking. // However, since we probably never want to do that, let's just note the limitation here. - const fieldChannel = potentialStackedChannel(encoding, 'x') || potentialStackedChannel(encoding, 'theta'); + const fieldChannel = + potentialStackedChannel(encoding, 'x', markDef) || potentialStackedChannel(encoding, 'theta', markDef); if (!fieldChannel) { return null; diff --git a/test/compile/mark/bar.test.ts b/test/compile/mark/bar.test.ts index 29fabebecd..1aa15b011e 100644 --- a/test/compile/mark/bar.test.ts +++ b/test/compile/mark/bar.test.ts @@ -903,8 +903,8 @@ describe('Mark: Bar', () => { const props = bar.encodeEntry(model); it('should produce horizontal bar using x, x2', () => { - expect(props.x).toEqual({scale: 'x', field: 'Acceleration'}); - expect(props.x2).toEqual({scale: 'x', value: 0}); + expect(props.x).toEqual({scale: 'x', field: 'Acceleration_end'}); + expect(props.x2).toEqual({scale: 'x', field: 'Acceleration_start'}); expect(props.yc).toEqual({scale: 'y', field: 'Horsepower'}); expect(props.height).toEqual({value: defaultBarConfig.continuousBandSize}); }); @@ -930,8 +930,8 @@ describe('Mark: Bar', () => { it('should produce horizontal bar using x, x2', () => { expect(props.xc).toEqual({scale: 'x', field: 'Acceleration'}); expect(props.width).toEqual({value: defaultBarConfig.continuousBandSize}); - expect(props.y).toEqual({scale: 'y', field: 'Horsepower'}); - expect(props.y2).toEqual({scale: 'y', value: 0}); + expect(props.y).toEqual({scale: 'y', field: 'Horsepower_end'}); + expect(props.y2).toEqual({scale: 'y', field: 'Horsepower_start'}); }); });