diff --git a/build/vega-lite-schema.json b/build/vega-lite-schema.json index e1c903ea02..7d986c3772 100644 --- a/build/vega-lite-schema.json +++ b/build/vega-lite-schema.json @@ -8580,7 +8580,14 @@ "description": "__Required.__ A string defining the name of the field from which to pull a data value or an object defining iterated values from the [`repeat`](https://vega.github.io/vega-lite/docs/repeat.html) operator.\n\n__See also:__ [`field`](https://vega.github.io/vega-lite/docs/field.html) documentation.\n\n__Notes:__ 1) Dots (`.`) and brackets (`[` and `]`) can be used to access nested objects (e.g., `\"field\": \"foo.bar\"` and `\"field\": \"foo['bar']\"`). If field names contain dots or brackets but are not nested, you can use `\\\\` to escape dots and brackets (e.g., `\"a\\\\.b\"` and `\"a\\\\[0\\\\]\"`). See more details about escaping in the [field documentation](https://vega.github.io/vega-lite/docs/field.html). 2) `field` is not required if `aggregate` is `count`." }, "header": { - "$ref": "#/definitions/Header", + "anyOf": [ + { + "$ref": "#/definitions/Header" + }, + { + "type": "null" + } + ], "description": "An object defining properties of a facet's header." }, "sort": { @@ -8672,7 +8679,14 @@ "description": "__Required.__ A string defining the name of the field from which to pull a data value or an object defining iterated values from the [`repeat`](https://vega.github.io/vega-lite/docs/repeat.html) operator.\n\n__See also:__ [`field`](https://vega.github.io/vega-lite/docs/field.html) documentation.\n\n__Notes:__ 1) Dots (`.`) and brackets (`[` and `]`) can be used to access nested objects (e.g., `\"field\": \"foo.bar\"` and `\"field\": \"foo['bar']\"`). If field names contain dots or brackets but are not nested, you can use `\\\\` to escape dots and brackets (e.g., `\"a\\\\.b\"` and `\"a\\\\[0\\\\]\"`). See more details about escaping in the [field documentation](https://vega.github.io/vega-lite/docs/field.html). 2) `field` is not required if `aggregate` is `count`." }, "header": { - "$ref": "#/definitions/Header", + "anyOf": [ + { + "$ref": "#/definitions/Header" + }, + { + "type": "null" + } + ], "description": "An object defining properties of a facet's header." }, "sort": { @@ -20688,7 +20702,14 @@ "description": "__Required.__ A string defining the name of the field from which to pull a data value or an object defining iterated values from the [`repeat`](https://vega.github.io/vega-lite/docs/repeat.html) operator.\n\n__See also:__ [`field`](https://vega.github.io/vega-lite/docs/field.html) documentation.\n\n__Notes:__ 1) Dots (`.`) and brackets (`[` and `]`) can be used to access nested objects (e.g., `\"field\": \"foo.bar\"` and `\"field\": \"foo['bar']\"`). If field names contain dots or brackets but are not nested, you can use `\\\\` to escape dots and brackets (e.g., `\"a\\\\.b\"` and `\"a\\\\[0\\\\]\"`). See more details about escaping in the [field documentation](https://vega.github.io/vega-lite/docs/field.html). 2) `field` is not required if `aggregate` is `count`." }, "header": { - "$ref": "#/definitions/Header", + "anyOf": [ + { + "$ref": "#/definitions/Header" + }, + { + "type": "null" + } + ], "description": "An object defining properties of a facet's header." }, "sort": { diff --git a/examples/compiled/trellis_bar_no_header.png b/examples/compiled/trellis_bar_no_header.png new file mode 100644 index 0000000000..6ca4a64523 Binary files /dev/null and b/examples/compiled/trellis_bar_no_header.png differ diff --git a/examples/compiled/trellis_bar_no_header.svg b/examples/compiled/trellis_bar_no_header.svg new file mode 100644 index 0000000000..43dcbf90a4 --- /dev/null +++ b/examples/compiled/trellis_bar_no_header.svg @@ -0,0 +1 @@ +02,000,0004,000,0006,000,0008,000,00010,000,00012,000,000population02,000,0004,000,0006,000,0008,000,00010,000,00012,000,000population051015202530354045505560657075808590ageFemaleMalegender \ No newline at end of file diff --git a/examples/compiled/trellis_bar_no_header.vg.json b/examples/compiled/trellis_bar_no_header.vg.json new file mode 100644 index 0000000000..1996c4b55d --- /dev/null +++ b/examples/compiled/trellis_bar_no_header.vg.json @@ -0,0 +1,173 @@ +{ + "$schema": "https://vega.github.io/schema/vega/v5.json", + "description": "A trellis bar chart showing the US population distribution of age groups and gender in 2000.", + "background": "white", + "padding": 5, + "data": [ + { + "name": "source_0", + "url": "data/population.json", + "format": {"type": "json"}, + "transform": [ + {"type": "filter", "expr": "datum.year == 2000"}, + { + "type": "formula", + "expr": "datum.sex == 2 ? 'Female' : 'Male'", + "as": "gender" + }, + { + "type": "aggregate", + "groupby": ["age", "gender"], + "ops": ["sum"], + "fields": ["people"], + "as": ["sum_people"] + }, + { + "type": "stack", + "groupby": ["age", "gender"], + "field": "sum_people", + "sort": {"field": ["gender"], "order": ["descending"]}, + "as": ["sum_people_start", "sum_people_end"], + "offset": "zero" + }, + { + "type": "filter", + "expr": "isValid(datum[\"sum_people\"]) && isFinite(+datum[\"sum_people\"])" + } + ] + }, + { + "name": "row_domain", + "source": "source_0", + "transform": [{"type": "aggregate", "groupby": ["gender"]}] + } + ], + "signals": [ + {"name": "x_step", "value": 17}, + { + "name": "child_width", + "update": "bandspace(domain('x').length, 0.1, 0.05) * x_step" + }, + {"name": "child_height", "value": 200} + ], + "layout": {"padding": 20, "columns": 1, "bounds": "full", "align": "all"}, + "marks": [ + { + "name": "row_header", + "type": "group", + "role": "row-header", + "from": {"data": "row_domain"}, + "sort": {"field": "datum[\"gender\"]", "order": "ascending"}, + "encode": {"update": {"height": {"signal": "child_height"}}}, + "axes": [ + { + "scale": "y", + "orient": "left", + "grid": false, + "title": "population", + "labelOverlap": true, + "tickCount": {"signal": "ceil(child_height/40)"}, + "zindex": 0 + } + ] + }, + { + "name": "column_footer", + "type": "group", + "role": "column-footer", + "encode": {"update": {"width": {"signal": "child_width"}}}, + "axes": [ + { + "scale": "x", + "orient": "bottom", + "grid": false, + "title": "age", + "labelAlign": "right", + "labelAngle": 270, + "labelBaseline": "middle", + "zindex": 0 + } + ] + }, + { + "name": "cell", + "type": "group", + "style": "cell", + "from": { + "facet": {"name": "facet", "data": "source_0", "groupby": ["gender"]} + }, + "sort": {"field": ["datum[\"gender\"]"], "order": ["ascending"]}, + "encode": { + "update": { + "width": {"signal": "child_width"}, + "height": {"signal": "child_height"} + } + }, + "marks": [ + { + "name": "child_marks", + "type": "rect", + "style": ["bar"], + "from": {"data": "facet"}, + "encode": { + "update": { + "fill": {"scale": "color", "field": "gender"}, + "ariaRoleDescription": {"value": "bar"}, + "description": { + "signal": "\"population: \" + (format(datum[\"sum_people\"], \"\")) + \"; age: \" + (isValid(datum[\"age\"]) ? datum[\"age\"] : \"\"+datum[\"age\"]) + \"; gender: \" + (isValid(datum[\"gender\"]) ? datum[\"gender\"] : \"\"+datum[\"gender\"])" + }, + "x": {"scale": "x", "field": "age"}, + "width": {"scale": "x", "band": 1}, + "y": {"scale": "y", "field": "sum_people_end"}, + "y2": {"scale": "y", "field": "sum_people_start"} + } + } + } + ], + "axes": [ + { + "scale": "y", + "orient": "left", + "gridScale": "x", + "grid": true, + "tickCount": {"signal": "ceil(child_height/40)"}, + "domain": false, + "labels": false, + "aria": false, + "maxExtent": 0, + "minExtent": 0, + "ticks": false, + "zindex": 0 + } + ] + } + ], + "scales": [ + { + "name": "x", + "type": "band", + "domain": {"data": "source_0", "field": "age", "sort": true}, + "range": {"step": {"signal": "x_step"}}, + "paddingInner": 0.1, + "paddingOuter": 0.05 + }, + { + "name": "y", + "type": "linear", + "domain": { + "data": "source_0", + "fields": ["sum_people_start", "sum_people_end"] + }, + "range": [{"signal": "child_height"}, 0], + "nice": true, + "zero": true + }, + { + "name": "color", + "type": "ordinal", + "domain": {"data": "source_0", "field": "gender", "sort": true}, + "range": ["#675193", "#ca8861"] + } + ], + "legends": [{"fill": "color", "symbolType": "square", "title": "gender"}] +} diff --git a/examples/specs/normalized/trellis_bar_no_header_normalized.vl.json b/examples/specs/normalized/trellis_bar_no_header_normalized.vl.json new file mode 100644 index 0000000000..e0454e3cb3 --- /dev/null +++ b/examples/specs/normalized/trellis_bar_no_header_normalized.vl.json @@ -0,0 +1,19 @@ +{ + "$schema": "https://vega.github.io/schema/vega-lite/v4.json", + "description": "A trellis bar chart showing the US population distribution of age groups and gender in 2000.", + "data": {"url": "data/population.json"}, + "transform": [ + {"filter": "datum.year == 2000"}, + {"calculate": "datum.sex == 2 ? 'Female' : 'Male'", "as": "gender"} + ], + "facet": {"row": {"field": "gender", "header": null}}, + "spec": { + "width": {"step": 17}, + "mark": "bar", + "encoding": { + "y": {"aggregate": "sum", "field": "people", "title": "population"}, + "x": {"field": "age"}, + "color": {"field": "gender", "scale": {"range": ["#675193", "#ca8861"]}} + } + } +} \ No newline at end of file diff --git a/examples/specs/trellis_bar_no_header.vl.json b/examples/specs/trellis_bar_no_header.vl.json new file mode 100644 index 0000000000..4eca493c57 --- /dev/null +++ b/examples/specs/trellis_bar_no_header.vl.json @@ -0,0 +1,23 @@ +{ + "$schema": "https://vega.github.io/schema/vega-lite/v4.json", + "description": "A trellis bar chart showing the US population distribution of age groups and gender in 2000.", + "data": { "url": "data/population.json"}, + "transform": [ + {"filter": "datum.year == 2000"}, + {"calculate": "datum.sex == 2 ? 'Female' : 'Male'", "as": "gender"} + ], + "width": {"step": 17}, + "mark": "bar", + "encoding": { + "row": {"field": "gender", "header": null}, + "y": { + "aggregate": "sum", "field": "people", + "title": "population" + }, + "x": {"field": "age"}, + "color": { + "field": "gender", + "scale": {"range": ["#675193", "#ca8861"]} + } + } +} diff --git a/src/channeldef.ts b/src/channeldef.ts index d90e40f8cc..8e4b45ffae 100644 --- a/src/channeldef.ts +++ b/src/channeldef.ts @@ -1154,16 +1154,18 @@ export function initFieldDef( if (isFacetFieldDef(fieldDef)) { const {header} = fieldDef; - const {orient, ...rest} = header; - if (orient) { - return { - ...fieldDef, - header: { - ...rest, - labelOrient: header.labelOrient || orient, - titleOrient: header.titleOrient || orient - } - }; + if (header) { + const {orient, ...rest} = header; + if (orient) { + return { + ...fieldDef, + header: { + ...rest, + labelOrient: header.labelOrient || orient, + titleOrient: header.titleOrient || orient + } + }; + } } } diff --git a/src/compile/facet.ts b/src/compile/facet.ts index a7c9f7d800..f8ba3361a7 100644 --- a/src/compile/facet.ts +++ b/src/compile/facet.ts @@ -79,12 +79,13 @@ export class FacetModel extends ModelWithField { } private initFacetFieldDef(fieldDef: FacetFieldDef, channel: FacetChannel) { - const {header, ...rest} = fieldDef; // Cast because we call initFieldDef, which assumes general FieldDef. // However, FacetFieldDef is a bit more constrained than the general FieldDef - const facetFieldDef = initFieldDef(rest, channel) as FacetFieldDef; - if (header) { - facetFieldDef.header = replaceExprRef(header); + const facetFieldDef = initFieldDef(fieldDef, channel) as FacetFieldDef; + if (facetFieldDef.header) { + facetFieldDef.header = replaceExprRef(facetFieldDef.header); + } else if (facetFieldDef.header === null) { + facetFieldDef.header = null; } return facetFieldDef; } diff --git a/src/compile/header/parse.ts b/src/compile/header/parse.ts index b01302e72c..b54ebb83f5 100644 --- a/src/compile/header/parse.ts +++ b/src/compile/header/parse.ts @@ -46,14 +46,14 @@ function parseFacetHeader(model: FacetModel, channel: FacetChannel) { child.component.layoutHeaders[channel].title = null; } - const labelOrient = getHeaderProperty('labelOrient', fieldDef, config, channel); + const labelOrient = getHeaderProperty('labelOrient', fieldDef.header, config, channel); - const header = fieldDef.header ?? {}; - const labels = getFirstDefined(header.labels, config.header.labels, true); + const labels = + fieldDef.header !== null ? getFirstDefined(fieldDef.header?.labels, config.header.labels, true) : false; const headerType = contains(['bottom', 'right'], labelOrient) ? 'footer' : 'header'; component.layoutHeaders[channel] = { - title, + title: fieldDef.header !== null ? title : null, facetFieldDef: fieldDef, [headerType]: channel === 'facet' ? [] : [makeHeaderComponent(model, channel, labels)] }; diff --git a/src/spec/facet.ts b/src/spec/facet.ts index f9af78db43..807bf0b541 100644 --- a/src/spec/facet.ts +++ b/src/spec/facet.ts @@ -14,7 +14,7 @@ export interface FacetFieldDef; + header?: Header | null; // Note: `"sort"` for facet field def is different from encoding field def as it does not support `SortByEncoding` diff --git a/test/compile/facet.test.ts b/test/compile/facet.test.ts index d53f234db3..334559fb9c 100644 --- a/test/compile/facet.test.ts +++ b/test/compile/facet.test.ts @@ -43,6 +43,36 @@ describe('FacetModel', () => { expect(localLogger.warns[0]).toEqual(log.message.facetChannelShouldBeDiscrete(ROW)); }) ); + + it('converts orient to titleOrient and labelOrient', () => { + const model = parseFacetModel({ + facet: { + row: {field: 'a', type: 'nominal', header: {orient: 'right'}} + }, + spec: { + mark: 'point', + encoding: {} + } + }); + expect(model.facet).toEqual({ + row: {field: 'a', type: 'nominal', header: {titleOrient: 'right', labelOrient: 'right'}} + }); + }); + + it('keeps header: null', () => { + const model = parseFacetModel({ + facet: { + row: {field: 'a', type: 'nominal', header: null} + }, + spec: { + mark: 'point', + encoding: {} + } + }); + expect(model.facet).toEqual({ + row: {field: 'a', type: 'nominal', header: null} + }); + }); }); describe('parseAxisAndHeader', () => {