diff --git a/build/vega-lite-schema.json b/build/vega-lite-schema.json index f8bf01f214..d71b6746d0 100644 --- a/build/vega-lite-schema.json +++ b/build/vega-lite-schema.json @@ -621,7 +621,7 @@ }, "encodings": { "items": { - "type": "string" + "$ref": "#/definitions/SingleDefChannel" }, "type": "array" }, @@ -1032,16 +1032,140 @@ "$ref": "#/definitions/CompositeUnitSpecAlias", "description": "Unit spec that can have a composite mark." }, - "Condition<(string|number)>": { + "ConditionLegendFieldDef": { + "additionalProperties": false, + "properties": { + "aggregate": { + "anyOf": [ + { + "$ref": "#/definitions/AggregateOp" + }, + { + "$ref": "#/definitions/CompositeAggregate" + } + ], + "description": "Aggregation function for the field\n(e.g., `mean`, `sum`, `median`, `min`, `max`, `count`).\n\n__Default value:__ `undefined` (None)" + }, + "bin": { + "anyOf": [ + { + "type": "boolean" + }, + { + "$ref": "#/definitions/Bin" + } + ], + "description": "Flag for binning a `quantitative` field, or a bin property object\nfor binning parameters." + }, + "field": { + "$ref": "#/definitions/Field", + "description": "__Required.__ Name of the field from which to pull a data value.\n\n__Note:__ `field` is not required if `aggregate` is `count`." + }, + "legend": { + "anyOf": [ + { + "$ref": "#/definitions/Legend" + }, + { + "type": "null" + } + ] + }, + "scale": { + "$ref": "#/definitions/Scale" + }, + "selection": { + "$ref": "#/definitions/LogicalOperand" + }, + "sort": { + "anyOf": [ + { + "$ref": "#/definitions/SortField" + }, + { + "$ref": "#/definitions/SortOrder" + } + ], + "description": "Sort order for a particular field.\nFor quantitative or temporal fields, this can be either `\"ascending\"` or `\"descending\"`\nFor quantitative or temporal fields, this can be `\"ascending\"`, `\"descending\"`, `\"none\"`, or a [sort field definition object](sort.html#sort-field) for sorting by an aggregate calculation of a specified sort field.\n\n__Default value:__ `\"ascending\"`" + }, + "timeUnit": { + "$ref": "#/definitions/TimeUnit", + "description": "Time unit for a `temporal` field (e.g., `year`, `yearmonth`, `month`, `hour`).\n\n__Default value:__ `undefined` (None)" + }, + "type": { + "$ref": "#/definitions/Type", + "description": "The encoded field's type of measurement. This can be either a full type\nname (`\"quantitative\"`, `\"temporal\"`, `\"ordinal\"`, and `\"nominal\"`)\nor an initial character of the type name (`\"Q\"`, `\"T\"`, `\"O\"`, `\"N\"`).\nThis property is case-insensitive." + } + }, + "required": [ + "selection", + "type" + ], + "type": "object" + }, + "ConditionTextFieldDef": { + "additionalProperties": false, + "properties": { + "aggregate": { + "anyOf": [ + { + "$ref": "#/definitions/AggregateOp" + }, + { + "$ref": "#/definitions/CompositeAggregate" + } + ], + "description": "Aggregation function for the field\n(e.g., `mean`, `sum`, `median`, `min`, `max`, `count`).\n\n__Default value:__ `undefined` (None)" + }, + "bin": { + "anyOf": [ + { + "type": "boolean" + }, + { + "$ref": "#/definitions/Bin" + } + ], + "description": "Flag for binning a `quantitative` field, or a bin property object\nfor binning parameters." + }, + "field": { + "$ref": "#/definitions/Field", + "description": "__Required.__ Name of the field from which to pull a data value.\n\n__Note:__ `field` is not required if `aggregate` is `count`." + }, + "format": { + "description": "The formatting pattern for text value. If not defined, this will be determined automatically.", + "type": "string" + }, + "selection": { + "$ref": "#/definitions/LogicalOperand" + }, + "timeUnit": { + "$ref": "#/definitions/TimeUnit", + "description": "Time unit for a `temporal` field (e.g., `year`, `yearmonth`, `month`, `hour`).\n\n__Default value:__ `undefined` (None)" + }, + "type": { + "$ref": "#/definitions/Type", + "description": "The encoded field's type of measurement. This can be either a full type\nname (`\"quantitative\"`, `\"temporal\"`, `\"ordinal\"`, and `\"nominal\"`)\nor an initial character of the type name (`\"Q\"`, `\"T\"`, `\"O\"`, `\"N\"`).\nThis property is case-insensitive." + } + }, + "required": [ + "selection", + "type" + ], + "type": "object" + }, + "ConditionTextValueDef": { "additionalProperties": false, "properties": { "selection": { "$ref": "#/definitions/LogicalOperand" }, "value": { + "description": "A constant value in visual domain.", "type": [ "string", - "number" + "number", + "boolean" ] } }, @@ -1051,71 +1175,349 @@ ], "type": "object" }, - "Condition<(string|number|boolean)>": { + "ConditionNumberValueDef": { + "additionalProperties": false, + "properties": { + "selection": { + "$ref": "#/definitions/LogicalOperand" + }, + "value": { + "description": "A constant value in visual domain.", + "type": "number" + } + }, + "required": [ + "selection", + "value" + ], + "type": "object" + }, + "ConditionStringValueDef": { "additionalProperties": false, "properties": { "selection": { "$ref": "#/definitions/LogicalOperand" }, - "value": { - "type": [ - "string", - "number", - "boolean" - ] - } - }, - "required": [ - "selection", - "value" - ], - "type": "object" - }, - "Condition": { - "additionalProperties": false, - "properties": { - "selection": { - "$ref": "#/definitions/LogicalOperand" + "value": { + "description": "A constant value in visual domain.", + "type": "string" + } + }, + "required": [ + "selection", + "value" + ], + "type": "object" + }, + "ConditionOnlyNumberLegendDef": { + "additionalProperties": false, + "properties": { + "condition": { + "anyOf": [ + { + "$ref": "#/definitions/ConditionLegendFieldDef" + }, + { + "$ref": "#/definitions/ConditionNumberValueDef" + } + ] + } + }, + "required": [ + "condition" + ], + "type": "object" + }, + "ConditionOnlyStringLegendDef": { + "additionalProperties": false, + "properties": { + "condition": { + "anyOf": [ + { + "$ref": "#/definitions/ConditionLegendFieldDef" + }, + { + "$ref": "#/definitions/ConditionStringValueDef" + } + ] + } + }, + "required": [ + "condition" + ], + "type": "object" + }, + "ConditionOnlyTextDef": { + "additionalProperties": false, + "properties": { + "condition": { + "anyOf": [ + { + "$ref": "#/definitions/ConditionTextFieldDef" + }, + { + "$ref": "#/definitions/ConditionTextValueDef" + } + ] + } + }, + "required": [ + "condition" + ], + "type": "object" + }, + "ConditionalNumberLegendDef": { + "anyOf": [ + { + "$ref": "#/definitions/ConditionalNumberLegendFieldDef" + }, + { + "$ref": "#/definitions/ConditionalNumberLegendValueDef" + }, + { + "$ref": "#/definitions/ConditionOnlyNumberLegendDef" + } + ], + "description": "Generic type for conditional channelDef.\nF defines the underlying FieldDef type while V defines the underlying ValueDef type." + }, + "ConditionalStringLegendDef": { + "anyOf": [ + { + "$ref": "#/definitions/ConditionalStringLegendFieldDef" + }, + { + "$ref": "#/definitions/ConditionalStringLegendValueDef" + }, + { + "$ref": "#/definitions/ConditionOnlyStringLegendDef" + } + ], + "description": "Generic type for conditional channelDef.\nF defines the underlying FieldDef type while V defines the underlying ValueDef type." + }, + "ConditionalTextDef": { + "anyOf": [ + { + "$ref": "#/definitions/ConditionalTextFieldDef" + }, + { + "$ref": "#/definitions/ConditionalTextValueDef" + }, + { + "$ref": "#/definitions/ConditionOnlyTextDef" + } + ], + "description": "Generic type for conditional channelDef.\nF defines the underlying FieldDef type while V defines the underlying ValueDef type." + }, + "ConditionalNumberLegendFieldDef": { + "additionalProperties": false, + "description": "A FieldDef with ConditionValueDef\n{\n condition: {value: ...},\n field: ...,\n ...\n}", + "properties": { + "aggregate": { + "anyOf": [ + { + "$ref": "#/definitions/AggregateOp" + }, + { + "$ref": "#/definitions/CompositeAggregate" + } + ], + "description": "Aggregation function for the field\n(e.g., `mean`, `sum`, `median`, `min`, `max`, `count`).\n\n__Default value:__ `undefined` (None)" + }, + "bin": { + "anyOf": [ + { + "type": "boolean" + }, + { + "$ref": "#/definitions/Bin" + } + ], + "description": "Flag for binning a `quantitative` field, or a bin property object\nfor binning parameters." + }, + "condition": { + "$ref": "#/definitions/ConditionNumberValueDef" + }, + "field": { + "$ref": "#/definitions/Field", + "description": "__Required.__ Name of the field from which to pull a data value.\n\n__Note:__ `field` is not required if `aggregate` is `count`." + }, + "legend": { + "anyOf": [ + { + "$ref": "#/definitions/Legend" + }, + { + "type": "null" + } + ] + }, + "scale": { + "$ref": "#/definitions/Scale" + }, + "sort": { + "anyOf": [ + { + "$ref": "#/definitions/SortField" + }, + { + "$ref": "#/definitions/SortOrder" + } + ], + "description": "Sort order for a particular field.\nFor quantitative or temporal fields, this can be either `\"ascending\"` or `\"descending\"`\nFor quantitative or temporal fields, this can be `\"ascending\"`, `\"descending\"`, `\"none\"`, or a [sort field definition object](sort.html#sort-field) for sorting by an aggregate calculation of a specified sort field.\n\n__Default value:__ `\"ascending\"`" + }, + "timeUnit": { + "$ref": "#/definitions/TimeUnit", + "description": "Time unit for a `temporal` field (e.g., `year`, `yearmonth`, `month`, `hour`).\n\n__Default value:__ `undefined` (None)" + }, + "type": { + "$ref": "#/definitions/Type", + "description": "The encoded field's type of measurement. This can be either a full type\nname (`\"quantitative\"`, `\"temporal\"`, `\"ordinal\"`, and `\"nominal\"`)\nor an initial character of the type name (`\"Q\"`, `\"T\"`, `\"O\"`, `\"N\"`).\nThis property is case-insensitive." + } + }, + "required": [ + "type" + ], + "type": "object" + }, + "ConditionalStringLegendFieldDef": { + "additionalProperties": false, + "description": "A FieldDef with ConditionValueDef\n{\n condition: {value: ...},\n field: ...,\n ...\n}", + "properties": { + "aggregate": { + "anyOf": [ + { + "$ref": "#/definitions/AggregateOp" + }, + { + "$ref": "#/definitions/CompositeAggregate" + } + ], + "description": "Aggregation function for the field\n(e.g., `mean`, `sum`, `median`, `min`, `max`, `count`).\n\n__Default value:__ `undefined` (None)" + }, + "bin": { + "anyOf": [ + { + "type": "boolean" + }, + { + "$ref": "#/definitions/Bin" + } + ], + "description": "Flag for binning a `quantitative` field, or a bin property object\nfor binning parameters." + }, + "condition": { + "$ref": "#/definitions/ConditionStringValueDef" + }, + "field": { + "$ref": "#/definitions/Field", + "description": "__Required.__ Name of the field from which to pull a data value.\n\n__Note:__ `field` is not required if `aggregate` is `count`." + }, + "legend": { + "anyOf": [ + { + "$ref": "#/definitions/Legend" + }, + { + "type": "null" + } + ] + }, + "scale": { + "$ref": "#/definitions/Scale" + }, + "sort": { + "anyOf": [ + { + "$ref": "#/definitions/SortField" + }, + { + "$ref": "#/definitions/SortOrder" + } + ], + "description": "Sort order for a particular field.\nFor quantitative or temporal fields, this can be either `\"ascending\"` or `\"descending\"`\nFor quantitative or temporal fields, this can be `\"ascending\"`, `\"descending\"`, `\"none\"`, or a [sort field definition object](sort.html#sort-field) for sorting by an aggregate calculation of a specified sort field.\n\n__Default value:__ `\"ascending\"`" + }, + "timeUnit": { + "$ref": "#/definitions/TimeUnit", + "description": "Time unit for a `temporal` field (e.g., `year`, `yearmonth`, `month`, `hour`).\n\n__Default value:__ `undefined` (None)" }, - "value": { - "type": "number" + "type": { + "$ref": "#/definitions/Type", + "description": "The encoded field's type of measurement. This can be either a full type\nname (`\"quantitative\"`, `\"temporal\"`, `\"ordinal\"`, and `\"nominal\"`)\nor an initial character of the type name (`\"Q\"`, `\"T\"`, `\"O\"`, `\"N\"`).\nThis property is case-insensitive." } }, "required": [ - "selection", - "value" + "type" ], "type": "object" }, - "Condition": { + "ConditionalTextFieldDef": { "additionalProperties": false, + "description": "A FieldDef with ConditionValueDef\n{\n condition: {value: ...},\n field: ...,\n ...\n}", "properties": { - "selection": { - "$ref": "#/definitions/LogicalOperand" + "aggregate": { + "anyOf": [ + { + "$ref": "#/definitions/AggregateOp" + }, + { + "$ref": "#/definitions/CompositeAggregate" + } + ], + "description": "Aggregation function for the field\n(e.g., `mean`, `sum`, `median`, `min`, `max`, `count`).\n\n__Default value:__ `undefined` (None)" }, - "value": { + "bin": { + "anyOf": [ + { + "type": "boolean" + }, + { + "$ref": "#/definitions/Bin" + } + ], + "description": "Flag for binning a `quantitative` field, or a bin property object\nfor binning parameters." + }, + "condition": { + "$ref": "#/definitions/ConditionTextValueDef" + }, + "field": { + "$ref": "#/definitions/Field", + "description": "__Required.__ Name of the field from which to pull a data value.\n\n__Note:__ `field` is not required if `aggregate` is `count`." + }, + "format": { + "description": "The formatting pattern for text value. If not defined, this will be determined automatically.", "type": "string" + }, + "timeUnit": { + "$ref": "#/definitions/TimeUnit", + "description": "Time unit for a `temporal` field (e.g., `year`, `yearmonth`, `month`, `hour`).\n\n__Default value:__ `undefined` (None)" + }, + "type": { + "$ref": "#/definitions/Type", + "description": "The encoded field's type of measurement. This can be either a full type\nname (`\"quantitative\"`, `\"temporal\"`, `\"ordinal\"`, and `\"nominal\"`)\nor an initial character of the type name (`\"Q\"`, `\"T\"`, `\"O\"`, `\"N\"`).\nThis property is case-insensitive." } }, "required": [ - "selection", - "value" + "type" ], "type": "object" }, - "ConditionalValueDef<(string|number|boolean)>": { + "ConditionalNumberLegendValueDef": { "additionalProperties": false, + "description": "A ValueDef with ConditionValueDef | FieldDef\n{\n condition: {field: ...} | {value: ...},\n value: ...,\n}", "properties": { "condition": { - "$ref": "#/definitions/Condition<(string|number|boolean)>" + "anyOf": [ + { + "$ref": "#/definitions/ConditionLegendFieldDef" + }, + { + "$ref": "#/definitions/ConditionNumberValueDef" + } + ] }, "value": { "description": "A constant value in visual domain.", - "type": [ - "string", - "number", - "boolean" - ] + "type": "number" } }, "required": [ @@ -1123,15 +1525,23 @@ ], "type": "object" }, - "ConditionalValueDef": { + "ConditionalStringLegendValueDef": { "additionalProperties": false, + "description": "A ValueDef with ConditionValueDef | FieldDef\n{\n condition: {field: ...} | {value: ...},\n value: ...,\n}", "properties": { "condition": { - "$ref": "#/definitions/Condition" + "anyOf": [ + { + "$ref": "#/definitions/ConditionLegendFieldDef" + }, + { + "$ref": "#/definitions/ConditionStringValueDef" + } + ] }, "value": { "description": "A constant value in visual domain.", - "type": "number" + "type": "string" } }, "required": [ @@ -1139,15 +1549,27 @@ ], "type": "object" }, - "ConditionalValueDef": { + "ConditionalTextValueDef": { "additionalProperties": false, + "description": "A ValueDef with ConditionValueDef | FieldDef\n{\n condition: {field: ...} | {value: ...},\n value: ...,\n}", "properties": { "condition": { - "$ref": "#/definitions/Condition" + "anyOf": [ + { + "$ref": "#/definitions/ConditionTextFieldDef" + }, + { + "$ref": "#/definitions/ConditionTextValueDef" + } + ] }, "value": { "description": "A constant value in visual domain.", - "type": "string" + "type": [ + "string", + "number", + "boolean" + ] } }, "required": [ @@ -1492,14 +1914,7 @@ "additionalProperties": false, "properties": { "color": { - "anyOf": [ - { - "$ref": "#/definitions/LegendFieldDef" - }, - { - "$ref": "#/definitions/ConditionalValueDef" - } - ], + "$ref": "#/definitions/ConditionalStringLegendDef", "description": "Color of the marks – either fill or stroke color based on mark type.\n(By default, fill color for `area`, `bar`, `tick`, `text`, `circle`, and `square` /\nstroke color for `line` and `point`.)" }, "detail": { @@ -1517,14 +1932,7 @@ "description": "Additional levels of detail for grouping data in aggregate views and\nin line and area marks without mapping data to a specific visual channel." }, "opacity": { - "anyOf": [ - { - "$ref": "#/definitions/LegendFieldDef" - }, - { - "$ref": "#/definitions/ConditionalValueDef" - } - ], + "$ref": "#/definitions/ConditionalNumberLegendDef", "description": "Opacity of the marks – either can be a value or a range." }, "order": { @@ -1542,47 +1950,19 @@ "description": "stack order for stacked marks or order of data points in line marks." }, "shape": { - "anyOf": [ - { - "$ref": "#/definitions/LegendFieldDef" - }, - { - "$ref": "#/definitions/ConditionalValueDef" - } - ], + "$ref": "#/definitions/ConditionalStringLegendDef", "description": "The symbol's shape (only for `point` marks). The supported values are\n`\"circle\"` (default), `\"square\"`, `\"cross\"`, `\"diamond\"`, `\"triangle-up\"`,\nor `\"triangle-down\"`, or else a custom SVG path string." }, "size": { - "anyOf": [ - { - "$ref": "#/definitions/LegendFieldDef" - }, - { - "$ref": "#/definitions/ConditionalValueDef" - } - ], + "$ref": "#/definitions/ConditionalNumberLegendDef", "description": "Size of the mark.\n- For `point`, `square` and `circle`\n– the symbol size, or pixel area of the mark.\n- For `bar` and `tick` – the bar and tick's size.\n- For `text` – the text's font size.\n- Size is currently unsupported for `line` and `area`." }, "text": { - "anyOf": [ - { - "$ref": "#/definitions/TextFieldDef" - }, - { - "$ref": "#/definitions/ConditionalValueDef<(string|number|boolean)>" - } - ], + "$ref": "#/definitions/ConditionalTextDef", "description": "Text of the `text` mark." }, "tooltip": { - "anyOf": [ - { - "$ref": "#/definitions/TextFieldDef" - }, - { - "$ref": "#/definitions/ConditionalValueDef" - } - ], + "$ref": "#/definitions/ConditionalTextDef", "description": "The tooltip text to show upon mouse hover." }, "x": { @@ -1591,7 +1971,7 @@ "$ref": "#/definitions/PositionFieldDef" }, { - "$ref": "#/definitions/ValueDef" + "$ref": "#/definitions/NumberValueDef" } ], "description": "X coordinates for `point`, `circle`, `square`,\n`line`, `rule`, `text`, and `tick`\n(or to width and height for `bar` and `area` marks)." @@ -1602,7 +1982,7 @@ "$ref": "#/definitions/FieldDef" }, { - "$ref": "#/definitions/ValueDef" + "$ref": "#/definitions/NumberValueDef" } ], "description": "X2 coordinates for ranged `bar`, `rule`, `area`." @@ -1613,7 +1993,7 @@ "$ref": "#/definitions/PositionFieldDef" }, { - "$ref": "#/definitions/ValueDef" + "$ref": "#/definitions/NumberValueDef" } ], "description": "Y coordinates for `point`, `circle`, `square`,\n`line`, `rule`, `text`, and `tick`\n(or to width and height for `bar` and `area` marks)." @@ -1624,7 +2004,7 @@ "$ref": "#/definitions/FieldDef" }, { - "$ref": "#/definitions/ValueDef" + "$ref": "#/definitions/NumberValueDef" } ], "description": "Y2 coordinates for ranged `bar`, `rule`, `area`." @@ -1636,14 +2016,7 @@ "additionalProperties": false, "properties": { "color": { - "anyOf": [ - { - "$ref": "#/definitions/LegendFieldDef" - }, - { - "$ref": "#/definitions/ConditionalValueDef" - } - ], + "$ref": "#/definitions/ConditionalStringLegendDef", "description": "Color of the marks – either fill or stroke color based on mark type.\n(By default, fill color for `area`, `bar`, `tick`, `text`, `circle`, and `square` /\nstroke color for `line` and `point`.)" }, "column": { @@ -1665,14 +2038,7 @@ "description": "Additional levels of detail for grouping data in aggregate views and\nin line and area marks without mapping data to a specific visual channel." }, "opacity": { - "anyOf": [ - { - "$ref": "#/definitions/LegendFieldDef" - }, - { - "$ref": "#/definitions/ConditionalValueDef" - } - ], + "$ref": "#/definitions/ConditionalNumberLegendDef", "description": "Opacity of the marks – either can be a value or a range." }, "order": { @@ -1694,47 +2060,19 @@ "description": "Vertical facets for trellis plots." }, "shape": { - "anyOf": [ - { - "$ref": "#/definitions/LegendFieldDef" - }, - { - "$ref": "#/definitions/ConditionalValueDef" - } - ], + "$ref": "#/definitions/ConditionalStringLegendDef", "description": "The symbol's shape (only for `point` marks). The supported values are\n`\"circle\"` (default), `\"square\"`, `\"cross\"`, `\"diamond\"`, `\"triangle-up\"`,\nor `\"triangle-down\"`, or else a custom SVG path string." }, "size": { - "anyOf": [ - { - "$ref": "#/definitions/LegendFieldDef" - }, - { - "$ref": "#/definitions/ConditionalValueDef" - } - ], + "$ref": "#/definitions/ConditionalNumberLegendDef", "description": "Size of the mark.\n- For `point`, `square` and `circle`\n– the symbol size, or pixel area of the mark.\n- For `bar` and `tick` – the bar and tick's size.\n- For `text` – the text's font size.\n- Size is currently unsupported for `line` and `area`." }, "text": { - "anyOf": [ - { - "$ref": "#/definitions/TextFieldDef" - }, - { - "$ref": "#/definitions/ConditionalValueDef<(string|number|boolean)>" - } - ], + "$ref": "#/definitions/ConditionalTextDef", "description": "Text of the `text` mark." }, "tooltip": { - "anyOf": [ - { - "$ref": "#/definitions/TextFieldDef" - }, - { - "$ref": "#/definitions/ConditionalValueDef" - } - ], + "$ref": "#/definitions/ConditionalTextDef", "description": "The tooltip text to show upon mouse hover." }, "x": { @@ -1743,7 +2081,7 @@ "$ref": "#/definitions/PositionFieldDef" }, { - "$ref": "#/definitions/ValueDef" + "$ref": "#/definitions/NumberValueDef" } ], "description": "X coordinates for `point`, `circle`, `square`,\n`line`, `rule`, `text`, and `tick`\n(or to width and height for `bar` and `area` marks)." @@ -1754,7 +2092,7 @@ "$ref": "#/definitions/FieldDef" }, { - "$ref": "#/definitions/ValueDef" + "$ref": "#/definitions/NumberValueDef" } ], "description": "X2 coordinates for ranged `bar`, `rule`, `area`." @@ -1765,7 +2103,7 @@ "$ref": "#/definitions/PositionFieldDef" }, { - "$ref": "#/definitions/ValueDef" + "$ref": "#/definitions/NumberValueDef" } ], "description": "Y coordinates for `point`, `circle`, `square`,\n`line`, `rule`, `text`, and `tick`\n(or to width and height for `bar` and `area` marks)." @@ -1776,7 +2114,7 @@ "$ref": "#/definitions/FieldDef" }, { - "$ref": "#/definitions/ValueDef" + "$ref": "#/definitions/NumberValueDef" } ], "description": "Y2 coordinates for ranged `bar`, `rule`, `area`." @@ -2733,77 +3071,7 @@ }, "type": "object" }, - "LegendFieldDef": { - "additionalProperties": false, - "properties": { - "aggregate": { - "anyOf": [ - { - "$ref": "#/definitions/AggregateOp" - }, - { - "$ref": "#/definitions/CompositeAggregate" - } - ], - "description": "Aggregation function for the field\n(e.g., `mean`, `sum`, `median`, `min`, `max`, `count`).\n\n__Default value:__ `undefined` (None)" - }, - "bin": { - "anyOf": [ - { - "type": "boolean" - }, - { - "$ref": "#/definitions/Bin" - } - ], - "description": "Flag for binning a `quantitative` field, or a bin property object\nfor binning parameters." - }, - "condition": { - "$ref": "#/definitions/Condition" - }, - "field": { - "$ref": "#/definitions/Field", - "description": "__Required.__ Name of the field from which to pull a data value.\n\n__Note:__ `field` is not required if `aggregate` is `count`." - }, - "legend": { - "anyOf": [ - { - "$ref": "#/definitions/Legend" - }, - { - "type": "null" - } - ] - }, - "scale": { - "$ref": "#/definitions/Scale" - }, - "sort": { - "anyOf": [ - { - "$ref": "#/definitions/SortField" - }, - { - "$ref": "#/definitions/SortOrder" - } - ], - "description": "Sort order for a particular field.\nFor quantitative or temporal fields, this can be either `\"ascending\"` or `\"descending\"`\nFor quantitative or temporal fields, this can be `\"ascending\"`, `\"descending\"`, `\"none\"`, or a [sort field definition object](sort.html#sort-field) for sorting by an aggregate calculation of a specified sort field.\n\n__Default value:__ `\"ascending\"`" - }, - "timeUnit": { - "$ref": "#/definitions/TimeUnit", - "description": "Time unit for a `temporal` field (e.g., `year`, `yearmonth`, `month`, `hour`).\n\n__Default value:__ `undefined` (None)" - }, - "type": { - "$ref": "#/definitions/Type", - "description": "The encoded field's type of measurement. This can be either a full type\nname (`\"quantitative\"`, `\"temporal\"`, `\"ordinal\"`, and `\"nominal\"`)\nor an initial character of the type name (`\"Q\"`, `\"T\"`, `\"O\"`, `\"N\"`).\nThis property is case-insensitive." - } - }, - "required": [ - "type" - ], - "type": "object" - }, - "LegendFieldDef": { + "LegendFieldDef": { "additionalProperties": false, "properties": { "aggregate": { @@ -2828,9 +3096,6 @@ ], "description": "Flag for binning a `quantitative` field, or a bin property object\nfor binning parameters." }, - "condition": { - "$ref": "#/definitions/Condition" - }, "field": { "$ref": "#/definitions/Field", "description": "__Required.__ Name of the field from which to pull a data value.\n\n__Note:__ `field` is not required if `aggregate` is `count`." @@ -4016,7 +4281,7 @@ }, "encodings": { "items": { - "type": "string" + "$ref": "#/definitions/SingleDefChannel" }, "type": "array" }, @@ -4133,6 +4398,25 @@ "string" ] }, + "SingleDefChannel": { + "enum": [ + "x", + "y", + "x2", + "y2", + "row", + "column", + "size", + "shape", + "color", + "opacity", + "text", + "tooltip" + ], + "type": [ + "string" + ] + }, "SortField": { "additionalProperties": false, "properties": { @@ -4409,9 +4693,6 @@ ], "description": "Flag for binning a `quantitative` field, or a bin property object\nfor binning parameters." }, - "condition": { - "$ref": "#/definitions/Condition<(string|number)>" - }, "field": { "$ref": "#/definitions/Field", "description": "__Required.__ Name of the field from which to pull a data value.\n\n__Note:__ `field` is not required if `aggregate` is `count`." @@ -4657,7 +4938,7 @@ ], "type": "object" }, - "TopLevel": { + "TopLevelFacetedUnitSpec": { "additionalProperties": false, "properties": { "$schema": { @@ -4727,7 +5008,7 @@ ], "type": "object" }, - "TopLevel": { + "TopLevelFacetedSpec": { "additionalProperties": false, "properties": { "$schema": { @@ -4793,7 +5074,7 @@ ], "type": "object" }, - "TopLevel": { + "TopLevelHConcatSpec": { "additionalProperties": false, "properties": { "$schema": { @@ -4858,7 +5139,7 @@ ], "type": "object" }, - "TopLevel": { + "TopLevelLayerSpec": { "additionalProperties": false, "properties": { "$schema": { @@ -4929,7 +5210,7 @@ ], "type": "object" }, - "TopLevel": { + "TopLevelRepeatSpec": { "additionalProperties": false, "properties": { "$schema": { @@ -4995,7 +5276,7 @@ ], "type": "object" }, - "TopLevel": { + "TopLevelVConcatSpec": { "additionalProperties": false, "properties": { "$schema": { @@ -5063,22 +5344,22 @@ "TopLevelExtendedSpec": { "anyOf": [ { - "$ref": "#/definitions/TopLevel" + "$ref": "#/definitions/TopLevelFacetedUnitSpec" }, { - "$ref": "#/definitions/TopLevel" + "$ref": "#/definitions/TopLevelLayerSpec" }, { - "$ref": "#/definitions/TopLevel" + "$ref": "#/definitions/TopLevelFacetedSpec" }, { - "$ref": "#/definitions/TopLevel" + "$ref": "#/definitions/TopLevelRepeatSpec" }, { - "$ref": "#/definitions/TopLevel" + "$ref": "#/definitions/TopLevelVConcatSpec" }, { - "$ref": "#/definitions/TopLevel" + "$ref": "#/definitions/TopLevelHConcatSpec" } ] }, @@ -5211,7 +5492,7 @@ }, "type": "object" }, - "ValueDef<(string|number|boolean)>": { + "TextValueDef": { "additionalProperties": false, "description": "Definition object for a constant value of an encoding channel.", "properties": { @@ -5229,7 +5510,7 @@ ], "type": "object" }, - "ValueDef": { + "NumberValueDef": { "additionalProperties": false, "description": "Definition object for a constant value of an encoding channel.", "properties": { @@ -5243,7 +5524,7 @@ ], "type": "object" }, - "ValueDef": { + "StringValueDef": { "additionalProperties": false, "description": "Definition object for a constant value of an encoding channel.", "properties": { diff --git a/examples/specs/brush.vl.json b/examples/specs/brush.vl.json index 2267019a44..68588e0737 100644 --- a/examples/specs/brush.vl.json +++ b/examples/specs/brush.vl.json @@ -12,8 +12,8 @@ "x": {"field": "Horsepower", "type": "quantitative"}, "y": {"field": "Miles_per_Gallon", "type": "quantitative"}, "color": { - "condition": {"selection": {"not": "brush"}, "value": "grey"}, - "field": "Cylinders", "type": "ordinal" + "condition": {"selection": "brush", "field": "Cylinders", "type": "ordinal"}, + "value": "grey" } } } diff --git a/examples/specs/interactive_splom.vl.json b/examples/specs/interactive_splom.vl.json index 41acab7485..bda2b709aa 100644 --- a/examples/specs/interactive_splom.vl.json +++ b/examples/specs/interactive_splom.vl.json @@ -31,10 +31,11 @@ "type": "quantitative" }, "color": { - "field": "Origin","type": "nominal", "condition": { - "selection": {"not": "brush"}, "value": "grey" - } + "selection": "brush", + "field": "Origin","type": "nominal" + }, + "value": "grey" } } } diff --git a/examples/specs/layered_selections.vl.json b/examples/specs/layered_selections.vl.json index 12f026c378..34fce76b5f 100644 --- a/examples/specs/layered_selections.vl.json +++ b/examples/specs/layered_selections.vl.json @@ -36,8 +36,8 @@ "x": {"field": "Horsepower", "type": "quantitative"}, "y": {"field": "Miles_per_Gallon", "type": "quantitative"}, "color": { - "condition": {"selection": {"not": "brush"}, "value": "grey"}, - "field": "Cylinders", "type": "ordinal" + "condition": {"selection": "brush", "field": "Cylinders", "type": "ordinal"}, + "value": "grey" }, "size": { "value": 50, diff --git a/examples/specs/query_widgets.vl.json b/examples/specs/query_widgets.vl.json index d1c3106a07..25ca2effc6 100644 --- a/examples/specs/query_widgets.vl.json +++ b/examples/specs/query_widgets.vl.json @@ -18,8 +18,8 @@ "x": {"field": "Horsepower", "type": "quantitative"}, "y": {"field": "Miles_per_Gallon", "type": "quantitative"}, "color": { - "field": "Origin", "type": "nominal", - "condition": {"selection": {"not": "CylYr"}, "value": "grey"} + "condition": {"selection": "CylYr", "field": "Origin", "type": "nominal"}, + "value": "grey" } } }, { diff --git a/examples/vg-specs/brush.vg.json b/examples/vg-specs/brush.vg.json index d048b1a929..019fc84f15 100644 --- a/examples/vg-specs/brush.vg.json +++ b/examples/vg-specs/brush.vg.json @@ -344,12 +344,12 @@ }, "stroke": [ { - "test": "!(vlInterval(\"brush_store\", \"\", datum, \"union\", \"all\"))", - "value": "grey" - }, - { + "test": "vlInterval(\"brush_store\", \"\", datum, \"union\", \"all\")", "scale": "color", "field": "Cylinders" + }, + { + "value": "grey" } ], "fill": { diff --git a/examples/vg-specs/interactive_splom.vg.json b/examples/vg-specs/interactive_splom.vg.json index ff4ebf59ab..9718a6e893 100644 --- a/examples/vg-specs/interactive_splom.vg.json +++ b/examples/vg-specs/interactive_splom.vg.json @@ -505,12 +505,12 @@ }, "stroke": [ { - "test": "!(vlInterval(\"brush_store\", \"child_Horsepower_Horsepower_\", datum, \"union\", \"all\"))", - "value": "grey" - }, - { + "test": "vlInterval(\"brush_store\", \"child_Horsepower_Horsepower_\", datum, \"union\", \"all\")", "scale": "color", "field": "Origin" + }, + { + "value": "grey" } ], "fill": { @@ -1063,12 +1063,12 @@ }, "stroke": [ { - "test": "!(vlInterval(\"brush_store\", \"child_Horsepower_Miles_per_Gallon_\", datum, \"union\", \"all\"))", - "value": "grey" - }, - { + "test": "vlInterval(\"brush_store\", \"child_Horsepower_Miles_per_Gallon_\", datum, \"union\", \"all\")", "scale": "color", "field": "Origin" + }, + { + "value": "grey" } ], "fill": { @@ -1622,12 +1622,12 @@ }, "stroke": [ { - "test": "!(vlInterval(\"brush_store\", \"child_Acceleration_Horsepower_\", datum, \"union\", \"all\"))", - "value": "grey" - }, - { + "test": "vlInterval(\"brush_store\", \"child_Acceleration_Horsepower_\", datum, \"union\", \"all\")", "scale": "color", "field": "Origin" + }, + { + "value": "grey" } ], "fill": { @@ -2181,12 +2181,12 @@ }, "stroke": [ { - "test": "!(vlInterval(\"brush_store\", \"child_Acceleration_Miles_per_Gallon_\", datum, \"union\", \"all\"))", - "value": "grey" - }, - { + "test": "vlInterval(\"brush_store\", \"child_Acceleration_Miles_per_Gallon_\", datum, \"union\", \"all\")", "scale": "color", "field": "Origin" + }, + { + "value": "grey" } ], "fill": { diff --git a/examples/vg-specs/layered_selections.vg.json b/examples/vg-specs/layered_selections.vg.json index 38a20bb9bf..008aa4b096 100644 --- a/examples/vg-specs/layered_selections.vg.json +++ b/examples/vg-specs/layered_selections.vg.json @@ -616,12 +616,12 @@ }, "fill": [ { - "test": "!(vlInterval(\"brush_store\", \"layer_1_\", datum, \"union\", \"all\"))", - "value": "grey" - }, - { + "test": "vlInterval(\"brush_store\", \"layer_1_\", datum, \"union\", \"all\")", "scale": "color", "field": "Cylinders" + }, + { + "value": "grey" } ], "size": [ diff --git a/examples/vg-specs/query_widgets.vg.json b/examples/vg-specs/query_widgets.vg.json index 198a33b7e3..fc8c8a0bb8 100644 --- a/examples/vg-specs/query_widgets.vg.json +++ b/examples/vg-specs/query_widgets.vg.json @@ -190,12 +190,12 @@ }, "fill": [ { - "test": "!(vlPoint(\"CylYr_store\", \"layer_0_\", datum, \"union\", \"all\"))", - "value": "grey" - }, - { + "test": "vlPoint(\"CylYr_store\", \"layer_0_\", datum, \"union\", \"all\")", "scale": "color", "field": "Origin" + }, + { + "value": "grey" } ], "shape": { diff --git a/scripts/rename-schema.sh b/scripts/rename-schema.sh index 5814ee7bc1..b6a25d1e2a 100755 --- a/scripts/rename-schema.sh +++ b/scripts/rename-schema.sh @@ -1,12 +1,34 @@ perl -pi -e s,'','',g build/vega-lite-schema.json +perl -pi -e s,'','LayerSpec',g build/vega-lite-schema.json perl -pi -e s,'GenericFacetSpec','FacetedSpec',g build/vega-lite-schema.json perl -pi -e s,'GenericRepeatSpec','RepeatSpec',g build/vega-lite-schema.json perl -pi -e s,'GenericVConcatSpec','VConcatSpec',g build/vega-lite-schema.json perl -pi -e s,'GenericHConcatSpec','HConcatSpec',g build/vega-lite-schema.json + perl -pi -e s,'GenericUnitSpec','FacetedCompositeUnitSpecAlias',g build/vega-lite-schema.json perl -pi -e s,'GenericUnitSpec','CompositeUnitSpecAlias',g build/vega-lite-schema.json + +perl -pi -e s,'TopLevel<(.*)>','TopLevel\1',g build/vega-lite-schema.json + +perl -pi -e s,'ValueDef<\(string\|number\|boolean\)>','TextValueDef',g build/vega-lite-schema.json +perl -pi -e s,'ValueDef','StringValueDef',g build/vega-lite-schema.json +perl -pi -e s,'ValueDef','NumberValueDef',g build/vega-lite-schema.json + +perl -pi -e s,'Condition<(.*)>','Condition\1',g build/vega-lite-schema.json + +perl -pi -e s,'Conditional','ConditionalTextDef',g build/vega-lite-schema.json +perl -pi -e s,'ConditionalFieldDef','ConditionalTextFieldDef',g build/vega-lite-schema.json +perl -pi -e s,'ConditionalValueDef','ConditionalTextValueDef',g build/vega-lite-schema.json +perl -pi -e s,'ConditionOnlyDef','ConditionOnlyTextDef',g build/vega-lite-schema.json + +perl -pi -e s,'Conditional','Conditional\1LegendDef',g build/vega-lite-schema.json +perl -pi -e s,'ConditionalFieldDef','Conditional\1LegendFieldDef',g build/vega-lite-schema.json +perl -pi -e s,'ConditionalValueDef','Conditional\1LegendValueDef',g build/vega-lite-schema.json +perl -pi -e s,'ConditionOnlyDef','ConditionOnly\1LegendDef',g build/vega-lite-schema.json + perl -pi -e s,'LogicalOperand','LogicalOperand',g build/vega-lite-schema.json perl -pi -e s,'LogicalAnd','LogicalAnd',g build/vega-lite-schema.json perl -pi -e s,'LogicalOr','LogicalOr',g build/vega-lite-schema.json diff --git a/src/channel.ts b/src/channel.ts index 8b57b1fe3b..82675096cc 100644 --- a/src/channel.ts +++ b/src/channel.ts @@ -55,6 +55,21 @@ export const TOOLTIP = Channel.TOOLTIP; export const CHANNELS = [X, Y, X2, Y2, ROW, COLUMN, SIZE, SHAPE, COLOR, ORDER, OPACITY, TEXT, DETAIL, TOOLTIP]; const CHANNEL_INDEX = toSet(CHANNELS); +/** + * Channels cannot have an array of channelDef. + * model.fieldDef, getFieldDef only work for these channels. + * + * (The only two channels that can have an array of channelDefs are "detail" and "order". + * Since there can be multiple fieldDefs for detail and order, getFieldDef/model.fieldDef + * are not applicable for them. Similarly, selection projecttion won't work with "detail" and "order".) + */ +export const SINGLE_DEF_CHANNELS = [X, Y, X2, Y2, ROW, COLUMN, SIZE, SHAPE, COLOR, OPACITY, TEXT, TOOLTIP]; + +// export type SingleDefChannel = typeof SINGLE_DEF_CHANNELS[0]; +// FIXME somehow the typeof above leads to the following error when running npm run schema +// UnknownNodeError: Unknown node "SingleDefChannel" (ts.SyntaxKind = 171) at /Users/kanitw/Documents/_code/_idl/_visrec/vega-lite/src/selection.ts(17,14) +export type SingleDefChannel = 'x' | 'y' | 'x2' | 'y2' | 'row' | 'column' | 'size' | 'shape' | 'color' | 'opacity' | 'text' | 'tooltip'; + export function isChannel(str: string): str is Channel { return !!CHANNEL_INDEX[str]; } diff --git a/src/compile/axis/encode.ts b/src/compile/axis/encode.ts index c755380578..ac6c1cf2dc 100644 --- a/src/compile/axis/encode.ts +++ b/src/compile/axis/encode.ts @@ -1,4 +1,4 @@ -import {Channel, X} from '../../channel'; +import {Channel, SpatialScaleChannel, X} from '../../channel'; import {NOMINAL, ORDINAL, TEMPORAL} from '../../type'; import {contains, extend, keys} from '../../util'; import {VgAxis} from '../../vega.schema'; @@ -7,8 +7,13 @@ import {ScaleType} from '../../scale'; import {timeFormatExpression} from '../common'; import {UnitModel} from '../unit'; -export function labels(model: UnitModel, channel: Channel, labelsSpec: any, def: VgAxis) { - const fieldDef = model.fieldDef(channel); +export function labels(model: UnitModel, channel: SpatialScaleChannel, labelsSpec: any, def: VgAxis) { + const fieldDef = model.fieldDef(channel) || + ( + channel === 'x' ? model.fieldDef('x2') : + channel === 'y' ? model.fieldDef('y2') : + undefined + ); const axis = model.axis(channel); const config = model.config; diff --git a/src/compile/axis/parse.ts b/src/compile/axis/parse.ts index 2d51a99e02..2a1a62ae0b 100644 --- a/src/compile/axis/parse.ts +++ b/src/compile/axis/parse.ts @@ -1,5 +1,5 @@ import {Axis, AXIS_PROPERTIES} from '../../axis'; -import {Channel} from '../../channel'; +import {Channel, SpatialScaleChannel} from '../../channel'; import {VgAxis} from '../../vega.schema'; import * as encode from './encode'; @@ -12,7 +12,7 @@ import {AxisComponent, AxisComponentIndex} from './component'; type AxisPart = 'domain' | 'grid' | 'labels' | 'ticks' | 'title'; const AXIS_PARTS: AxisPart[] = ['domain', 'grid', 'labels', 'ticks', 'title']; -export function parseAxisComponent(model: UnitModel, axisChannels: Channel[]): AxisComponentIndex { +export function parseAxisComponent(model: UnitModel, axisChannels: SpatialScaleChannel[]): AxisComponentIndex { return axisChannels.reduce(function(axis, channel) { const axisComponent: AxisComponent = {axes:[], gridAxes: []}; if (model.axis(channel)) { @@ -58,16 +58,16 @@ function hasAxisPart(axis: VgAxis, part: AxisPart) { /** * Make an inner axis for showing grid for shared axis. */ -export function parseGridAxis(channel: Channel, model: UnitModel): VgAxis { +export function parseGridAxis(channel: SpatialScaleChannel, model: UnitModel): VgAxis { // FIXME: support adding ticks for grid axis that are inner axes of faceted plots. return parseAxis(channel, model, true); } -export function parseMainAxis(channel: Channel, model: UnitModel) { +export function parseMainAxis(channel: SpatialScaleChannel, model: UnitModel) { return parseAxis(channel, model, false); } -function parseAxis(channel: Channel, model: UnitModel, isGridAxis: boolean): VgAxis { +function parseAxis(channel: SpatialScaleChannel, model: UnitModel, isGridAxis: boolean): VgAxis { const axis = model.axis(channel); const vgAxis: VgAxis = { @@ -114,7 +114,7 @@ function parseAxis(channel: Channel, model: UnitModel, isGridAxis: boolean): VgA return vgAxis; } -function getSpecifiedOrDefaultValue(property: keyof VgAxis, specifiedAxis: Axis, channel: Channel, model: UnitModel, isGridAxis: boolean) { +function getSpecifiedOrDefaultValue(property: keyof VgAxis, specifiedAxis: Axis, channel: SpatialScaleChannel, model: UnitModel, isGridAxis: boolean) { const fieldDef = model.fieldDef(channel); switch (property) { diff --git a/src/compile/axis/rules.ts b/src/compile/axis/rules.ts index f3f8e44eab..c56683993c 100644 --- a/src/compile/axis/rules.ts +++ b/src/compile/axis/rules.ts @@ -1,7 +1,7 @@ import * as log from '../../log'; import {Axis} from '../../axis'; -import {Channel, COLUMN, ROW, X, Y} from '../../channel'; +import {Channel, COLUMN, ROW, SpatialScaleChannel, X, Y} from '../../channel'; import {Config} from '../../config'; import {DateTime, dateTimeExpr, isDateTime} from '../../datetime'; import {FieldDef, title as fieldDefTitle} from '../../fielddef'; @@ -20,7 +20,7 @@ export function format(specifiedAxis: Axis, channel: Channel, fieldDef: FieldDef * Default rules for whether to show a grid should be shown for a channel. * If `grid` is unspecified, the default value is `true` for ordinal scales that are not binned */ -export function gridShow(model: UnitModel, channel: Channel) { +export function gridShow(model: UnitModel, channel: SpatialScaleChannel) { const grid = model.axis(channel).grid; if (grid !== undefined) { return grid; @@ -29,12 +29,7 @@ export function gridShow(model: UnitModel, channel: Channel) { return !model.hasDiscreteDomain(channel) && !model.fieldDef(channel).bin; } -export function grid(model: UnitModel, channel: Channel, isGridAxis: boolean) { - if (channel === ROW || channel === COLUMN) { - // never apply grid for ROW and COLUMN since we manually create rule-group for them - return false; - } - +export function grid(model: UnitModel, channel: SpatialScaleChannel, isGridAxis: boolean) { if (!isGridAxis) { return undefined; } diff --git a/src/compile/data/bin.ts b/src/compile/data/bin.ts index cae55ab1c5..4238c9fd91 100644 --- a/src/compile/data/bin.ts +++ b/src/compile/data/bin.ts @@ -74,7 +74,7 @@ export class BinNode extends DataFlowNode { public static makeBinFromEncoding(model: ModelWithField) { const bins = model.reduceFieldDef((binComponent: Dict, fieldDef, channel) => { - const fieldDefBin = model.fieldDef(channel).bin; + const fieldDefBin = fieldDef.bin; if (fieldDefBin) { const bin = normalizeBin(fieldDefBin, undefined) || {}; const key = binKey(fieldDefBin, fieldDef.field); diff --git a/src/compile/data/nonpositivefilter.ts b/src/compile/data/nonpositivefilter.ts index e725ad63ed..6f4fb1a5ba 100644 --- a/src/compile/data/nonpositivefilter.ts +++ b/src/compile/data/nonpositivefilter.ts @@ -1,3 +1,4 @@ +import {SCALE_CHANNELS} from '../../channel'; import {ScaleType} from '../../scale'; import {Dict, duplicate, extend, keys} from '../../util'; import {VgFilterTransform, VgTransform} from '../../vega.schema'; @@ -18,7 +19,7 @@ export class NonPositiveFilterNode extends DataFlowNode { } public static make(model: UnitModel) { - const filter = model.channels().reduce(function(nonPositiveComponent, channel) { + const filter = SCALE_CHANNELS.reduce(function(nonPositiveComponent, channel) { const scale = model.getScaleComponent(channel); if (!scale || !model.field(channel)) { // don't set anything diff --git a/src/compile/facet.ts b/src/compile/facet.ts index b718c84319..dd4508b84f 100644 --- a/src/compile/facet.ts +++ b/src/compile/facet.ts @@ -307,10 +307,6 @@ export class FacetModel extends ModelWithField { return marks; } - public channels() { - return [ROW, COLUMN]; - } - protected getMapping() { return this.facet; } diff --git a/src/compile/legend/parse.ts b/src/compile/legend/parse.ts index df7ae3db85..9b4fd33c7a 100644 --- a/src/compile/legend/parse.ts +++ b/src/compile/legend/parse.ts @@ -1,4 +1,4 @@ -import {Channel, COLOR, OPACITY, SHAPE, SIZE} from '../../channel'; +import {Channel, COLOR, NonspatialScaleChannel, OPACITY, SHAPE, SIZE} from '../../channel'; import {Legend, LEGEND_PROPERTIES} from '../../legend'; import {Dict, keys} from '../../util'; import {VgLegend} from '../../vega.schema'; @@ -36,7 +36,7 @@ function getLegendDefWithScale(model: UnitModel, channel: Channel): LegendCompon return null; } -export function parseLegend(model: UnitModel, channel: Channel): LegendComponent { +export function parseLegend(model: UnitModel, channel: NonspatialScaleChannel): LegendComponent { const fieldDef = model.fieldDef(channel); const legend = model.legend(channel); @@ -64,7 +64,7 @@ export function parseLegend(model: UnitModel, channel: Channel): LegendComponent return def; } -function getSpecifiedOrDefaultValue(property: keyof VgLegend, specifiedLegend: Legend, channel: Channel, model: UnitModel) { +function getSpecifiedOrDefaultValue(property: keyof VgLegend, specifiedLegend: Legend, channel: NonspatialScaleChannel, model: UnitModel) { const fieldDef = model.fieldDef(channel); switch (property) { diff --git a/src/compile/mark/mark.ts b/src/compile/mark/mark.ts index ff78106dfa..614c224a26 100644 --- a/src/compile/mark/mark.ts +++ b/src/compile/mark/mark.ts @@ -18,8 +18,7 @@ import {UnitModel} from '../unit'; import {isArray} from 'vega-util'; import {X, Y} from '../../channel'; -import {getFieldDef} from '../../encoding'; -import {field} from '../../fielddef'; +import {field, getFieldDef} from '../../fielddef'; import {isSelectionDomain} from '../../scale'; const markCompiler: {[type: string]: MarkCompiler} = { @@ -131,7 +130,7 @@ function detailFields(model: UnitModel): string[] { }); } } else { - const fieldDef = getFieldDef(encoding, channel); + const fieldDef = getFieldDef(encoding[channel]); if (fieldDef && !fieldDef.aggregate) { details.push(field(fieldDef, {binSuffix: 'start'})); } diff --git a/src/compile/mark/mixins.ts b/src/compile/mark/mixins.ts index 401c9f423f..8b5bea4314 100644 --- a/src/compile/mark/mixins.ts +++ b/src/compile/mark/mixins.ts @@ -8,7 +8,7 @@ import {UnitModel} from '../unit'; import * as ref from './valueref'; import {NONSPATIAL_SCALE_CHANNELS} from '../../channel'; -import {Condition, FieldDef, isFieldDef, isValueDef} from '../../fielddef'; +import {ChannelDef, Condition, ConditionalValueDef, FieldDef, getFieldDef, isValueDef} from '../../fielddef'; import {predicate} from '../selection/selection'; export function color(model: UnitModel) { @@ -54,21 +54,27 @@ export function nonPosition(channel: typeof NONSPATIAL_SCALE_CHANNELS[0], model: const defaultRef = opt.defaultRef || (defaultValue !== undefined ? {value: defaultValue} : undefined); const channelDef = model.encoding[channel]; - const valueRef = ref.midPoint(channel, channelDef, model.scaleName(channel), model.getScaleComponent(channel), defaultRef); - return wrapCondition(model, channelDef && channelDef.condition, vgChannel || channel, valueRef); + return wrapCondition(model, channelDef, vgChannel || channel, (cDef) => { + return ref.midPoint(channel, cDef, model.scaleName(channel), model.getScaleComponent(channel), defaultRef); + }); } /** * Return a mixin that include a Vega production rule for a Vega-Lite conditional channel definition. * or a simple mixin if channel def has no condition. */ -function wrapCondition(model: UnitModel, condition: Condition, vgChannel: string, valueRef: VgValueRef): VgEncodeEntry { +function wrapCondition( + model: UnitModel, channelDef: ChannelDef, vgChannel: string, + refFn: (cDef: ChannelDef) => VgValueRef + ): VgEncodeEntry { + const condition = channelDef && channelDef.condition; + const valueRef = refFn(channelDef); if (condition) { - const {selection, value} = condition; + const conditionValueRef = refFn(condition); return { [vgChannel]: [ - {test: predicate(model, selection), value}, + {test: predicate(model, condition.selection), ...conditionValueRef}, ...(valueRef !== undefined ? [valueRef] : []) ] }; @@ -77,10 +83,9 @@ function wrapCondition(model: UnitModel, condition: Condition, vgChannel: s } } -export function text(model: UnitModel, vgChannel: 'text' | 'tooltip' = 'text') { - const channelDef = model.encoding[vgChannel]; - const valueRef = (vgChannel === 'tooltip' && !channelDef) ? undefined : ref.text(channelDef, model.config); - return wrapCondition(model, channelDef && channelDef.condition, vgChannel, valueRef); +export function text(model: UnitModel, channel: 'text' | 'tooltip' = 'text') { + const channelDef = model.encoding[channel]; + return wrapCondition(model, channelDef, channel, (cDef) => ref.text(cDef, model.config)); } export function bandPosition(fieldDef: FieldDef, channel: 'x'|'y', model: UnitModel) { @@ -96,7 +101,7 @@ export function bandPosition(fieldDef: FieldDef, channel: 'x'|'y', model [channel+'c']: ref.fieldRef(fieldDef, scaleName, {}, {band: 0.5}) }; - if (isFieldDef(model.encoding.size)) { + if (getFieldDef(model.encoding.size)) { log.warn(log.message.cannotUseSizeFieldWithBandSize(channel)); // TODO: apply size to band and set scale range to some values between 0-1. // return { diff --git a/src/compile/mark/valueref.ts b/src/compile/mark/valueref.ts index 9b27f313c6..72eeb11e48 100644 --- a/src/compile/mark/valueref.ts +++ b/src/compile/mark/valueref.ts @@ -4,7 +4,7 @@ import {Channel, X, X2, Y, Y2} from '../../channel'; import {Config} from '../../config'; -import {ChannelDef, field, FieldDef, FieldRefOption, isFieldDef, TextFieldDef, ValueDef} from '../../fielddef'; +import {ChannelDef, Conditional, field, FieldDef, FieldRefOption, isFieldDef, isValueDef, TextFieldDef, ValueDef} from '../../fielddef'; import {hasDiscreteDomain, isBinScale, ScaleType} from '../../scale'; import {StackProperties} from '../../stack'; import {contains} from '../../util'; @@ -93,6 +93,7 @@ export function midPoint(channel: Channel, channelDef: ChannelDef, scale if (channelDef) { /* istanbul ignore else */ + if (isFieldDef(channelDef)) { if (isBinScale(scale.type)) { // Use middle only for x an y to place marks in the center between start and end of the bin range. @@ -112,7 +113,7 @@ export function midPoint(channel: Channel, channelDef: ChannelDef, scale } else { return fieldRef(channelDef, scaleName, {}); // no need for bin suffix } - } else if (channelDef.value !== undefined) { + } else if (isValueDef(channelDef)) { return {value: channelDef.value}; } else { throw new Error('FieldDef without field or value.'); // FIXME add this to log.message @@ -141,16 +142,16 @@ export function midPoint(channel: Channel, channelDef: ChannelDef, scale return defaultRef; } -export function text(textDef: TextFieldDef | ValueDef, config: Config): VgValueRef { +export function text(textDef: Conditional, ValueDef>, config: Config): VgValueRef { // text if (textDef) { if (isFieldDef(textDef)) { return formatSignalRef(textDef, textDef.format, 'datum', config); - } else if (textDef.value) { + } else if (isValueDef(textDef)) { return {value: textDef.value}; } } - return {value: config.text.text}; + return undefined; } export function midX(width: number, config: Config): VgValueRef { diff --git a/src/compile/model.ts b/src/compile/model.ts index 05a126747c..4e7a489319 100644 --- a/src/compile/model.ts +++ b/src/compile/model.ts @@ -1,9 +1,9 @@ import {Axis} from '../axis'; -import {Channel, COLUMN, isChannel, NonspatialScaleChannel, ScaleChannel, X} from '../channel'; +import {Channel, COLUMN, isChannel, NonspatialScaleChannel, ScaleChannel, SingleDefChannel, X} from '../channel'; import {CellConfig, Config} from '../config'; import {Data, DataSourceType, MAIN, RAW} from '../data'; import {forEach, reduce} from '../encoding'; -import {ChannelDef, field, FieldDef, FieldRefOption, isFieldDef, isRepeatRef} from '../fielddef'; +import {ChannelDef, field, FieldDef, FieldRefOption, getFieldDef, isFieldDef, isRepeatRef} from '../fielddef'; import {Legend} from '../legend'; import {hasDiscreteDomain, Scale} from '../scale'; import {SortField, SortOrder} from '../sort'; @@ -376,10 +376,10 @@ export abstract class Model { /** Abstract class for UnitModel and FacetModel. Both of which can contain fieldDefs as a part of its own specification. */ export abstract class ModelWithField extends Model { - public abstract fieldDef(channel: Channel): FieldDef; + public abstract fieldDef(channel: SingleDefChannel): FieldDef; /** Get "field" reference for vega */ - public field(channel: Channel, opt: FieldRefOption = {}) { + public field(channel: SingleDefChannel, opt: FieldRefOption = {}) { const fieldDef = this.fieldDef(channel); if (fieldDef.bin) { // bin has default suffix that depends on scaleType @@ -394,20 +394,24 @@ export abstract class ModelWithField extends Model { public abstract hasDiscreteDomain(channel: Channel): boolean; - public abstract channels(): Channel[]; protected abstract getMapping(): {[key: string]: any}; public reduceFieldDef(f: (acc: U, fd: FieldDef, c: Channel) => U, init: T, t?: any) { return reduce(this.getMapping(), (acc:U , cd: ChannelDef, c: Channel) => { - return isFieldDef(cd) ? f(acc, cd, c) : acc; + const fieldDef = getFieldDef(cd); + if (fieldDef) { + return f(acc, fieldDef, c); + } + return acc; }, init, t); } public forEachFieldDef(f: (fd: FieldDef, c: Channel) => void, t?: any) { forEach(this.getMapping(), (cd: ChannelDef, c: Channel) => { - if (isFieldDef(cd)) { - f(cd, c); + const fieldDef = getFieldDef(cd); + if (fieldDef) { + f(fieldDef, c); } }, t); } diff --git a/src/compile/scale/domain.ts b/src/compile/scale/domain.ts index be55a85f33..8ccd94c630 100644 --- a/src/compile/scale/domain.ts +++ b/src/compile/scale/domain.ts @@ -1,6 +1,6 @@ import {SHARED_DOMAIN_OP_INDEX} from '../../aggregate'; import {binToString} from '../../bin'; -import {Channel} from '../../channel'; +import {Channel, ScaleChannel} from '../../channel'; import {MAIN, RAW} from '../../data'; import {DateTime, dateTimeExpr, isDateTime} from '../../datetime'; import {FieldDef} from '../../fielddef'; @@ -42,7 +42,7 @@ export function initDomain(domain: Domain, fieldDef: FieldDef, scale: Sc // FIXME(https://github.com/vega/vega-lite/issues/2251#issuecomment-306683920) // parseDomain must be called separately after parseScale -export function parseDomain(model: UnitModel, channel: Channel): VgDomain { +export function parseDomain(model: UnitModel, channel: ScaleChannel): VgDomain { // FIXME replace this with getScaleComponent once parseScaleDomain a separate parseStep from scale const scaleType = model.scale(channel).type; const domain = model.scaleDomain(channel); @@ -64,7 +64,7 @@ export function parseDomain(model: UnitModel, channel: Channel): VgDomain { return parseSingleChannelDomain(scaleType, domain, model, channel); } -function parseSingleChannelDomain(scaleType: ScaleType, domain: Domain, model: UnitModel, channel:Channel): VgDomain { +function parseSingleChannelDomain(scaleType: ScaleType, domain: Domain, model: UnitModel, channel: ScaleChannel | 'x2' | 'y2'): VgDomain { const fieldDef = model.fieldDef(channel); if (domain && domain !== 'unaggregated' && !isSelectionDomain(domain)) { // explicit value diff --git a/src/compile/scale/parse.ts b/src/compile/scale/parse.ts index d666c4d252..f576adec8b 100644 --- a/src/compile/scale/parse.ts +++ b/src/compile/scale/parse.ts @@ -1,4 +1,4 @@ -import {Channel} from '../../channel'; +import {Channel, SCALE_CHANNELS, ScaleChannel} from '../../channel'; import {isSelectionDomain, Scale} from '../../scale'; import {isSortField} from '../../sort'; import {Dict} from '../../util'; @@ -17,8 +17,7 @@ import {SELECTION_DOMAIN} from '../selection/selection'; * Parse scales for all channels of a model. */ export default function parseScaleComponent(model: UnitModel): ScaleComponentIndex { - // TODO: should model.channels() inlcude X2/Y2? - return model.channels().reduce(function(scaleComponentsIndex: ScaleComponentIndex, channel: Channel) { + return SCALE_CHANNELS.reduce(function(scaleComponentsIndex: ScaleComponentIndex, channel: ScaleChannel) { const scaleComponents = parseScale(model, channel); if (scaleComponents) { scaleComponentsIndex[channel] = scaleComponents; @@ -40,7 +39,7 @@ export const NON_TYPE_DOMAIN_RANGE_VEGA_SCALE_PROPERTIES: (keyof Scale)[] = [ /** * Parse scales for a single channel of a model. */ -export function parseScale(model: UnitModel, channel: Channel) { +export function parseScale(model: UnitModel, channel: ScaleChannel) { if (!model.scale(channel)) { return null; } diff --git a/src/compile/selection/multi.ts b/src/compile/selection/multi.ts index 9fded5c325..7ddbfb87bd 100644 --- a/src/compile/selection/multi.ts +++ b/src/compile/selection/multi.ts @@ -15,8 +15,10 @@ const multi:SelectionCompiler = { const fields = proj.map((p) => stringValue(p.field)).join(', '); const values = proj.map((p) => { const channel = p.encoding; + const fieldDef = model.fieldDef(channel); // Binned fields should capture extents, for a range test against the raw field. - return model.fieldDef(channel).bin ? (bins[p.field] = 1, + // FIXME: Arvind -- please log proper warning when the specified encoding channel has no field + return (fieldDef && fieldDef.bin) ? (bins[p.field] = 1, `[${datum}[${stringValue(model.field(channel, {binSuffix: 'start'}))}], ` + `${datum}[${stringValue(model.field(channel, {binSuffix: 'end'}))}]]`) : `${datum}[${stringValue(p.field)}]`; diff --git a/src/compile/selection/selection.ts b/src/compile/selection/selection.ts index 02f83ca2c9..10cb3527d4 100644 --- a/src/compile/selection/selection.ts +++ b/src/compile/selection/selection.ts @@ -1,5 +1,5 @@ import {selector as parseSelector} from 'vega-event-selector'; -import {Channel} from '../../channel'; +import {Channel, SingleDefChannel} from '../../channel'; import {warn} from '../../log'; import {LogicalOperand} from '../../logical'; import {SelectionDomain} from '../../scale'; @@ -40,7 +40,7 @@ export interface SelectionComponent { export interface ProjectComponent { field?: string; - encoding?: Channel; + encoding?: SingleDefChannel; } export interface SelectionCompiler { diff --git a/src/compile/selection/transforms/project.ts b/src/compile/selection/transforms/project.ts index c70a426122..5d944f3f2e 100644 --- a/src/compile/selection/transforms/project.ts +++ b/src/compile/selection/transforms/project.ts @@ -1,4 +1,5 @@ -import {Channel} from '../../../channel'; +import {Channel, SingleDefChannel} from '../../../channel'; +import * as log from '../../../log'; import {SelectionDef} from '../../../selection'; import {TransformCompiler} from './transforms'; @@ -11,12 +12,16 @@ const project:TransformCompiler = { let fields = {}; // TODO: find a possible channel mapping for these fields. (selDef.fields || []).forEach((field) => fields[field] = null); - (selDef.encodings || []).forEach((channel: Channel) => { + (selDef.encodings || []).forEach((channel: SingleDefChannel) => { const fieldDef = model.fieldDef(channel); - if (fieldDef.timeUnit) { - fields[model.field(channel)] = channel; + if (fieldDef) { + if (fieldDef.timeUnit) { + fields[model.field(channel)] = channel; + } else { + fields[fieldDef.field] = channel; + } } else { - fields[fieldDef.field] = channel; + log.warn(log.message.cannotProjectOnChannelWithoutField(channel)); } }); diff --git a/src/compile/unit.ts b/src/compile/unit.ts index bb160c216b..1a9b4a569c 100644 --- a/src/compile/unit.ts +++ b/src/compile/unit.ts @@ -1,9 +1,9 @@ import {Axis} from '../axis'; -import {Channel, NONSPATIAL_SCALE_CHANNELS, UNIT_CHANNELS, UNIT_SCALE_CHANNELS, X, X2, Y, Y2} from '../channel'; +import {Channel, NONSPATIAL_SCALE_CHANNELS, SingleDefChannel, UNIT_CHANNELS, UNIT_SCALE_CHANNELS, X, X2, Y, Y2} from '../channel'; import {CellConfig, Config} from '../config'; import {Encoding, normalizeEncoding} from '../encoding'; import * as vlEncoding from '../encoding'; // TODO: remove -import {field, FieldDef, FieldRefOption, isFieldDef} from '../fielddef'; +import {ChannelDef, field, FieldDef, FieldRefOption, getFieldDef, isConditionalDef, isFieldDef} from '../fielddef'; import {Legend} from '../legend'; import {FILL_STROKE_CONFIG, isMarkDef, Mark, MarkDef, TEXT as TEXT_MARK} from '../mark'; import {defaultScaleConfig, Domain, hasDiscreteDomain, Scale} from '../scale'; @@ -156,10 +156,13 @@ export class UnitModel extends ModelWithField { if (isFieldDef(channelDef)) { fieldDef = channelDef; specifiedScale = channelDef.scale; + } else if (isConditionalDef(channelDef) && isFieldDef(channelDef.condition)) { + fieldDef = channelDef.condition; + specifiedScale = channelDef.condition.scale; } else if (channel === 'x') { - fieldDef = vlEncoding.getFieldDef(encoding, 'x2'); + fieldDef = getFieldDef(encoding.x2); } else if (channel === 'y') { - fieldDef = vlEncoding.getFieldDef(encoding, 'y2'); + fieldDef = getFieldDef(encoding.y2); } if (fieldDef) { @@ -249,12 +252,15 @@ export class UnitModel extends ModelWithField { private initLegend(encoding: Encoding): Dict { return NONSPATIAL_SCALE_CHANNELS.reduce(function(_legend, channel) { const channelDef = encoding[channel]; - if (isFieldDef(channelDef)) { - const legendSpec = channelDef.legend; - if (legendSpec !== null && legendSpec !== false) { - _legend[channel] = {...legendSpec}; + if (channelDef) { + const legend = isFieldDef(channelDef) ? channelDef.legend : + (channelDef.condition && isFieldDef(channelDef.condition)) ? channelDef.condition.legend : null; + + if (legend !== null && legend !== false) { + _legend[channel] = {...legend}; } } + return _legend; }, {}); } @@ -332,10 +338,6 @@ export class UnitModel extends ModelWithField { }; } - public channels() { - return UNIT_CHANNELS; - } - protected getMapping() { return this.encoding; } @@ -369,16 +371,19 @@ export class UnitModel extends ModelWithField { return vlEncoding.channelHasField(this.encoding, channel); } - public fieldDef(channel: Channel): FieldDef { - // TODO: remove this || {} - // Currently we have it to prevent null pointer exception. - return this.encoding[channel] || {}; + public fieldDef(channel: SingleDefChannel): FieldDef { + const channelDef = this.encoding[channel] as ChannelDef; + return getFieldDef(channelDef); } /** Get "field" reference for vega */ - public field(channel: Channel, opt: FieldRefOption = {}) { + public field(channel: SingleDefChannel, opt: FieldRefOption = {}) { const fieldDef = this.fieldDef(channel); + if (!fieldDef) { + return undefined; + } + if (fieldDef.bin) { // bin has default suffix that depends on scaleType opt = extend({ binSuffix: hasDiscreteDomain(this.scale(channel).type) ? 'range' : 'start' diff --git a/src/encoding.ts b/src/encoding.ts index 73f2e85772..a64437d448 100644 --- a/src/encoding.ts +++ b/src/encoding.ts @@ -5,19 +5,23 @@ import {CompositeAggregate} from './compositemark'; import {Facet} from './facet'; import { ChannelDef, - ConditionalValueDef, + Condition, + Conditional, Field, FieldDef, + getFieldDef, + hasConditionFieldDef, + isConditionalDef, isFieldDef, isValueDef, LegendFieldDef, normalize, + normalizeFieldDef, OrderFieldDef, PositionFieldDef, TextFieldDef, ValueDef } from './fielddef'; -import {normalizeFieldDef} from './fielddef'; import * as log from './log'; import {Mark} from './mark'; import {isArray, some} from './util'; @@ -56,12 +60,12 @@ export interface Encoding { * (By default, fill color for `area`, `bar`, `tick`, `text`, `circle`, and `square` / * stroke color for `line` and `point`.) */ - color?: LegendFieldDef | ConditionalValueDef; + color?: Conditional, ValueDef>; /** * Opacity of the marks – either can be a value or a range. */ - opacity?: LegendFieldDef | ConditionalValueDef; + opacity?: Conditional, ValueDef>; /** * Size of the mark. @@ -71,14 +75,14 @@ export interface Encoding { * - For `text` – the text's font size. * - Size is currently unsupported for `line` and `area`. */ - size?: LegendFieldDef | ConditionalValueDef; + size?: Conditional, ValueDef>; /** * The symbol's shape (only for `point` marks). The supported values are * `"circle"` (default), `"square"`, `"cross"`, `"diamond"`, `"triangle-up"`, * or `"triangle-down"`, or else a custom SVG path string. */ - shape?: LegendFieldDef | ConditionalValueDef; // TODO: maybe distinguish ordinal-only + shape?: Conditional, ValueDef>; // TODO: maybe distinguish ordinal-only /** * Additional levels of detail for grouping data in aggregate views and @@ -89,12 +93,12 @@ export interface Encoding { /** * Text of the `text` mark. */ - text?: TextFieldDef | ConditionalValueDef; + text?: Conditional, ValueDef>; /** * The tooltip text to show upon mouse hover. */ - tooltip?: TextFieldDef | ConditionalValueDef; + tooltip?: Conditional, ValueDef>; /** * stack order for stacked marks or order of data points in line marks. @@ -110,23 +114,12 @@ export function channelHasField(encoding: EncodingWithFacet, channel: Cha if (isArray(channelDef)) { return some(channelDef, (fieldDef) => !!fieldDef.field); } else { - return isFieldDef(channelDef); + return isFieldDef(channelDef) || hasConditionFieldDef(channelDef); } } return false; } -export function getFieldDef(encoding: EncodingWithFacet, channel: Channel): FieldDef { - const channelDef = encoding[channel]; - if (isArray(channelDef)) { - throw new Error('getFieldDef should be never used with detail or order when they have multiple fields'); - } else if (isFieldDef(channelDef)) { - return channelDef; - } - // TODO: if hasConditionFieldDef - - return undefined; -} export function isAggregate(encoding: EncodingWithFacet) { return some(CHANNELS, (channel) => { @@ -135,7 +128,8 @@ export function isAggregate(encoding: EncodingWithFacet) { if (isArray(channelDef)) { return some(channelDef, (fieldDef) => !!fieldDef.aggregate); } else { - return isFieldDef(channelDef) && !!channelDef.aggregate; + const fieldDef = getFieldDef(channelDef); + return fieldDef && !!fieldDef.aggregate; } } return false; @@ -153,8 +147,8 @@ export function normalizeEncoding(encoding: Encoding, mark: Mark): Encod // Drop line's size if the field is aggregated. if (channel === 'size' && mark === 'line') { - const channelDef = encoding[channel]; - if (isFieldDef(channelDef) && channelDef.aggregate) { + const fieldDef = getFieldDef(encoding[channel]); + if (fieldDef && fieldDef.aggregate) { log.warn(log.message.incompatibleChannel(channel, mark, 'when the field is aggregated.')); return normalizedEncoding; } @@ -177,7 +171,7 @@ export function normalizeEncoding(encoding: Encoding, mark: Mark): Encod } else { // FIXME: remove this casting. (I don't know why Typescript doesn't infer this correctly here.) const channelDef = encoding[channel] as ChannelDef; - if (!isFieldDef(channelDef) && !isValueDef(channelDef)) { + if (!isFieldDef(channelDef) && !isValueDef(channelDef) && !isConditionalDef(channelDef)) { log.warn(log.message.emptyFieldDef(channelDef, channel)); return normalizedEncoding; } @@ -200,6 +194,8 @@ export function fieldDefs(encoding: EncodingWithFacet): FieldDef[] (isArray(channelDef) ? channelDef : [channelDef]).forEach((def) => { if (isFieldDef(def)) { arr.push(def); + } else if (hasConditionFieldDef(def)) { + arr.push(def.condition); } }); } diff --git a/src/fielddef.ts b/src/fielddef.ts index 3d6156b0ef..72bf88f08f 100644 --- a/src/fielddef.ts +++ b/src/fielddef.ts @@ -27,10 +27,41 @@ export interface ValueDef { value: T; } -export interface ConditionalValueDef extends ValueDef { - condition?: Condition; +/** + * Generic type for conditional channelDef. + * F defines the underlying FieldDef type while V defines the underlying ValueDef type. + */ +export type Conditional, V extends ValueDef> = ConditionalFieldDef | ConditionalValueDef | ConditionOnlyDef; + + +export type Condition = { + selection: LogicalOperand; +} & T; + +/** + * A FieldDef with Condition + * { + * condition: {value: ...}, + * field: ..., + * ... + * } + */ +export type ConditionalFieldDef, V extends ValueDef> = F & {condition?: Condition}; + +export interface ConditionOnlyDef , V extends ValueDef> { + condition: Condition | Condition; } + +/** + * A ValueDef with Condition + * { + * condition: {field: ...} | {value: ...}, + * value: ..., + * } + */ +export type ConditionalValueDef, V extends ValueDef> = V & {condition?: Condition | Condition;}; + /** * Reference to a repeated value. */ @@ -92,11 +123,6 @@ export interface FieldDef extends FieldDefBase { type: Type; } -export interface Condition { - selection: LogicalOperand; - value: T; -} - export interface ScaleFieldDef extends FieldDef { scale?: Scale; /** @@ -122,13 +148,12 @@ export interface PositionFieldDef extends ScaleFieldDef { */ stack?: StackOffset; } -export interface LegendFieldDef extends ScaleFieldDef { + +export interface LegendFieldDef extends ScaleFieldDef { /** * @nullable */ legend?: Legend; - - condition?: Condition; } // Detail @@ -141,17 +166,26 @@ export interface OrderFieldDef extends FieldDef { export interface TextFieldDef extends FieldDef { // FIXME: add more reference to Vega's format pattern or d3's format pattern. - condition?: Condition; - /** * The formatting pattern for text value. If not defined, this will be determined automatically. */ format?: string; } -export type ChannelDef = FieldDef | ValueDef; +export type ChannelDef = Conditional, ValueDef>; + +export function isConditionalDef(channelDef: ChannelDef): channelDef is Conditional, ValueDef> { + return !!channelDef && !!channelDef.condition; +} + +/** + * Return if a channelDef is a ConditionalValueDef with ConditionFieldDef + */ +export function hasConditionFieldDef(channelDef: ChannelDef): channelDef is (ValueDef & {condition: FieldDef}) { + return !!channelDef && !!channelDef.condition && isFieldDef(channelDef.condition); +} -export function isFieldDef(channelDef: ChannelDef): channelDef is FieldDef | PositionFieldDef | LegendFieldDef | OrderFieldDef | TextFieldDef { +export function isFieldDef(channelDef: ChannelDef): channelDef is FieldDef | PositionFieldDef | LegendFieldDef | OrderFieldDef | TextFieldDef { return !!channelDef && (!!channelDef['field'] || channelDef['aggregate'] === 'count'); } @@ -272,13 +306,32 @@ export function defaultType(fieldDef: FieldDef, channel: Channel): Type { } } +/** + * Returns the fieldDef -- either from the outer channelDef or from the condition of channelDef. + * @param channelDef + */ +export function getFieldDef(channelDef: ChannelDef): FieldDef { + if (isFieldDef(channelDef)) { + return channelDef; + } else if (hasConditionFieldDef(channelDef)) { + return channelDef.condition; + } + return undefined; +} + /** * Convert type to full, lowercase type, or augment the fieldDef with a default type if missing. */ -export function normalize(channelDef: ChannelDef, channel: Channel) { +export function normalize(channelDef: ChannelDef, channel: Channel): ChannelDef { // If a fieldDef contains a field, we need type. if (isFieldDef(channelDef)) { return normalizeFieldDef(channelDef, channel); + } else if (hasConditionFieldDef(channelDef)) { + return { + ...channelDef, + // Need to cast as normalizeFieldDef normally return FieldDef, but here we know that it is definitely Condition + condition: normalizeFieldDef(channelDef.condition, channel) as Condition> + }; } return channelDef; } @@ -323,7 +376,6 @@ export function normalizeFieldDef(fieldDef: FieldDef, channel: Channel) log.warn(warning); } return fieldDef; - } export function normalizeBin(bin: Bin|boolean, channel: Channel) { diff --git a/src/log.ts b/src/log.ts index c287ec6143..dd0af6ac41 100644 --- a/src/log.ts +++ b/src/log.ts @@ -99,6 +99,11 @@ export function debug(..._: any[]) { export namespace message { export const INVALID_SPEC = 'Invalid spec'; + // SELECTION + export function cannotProjectOnChannelWithoutField(channel: Channel) { + return `Cannot project a selection on encoding channel ${channel}, which has no field.`; + } + // REPEAT export function noSuchRepeatedValue(field: string) { return `Unknown repeated value "${field}".`; diff --git a/src/selection.ts b/src/selection.ts index 2aa4d41bde..7da4a0e225 100644 --- a/src/selection.ts +++ b/src/selection.ts @@ -1,3 +1,4 @@ +import {SingleDefChannel} from './channel'; import {VgBinding} from './vega.schema'; export type SelectionTypes = 'single' | 'multi' | 'interval'; @@ -13,7 +14,7 @@ export interface BaseSelectionDef { // Transforms fields?: string[]; - encodings?: string[]; + encodings?: SingleDefChannel[]; toggle?: string | boolean; translate?: string | boolean; zoom?: string | boolean; diff --git a/src/stack.ts b/src/stack.ts index 85a4bd2796..f84cd21e96 100644 --- a/src/stack.ts +++ b/src/stack.ts @@ -3,7 +3,7 @@ import * as log from './log'; import {SUM_OPS} from './aggregate'; import {Channel, STACK_GROUP_CHANNELS, X, X2, Y, Y2} from './channel'; import {channelHasField, Encoding, isAggregate} from './encoding'; -import {Field, FieldDef, isFieldDef, PositionFieldDef} from './fielddef'; +import {Field, FieldDef, getFieldDef, isFieldDef, PositionFieldDef} from './fielddef'; import {AREA, BAR, CIRCLE, isMarkDef, LINE, Mark, MarkDef, POINT, RULE, SQUARE, TEXT, TICK} from './mark'; import {ScaleType} from './scale'; import {contains, isArray} from './util'; @@ -57,8 +57,9 @@ export function stack(m: Mark | MarkDef, encoding: Encoding, stackConfig: const stackBy = STACK_GROUP_CHANNELS.reduce((sc, channel) => { if (channelHasField(encoding, channel)) { const channelDef = encoding[channel]; - (isArray(channelDef) ? channelDef : [channelDef]).forEach((fieldDef) => { - if (isFieldDef(fieldDef) && !fieldDef.aggregate) { + (isArray(channelDef) ? channelDef : [channelDef]).forEach((cDef) => { + const fieldDef = getFieldDef(cDef); + if (!fieldDef.aggregate) { sc.push({ channel: channel, fieldDef: fieldDef diff --git a/test/channel.test.ts b/test/channel.test.ts index 64407a342c..757af38847 100644 --- a/test/channel.test.ts +++ b/test/channel.test.ts @@ -1,5 +1,5 @@ import {assert} from 'chai'; -import {Channel, hasScale, rangeType, supportScaleType} from '../src/channel'; +import {Channel, hasScale, rangeType, SINGLE_DEF_CHANNELS, supportScaleType} from '../src/channel'; import {CHANNELS, NONSPATIAL_CHANNELS, NONSPATIAL_SCALE_CHANNELS, SCALE_CHANNELS, UNIT_CHANNELS, UNIT_SCALE_CHANNELS} from '../src/channel'; import {SCALE_TYPES, ScaleType} from '../src/scale'; import {some, without} from '../src/util'; @@ -12,6 +12,12 @@ describe('channel', () => { }); }); + describe('SINGLE_DEF_CHANNELS', () => { + it('should be CHANNELS without detail and order', () => { + assert.deepEqual(SINGLE_DEF_CHANNELS, without(CHANNELS, ['detail', 'order'])); + }); + }); + describe('UNIT_SCALE_CHANNELS', () => { it('should be UNIT_CHANNELS without X2, Y2, ORDER, DETAIL, TEXT, LABEL, TOOLTIP', () => { assert.deepEqual(UNIT_SCALE_CHANNELS, without(UNIT_CHANNELS, ['x2', 'y2', 'order', 'detail', 'text', 'label', 'tooltip'])); diff --git a/test/compile/axis/rules.test.ts b/test/compile/axis/rules.test.ts index bf9f7d3949..4a6cc24594 100644 --- a/test/compile/axis/rules.test.ts +++ b/test/compile/axis/rules.test.ts @@ -25,27 +25,9 @@ describe('compile/axis', ()=> { } }), X, true); assert.deepEqual(grid, true); - }); - it('should return undefined for COLUMN', function () { - const grid = rules.grid(parseUnitModel({ - mark: "point", - encoding: { - x: {field: 'a', type: 'quantitative'} - } - }), COLUMN, true); - assert.deepEqual(grid, false); }); - it('should return undefined for ROW', function () { - const grid = rules.grid(parseUnitModel({ - mark: "point", - encoding: { - x: {field: 'a', type: 'quantitative'} - } - }), ROW, true); - assert.deepEqual(grid, false); - }); it('should return undefined for non-gridAxis', function () { const grid = rules.grid(parseUnitModel({ mark: "point", diff --git a/test/compile/data/aggregate.test.ts b/test/compile/data/aggregate.test.ts index 18857c19e9..6d42ee1236 100644 --- a/test/compile/data/aggregate.test.ts +++ b/test/compile/data/aggregate.test.ts @@ -3,6 +3,7 @@ import {assert} from 'chai'; import {AggregateNode} from '../../../src/compile/data/aggregate'; +import {Condition} from '../../../src/fielddef'; import {SummarizeTransform} from '../../../src/transform'; import {StringSet} from '../../../src/util'; import {VgAggregateTransform} from '../../../src/vega.schema'; @@ -79,6 +80,28 @@ describe('compile/data/summary', function () { }); }); + it('should include conditional field in the summary component', function() { + const model = parseUnitModel({ + mark: "point", + encoding: { + 'x': {'aggregate': 'mean', 'field': 'Displacement', 'type': "quantitative"}, + color: { + condition: {selection: 'a', field: 'Origin', 'type': "ordinal"}, + value: 'red' + } + } + }); + + const agg = AggregateNode.makeFromEncoding(model); + assert.deepEqual(agg.assemble(), { + type: 'aggregate', + groupby: ['Origin'], + ops: ['mean'], + fields: ['Displacement'], + as: ['mean_Displacement'] + }); + }); + it('should add min and max if needed for unaggregated scale domain', function() { const model = parseUnitModel({ mark: "point", diff --git a/test/compile/scale/domain.test.ts b/test/compile/scale/domain.test.ts index 5c20b8652d..16092bb2b8 100644 --- a/test/compile/scale/domain.test.ts +++ b/test/compile/scale/domain.test.ts @@ -44,6 +44,20 @@ describe('compile/scale', () => { assert.deepEqual(xDomain, {data: 'main', field: 'a'}); }); + it('should have correct domain for color ConditionField', function() { + const model = parseUnitModel({ + mark: 'bar', + encoding: { + color: { + condition: {selection: 'sel', field: 'a', type: 'quantitative'} + } + } + }); + + const xDomain = parseDomain(model, 'color'); + assert.deepEqual(xDomain, {data: 'main', field: 'a'}); + }); + it('should return domain for stack', function() { const model = parseUnitModel({ mark: "bar",