diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e74b7bfcba..a45dc8c871 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -22,7 +22,7 @@ You can find [tasks with the "Help wanted" label in the issue tracker](https://g ### Help Create New Examples -To submit a new example, fork our [example Block](https://bl.ocks.org/domoritz/455e1c7872c4b38a58b90df0c3d7b1b9) and send us a [pull request to add a link](https://github.com/vega/vega-lite/edit/master/site/examples/gallery.md) to it to our [example gallery](https://vega.github.io/vega-lite/examples/). +To submit a new example, fork our [example Block](https://bl.ocks.org/domoritz/455e1c7872c4b38a58b90df0c3d7b1b9) and send us a [pull request to add a link](https://github.com/vega/vega-lite/edit/master/site/examples/index.md) to it to our [example gallery](https://vega.github.io/vega-lite/examples/). ## Documentation and Website diff --git a/_data/link.yml b/_data/link.yml index 75e11b111c..c19c3f6d07 100644 --- a/_data/link.yml +++ b/_data/link.yml @@ -50,6 +50,8 @@ TimeUnit: link: timeunit.html LookupData: link: "lookup.html#lookup-data" +Window: + link: window.html # Transform - Time Unit Month: @@ -57,6 +59,14 @@ Month: Day: name: Number +# Transform - Window +WindowFieldDef: + link: window.html#field-def +WindowSortField: + link: window.html#sort-field-def +WindowOnlyOp: + link: window.html#ops + # Mark AnyMark: name: Mark diff --git a/_includes/docs_toc.md b/_includes/docs_toc.md index 44879bf926..a136b2e511 100644 --- a/_includes/docs_toc.md +++ b/_includes/docs_toc.md @@ -52,6 +52,12 @@ - [Time Unit in Encoding Field Definition]({{site.baseurl}}/docs/timeunit.html#encoding) - [Time Unit Transform]({{site.baseurl}}/docs/timeunit.html#transform) - [UTC time]({{site.baseurl}}/docs/timeunit.html#utc) + - [Window]({{site.baseurl}}/docs/window.html) + - [Window Field Definition]({{site.baseurl}}/docs/window.html#window-field-definition) + - [Window Transform Definition]({{site.baseurl}}/docs/window.html#window-transform-definition) + - [ + Window Only Operation Reference]({{site.baseurl}}/docs/window.html#-window-only-operation-reference) + - [Examples]({{site.baseurl}}/docs/window.html#examples) - [Mark]({{site.baseurl}}/docs/mark.html) - [Documentation Overview]({{site.baseurl}}/docs/mark.html#documentation-overview) - [Mark Definition Object]({{site.baseurl}}/docs/mark.html#mark-def) @@ -59,64 +65,51 @@ - [Mark Style Config]({{site.baseurl}}/docs/mark.html#style-config) - [Area]({{site.baseurl}}/docs/area.html) - [Documentation Overview]({{site.baseurl}}/docs/area.html#documentation-overview) - - [Area Chart]({{site.baseurl}}/docs/area.html#area-chart) - - [Stacked Area Chart]({{site.baseurl}}/docs/area.html#stacked-area-chart) - - [Normalized Stacked Area Chart]({{site.baseurl}}/docs/area.html#normalized-stacked-area-chart) - - [Streamgraph]({{site.baseurl}}/docs/area.html#streamgraph) - - [Ranged Area]({{site.baseurl}}/docs/area.html#ranged) + - [Area Mark Properties]({{site.baseurl}}/docs/area.html#properties) + - [Examples]({{site.baseurl}}/docs/area.html#examples) - [Area Config]({{site.baseurl}}/docs/area.html#config) - [Bar]({{site.baseurl}}/docs/bar.html) - [Documentation Overview]({{site.baseurl}}/docs/bar.html#documentation-overview) - - [Single Bar Chart]({{site.baseurl}}/docs/bar.html#single-bar-chart) - - [Bar Chart]({{site.baseurl}}/docs/bar.html#bar-chart) - - [Histogram]({{site.baseurl}}/docs/bar.html#histogram) - - [Stacked Bar Chart]({{site.baseurl}}/docs/bar.html#stack) - - [Layered Bar Chart]({{site.baseurl}}/docs/bar.html#layered-bar-chart) - - [Normalized Stacked Bar Chart]({{site.baseurl}}/docs/bar.html#normalized-stacked-bar-chart) - - [Grouped Bar Chart]({{site.baseurl}}/docs/bar.html#grouped-bar-chart) - - [Ranged Bars]({{site.baseurl}}/docs/bar.html#ranged) + - [Bar Mark Properties]({{site.baseurl}}/docs/bar.html#properties) + - [Examples]({{site.baseurl}}/docs/bar.html#examples) - [Bar Config]({{site.baseurl}}/docs/bar.html#config) - [Circle]({{site.baseurl}}/docs/circle.html) - - [Scatterplot with Circle]({{site.baseurl}}/docs/circle.html#scatterplot-with-circle) + - [Circle Mark Properties]({{site.baseurl}}/docs/circle.html#properties) + - [Examples]({{site.baseurl}}/docs/circle.html#examples) - [Circle Config]({{site.baseurl}}/docs/circle.html#config) - [Line]({{site.baseurl}}/docs/line.html) - [Documentation Overview]({{site.baseurl}}/docs/line.html#documentation-overview) - - [Line Chart]({{site.baseurl}}/docs/line.html#line-chart) - - [Multi-series Line Chart with the Detail Channel]({{site.baseurl}}/docs/line.html#line-detail) - - [Connected Scatter Plot (Line Chart with Custom Path)]({{site.baseurl}}/docs/line.html#connected-scatter-plot) - - [Line interpolation]({{site.baseurl}}/docs/line.html#line-interpolation) - - [Geo Line]({{site.baseurl}}/docs/line.html#geo-line) + - [Line Mark Properties]({{site.baseurl}}/docs/line.html#properties) + - [Examples]({{site.baseurl}}/docs/line.html#examples) - [Line Config]({{site.baseurl}}/docs/line.html#config) - [Point]({{site.baseurl}}/docs/point.html) - [Documentation Overview]({{site.baseurl}}/docs/point.html#documentation-overview) - - [Dot Plot]({{site.baseurl}}/docs/point.html#dot-plot) - - [Scatter Plot]({{site.baseurl}}/docs/point.html#scatter-plot) - - [Bubble Plot]({{site.baseurl}}/docs/point.html#bubble-plot) - - [Scatter Plot with Color and/or Shape]({{site.baseurl}}/docs/point.html#color) - - [Geo Point]({{site.baseurl}}/docs/point.html#geo-point) + - [Point Mark Properties]({{site.baseurl}}/docs/point.html#properties) + - [Examples]({{site.baseurl}}/docs/point.html#examples) - [Point Config]({{site.baseurl}}/docs/point.html#config) - [Rect]({{site.baseurl}}/docs/rect.html) - [Documentation Overview]({{site.baseurl}}/docs/rect.html#documentation-overview) + - [Rect Mark Properties]({{site.baseurl}}/docs/rect.html#rect-mark-properties) + - [Examples]({{site.baseurl}}/docs/rect.html#examples) - [Rect Config]({{site.baseurl}}/docs/rect.html#config) - [Rule]({{site.baseurl}}/docs/rule.html) - [Documentation Overview]({{site.baseurl}}/docs/rule.html#documentation-overview) - - [Width/Height-Spanning Rules]({{site.baseurl}}/docs/rule.html#widthheight-spanning-rules) - - [Ranged Rules]({{site.baseurl}}/docs/rule.html#ranged) + - [Rule Mark Properties]({{site.baseurl}}/docs/rule.html#properties) + - [Examples]({{site.baseurl}}/docs/rule.html#examples) - [Rule Config]({{site.baseurl}}/docs/rule.html#config) - [Square]({{site.baseurl}}/docs/square.html) - - [Scatterplot with Square]({{site.baseurl}}/docs/square.html#scatterplot-with-square) + - [Square Mark Properties]({{site.baseurl}}/docs/square.html#properties) + - [Example: Scatterplot with Square]({{site.baseurl}}/docs/square.html#example-scatterplot-with-square) - [Square Config]({{site.baseurl}}/docs/square.html#config) - [Text]({{site.baseurl}}/docs/text.html) - [Documentation Overview]({{site.baseurl}}/docs/text.html#documentation-overview) - - [Text Table Heatmap]({{site.baseurl}}/docs/text.html#text-table-heatmap) - - [Labels]({{site.baseurl}}/docs/text.html#labels) - - [Scatterplot with Text]({{site.baseurl}}/docs/text.html#scatterplot-with-text) - - [Geo Text]({{site.baseurl}}/docs/text.html#geo-text) + - [Text Mark Properties]({{site.baseurl}}/docs/text.html#properties) + - [Examples]({{site.baseurl}}/docs/text.html#examples) - [Text Config]({{site.baseurl}}/docs/text.html#config) - [Tick]({{site.baseurl}}/docs/tick.html) - [Documentation Overview]({{site.baseurl}}/docs/tick.html#documentation-overview) - - [Dot Plot]({{site.baseurl}}/docs/tick.html#dot-plot) - - [Strip Plot]({{site.baseurl}}/docs/tick.html#strip-plot) + - [Tick Mark Properties]({{site.baseurl}}/docs/tick.html#properties) + - [Examples]({{site.baseurl}}/docs/tick.html#examples) - [Tick Config]({{site.baseurl}}/docs/tick.html#config) - [Geoshape]({{site.baseurl}}/docs/geoshape.html) - [Geoshape Config]({{site.baseurl}}/docs/geoshape.html#config) diff --git a/_layouts/docs.html b/_layouts/docs.html index 0534fd144a..790a9b0e4f 100644 --- a/_layouts/docs.html +++ b/_layouts/docs.html @@ -30,6 +30,8 @@ url: lookup - text: Time Unit url: timeunit + - text: Window + url: window - text: Mark url: mark sub-sidebar: diff --git a/build/vega-lite-schema.json b/build/vega-lite-schema.json index ddbb87b4e4..4762a93060 100644 --- a/build/vega-lite-schema.json +++ b/build/vega-lite-schema.json @@ -7314,11 +7314,11 @@ "additionalProperties": false, "properties": { "as": { - "description": "The output name for each field. If none is defined will use the format op_field. For example, count_field for count,\n and sum_field for sum.", + "description": "The output name for each field.", "type": "string" }, "field": { - "description": "The data fields for which to compute aggregate or window functions. Field can be omitted for operations that do not\noperate over a specific data field, including count, rank, and dense_rank.", + "description": "The data field for which to compute the aggregate or window function. This can be omitted for functions that do not operate over a field such as `count`, `rank`, `dense_rank`.", "type": "string" }, "op": { @@ -7330,15 +7330,16 @@ "$ref": "#/definitions/WindowOnlyOp" } ], - "description": "The operations supported for the window aggregation. See the list of supported operations here:\n https://vega.github.io/vega-lite/docs/transforms/window.html" + "description": "The window or aggregation operations to apply within a window, including `rank`, `lead`, `sum`, `average` or `count`. See the list of all supported operations [here](https://vega.github.io/vega-lite/docs/transforms/window.html)." }, "param": { - "description": "Parameter values for the window functions. Parameter value can be null for operations that do not accept a\nparameter.", + "description": "Parameter values for the window functions. Parameter values can be omitted for operations that do not accept a parameter.\n\nSee the list of all supported operations and their parameters [here](https://vega.github.io/vega-lite/docs/transforms/window.html).", "type": "number" } }, "required": [ - "op" + "op", + "as" ], "type": "object" }, @@ -7363,9 +7364,11 @@ "description": "A compartor for fields within the window transform", "properties": { "field": { + "description": "The name of the field to sort.", "type": "string" }, "order": { + "description": "Whether to sort the field in ascending or descending order.", "enum": [ "ascending", "descending" @@ -7382,7 +7385,7 @@ "additionalProperties": false, "properties": { "frame": { - "description": "The frame for the window, if none is set the default is `[null, 0]` everything before the\ncurrent item.", + "description": "A frame specification as a two-element array indicating how the sliding window should proceed. The array entries should either be a number indicating the offset from the current data object, or null to indicate unbounded rows preceding or following the current data object. The default value is `[null, 0]`, indicating that the sliding window includes the current object and all preceding objects. The value `[-5, 5]` indicates that the window should include five objects preceding and five objects following the current object. Finally, `[null, null]` indicates that the window frame should always include all data objects.\n\n__Default value:__: `[null, 0]` (includes the current object and all preceding objects)", "items": { "type": [ "null", @@ -7392,18 +7395,18 @@ "type": "array" }, "groupby": { - "description": "The fields to group by.", + "description": "The data fields for partitioning the data objects into separate windows. If unspecified, all data points will be a single group.", "items": { "type": "string" }, "type": "array" }, "ignorePeers": { - "description": "Will indicate whether to ignore peer values (items with the same rank) in the window. The default value is `False`.", + "description": "Indicates if the sliding window frame should ignore peer values. (Peer values are those considered identical by the sort criteria). The default is false, causing the window frame to expand to include all peer values. If set to true, the window frame will be defined by offset values only. This setting only affects those operations that depend on the window frame, namely aggregation operations and the first_value, last_value, and nth_value window operations.\n\n__Default value:__ `false`", "type": "boolean" }, "sort": { - "description": "The definitions of how to sort each of the fields in the window.", + "description": "A comparator definition for sorting data objects within a window. If two data objects are considered equal by the comparator, they are considered “peer” values of equal rank. If sort is not specified, the order is undefined: data objects are processed in the order they are observed and none are considered peers (the ignorePeers parameter is ignored and treated as if set to `true`).", "items": { "$ref": "#/definitions/WindowSortField" }, diff --git a/examples/compiled/area_horizon.vg.json b/examples/compiled/area_horizon.vg.json index 07fc64b07b..fc8725269c 100644 --- a/examples/compiled/area_horizon.vg.json +++ b/examples/compiled/area_horizon.vg.json @@ -168,9 +168,8 @@ "field": "y" }, "y2": { - "field": { - "group": "height" - } + "scale": "y", + "value": 0 } } } @@ -209,9 +208,8 @@ "field": "ny" }, "y2": { - "field": { - "group": "height" - } + "scale": "y", + "value": 0 } } } diff --git a/examples/compiled/circle_natural_disasters.vg.json b/examples/compiled/circle_natural_disasters.vg.json index 5bc51449d2..948295e0c6 100644 --- a/examples/compiled/circle_natural_disasters.vg.json +++ b/examples/compiled/circle_natural_disasters.vg.json @@ -134,7 +134,6 @@ "scale": "x", "orient": "bottom", "title": "Year", - "labelOverlap": true, "encode": { "labels": { "update": { @@ -147,6 +146,7 @@ } } }, + "labelOverlap": true, "zindex": 1 }, { diff --git a/examples/compiled/layer_bar_annotations.vg.json b/examples/compiled/layer_bar_annotations.vg.json index 093f591cff..c65b3035bf 100644 --- a/examples/compiled/layer_bar_annotations.vg.json +++ b/examples/compiled/layer_bar_annotations.vg.json @@ -360,7 +360,6 @@ "scale": "x", "orient": "bottom", "title": "Day", - "labelOverlap": true, "encode": { "labels": { "update": { @@ -373,6 +372,7 @@ } } }, + "labelOverlap": true, "zindex": 1 }, { diff --git a/examples/compiled/window_activities.svg b/examples/compiled/window_activities.svg new file mode 100644 index 0000000000..0f017c3c72 --- /dev/null +++ b/examples/compiled/window_activities.svg @@ -0,0 +1 @@ +05101520253035% of total TimeEatingExerciseSleepingTVWorkActivity \ No newline at end of file diff --git a/examples/compiled/window_activities.vg.json b/examples/compiled/window_activities.vg.json new file mode 100644 index 0000000000..a0c4bd2ccb --- /dev/null +++ b/examples/compiled/window_activities.vg.json @@ -0,0 +1,191 @@ +{ + "$schema": "https://vega.github.io/schema/vega/v3.0.json", + "description": "A bar graph showing what activites consume what percentage of the day.", + "autosize": "pad", + "padding": 5, + "width": 200, + "style": "cell", + "data": [ + { + "name": "source_0", + "values": [ + { + "Activity": "Sleeping", + "Time": 8 + }, + { + "Activity": "Eating", + "Time": 2 + }, + { + "Activity": "TV", + "Time": 4 + }, + { + "Activity": "Work", + "Time": 8 + }, + { + "Activity": "Exercise", + "Time": 2 + } + ] + }, + { + "name": "data_0", + "source": "source_0", + "transform": [ + { + "type": "window", + "params": [ + null + ], + "as": [ + "TotalTime" + ], + "ops": [ + "sum" + ], + "fields": [ + "Time" + ], + "sort": { + "field": [], + "order": [] + }, + "frame": [ + null, + null + ] + }, + { + "type": "formula", + "expr": "datum.Time/datum.TotalTime * 100", + "as": "PercentOfTotal" + }, + { + "type": "filter", + "expr": "datum[\"PercentOfTotal\"] !== null && !isNaN(datum[\"PercentOfTotal\"])" + } + ] + } + ], + "signals": [ + { + "name": "y_step", + "value": 12 + }, + { + "name": "height", + "update": "bandspace(domain('y').length, 0.1, 0.05) * y_step" + } + ], + "marks": [ + { + "name": "marks", + "type": "rect", + "style": [ + "bar" + ], + "from": { + "data": "data_0" + }, + "encode": { + "update": { + "fill": { + "value": "#4c78a8" + }, + "x": { + "scale": "x", + "field": "PercentOfTotal" + }, + "x2": { + "scale": "x", + "value": 0 + }, + "y": { + "scale": "y", + "field": "Activity" + }, + "height": { + "scale": "y", + "band": true + } + } + } + } + ], + "scales": [ + { + "name": "x", + "type": "linear", + "domain": { + "data": "data_0", + "field": "PercentOfTotal" + }, + "range": [ + 0, + { + "signal": "width" + } + ], + "nice": true, + "zero": true + }, + { + "name": "y", + "type": "band", + "domain": { + "data": "data_0", + "field": "Activity", + "sort": true + }, + "range": { + "step": { + "signal": "y_step" + } + }, + "paddingInner": 0.1, + "paddingOuter": 0.05 + } + ], + "axes": [ + { + "scale": "x", + "orient": "bottom", + "title": "% of total Time", + "labelFlush": true, + "labelOverlap": true, + "tickCount": { + "signal": "ceil(width/40)" + }, + "zindex": 1 + }, + { + "scale": "x", + "orient": "bottom", + "grid": true, + "tickCount": { + "signal": "ceil(width/40)" + }, + "gridScale": "y", + "domain": false, + "labels": false, + "maxExtent": 0, + "minExtent": 0, + "ticks": false, + "zindex": 0 + }, + { + "scale": "y", + "orient": "left", + "title": "Activity", + "zindex": 1 + } + ], + "config": { + "axisY": { + "minExtent": 30 + } + } +} diff --git a/examples/compiled/window_mean_difference.svg b/examples/compiled/window_mean_difference.svg new file mode 100644 index 0000000000..256e171c36 --- /dev/null +++ b/examples/compiled/window_mean_difference.svg @@ -0,0 +1 @@ +0246810IMDB Rating12 Angry MenC'era una volta il WestCasablancaCidade de DeusFight ClubGoodfellasInceptionOne Flew Over the Cuckoo's NestPulp FictionSchindler's ListShichinin no samuraiThe Dark KnightThe GodfatherThe Godfather: Part IIThe Lord of the Rings: The Fellowship…The Lord of the Rings: The Return of t…The Shawshank RedemptionToy Story 3Title \ No newline at end of file diff --git a/examples/compiled/window_mean_difference.vg.json b/examples/compiled/window_mean_difference.vg.json new file mode 100644 index 0000000000..e8ddf2840e --- /dev/null +++ b/examples/compiled/window_mean_difference.vg.json @@ -0,0 +1,240 @@ +{ + "$schema": "https://vega.github.io/schema/vega/v3.0.json", + "autosize": "pad", + "padding": 5, + "width": 200, + "style": "cell", + "data": [ + { + "name": "source_0", + "url": "data/movies.json", + "format": { + "type": "json" + }, + "transform": [ + { + "type": "window", + "params": [ + null + ], + "as": [ + "AverageRating" + ], + "ops": [ + "mean" + ], + "fields": [ + "IMDB_Rating" + ], + "sort": { + "field": [], + "order": [] + }, + "frame": [ + null, + null + ] + }, + { + "type": "filter", + "expr": "(datum.IMDB_Rating - datum.AverageRating) > 2.5" + } + ] + }, + { + "name": "data_0", + "source": "source_0", + "transform": [ + { + "type": "formula", + "expr": "toNumber(datum[\"IMDB_Rating\"])", + "as": "IMDB_Rating" + }, + { + "type": "filter", + "expr": "datum[\"IMDB_Rating\"] !== null && !isNaN(datum[\"IMDB_Rating\"])" + } + ] + }, + { + "name": "data_1", + "source": "source_0", + "transform": [ + { + "type": "formula", + "expr": "toNumber(datum[\"AverageRating\"])", + "as": "AverageRating" + }, + { + "type": "aggregate", + "groupby": [], + "ops": [ + "average" + ], + "fields": [ + "AverageRating" + ], + "as": [ + "average_AverageRating" + ] + } + ] + } + ], + "signals": [ + { + "name": "y_step", + "value": 21 + }, + { + "name": "height", + "update": "bandspace(domain('y').length, 0.1, 0.05) * y_step" + } + ], + "marks": [ + { + "name": "layer_0_marks", + "type": "rect", + "style": [ + "bar" + ], + "from": { + "data": "data_0" + }, + "encode": { + "update": { + "fill": { + "value": "#4c78a8" + }, + "x": { + "scale": "x", + "field": "IMDB_Rating" + }, + "x2": { + "scale": "x", + "value": 0 + }, + "y": { + "scale": "y", + "field": "Title" + }, + "height": { + "scale": "y", + "band": true + } + } + } + }, + { + "name": "layer_1_marks", + "type": "rule", + "style": [ + "rule" + ], + "from": { + "data": "data_1" + }, + "encode": { + "update": { + "stroke": { + "value": "red" + }, + "x": { + "scale": "x", + "field": "average_AverageRating" + }, + "y": { + "field": { + "group": "height" + } + }, + "y2": { + "value": 0 + } + } + } + } + ], + "scales": [ + { + "name": "x", + "type": "linear", + "domain": { + "fields": [ + { + "data": "data_0", + "field": "IMDB_Rating" + }, + { + "data": "data_1", + "field": "average_AverageRating" + } + ] + }, + "range": [ + 0, + { + "signal": "width" + } + ], + "nice": true, + "zero": true + }, + { + "name": "y", + "type": "band", + "domain": { + "data": "data_0", + "field": "Title", + "sort": true + }, + "range": { + "step": { + "signal": "y_step" + } + }, + "paddingInner": 0.1, + "paddingOuter": 0.05 + } + ], + "axes": [ + { + "scale": "x", + "orient": "bottom", + "title": "IMDB Rating", + "labelFlush": true, + "labelOverlap": true, + "tickCount": { + "signal": "ceil(width/40)" + }, + "zindex": 1 + }, + { + "scale": "x", + "orient": "bottom", + "grid": true, + "tickCount": { + "signal": "ceil(width/40)" + }, + "gridScale": "y", + "domain": false, + "labels": false, + "maxExtent": 0, + "minExtent": 0, + "ticks": false, + "zindex": 0 + }, + { + "scale": "y", + "orient": "left", + "title": "Title", + "labelOverlap": true, + "zindex": 1 + } + ], + "config": { + "axisY": { + "minExtent": 30 + } + } +} diff --git a/examples/compiled/window_mean_difference_by_year.svg b/examples/compiled/window_mean_difference_by_year.svg new file mode 100644 index 0000000000..72592fa41d --- /dev/null +++ b/examples/compiled/window_mean_difference_by_year.svg @@ -0,0 +1 @@ +0246810IMDB RatingAmerican History XCidade de DeusFight ClubInceptionMementoPulp FictionRaiders of the Lost ArkRequiem for a DreamSchindler's ListShichinin no samuraiThe Dark KnightThe Lord of the Rings: The Fellowship…The Lord of the Rings: The Return of t…The Lord of the Rings: The Two TowersThe MatrixThe Shawshank RedemptionThe Silence of the LambsTitle \ No newline at end of file diff --git a/examples/compiled/window_mean_difference_by_year.vg.json b/examples/compiled/window_mean_difference_by_year.vg.json new file mode 100644 index 0000000000..ff80077494 --- /dev/null +++ b/examples/compiled/window_mean_difference_by_year.vg.json @@ -0,0 +1,259 @@ +{ + "$schema": "https://vega.github.io/schema/vega/v3.0.json", + "description": "Bar graph showing the best films for the year they were produced, where best is defined by at least 2.5 points above average for that year. The red point shows the average rating for a film in that year, and the bar is the rating that the film recieved.", + "autosize": "pad", + "padding": 5, + "width": 200, + "style": "cell", + "data": [ + { + "name": "source_0", + "url": "data/movies.json", + "format": { + "type": "json", + "parse": { + "Release_Date": "date:'%d-%b-%y'" + } + }, + "transform": [ + { + "type": "formula", + "as": "year", + "expr": "datetime(year(datum[\"Release_Date\"]), 0, 1, 0, 0, 0, 0)" + }, + { + "type": "window", + "params": [ + null + ], + "as": [ + "AverageYearRating" + ], + "ops": [ + "mean" + ], + "fields": [ + "IMDB_Rating" + ], + "sort": { + "field": [], + "order": [] + }, + "groupby": [ + "year" + ], + "frame": [ + null, + null + ] + }, + { + "type": "filter", + "expr": "(datum.IMDB_Rating - datum.AverageYearRating) > 2.5" + } + ] + }, + { + "name": "data_0", + "source": "source_0", + "transform": [ + { + "type": "formula", + "expr": "toNumber(datum[\"IMDB_Rating\"])", + "as": "IMDB_Rating" + }, + { + "type": "filter", + "expr": "datum[\"IMDB_Rating\"] !== null && !isNaN(datum[\"IMDB_Rating\"])" + } + ] + }, + { + "name": "data_1", + "source": "source_0", + "transform": [ + { + "type": "formula", + "expr": "toNumber(datum[\"AverageYearRating\"])", + "as": "AverageYearRating" + }, + { + "type": "filter", + "expr": "datum[\"AverageYearRating\"] !== null && !isNaN(datum[\"AverageYearRating\"])" + } + ] + } + ], + "signals": [ + { + "name": "y_step", + "value": 21 + }, + { + "name": "height", + "update": "bandspace(domain('y').length, 0.1, 0.05) * y_step" + } + ], + "marks": [ + { + "name": "layer_0_marks", + "type": "rect", + "clip": true, + "style": [ + "bar" + ], + "from": { + "data": "data_0" + }, + "encode": { + "update": { + "fill": { + "value": "#4c78a8" + }, + "x": { + "scale": "x", + "field": "IMDB_Rating" + }, + "x2": { + "scale": "x", + "value": 0 + }, + "y": { + "scale": "y", + "field": "Title" + }, + "height": { + "scale": "y", + "band": true + } + } + } + }, + { + "name": "layer_1_marks", + "type": "rect", + "clip": true, + "style": [ + "tick" + ], + "from": { + "data": "data_1" + }, + "encode": { + "update": { + "opacity": { + "value": 0.7 + }, + "fill": { + "value": "red" + }, + "xc": { + "scale": "x", + "field": "AverageYearRating" + }, + "yc": { + "scale": "y", + "field": "Title", + "band": 0.5 + }, + "height": { + "value": 14 + }, + "width": { + "value": 1 + } + } + } + } + ], + "scales": [ + { + "name": "x", + "type": "linear", + "domain": { + "fields": [ + { + "data": "data_0", + "field": "IMDB_Rating" + }, + { + "data": "data_1", + "field": "AverageYearRating" + } + ] + }, + "range": [ + 0, + { + "signal": "width" + } + ], + "nice": true, + "zero": true + }, + { + "name": "y", + "type": "band", + "domain": { + "fields": [ + { + "data": "data_0", + "field": "Title" + }, + { + "data": "data_1", + "field": "Title" + } + ], + "sort": true + }, + "range": { + "step": { + "signal": "y_step" + } + }, + "paddingInner": 0.1, + "paddingOuter": 0.05 + } + ], + "axes": [ + { + "scale": "x", + "orient": "bottom", + "title": "IMDB Rating", + "labelFlush": true, + "labelOverlap": true, + "tickCount": { + "signal": "ceil(width/40)" + }, + "zindex": 1 + }, + { + "scale": "x", + "orient": "bottom", + "grid": true, + "tickCount": { + "signal": "ceil(width/40)" + }, + "gridScale": "y", + "domain": false, + "labels": false, + "maxExtent": 0, + "minExtent": 0, + "ticks": false, + "zindex": 0 + }, + { + "scale": "y", + "orient": "left", + "title": "Title", + "labelOverlap": true, + "zindex": 1 + } + ], + "config": { + "axisY": { + "minExtent": 30 + } + } +} diff --git a/examples/compiled/window_residual_graph.svg b/examples/compiled/window_residual_graph.svg new file mode 100644 index 0000000000..f1a3b4d23e --- /dev/null +++ b/examples/compiled/window_residual_graph.svg @@ -0,0 +1 @@ +Jan 01, 1970Jan 01, 1980Jan 01, 1990Jan 01, 2000Jan 01, 2010Release_Date-4-202Rating Delta \ No newline at end of file diff --git a/examples/compiled/window_residual_graph.vg.json b/examples/compiled/window_residual_graph.vg.json new file mode 100644 index 0000000000..7b732f0b5f --- /dev/null +++ b/examples/compiled/window_residual_graph.vg.json @@ -0,0 +1,196 @@ +{ + "$schema": "https://vega.github.io/schema/vega/v3.0.json", + "description": "A dot plot showing each movie in the database, and the difference from the average movie rating. The display is sorted by year to visualize everything in sequential order. The graph is for all Movies before 2019.", + "autosize": "pad", + "padding": 5, + "width": 200, + "height": 200, + "style": "cell", + "data": [ + { + "name": "source_0", + "url": "data/movies.json", + "format": { + "type": "json", + "parse": { + "Release_Date": "date:'%d-%b-%y'" + } + }, + "transform": [ + { + "type": "filter", + "expr": "datum.IMDB_Rating != null" + }, + { + "type": "filter", + "expr": "time(datetime(year(datum[\"Release_Date\"]), 0, 1, 0, 0, 0, 0)) <= time(datetime(2019, 0, 1, 0, 0, 0, 0))" + }, + { + "type": "window", + "params": [ + null + ], + "as": [ + "AverageRating" + ], + "ops": [ + "mean" + ], + "fields": [ + "IMDB_Rating" + ], + "sort": { + "field": [], + "order": [] + }, + "frame": [ + null, + null + ] + }, + { + "type": "formula", + "expr": "datum.IMDB_Rating - datum.AverageRating", + "as": "RatingDelta" + }, + { + "type": "filter", + "expr": "datum[\"Release_Date\"] !== null && !isNaN(datum[\"Release_Date\"]) && datum[\"RatingDelta\"] !== null && !isNaN(datum[\"RatingDelta\"])" + } + ] + } + ], + "marks": [ + { + "name": "marks", + "type": "symbol", + "clip": true, + "style": [ + "point" + ], + "from": { + "data": "source_0" + }, + "encode": { + "update": { + "opacity": { + "value": 0.7 + }, + "fill": { + "value": "transparent" + }, + "stroke": { + "value": "#4c78a8" + }, + "x": { + "scale": "x", + "field": "Release_Date" + }, + "y": { + "scale": "y", + "field": "RatingDelta" + } + } + } + } + ], + "scales": [ + { + "name": "x", + "type": "time", + "domain": { + "data": "source_0", + "field": "Release_Date" + }, + "range": [ + 0, + { + "signal": "width" + } + ] + }, + { + "name": "y", + "type": "linear", + "domain": { + "data": "source_0", + "field": "RatingDelta" + }, + "range": [ + { + "signal": "height" + }, + 0 + ], + "nice": true, + "zero": true + } + ], + "axes": [ + { + "scale": "x", + "orient": "bottom", + "title": "Release_Date", + "labelFlush": true, + "labelOverlap": true, + "tickCount": { + "signal": "ceil(width/40)" + }, + "encode": { + "labels": { + "update": { + "text": { + "signal": "timeFormat(datum.value, '%b %d, %Y')" + } + } + } + }, + "zindex": 1 + }, + { + "scale": "x", + "orient": "bottom", + "grid": true, + "tickCount": { + "signal": "ceil(width/40)" + }, + "gridScale": "y", + "domain": false, + "labels": false, + "maxExtent": 0, + "minExtent": 0, + "ticks": false, + "zindex": 0 + }, + { + "scale": "y", + "orient": "left", + "title": "Rating Delta", + "labelOverlap": true, + "tickCount": { + "signal": "ceil(height/40)" + }, + "zindex": 1 + }, + { + "scale": "y", + "orient": "left", + "grid": true, + "tickCount": { + "signal": "ceil(height/40)" + }, + "gridScale": "x", + "domain": false, + "labels": false, + "maxExtent": 0, + "minExtent": 0, + "ticks": false, + "zindex": 0 + } + ], + "config": { + "axisY": { + "minExtent": 30 + } + } +} diff --git a/examples/compiled/window_student_rank.svg b/examples/compiled/window_student_rank.svg new file mode 100644 index 0000000000..77d8326c09 --- /dev/null +++ b/examples/compiled/window_student_rank.svg @@ -0,0 +1 @@ +student0Score \ No newline at end of file diff --git a/examples/compiled/window_student_rank.vg.json b/examples/compiled/window_student_rank.vg.json new file mode 100644 index 0000000000..0fa9533e5c --- /dev/null +++ b/examples/compiled/window_student_rank.vg.json @@ -0,0 +1,302 @@ +{ + "$schema": "https://vega.github.io/schema/vega/v3.0.json", + "description": "A bar graph showing the scores of the top 5 students. This shows an example of the window transform, for how the top X can be filtered, and also how a rank can be computed for each student.", + "autosize": "pad", + "padding": 5, + "width": 300, + "height": 50, + "style": "cell", + "data": [ + { + "name": "source_0", + "values": [ + { + "student": "A", + "score": 100 + }, + { + "student": "B", + "score": 56 + }, + { + "student": "C", + "score": 88 + }, + { + "student": "D", + "score": 65 + }, + { + "student": "E", + "score": 45 + }, + { + "student": "F", + "score": 23 + }, + { + "student": "G", + "score": 66 + }, + { + "student": "H", + "score": 67 + }, + { + "student": "I", + "score": 13 + }, + { + "student": "J", + "score": 12 + }, + { + "student": "K", + "score": 50 + }, + { + "student": "L", + "score": 78 + }, + { + "student": "M", + "score": 66 + }, + { + "student": "N", + "score": 30 + }, + { + "student": "O", + "score": 97 + }, + { + "student": "P", + "score": 75 + }, + { + "student": "Q", + "score": 24 + }, + { + "student": "R", + "score": 42 + }, + { + "student": "S", + "score": 76 + }, + { + "student": "T", + "score": 78 + }, + { + "student": "U", + "score": 21 + }, + { + "student": "V", + "score": 46 + } + ] + }, + { + "name": "data_0", + "source": "source_0", + "transform": [ + { + "type": "formula", + "expr": "toNumber(datum[\"Score\"])", + "as": "Score" + }, + { + "type": "window", + "params": [ + null + ], + "as": [ + "rank" + ], + "ops": [ + "rank" + ], + "fields": [ + "score" + ], + "sort": { + "field": [ + "score" + ], + "order": [ + "ascending" + ] + }, + "groupby": [ + "Student" + ], + "frame": [ + null, + 0 + ] + }, + { + "type": "window", + "params": [ + null + ], + "as": [ + "totalStudents" + ], + "ops": [ + "count" + ], + "fields": [ + "score" + ], + "sort": { + "field": [ + "score" + ], + "order": [ + "ascending" + ] + }, + "groupby": [ + "Student" + ], + "frame": [ + null, + null + ] + }, + { + "type": "filter", + "expr": "datum.totalStudents - datum.rank > 5" + }, + { + "type": "filter", + "expr": "datum[\"Score\"] !== null && !isNaN(datum[\"Score\"])" + } + ] + } + ], + "marks": [ + { + "name": "marks", + "type": "rect", + "style": [ + "bar" + ], + "from": { + "data": "data_0" + }, + "encode": { + "update": { + "fill": { + "scale": "color", + "field": "student" + }, + "x": { + "scale": "x", + "field": "Score" + }, + "x2": { + "scale": "x", + "value": 0 + }, + "y": { + "scale": "y", + "field": "student" + }, + "height": { + "scale": "y", + "band": true + } + } + } + } + ], + "scales": [ + { + "name": "x", + "type": "linear", + "domain": { + "data": "data_0", + "field": "Score" + }, + "range": [ + 0, + { + "signal": "width" + } + ], + "nice": true, + "zero": true + }, + { + "name": "y", + "type": "band", + "domain": { + "data": "data_0", + "field": "student", + "sort": true + }, + "range": [ + 0, + { + "signal": "height" + } + ], + "paddingInner": 0.1, + "paddingOuter": 0.05 + }, + { + "name": "color", + "type": "ordinal", + "domain": { + "data": "data_0", + "field": "student", + "sort": true + }, + "range": "category" + } + ], + "axes": [ + { + "scale": "x", + "orient": "bottom", + "title": "Score", + "labelFlush": true, + "labelOverlap": true, + "tickCount": { + "signal": "ceil(width/40)" + }, + "zindex": 1 + }, + { + "scale": "y", + "orient": "left", + "zindex": 1 + } + ], + "legends": [ + { + "fill": "color", + "title": "student", + "encode": { + "symbols": { + "update": { + "shape": { + "value": "square" + } + } + } + } + } + ], + "config": { + "axisY": { + "minExtent": 30 + } + } +} diff --git a/examples/specs/window_activities.vl.json b/examples/specs/window_activities.vl.json new file mode 100644 index 0000000000..ac73fa0eca --- /dev/null +++ b/examples/specs/window_activities.vl.json @@ -0,0 +1,42 @@ +{ + "$schema": "https://vega.github.io/schema/vega-lite/v2.json", + "description": "A bar graph showing what activites consume what percentage of the day.", + "data": { + "values": [ + {"Activity": "Sleeping","Time": 8}, + {"Activity": "Eating","Time": 2}, + {"Activity": "TV","Time": 4}, + {"Activity": "Work","Time": 8}, + {"Activity": "Exercise","Time": 2} + ] + }, + "transform": [{ + "window": [{ + "op": "sum", + "field": "Time", + "as": "TotalTime" + }], + "frame": [null, null] + }, + { + "calculate": "datum.Time/datum.TotalTime * 100", + "as": "PercentOfTotal" + }], + "mark": "bar", + "encoding": { + "x": { + "field": "PercentOfTotal", + "type": "quantitative", + "axis": { + "title": "% of total Time" + } + }, + "y": { + "field": "Activity", + "type": "nominal", + "scale": { + "rangeStep": 12 + } + } + } +} diff --git a/examples/specs/window_mean_difference.vl.json b/examples/specs/window_mean_difference.vl.json new file mode 100644 index 0000000000..c8ce507c1b --- /dev/null +++ b/examples/specs/window_mean_difference.vl.json @@ -0,0 +1,41 @@ +{ + "data": { + "url": "data/movies.json" + }, + "transform": [{ + "window": [{ + "op": "mean", + "field": "IMDB_Rating", + "as": "AverageRating" + }], + "frame": [ + null, + null + ] + }, + { + "filter": "(datum.IMDB_Rating - datum.AverageRating) > 2.5" + }], + "layer": [ + { + "mark": "bar", + "encoding": { + "x": { + "field": "IMDB_Rating", "type": "quantitative", + "axis": {"title": "IMDB Rating"} + }, + "y": {"field": "Title", "type": "ordinal"} + } + }, + { + "mark": {"type": "rule", "color": "red"}, + "encoding": { + "x": { + "aggregate": "average", + "field": "AverageRating", + "type": "quantitative" + } + } + } + ] +} diff --git a/examples/specs/window_mean_difference_by_year.vl.json b/examples/specs/window_mean_difference_by_year.vl.json new file mode 100644 index 0000000000..104f3f7b72 --- /dev/null +++ b/examples/specs/window_mean_difference_by_year.vl.json @@ -0,0 +1,57 @@ +{ + "$schema": "https://vega.github.io/schema/vega-lite/v2.json", + "description": "Bar graph showing the best films for the year they were produced, where best is defined by at least 2.5 points above average for that year. The red point shows the average rating for a film in that year, and the bar is the rating that the film recieved.", + "data": { + "url": "data/movies.json", + "format": { + "parse": {"Release_Date": "date:'%d-%b-%y'"} + } + }, + "transform": [ + {"timeUnit": "year", "field": "Release_Date", "as": "year"}, + { + "window": [{ + "op": "mean", + "field": "IMDB_Rating", + "as": "AverageYearRating" + }], + "groupby": [ + "year" + ], + "frame": [null, null] + }, + { + "filter": "(datum.IMDB_Rating - datum.AverageYearRating) > 2.5" + } + ], + "layer": [{ + "mark": {"type": "bar", "clip": true}, + "encoding": { + "x": { + "field": "IMDB_Rating", + "type": "quantitative", + "axis": {"title": "IMDB Rating"} + }, + "y": { + "field": "Title", + "type": "ordinal", + "sort": {"field": "IMDB_Rating", "order": "ascending"} + } + } + }, + { + "mark": "tick", + "encoding": { + "x": { + "field": "AverageYearRating", + "type": "quantitative" + }, + "y": { + "field": "Title", + "type": "ordinal" + }, + "color": {"value": "red"} + } + } + ] +} diff --git a/examples/specs/window_residual_graph.vl.json b/examples/specs/window_residual_graph.vl.json new file mode 100644 index 0000000000..d56dec3ed8 --- /dev/null +++ b/examples/specs/window_residual_graph.vl.json @@ -0,0 +1,39 @@ +{ + "$schema": "https://vega.github.io/schema/vega-lite/v2.json", + "description": "A dot plot showing each movie in the database, and the difference from the average movie rating. The display is sorted by year to visualize everything in sequential order. The graph is for all Movies before 2019.", + "data": { + "url": "data/movies.json", + "format": { + "parse": {"Release_Date": "date:'%d-%b-%y'"} + } + }, + "transform": [ + {"filter": "datum.IMDB_Rating != null"}, + {"filter": {"timeUnit": "year", "field": "Release_Date", "range": [null, 2019]}}, + { + "window": [{ + "op": "mean", + "field": "IMDB_Rating", + "as": "AverageRating" + }], + "frame": [null, null] + }, + { + "calculate": "datum.IMDB_Rating - datum.AverageRating", + "as": "RatingDelta" + } + ], + "mark": "point", + "encoding": { + "x": { + "field": "Release_Date", + "type": "temporal", + "sort": {"field": "Release_Date", "order": "ascending"} + }, + "y": { + "field": "RatingDelta", + "type": "quantitative", + "axis": {"title": "Rating Delta"} + } + } +} diff --git a/examples/specs/window_student_rank.vl.json b/examples/specs/window_student_rank.vl.json new file mode 100644 index 0000000000..190a916ba8 --- /dev/null +++ b/examples/specs/window_student_rank.vl.json @@ -0,0 +1,66 @@ +{ + "$schema": "https://vega.github.io/schema/vega-lite/v2.json", + "description": "A bar graph showing the scores of the top 5 students. This shows an example of the window transform, for how the top X can be filtered, and also how a rank can be computed for each student.", + "width": 300, + "height": 50, + "data": { + "values": [ + {"student": "A", "score": 100}, {"student": "B", "score": 56}, + {"student": "C", "score": 88}, {"student": "D", "score": 65}, + {"student": "E", "score": 45}, {"student": "F", "score": 23}, + {"student": "G", "score": 66}, {"student": "H", "score": 67}, + {"student": "I", "score": 13}, {"student": "J", "score": 12}, + {"student": "K", "score": 50}, {"student": "L", "score": 78}, + {"student": "M", "score": 66}, {"student": "N", "score": 30}, + {"student": "O", "score": 97}, {"student": "P", "score": 75}, + {"student": "Q", "score": 24}, {"student": "R", "score": 42}, + {"student": "S", "score": 76}, {"student": "T", "score": 78}, + {"student": "U", "score": 21}, {"student": "V", "score": 46} + ] + }, + "transform": [{ + "window": [{ + "op": "rank", + "field": "score", + "as": "rank" + }], + "sort": [{ "field": "score", "order": "ascending" }], + "groupby": [ + "Student" + ], + "frame": [null, 0] + }, + { + "window": [{ + "op": "count", + "field": "score", + "as": "totalStudents" + }], + "sort": [{ "field": "score", "order": "ascending" }], + "groupby": [ + "Student" + ], + "frame": [null, null] + }, + { + "filter": "datum.totalStudents - datum.rank > 5" + }], + "mark": "bar", + "encoding": { + "x": { + "field": "Score", + "type": "quantitative", + "axis": { "title": "Score", "grid": false } + }, + "y": { + "field": "student", + "type": "nominal", + "scale": { "rangeStep": 12 }, + "axis": { "title": "" } + }, + "color": { + "field": "student", + "type": "nominal" + } + } +} \ No newline at end of file diff --git a/site/docs/encoding/scale.md b/site/docs/encoding/scale.md index 6f240b5447..c068ccb8fc 100644 --- a/site/docs/encoding/scale.md +++ b/site/docs/encoding/scale.md @@ -124,15 +124,15 @@ Color schemes provide a set of named color palettes as a scale range for the `co By default, Vega-Lite assigns different [default color schemes](#range-config) based on the types of the encoded fields: -- _Nominal_ fields use the `"categorical"` [pre-defined named range](#range-config) (the `"category20"` scheme by default). +- _Nominal_ fields use the `"categorical"` [pre-defined named range](#range-config) (the [`"tableau10"`](https://vega.github.io/vega/docs/schemes/#tableau10) scheme by default).
-- _Ordinal_ fields use the `"ordinal"` [pre-defined named color range](#range-config) (the `"blues"` color scheme by default). +- _Ordinal_ fields use the `"ordinal"` [pre-defined named color range](#range-config) (the [`"blues"`](https://vega.github.io/vega/docs/schemes/#blues) color scheme by default).
-- _Quantitative_ and _temporal_ fields use the [pre-defined named color range](#range-config) `"heatmap"` (the `"viridis"` scheme by default) for rect marks and `"ramp"` (the `"blues"` scheme by default) for other marks. +- _Quantitative_ and _temporal_ fields use the [pre-defined named color range](#range-config) `"heatmap"` (the [`"viridis"`](https://vega.github.io/vega/docs/schemes/#viridis) scheme by default) for rect marks and `"ramp"` (the [`"blues"`](https://vega.github.io/vega/docs/schemes/#blues) scheme by default) for other marks.
diff --git a/site/docs/transform/transform.md b/site/docs/transform/transform.md index 0a78d99371..3601a117a5 100644 --- a/site/docs/transform/transform.md +++ b/site/docs/transform/transform.md @@ -32,3 +32,4 @@ Vega-Lite's `transform` supports the following types of transformations: - [Calculate](calculate.html) - [Filter](filter.html) - [TimeUnit](timeunit.html#transform) +- [Window](window.html) diff --git a/site/docs/transform/window.md b/site/docs/transform/window.md new file mode 100644 index 0000000000..0333b89192 --- /dev/null +++ b/site/docs/transform/window.md @@ -0,0 +1,91 @@ +--- +layout: docs +menu: docs +title: Window +permalink: /docs/window.html +--- + +The window transform performs calculations over sorted groups of data objects. These calculations including ranking, lead/lag analysis, and aggregates such as running sums and averages. Calculated values are written back to the input data stream. + +## Window Field Definition + +{: .suppress-error} +```json +// A View Specification +{ + ... + "transform": [ + { + // Window Transform + "window": [{ + "op": "...", + "field": "...", + "as": "..." + }], + "groupby": [ + "..." + ], + "frame": "..." + } + ... + ], + ... +} +``` + +## Window Transform Definition + +{% include table.html props="window,frame,ignorePeers,groupby,sort" source="WindowTransform" %} + +{:#field-def} +### Window Transform Field Definition + +{% include table.html props="op,param,field,as" source="WindowFieldDef" %} + +{:#sort-field-def} +### Window Sort Field Definition + +{% include table.html props="field,order" source="WindowSortField" %} + +{:#ops} +## Window Only Operation Reference + +The valid operations include all [valid aggregate operations](../aggregate/#ops) plus the following window operations. + +| Operation | Parameter | Description | +| :----------- | :-------: | :------------| +| row_number | _None_ | Assigns each data object a consecutive row number, starting from 1.| +| rank | _None_ | Assigns a rank order value to each data object in a window, starting from 1. Peer values are assigned the same rank. Subsequent rank scores incorporate the number of prior values. For example, if the first two values tie for rank 1, the third value is assigned rank 3.| +| dense_rank | _None_ | Assigns dense rank order values to each data object in a window, starting from 1. Peer values are assigned the same rank. Subsequent rank scores do not incorporate the number of prior values. For example, if the first two values tie for rank 1, the third value is assigned rank 2.| +| percent_rank | _None_ | Assigns a percentage rank order value to each data object in a window. The percent is calculated as _(rank - 1) / (group_size - 1)_. | +| cume_dist | _None_ | Assigns a cumulative distribution value between 0 and 1 to each data object in a window.| +| ntile | Number | Assigns a quantile (e.g., percentile) value to each data object in a window. Accepts an integer parameter indicating the number of buckets to use (e.g., 100 for percentiles, 5 for quintiles).| +| lag | Number | Assigns a value from the data object that precedes the current object by a specified number of positions. If no such object exists, assigns `null`. Accepts an offset parameter (default `1`) that indicates the number of positions. This operation must have a corresponding entry in the _fields_ parameter array.| +| lead | Number | Assigns a value from the data object that follows the current object by a specified number of positions. If no such object exists, assigns `null`. Accepts an offset parameter (default `1`) that indicates the number of positions. This operation must have a corresponding entry in the _fields_ parameter array.| +| first_value | _None_ | Assigns a value from the first data object in the current sliding window frame. This operation must have a corresponding entry in the _fields_ parameter array.| +| last_value | _None_ | Assigns a value from the last data object in the current sliding window frame. This operation must have a corresponding entry in the _fields_ parameter array.| +| nth_value | Number | Assigns a value from the nth data object in the current sliding window frame. If no such object exists, assigns `null`. Requires a non-negative integer parameter that indicates the offset from the start of the window frame. This operation must have a corresponding entry in the _fields_ parameter array.| + +## Examples + +Below are some common use cases for the window transform. Shown are the examples along side the Vega Lite spec. + +### Percent of Total + +
+ +### Difference from Mean + +
+ +Another example is to show the best movies for the year they were released. Here best is defined by having a score that is 2.5 points higher than the average for the year it was released in. + +
+ +Rather than filtering the above two examples we can also show a residual graph using the window transform. + +
+ +To replace any of the examples from total to `mean`, the operation just needs to be changed from `sum` to `mean`. + +To replace any of the examples from total to `mean`, the operation just needs to be changed from `sum` to `mean`. \ No newline at end of file diff --git a/src/compile/axis/parse.ts b/src/compile/axis/parse.ts index 9a9af05dea..3936a20722 100644 --- a/src/compile/axis/parse.ts +++ b/src/compile/axis/parse.ts @@ -199,7 +199,7 @@ function parseAxis(channel: PositionScaleChannel, model: UnitModel): AxisCompone // FIXME: By having encode as one property, we won't have fine grained encode merging. if (keys(axisEncode).length > 0) { - axisComponent.set('encode', axisEncode, !!axis.encoding || !!axis.labelAngle); + axisComponent.set('encode', axisEncode, !!axis.encoding || axis.labelAngle !== undefined); } return axisComponent; diff --git a/src/compile/data/window.ts b/src/compile/data/window.ts index fc9ef907ea..1833b5c07d 100644 --- a/src/compile/data/window.ts +++ b/src/compile/data/window.ts @@ -1,8 +1,7 @@ import {AggregateOp} from 'vega'; -import {WindowFieldDef, WindowTransform} from '../../transform'; +import {WindowFieldDef, WindowOnlyOp, WindowTransform} from '../../transform'; import {duplicate} from '../../util'; import {VgComparator, VgComparatorOrder, VgWindowTransform} from '../../vega.schema'; -import {WindowOnlyOp} from '../../window'; import {DataFlowNode} from './dataflow'; /** @@ -19,15 +18,15 @@ export class WindowTransformNode extends DataFlowNode { public producedFields() { const out = {}; - this.transform.window.forEach(element => { - out[this.getDefaultName(element)] = true; + this.transform.window.forEach(windowFieldDef => { + out[this.getDefaultName(windowFieldDef)] = true; }); return out; } - private getDefaultName(window: WindowFieldDef): string { - return window.as === undefined ? String(window.op) + '_field' : window.as; + private getDefaultName(windowFieldDef: WindowFieldDef): string { + return windowFieldDef.as === undefined ? String(windowFieldDef.op) + '_field' : windowFieldDef.as; } public assemble(): VgWindowTransform { @@ -47,9 +46,9 @@ export class WindowTransformNode extends DataFlowNode { const sortFields: string[] = []; const sortOrder: VgComparatorOrder[] = []; if (this.transform.sort !== undefined) { - for (const compField of this.transform.sort) { - sortFields.push(compField.field); - sortOrder.push(compField.order === undefined ? null : compField.order as VgComparatorOrder); + for (const sortField of this.transform.sort) { + sortFields.push(sortField.field); + sortOrder.push(sortField.order === undefined ? null : sortField.order as VgComparatorOrder); } } const sort: VgComparator = { diff --git a/src/compile/layoutsize/parse.ts b/src/compile/layoutsize/parse.ts index a9b488d94d..ab13730e3c 100644 --- a/src/compile/layoutsize/parse.ts +++ b/src/compile/layoutsize/parse.ts @@ -107,7 +107,7 @@ function defaultUnitSize(model: UnitModel, sizeType: 'width' | 'height'): Layout } } else { // No scale - set default size - if (sizeType === 'width' && model.mark() === 'text') { + if (sizeType === 'width' && model.mark === 'text') { // width for text mark without x-field is a bit wider than typical range step return config.scale.textXRangeStep; } diff --git a/src/compile/legend/encode.ts b/src/compile/legend/encode.ts index 89db975e52..44359bd1e3 100644 --- a/src/compile/legend/encode.ts +++ b/src/compile/legend/encode.ts @@ -25,13 +25,12 @@ export function symbols(fieldDef: FieldDef, symbolsSpec: any, model: Uni return undefined; } - const mark = model.mark(); let out = { ...applyMarkConfig({}, model, FILL_STROKE_CONFIG), ...mixins.color(model) }; - switch (mark) { + switch (model.mark) { case BAR: case TICK: case TEXT: @@ -39,7 +38,7 @@ export function symbols(fieldDef: FieldDef, symbolsSpec: any, model: Uni break; case CIRCLE: case SQUARE: - out.shape = {value: mark}; + out.shape = {value: model.mark}; break; case POINT: case LINE: diff --git a/src/compile/mark/mark.ts b/src/compile/mark/mark.ts index 75ee40fb91..e451675e6a 100644 --- a/src/compile/mark/mark.ts +++ b/src/compile/mark/mark.ts @@ -35,7 +35,7 @@ const markCompiler: {[type: string]: MarkCompiler} = { export function parseMarkGroup(model: UnitModel): any[] { - if (contains([LINE, AREA], model.mark())) { + if (contains([LINE, AREA], model.mark)) { return parsePathMark(model); } else { return getMarkGroups(model); @@ -82,7 +82,7 @@ export function getSort(model: UnitModel) { if (model.channelHasField('order') && !model.stack) { // Sort by the order field if it is specified and the field is not stacked. (For stacked field, order specify stack order.) return sortParams(model.encoding.order, {expr: 'datum'}); - } else if (contains(['line', 'area'], model.mark())) { + } else if (contains(['line', 'area'], model.mark)) { // For both line and area, we sort values based on dimension by default const dimensionChannel: 'x' | 'y' = model.markDef.orient === 'horizontal' ? 'y' : 'x'; const s = model.sort(dimensionChannel); @@ -112,7 +112,7 @@ export function getSort(model: UnitModel) { function getMarkGroups(model: UnitModel, opt: { fromPrefix: string } = {fromPrefix: ''}) { - const mark = model.mark(); + const mark = model.mark; const clip = model.markDef.clip !== undefined ? !!model.markDef.clip : scaleClip(model); diff --git a/src/compile/mark/mixins.ts b/src/compile/mark/mixins.ts index 32ae681dc1..fc51263ec3 100644 --- a/src/compile/mark/mixins.ts +++ b/src/compile/mark/mixins.ts @@ -242,14 +242,18 @@ export function binnedPosition(fieldDef: FieldDef, channel: 'x'|'y', sca export function pointPosition(channel: 'x'|'y', model: UnitModel, defaultRef: VgValueRef | 'zeroOrMin' | 'zeroOrMax', vgChannel?: 'x'|'y'|'xc'|'yc') { // TODO: refactor how refer to scale as discussed in https://github.com/vega/vega-lite/pull/1613 - const {encoding, stack} = model; + const {encoding, mark, stack} = model; const channelDef = encoding[channel]; + const scaleName = model.scaleName(channel); + const scale = model.getScaleComponent(channel); const valueRef = !channelDef && (encoding.latitude || encoding.longitude) ? // use geopoint output if there are lat/long and there is no point position overriding lat/long. {field: model.getName(channel)} : - ref.stackable(channel, encoding[channel], model.scaleName(channel), model.getScaleComponent(channel), stack, defaultRef); + ref.stackable(channel, encoding[channel], scaleName, scale, stack, + ref.getDefaultRef(defaultRef, channel, scaleName, scale, mark) + ); return { [vgChannel || channel]: valueRef @@ -261,15 +265,19 @@ export function pointPosition(channel: 'x'|'y', model: UnitModel, defaultRef: Vg * If channel is not specified, return one channel based on orientation. */ export function pointPosition2(model: UnitModel, defaultRef: 'zeroOrMin' | 'zeroOrMax', channel?: 'x2' | 'y2') { - const {encoding, markDef, stack} = model; + const {encoding, mark, markDef, stack} = model; channel = channel || (markDef.orient === 'horizontal' ? 'x2' : 'y2'); const baseChannel = channel === 'x2' ? 'x' : 'y'; const channelDef = encoding[baseChannel]; + const scaleName = model.scaleName(baseChannel); + const scale = model.getScaleComponent(baseChannel); const valueRef = !channelDef && (encoding.latitude || encoding.longitude) ? // use geopoint output if there are lat2/long2 and there is no point position2 overriding lat2/long2. {field: model.getName(channel)}: - ref.stackable2(channel, channelDef, encoding[channel], model.scaleName(baseChannel), model.getScaleComponent(baseChannel), stack, defaultRef); + ref.stackable2(channel, channelDef, encoding[channel], scaleName, scale, stack, + ref.getDefaultRef(defaultRef, baseChannel, scaleName, scale, mark) + ); return {[channel]: valueRef}; } diff --git a/src/compile/mark/valueref.ts b/src/compile/mark/valueref.ts index 7cd115fc0e..ceb9f06fe8 100644 --- a/src/compile/mark/valueref.ts +++ b/src/compile/mark/valueref.ts @@ -1,7 +1,9 @@ /** * Utility files for producing Vega ValueRef for marks */ -import {Channel, X, X2, Y, Y2} from '../../channel'; +import {isArray} from 'util'; +import {isString} from 'vega-util'; +import {Channel, X, Y} from '../../channel'; import {Config} from '../../config'; import { ChannelDef, @@ -13,10 +15,12 @@ import { TextFieldDef, vgField, } from '../../fielddef'; +import * as log from '../../log'; +import {Mark} from '../../mark'; import {hasDiscreteDomain, ScaleType} from '../../scale'; import {StackProperties} from '../../stack'; import {QUANTITATIVE} from '../../type'; -import {contains} from '../../util'; +import {contains, some} from '../../util'; import {VgSignalRef, VgValueRef} from '../../vega.schema'; import {binRequiresRange, formatSignalRef} from '../common'; import {ScaleComponent} from '../scale/component'; @@ -29,7 +33,7 @@ import {ScaleComponent} from '../scale/component'; * @return Vega ValueRef for stackable x or y */ export function stackable(channel: 'x' | 'y', channelDef: ChannelDef, scaleName: string, scale: ScaleComponent, - stack: StackProperties, defaultRef: VgValueRef | 'zeroOrMin' | 'zeroOrMax'): VgValueRef { + stack: StackProperties, defaultRef: VgValueRef): VgValueRef { if (isFieldDef(channelDef) && stack && channel === stack.fieldChannel) { // x or y use stack_end so that stacked line's point mark use stack_end too. return fieldRef(channelDef, scaleName, {suffix: 'end'}); @@ -41,7 +45,7 @@ export function stackable(channel: 'x' | 'y', channelDef: ChannelDef, sc * @return Vega ValueRef for stackable x2 or y2 */ export function stackable2(channel: 'x2' | 'y2', aFieldDef: ChannelDef, a2fieldDef: ChannelDef, scaleName: string, scale: ScaleComponent, - stack: StackProperties, defaultRef: VgValueRef | 'zeroOrMin' | 'zeroOrMax'): VgValueRef { + stack: StackProperties, defaultRef: VgValueRef): VgValueRef { if (isFieldDef(aFieldDef) && stack && // If fieldChannel is X and channel is X2 (or Y and Y2) channel.charAt(0) === stack.fieldChannel.charAt(0) @@ -101,8 +105,7 @@ function binMidSignal(fieldDef: FieldDef, scaleName: string) { /** * @returns {VgValueRef} Value Ref for xc / yc or mid point for other channels. */ -export function midPoint(channel: Channel, channelDef: ChannelDef, scaleName: string, scale: ScaleComponent, stack: StackProperties, - defaultRef: VgValueRef | 'zeroOrMin' | 'zeroOrMax'): VgValueRef { +export function midPoint(channel: Channel, channelDef: ChannelDef, scaleName: string, scale: ScaleComponent, stack: StackProperties, defaultRef: VgValueRef): VgValueRef { // TODO: datum support if (channelDef) { @@ -142,25 +145,6 @@ export function midPoint(channel: Channel, channelDef: ChannelDef, scale // In such case, we will use default ref. } - if (defaultRef === 'zeroOrMin') { - /* istanbul ignore else */ - if (channel === X || channel === X2) { - return zeroOrMinX(scaleName, scale); - } else if (channel === Y || channel === Y2) { - return zeroOrMinY(scaleName, scale); - } else { - throw new Error(`Unsupported channel ${channel} for base function`); // FIXME add this to log.message - } - } else if (defaultRef === 'zeroOrMax') { - /* istanbul ignore else */ - if (channel === X || channel === X2) { - return zeroOrMaxX(scaleName, scale); - } else if (channel === Y || channel === Y2) { - return zeroOrMaxY(scaleName, scale); - } else { - throw new Error(`Unsupported channel ${channel} for base function`); // FIXME add this to log.message - } - } return defaultRef; } @@ -180,71 +164,56 @@ export function mid(sizeRef: VgSignalRef): VgValueRef { return {...sizeRef, mult: 0.5}; } -function zeroOrMinX(scaleName: string, scale: ScaleComponent): VgValueRef { - if (scaleName) { - // Log / Time / UTC scale do not support zero - if (!contains([ScaleType.LOG, ScaleType.TIME, ScaleType.UTC], scale.get('type')) && - scale.get('zero') !== false) { - - return { - scale: scaleName, - value: 0 - }; - } - } - // Put the mark on the x-axis - return {value: 0}; -} - /** - * @returns {VgValueRef} base value if scale exists and return max value if scale does not exist + * Whether the scale definitely includes zero in the domain */ -function zeroOrMaxX(scaleName: string, scale: ScaleComponent): VgValueRef { - if (scaleName) { - // Log / Time / UTC scale do not support zero - if (!contains([ScaleType.LOG, ScaleType.TIME, ScaleType.UTC], scale.get('type')) && - scale.get('zero') !== false) { - - return { - scale: scaleName, - value: 0 - }; - } +function domainDefinitelyIncludeZero(scale: ScaleComponent) { + if (scale.get('zero') !== false) { + return true; + } + const domains = scale.domains; + if (isArray(domains)) { + return some(domains, (d) => isArray(d) && d.length === 2 && d[0] <=0 && d[1] >= 0); } - return {field: {group: 'width'}}; + return false; } -function zeroOrMinY(scaleName: string, scale: ScaleComponent): VgValueRef { - if (scaleName) { - // Log / Time / UTC scale do not support zero - if (!contains([ScaleType.LOG, ScaleType.TIME, ScaleType.UTC], scale.get('type')) && - scale.get('zero') !== false) { - - return { - scale: scaleName, - value: 0 - }; +export function getDefaultRef( + defaultRef: VgValueRef | 'zeroOrMin' | 'zeroOrMax', + channel: 'x' | 'y', scaleName: string, scale: ScaleComponent, mark: Mark +) { + if (isString(defaultRef)) { + if (scaleName) { + const scaleType = scale.get('type'); + if (contains([ScaleType.LOG, ScaleType.TIME, ScaleType.UTC], scaleType)) { + // Log scales cannot have zero. + // Zero in time scale is arbitrary, and does not affect ratio. + // (Time is an interval level of measurement, not ratio). + // See https://en.wikipedia.org/wiki/Level_of_measurement for more info. + if (mark === 'bar' || mark === 'area') { + log.warn(log.message.nonZeroScaleUsedWithLengthMark(mark, channel, {scaleType})); + } + } else { + if (domainDefinitelyIncludeZero(scale)) { + return { + scale: scaleName, + value: 0 + }; + } + if (mark === 'bar' || mark === 'area') { + log.warn(log.message.nonZeroScaleUsedWithLengthMark( + mark, channel, {zeroFalse: scale.explicit.zero === false} + )); + } + } } - } - // Put the mark on the y-axis - return {field: {group: 'height'}}; -} -/** - * @returns {VgValueRef} base value if scale exists and return max value if scale does not exist - */ -function zeroOrMaxY(scaleName: string, scale: ScaleComponent): VgValueRef { - if (scaleName) { - // Log / Time / UTC scale do not support zero - if (!contains([ScaleType.LOG, ScaleType.TIME, ScaleType.UTC], scale.get('type')) && - scale.get('zero') !== false) { - - return { - scale: scaleName, - value: 0 - }; + if (defaultRef === 'zeroOrMin') { + return channel === 'x' ? {value: 0} : {field: {group: 'height'}}; + } else { // zeroOrMax + return channel === 'x' ? {field: {group: 'width'}} : {value: 0}; } } - // Put the mark on the y-axis - return {value: 0}; + return defaultRef; } + diff --git a/src/compile/scale/parse.ts b/src/compile/scale/parse.ts index 54e8a412e6..89898573e5 100644 --- a/src/compile/scale/parse.ts +++ b/src/compile/scale/parse.ts @@ -43,8 +43,7 @@ export function parseScaleCore(model: Model) { * Parse scales for all channels of a model. */ function parseUnitScaleCore(model: UnitModel): ScaleComponentIndex { - const {encoding, config} = model; - const mark = model.mark(); + const {encoding, config, mark} = model; return SCALE_CHANNELS.reduce((scaleComponents: ScaleComponentIndex, channel: ScaleChannel) => { let fieldDef: FieldDef; diff --git a/src/compile/scale/range.ts b/src/compile/scale/range.ts index 6e4c1c40fa..dcc49ee3dc 100644 --- a/src/compile/scale/range.ts +++ b/src/compile/scale/range.ts @@ -70,7 +70,7 @@ function parseUnitScaleRange(model: UnitModel) { const rangeWithExplicit = parseRangeForChannel( channel, scaleType, fieldDef.type, specifiedScale, model.config, - localScaleCmpt.get('zero'), model.mark(), sizeSpecified, model.getName(sizeType), xyRangeSteps + localScaleCmpt.get('zero'), model.mark, sizeSpecified, model.getName(sizeType), xyRangeSteps ); localScaleCmpt.setWithExplicit('range', rangeWithExplicit); diff --git a/src/compile/selection/transforms/nearest.ts b/src/compile/selection/transforms/nearest.ts index f9b46d3ddc..c99894fe9a 100644 --- a/src/compile/selection/transforms/nearest.ts +++ b/src/compile/selection/transforms/nearest.ts @@ -11,7 +11,7 @@ const nearest:TransformCompiler = { marks: function(model, selCmpt, marks) { const {x, y} = positionalProjections(selCmpt); - const markType = model.mark(); + const markType = model.mark; if (markType === 'line' || markType === 'area') { log.warn(log.message.nearestNotSupportForContinuous(markType)); return marks; diff --git a/src/compile/unit.ts b/src/compile/unit.ts index 72b2de1409..5c2c295564 100644 --- a/src/compile/unit.ts +++ b/src/compile/unit.ts @@ -253,7 +253,7 @@ export class UnitModel extends ModelWithField { return spec; } - public mark(): Mark { + public get mark(): Mark { return this.markDef.type; } diff --git a/src/log.ts b/src/log.ts index 7dcadb6af9..0ba4d30376 100644 --- a/src/log.ts +++ b/src/log.ts @@ -164,6 +164,17 @@ export namespace message { return `Invalid field type "${type}"`; } + export function nonZeroScaleUsedWithLengthMark( + mark: 'bar' | 'area', channel: Channel, + opt: {scaleType?: ScaleType, zeroFalse?: boolean} + ) { + const scaleText = opt.scaleType ? `${opt.scaleType} scale` : + opt.zeroFalse ? 'scale with zero=false' : + 'scale with custom domain that excludes zero'; + + return `A ${scaleText} is used with ${mark} mark. This can be misleading as the ${channel === 'x' ? 'width' : 'height'} of the ${mark} can be arbitrary based on the scale domain. You may want to use point mark instead.`; + } + export function invalidFieldTypeForCountAggregate(type: Type, aggregate: string) { return `Invalid field type "${type}" for aggregate: "${aggregate}", using "quantitative" instead.`; } diff --git a/src/transform.ts b/src/transform.ts index 489efa9901..86fc013d2a 100644 --- a/src/transform.ts +++ b/src/transform.ts @@ -4,8 +4,6 @@ import {Data} from './data'; import {LogicalOperand, normalizeLogicalOperand} from './logical'; import {normalizePredicate, Predicate} from './predicate'; import {TimeUnit} from './timeunit'; -import {WindowOnlyOp} from './window'; - export interface FilterTransform { /** @@ -102,30 +100,42 @@ export interface AggregatedFieldDef { as: string; } + +export type WindowOnlyOp = + 'row_number' | + 'rank' | + 'dense_rank' | + 'percent_rank' | + 'cume_dist' | + 'ntile' | + 'lag' | + 'lead' | + 'first_value' | + 'last_value' | + 'nth_value'; + export interface WindowFieldDef { /** - * The operations supported for the window aggregation. See the list of supported operations here: - * https://vega.github.io/vega-lite/docs/transforms/window.html + * The window or aggregation operations to apply within a window, including `rank`, `lead`, `sum`, `average` or `count`. See the list of all supported operations [here](https://vega.github.io/vega-lite/docs/transforms/window.html). */ op: AggregateOp | WindowOnlyOp; /** - * Parameter values for the window functions. Parameter value can be null for operations that do not accept a - * parameter. + * Parameter values for the window functions. Parameter values can be omitted for operations that do not accept a parameter. + * + * See the list of all supported operations and their parameters [here](https://vega.github.io/vega-lite/docs/transforms/window.html). */ param?: number; /** - * The data fields for which to compute aggregate or window functions. Field can be omitted for operations that do not - * operate over a specific data field, including count, rank, and dense_rank. + * The data field for which to compute the aggregate or window function. This can be omitted for functions that do not operate over a field such as `count`, `rank`, `dense_rank`. */ field?: string; /** - * The output name for each field. If none is defined will use the format op_field. For example, count_field for count, - * and sum_field for sum. + * The output name for each field. */ - as?: string; + as: string; } export interface WindowTransform { @@ -135,23 +145,26 @@ export interface WindowTransform { window: WindowFieldDef[]; /** - * The frame for the window, if none is set the default is `[null, 0]` everything before the - * current item. + * A frame specification as a two-element array indicating how the sliding window should proceed. The array entries should either be a number indicating the offset from the current data object, or null to indicate unbounded rows preceding or following the current data object. The default value is `[null, 0]`, indicating that the sliding window includes the current object and all preceding objects. The value `[-5, 5]` indicates that the window should include five objects preceding and five objects following the current object. Finally, `[null, null]` indicates that the window frame should always include all data objects. + * + * __Default value:__: `[null, 0]` (includes the current object and all preceding objects) */ frame?: (null | number)[]; /** - * Will indicate whether to ignore peer values (items with the same rank) in the window. The default value is `False`. + * Indicates if the sliding window frame should ignore peer values. (Peer values are those considered identical by the sort criteria). The default is false, causing the window frame to expand to include all peer values. If set to true, the window frame will be defined by offset values only. This setting only affects those operations that depend on the window frame, namely aggregation operations and the first_value, last_value, and nth_value window operations. + * + * __Default value:__ `false` */ ignorePeers?: boolean; /** - * The fields to group by. + * The data fields for partitioning the data objects into separate windows. If unspecified, all data points will be a single group. */ groupby?: string[]; /** - * The definitions of how to sort each of the fields in the window. + * A comparator definition for sorting data objects within a window. If two data objects are considered equal by the comparator, they are considered “peer” values of equal rank. If sort is not specified, the order is undefined: data objects are processed in the order they are observed and none are considered peers (the ignorePeers parameter is ignored and treated as if set to `true`). */ sort?: WindowSortField[]; } @@ -203,8 +216,15 @@ export interface LookupTransform { * A compartor for fields within the window transform */ export interface WindowSortField { + /** + * The name of the field to sort. + */ field: string; - order?: ('ascending' | 'descending'); + + /** + * Whether to sort the field in ascending or descending order. + */ + order?: 'ascending' | 'descending'; } export function isLookup(t: Transform): t is LookupTransform { diff --git a/src/vega.schema.ts b/src/vega.schema.ts index 5d0ac6b5ca..3a7b649e6f 100644 --- a/src/vega.schema.ts +++ b/src/vega.schema.ts @@ -4,8 +4,8 @@ import {BaseBin} from './bin'; import {NiceTime, ScaleType} from './scale'; import {SortOrder} from './sort'; import {StackOffset} from './stack'; +import {WindowOnlyOp} from './transform'; import {Flag, flagKeys} from './util'; -import {WindowOnlyOp} from './window'; export interface VgData { name: string; diff --git a/src/window.ts b/src/window.ts deleted file mode 100644 index c6cd34cc1e..0000000000 --- a/src/window.ts +++ /dev/null @@ -1,2 +0,0 @@ -export type WindowOnlyOp = 'row_number' | 'rank' | 'dense_rank' | 'percent_rank' | 'cume_dist' -| 'ntile' | 'lag' | 'lead' | 'first_value' | 'last_value' | 'nth_value'; diff --git a/test/compile/data/window.test.ts b/test/compile/data/window.test.ts index f2e1f334e3..73a49351ed 100644 --- a/test/compile/data/window.test.ts +++ b/test/compile/data/window.test.ts @@ -49,10 +49,12 @@ describe('compile/data/window', () => { as: 'ordered_row_number', }, { - op: 'count' + op: 'count', + as: 'count_field' }, { - op: 'sum' + op: 'sum', + as: 'sum_field' } ], ignorePeers: false, diff --git a/test/compile/mark/bar.test.ts b/test/compile/mark/bar.test.ts index 94584ae779..ab2146dc22 100644 --- a/test/compile/mark/bar.test.ts +++ b/test/compile/mark/bar.test.ts @@ -28,6 +28,78 @@ describe('Mark: Bar', function() { }); }); + it('should draw vertical bar, with y from zero to field value and with band value for x/width when domain that includes zero is specified', function () { + const model = parseUnitModelWithScaleAndLayoutSize({ + "data": {"url": 'data/cars.json'}, + "mark": "bar", + "encoding": { + "x": {"field": "Origin", "type": "nominal"}, + "y": {"type": "quantitative", "field": 'Acceleration', "aggregate": "mean", "scale": {"domain": [-1, 1]}} + } + }); + const props = bar.encodeEntry(model); + + assert.deepEqual(props.x, {scale: 'x', field: 'Origin'}); + assert.deepEqual(props.width, {scale: 'x', band: true}); + assert.deepEqual(props.y, {scale: 'y', field: 'mean_Acceleration'}); + assert.deepEqual(props.y2, {scale: 'y', value: 0}); + assert.isUndefined(props.height); + }); + + it('should draw vertical bar, with y from "group: height" to field value when domain that excludes zero is specified', log.wrap((logger) => { + const model = parseUnitModelWithScaleAndLayoutSize({ + "data": {"url": 'data/cars.json'}, + "mark": "bar", + "encoding": { + "x": {"field": "Origin", "type": "nominal"}, + "y": {"type": "quantitative", "field": 'Acceleration', "aggregate": "mean", "scale": {"domain": [1, 2]}} + } + }); + const props = bar.encodeEntry(model); + + assert.deepEqual(props.y, {scale: 'y', field: 'mean_Acceleration'}); + assert.deepEqual(props.y2, {field: {group: 'height'}}); + assert.isUndefined(props.height); + + assert.equal(logger.warns[0], log.message.nonZeroScaleUsedWithLengthMark('bar', 'y', {zeroFalse: false})); + })); + + it('should draw vertical bar, with y from "group: height" to field value when zero=false for y-scale', log.wrap((logger) => { + const model = parseUnitModelWithScaleAndLayoutSize({ + "data": {"url": 'data/cars.json'}, + "mark": "bar", + "encoding": { + "x": {"field": "Origin", "type": "nominal"}, + "y": {"type": "quantitative", "field": 'Acceleration', "aggregate": "mean", "scale": {"zero": false}} + } + }); + const props = bar.encodeEntry(model); + + assert.deepEqual(props.y, {scale: 'y', field: 'mean_Acceleration'}); + assert.deepEqual(props.y2, {field: {group: 'height'}}); + assert.isUndefined(props.height); + + assert.equal(logger.warns[0], log.message.nonZeroScaleUsedWithLengthMark('bar', 'y', {zeroFalse: true})); + })); + + it('should draw vertical bar, with y from "group: height" to field value when y-scale type is log', log.wrap((logger) => { + const model = parseUnitModelWithScaleAndLayoutSize({ + "data": {"url": 'data/cars.json'}, + "mark": "bar", + "encoding": { + "x": {"field": "Origin", "type": "nominal"}, + "y": {"type": "quantitative", "field": 'Acceleration', "aggregate": "mean", "scale": {"type": "log"}} + } + }); + const props = bar.encodeEntry(model); + + assert.deepEqual(props.y, {scale: 'y', field: 'mean_Acceleration'}); + assert.deepEqual(props.y2, {field: {group: 'height'}}); + assert.isUndefined(props.height); + + assert.equal(logger.warns[0], log.message.nonZeroScaleUsedWithLengthMark('bar', 'y', {scaleType: 'log'})); + })); + describe('simple horizontal', function() { const model = parseUnitModelWithScaleAndLayoutSize({ "data": {"url": 'data/cars.json'},