diff --git a/examples/compiled/arc_facet.png b/examples/compiled/arc_facet.png new file mode 100644 index 0000000000..8bdefdc831 Binary files /dev/null and b/examples/compiled/arc_facet.png differ diff --git a/examples/compiled/arc_facet.svg b/examples/compiled/arc_facet.svg new file mode 100644 index 0000000000..2627fb5bde --- /dev/null +++ b/examples/compiled/arc_facet.svg @@ -0,0 +1 @@ +year19311932CrookstonDuluthGrand RapidsMorrisUniversity FarmWasecasite \ No newline at end of file diff --git a/examples/compiled/arc_facet.vg.json b/examples/compiled/arc_facet.vg.json new file mode 100644 index 0000000000..358c80290f --- /dev/null +++ b/examples/compiled/arc_facet.vg.json @@ -0,0 +1,132 @@ +{ + "$schema": "https://vega.github.io/schema/vega/v5.json", + "background": "white", + "padding": 5, + "data": [ + { + "name": "source_0", + "url": "data/barley.json", + "format": {"type": "json"}, + "transform": [ + { + "type": "aggregate", + "groupby": ["site", "year"], + "ops": ["sum"], + "fields": ["yield"], + "as": ["sum_yield"] + }, + { + "type": "stack", + "groupby": ["year"], + "field": "sum_yield", + "sort": {"field": ["site"], "order": ["ascending"]}, + "as": ["sum_yield_start", "sum_yield_end"], + "offset": "zero" + }, + { + "type": "filter", + "expr": "isValid(datum[\"sum_yield\"]) && isFinite(+datum[\"sum_yield\"])" + } + ] + }, + { + "name": "column_domain", + "source": "source_0", + "transform": [{"type": "aggregate", "groupby": ["year"]}] + } + ], + "signals": [ + {"name": "child_width", "value": 200}, + {"name": "child_height", "value": 200} + ], + "layout": { + "padding": 20, + "offset": {"columnTitle": 10}, + "columns": {"signal": "length(data('column_domain'))"}, + "bounds": "full", + "align": "all" + }, + "marks": [ + { + "name": "column-title", + "type": "group", + "role": "column-title", + "title": {"text": "year", "style": "guide-title", "offset": 10} + }, + { + "name": "column_header", + "type": "group", + "role": "column-header", + "from": {"data": "column_domain"}, + "sort": {"field": "datum[\"year\"]", "order": "ascending"}, + "title": { + "text": { + "signal": "isValid(parent[\"year\"]) ? parent[\"year\"] : \"\"+parent[\"year\"]" + }, + "style": "guide-label", + "frame": "group", + "offset": 10 + }, + "encode": {"update": {"width": {"signal": "child_width"}}} + }, + { + "name": "cell", + "type": "group", + "style": "cell", + "from": { + "facet": {"name": "facet", "data": "source_0", "groupby": ["year"]} + }, + "sort": {"field": ["datum[\"year\"]"], "order": ["ascending"]}, + "encode": { + "update": { + "width": {"signal": "child_width"}, + "height": {"signal": "child_height"}, + "stroke": {"value": null} + } + }, + "marks": [ + { + "name": "child_marks", + "type": "arc", + "style": ["arc"], + "from": {"data": "facet"}, + "encode": { + "update": { + "fill": {"scale": "color", "field": "site"}, + "description": { + "signal": "\"Sum of yield: \" + (format(datum[\"sum_yield\"], \"\")) + \"; site: \" + (isValid(datum[\"site\"]) ? datum[\"site\"] : \"\"+datum[\"site\"])" + }, + "x": {"signal": "child_width", "mult": 0.5}, + "y": {"signal": "child_height", "mult": 0.5}, + "outerRadius": {"signal": "min(child_width,child_height)/2"}, + "innerRadius": {"value": 0}, + "startAngle": {"scale": "child_theta", "field": "sum_yield_end"}, + "endAngle": {"scale": "child_theta", "field": "sum_yield_start"} + } + } + } + ], + "scales": [ + { + "name": "child_theta", + "type": "linear", + "domain": { + "data": "facet", + "fields": ["sum_yield_start", "sum_yield_end"] + }, + "range": [0, 6.283185307179586], + "zero": true + } + ] + } + ], + "scales": [ + { + "name": "color", + "type": "ordinal", + "domain": {"data": "source_0", "field": "site", "sort": true}, + "range": "category" + } + ], + "legends": [{"fill": "color", "symbolType": "circle", "title": "site"}] +} diff --git a/examples/compiled/arc_params.vg.json b/examples/compiled/arc_params.vg.json index 4bd4143459..ae235dc7d8 100644 --- a/examples/compiled/arc_params.vg.json +++ b/examples/compiled/arc_params.vg.json @@ -142,8 +142,8 @@ "y": {"signal": "height", "mult": 0.5}, "outerRadius": {"signal": "radius"}, "innerRadius": {"signal": "radius2"}, - "startAngle": {"scale": "theta", "field": "value_end"}, - "endAngle": {"scale": "theta", "field": "value_start"} + "startAngle": {"scale": "concat_1_theta", "field": "value_end"}, + "endAngle": {"scale": "concat_1_theta", "field": "value_start"} } } } @@ -151,18 +151,18 @@ } ], "scales": [ - { - "name": "theta", - "type": "linear", - "domain": {"data": "data_0", "fields": ["value_start", "value_end"]}, - "range": [0, 6.283185307179586], - "zero": true - }, { "name": "color", "type": "ordinal", "domain": {"data": "data_0", "field": "category", "sort": true}, "range": "category" + }, + { + "name": "concat_1_theta", + "type": "linear", + "domain": {"data": "data_0", "fields": ["value_start", "value_end"]}, + "range": [0, 6.283185307179586], + "zero": true } ], "legends": [{"fill": "color", "symbolType": "circle", "title": "category"}], diff --git a/examples/specs/arc_facet.vl.json b/examples/specs/arc_facet.vl.json new file mode 100644 index 0000000000..d2f71eb7a2 --- /dev/null +++ b/examples/specs/arc_facet.vl.json @@ -0,0 +1,13 @@ +{ + "$schema": "https://vega.github.io/schema/vega-lite/v5.json", + "data": {"url": "data/barley.json"}, + "mark": "arc", + "encoding": { + "column": {"field": "year"}, + "theta": {"field": "yield", "type": "quantitative", "aggregate": "sum"}, + "color": {"field": "site", "type": "nominal"} + }, + "view": { + "stroke": null + } +} diff --git a/examples/specs/normalized/arc_facet_normalized.vl.json b/examples/specs/normalized/arc_facet_normalized.vl.json new file mode 100644 index 0000000000..ae6f196738 --- /dev/null +++ b/examples/specs/normalized/arc_facet_normalized.vl.json @@ -0,0 +1,13 @@ +{ + "$schema": "https://vega.github.io/schema/vega-lite/v5.json", + "data": {"url": "data/barley.json"}, + "facet": {"column": {"field": "year"}}, + "spec": { + "view": {"stroke": null}, + "mark": "arc", + "encoding": { + "theta": {"field": "yield", "type": "quantitative", "aggregate": "sum"}, + "color": {"field": "site", "type": "nominal"} + } + } +} \ No newline at end of file diff --git a/site/docs/mark/arc.md b/site/docs/mark/arc.md index 582cf305bf..54017f28e3 100644 --- a/site/docs/mark/arc.md +++ b/site/docs/mark/arc.md @@ -83,3 +83,9 @@ You can also add a text layer to add labels to a pie chart. The `arc` property of the top-level [`config`](config.html) object sets the default properties for all arc marks. If [mark property encoding channels](encoding.html#mark-prop) are specified for marks, these config values will be overridden. The arc config can contain any [arc mark properties](#properties) (except `type`, `style`, and `clip`). + +## Faceted Pie Charts + +By default, the theta channel in faceted charts [resolves](resolve.html) to independent scales so that the ratios are comparable. + + diff --git a/src/compile/resolve.ts b/src/compile/resolve.ts index 23de0f1c39..a45e8460f6 100644 --- a/src/compile/resolve.ts +++ b/src/compile/resolve.ts @@ -4,10 +4,12 @@ import {Resolve, ResolveMode} from '../resolve'; import {isConcatModel, isFacetModel, isLayerModel, Model} from './model'; export function defaultScaleResolve(channel: ScaleChannel, model: Model): ResolveMode { - if (isLayerModel(model) || isFacetModel(model)) { + if (isFacetModel(model)) { + return channel === 'theta' ? 'independent' : 'shared'; + } else if (isLayerModel(model)) { return 'shared'; } else if (isConcatModel(model)) { - return isXorY(channel) ? 'independent' : 'shared'; + return isXorY(channel) || channel === 'theta' || channel === 'radius' ? 'independent' : 'shared'; } /* istanbul ignore next: should never reach here. */ throw new Error('invalid model type for resolve'); diff --git a/test/compile/resolve.test.ts b/test/compile/resolve.test.ts index f91278273c..24e2d6211a 100644 --- a/test/compile/resolve.test.ts +++ b/test/compile/resolve.test.ts @@ -9,6 +9,10 @@ describe('compile/resolve', () => { layer: [] }); expect(defaultScaleResolve('x', model)).toBe('shared'); + expect(defaultScaleResolve('y', model)).toBe('shared'); + expect(defaultScaleResolve('color', model)).toBe('shared'); + expect(defaultScaleResolve('theta', model)).toBe('shared'); + expect(defaultScaleResolve('radius', model)).toBe('shared'); }); it('shares scales for facet model by default.', () => { @@ -19,23 +23,39 @@ describe('compile/resolve', () => { spec: {mark: 'point', encoding: {}} }); expect(defaultScaleResolve('x', model)).toBe('shared'); + expect(defaultScaleResolve('y', model)).toBe('shared'); + expect(defaultScaleResolve('color', model)).toBe('shared'); + expect(defaultScaleResolve('radius', model)).toBe('shared'); + }); + + it('separates theta scales for facet model by default.', () => { + const model = parseFacetModel({ + facet: { + row: {field: 'a', type: 'nominal'} + }, + spec: {mark: 'arc', encoding: {}} + }); + expect(defaultScaleResolve('theta', model)).toBe('independent'); }); - it('separates xy scales for concat model by default.', () => { + it('separates x, y, theta, and radius scales for concat model by default.', () => { const model = parseConcatModel({ hconcat: [] }); expect(defaultScaleResolve('x', model)).toBe('independent'); + expect(defaultScaleResolve('y', model)).toBe('independent'); + expect(defaultScaleResolve('theta', model)).toBe('independent'); + expect(defaultScaleResolve('radius', model)).toBe('independent'); }); - it('shares non-xy scales for concat model by default.', () => { + it('shares non-positional scales for concat model by default.', () => { const model = parseConcatModel({ hconcat: [] }); expect(defaultScaleResolve('color', model)).toBe('shared'); }); - it('separates xy scales for repeat model by default.', () => { + it('separates x, y, theta, and radius scales for repeat model by default.', () => { const model = parseModel({ repeat: { row: ['a', 'b'] @@ -49,9 +69,12 @@ describe('compile/resolve', () => { } }); expect(defaultScaleResolve('x', model)).toBe('independent'); + expect(defaultScaleResolve('y', model)).toBe('independent'); + expect(defaultScaleResolve('theta', model)).toBe('independent'); + expect(defaultScaleResolve('radius', model)).toBe('independent'); }); - it('shares non-xy scales for repeat model by default.', () => { + it('shares non-positional scales for repeat model by default.', () => { const model = parseModel({ repeat: { row: ['a', 'b']