diff --git a/examples/compiled/bar_binned_yearmonth_grouped_center_band.png b/examples/compiled/bar_binned_yearmonth_grouped_center_band.png new file mode 100644 index 0000000000..c47310973c Binary files /dev/null and b/examples/compiled/bar_binned_yearmonth_grouped_center_band.png differ diff --git a/examples/compiled/bar_binned_yearmonth_grouped_center_band.svg b/examples/compiled/bar_binned_yearmonth_grouped_center_band.svg new file mode 100644 index 0000000000..1d3814b562 --- /dev/null +++ b/examples/compiled/bar_binned_yearmonth_grouped_center_band.svg @@ -0,0 +1 @@ +Jan 2005Feb 2005Mar 2005date050100150200priceAAPLAMZNGOOGIBMMSFTsymbol \ No newline at end of file diff --git a/examples/compiled/bar_binned_yearmonth_grouped_center_band.vg.json b/examples/compiled/bar_binned_yearmonth_grouped_center_band.vg.json new file mode 100644 index 0000000000..addd923f0b --- /dev/null +++ b/examples/compiled/bar_binned_yearmonth_grouped_center_band.vg.json @@ -0,0 +1,171 @@ +{ + "$schema": "https://vega.github.io/schema/vega/v5.json", + "description": "Stock price over time.", + "background": "white", + "padding": 5, + "width": 200, + "height": 200, + "style": "cell", + "data": [ + { + "name": "source_0", + "url": "data/stocks.csv", + "format": {"type": "csv", "parse": {"date": "date"}}, + "transform": [ + { + "type": "filter", + "expr": "inrange(time(datum[\"date\"]), [time(datetime(2005, 0, 1, 0, 0, 0, 0)), time(datetime(2005, 2, 1, 0, 0, 0, 0))])" + }, + { + "type": "formula", + "expr": "timeOffset('month', datum['date'], 1)", + "as": "date_end" + }, + { + "type": "formula", + "expr": "0.5 * timeOffset('month', datum['date'], -1) + 0.5 * datum['date']", + "as": "date_offsetted_rect_start" + }, + { + "type": "formula", + "expr": "0.5 * datum['date'] + 0.5 * datum['date_end']", + "as": "date_offsetted_rect_end" + }, + { + "type": "stack", + "groupby": ["date", "symbol"], + "field": "price", + "sort": {"field": [], "order": []}, + "as": ["price_start", "price_end"], + "offset": "zero" + }, + { + "type": "filter", + "expr": "(isDate(datum[\"date\"]) || (isValid(datum[\"date\"]) && isFinite(+datum[\"date\"]))) && isValid(datum[\"price\"]) && isFinite(+datum[\"price\"])" + } + ] + } + ], + "marks": [ + { + "name": "marks", + "type": "rect", + "style": ["bar"], + "from": {"data": "source_0"}, + "encode": { + "update": { + "fill": {"scale": "color", "field": "symbol"}, + "ariaRoleDescription": {"value": "bar"}, + "description": { + "signal": "\"date: \" + (timeFormat(datum[\"date\"], timeUnitSpecifier([\"year\",\"month\"], {\"year-month\":\"%b %Y \",\"year-month-date\":\"%b %d, %Y \"}))) + \"; price: \" + (format(datum[\"price\"], \"\")) + \"; symbol: \" + (isValid(datum[\"symbol\"]) ? datum[\"symbol\"] : \"\"+datum[\"symbol\"])" + }, + "xc": { + "scale": "x", + "field": "date", + "offset": {"scale": "xOffset", "field": "symbol", "band": 0.5} + }, + "width": {"signal": "max(0.25, bandwidth('xOffset'))"}, + "y": {"scale": "y", "field": "price_end"}, + "y2": {"scale": "y", "field": "price_start"} + } + } + } + ], + "scales": [ + { + "name": "x", + "type": "time", + "domain": { + "data": "source_0", + "fields": ["date_offsetted_rect_start", "date_offsetted_rect_end"] + }, + "range": [0, {"signal": "width"}] + }, + { + "name": "y", + "type": "linear", + "domain": {"data": "source_0", "fields": ["price_start", "price_end"]}, + "range": [{"signal": "height"}, 0], + "nice": true, + "zero": true + }, + { + "name": "xOffset", + "type": "band", + "domain": {"data": "source_0", "field": "symbol", "sort": true}, + "range": [ + { + "signal": "-0.4 * (scale('x', datetime(2001, 1, 1, 0, 0, 0, 0)) - scale('x', datetime(2001, 0, 1, 0, 0, 0, 0)))" + }, + { + "signal": "0.4 * (scale('x', datetime(2001, 1, 1, 0, 0, 0, 0)) - scale('x', datetime(2001, 0, 1, 0, 0, 0, 0)))" + } + ] + }, + { + "name": "color", + "type": "ordinal", + "domain": {"data": "source_0", "field": "symbol", "sort": true}, + "range": "category" + } + ], + "axes": [ + { + "scale": "x", + "orient": "bottom", + "gridScale": "y", + "grid": true, + "tickCount": {"signal": "ceil(width/40)"}, + "tickMinStep": { + "signal": "datetime(2001, 1, 1, 0, 0, 0, 0) - datetime(2001, 0, 1, 0, 0, 0, 0)" + }, + "domain": false, + "labels": false, + "aria": false, + "maxExtent": 0, + "minExtent": 0, + "ticks": false, + "zindex": 0 + }, + { + "scale": "y", + "orient": "left", + "gridScale": "x", + "grid": true, + "tickCount": {"signal": "ceil(height/40)"}, + "domain": false, + "labels": false, + "aria": false, + "maxExtent": 0, + "minExtent": 0, + "ticks": false, + "zindex": 0 + }, + { + "scale": "x", + "orient": "bottom", + "grid": false, + "title": "date", + "format": { + "signal": "timeUnitSpecifier([\"year\",\"month\"], {\"year-month\":\"%b %Y \",\"year-month-date\":\"%b %d, %Y \"})" + }, + "labelFlush": true, + "labelOverlap": true, + "tickCount": {"signal": "ceil(width/40)"}, + "tickMinStep": { + "signal": "datetime(2001, 1, 1, 0, 0, 0, 0) - datetime(2001, 0, 1, 0, 0, 0, 0)" + }, + "zindex": 0 + }, + { + "scale": "y", + "orient": "left", + "grid": false, + "title": "price", + "labelOverlap": true, + "tickCount": {"signal": "ceil(height/40)"}, + "zindex": 0 + } + ], + "legends": [{"fill": "color", "symbolType": "square", "title": "symbol"}] +} diff --git a/examples/compiled/bar_binned_yearmonth_label_band_center.png b/examples/compiled/bar_binned_yearmonth_label_band_center.png new file mode 100644 index 0000000000..76e09f342d Binary files /dev/null and b/examples/compiled/bar_binned_yearmonth_label_band_center.png differ diff --git a/examples/compiled/bar_binned_yearmonth_label_band_center.svg b/examples/compiled/bar_binned_yearmonth_label_band_center.svg new file mode 100644 index 0000000000..010afea05f --- /dev/null +++ b/examples/compiled/bar_binned_yearmonth_label_band_center.svg @@ -0,0 +1 @@ +Jan 2005Feb 2005Mar 2005date050100150200price195.62187.99180.51 \ No newline at end of file diff --git a/examples/compiled/bar_binned_yearmonth_label_band_center.vg.json b/examples/compiled/bar_binned_yearmonth_label_band_center.vg.json new file mode 100644 index 0000000000..91204d2334 --- /dev/null +++ b/examples/compiled/bar_binned_yearmonth_label_band_center.vg.json @@ -0,0 +1,205 @@ +{ + "$schema": "https://vega.github.io/schema/vega/v5.json", + "description": "Google's stock price over time.", + "background": "white", + "padding": 5, + "width": 200, + "height": 200, + "style": "cell", + "data": [ + { + "name": "source_0", + "url": "data/stocks.csv", + "format": {"type": "csv", "parse": {"date": "date"}}, + "transform": [ + {"type": "filter", "expr": "datum.symbol==='GOOG'"}, + { + "type": "filter", + "expr": "inrange(time(datum[\"date\"]), [time(datetime(2005, 0, 1, 0, 0, 0, 0)), time(datetime(2005, 2, 1, 0, 0, 0, 0))])" + } + ] + }, + { + "name": "data_0", + "source": "source_0", + "transform": [ + { + "type": "formula", + "expr": "timeOffset('month', datum['date'], 1)", + "as": "date_end" + }, + { + "type": "formula", + "expr": "0.5 * timeOffset('month', datum['date'], -1) + 0.5 * datum['date']", + "as": "date_offsetted_rect_start" + }, + { + "type": "formula", + "expr": "0.5 * datum['date'] + 0.5 * datum['date_end']", + "as": "date_offsetted_rect_end" + }, + { + "type": "stack", + "groupby": ["date"], + "field": "price", + "sort": {"field": [], "order": []}, + "as": ["price_start", "price_end"], + "offset": "zero" + }, + { + "type": "filter", + "expr": "(isDate(datum[\"date\"]) || (isValid(datum[\"date\"]) && isFinite(+datum[\"date\"]))) && isValid(datum[\"price\"]) && isFinite(+datum[\"price\"])" + } + ] + }, + { + "name": "data_1", + "source": "source_0", + "transform": [ + { + "type": "filter", + "expr": "(isDate(datum[\"date\"]) || (isValid(datum[\"date\"]) && isFinite(+datum[\"date\"]))) && isValid(datum[\"price\"]) && isFinite(+datum[\"price\"])" + } + ] + } + ], + "marks": [ + { + "name": "layer_0_marks", + "type": "rect", + "style": ["bar"], + "from": {"data": "data_0"}, + "encode": { + "update": { + "fill": {"value": "#4c78a8"}, + "ariaRoleDescription": {"value": "bar"}, + "description": { + "signal": "\"date: \" + (timeFormat(datum[\"date\"], timeUnitSpecifier([\"year\",\"month\"], {\"year-month\":\"%b %Y \",\"year-month-date\":\"%b %d, %Y \"}))) + \"; price: \" + (format(datum[\"price\"], \"\"))" + }, + "x2": { + "scale": "x", + "field": "date_offsetted_rect_start", + "offset": { + "signal": "0.5 + (abs(scale(\"x\", datum[\"date_end\"]) - scale(\"x\", datum[\"date\"])) < 0.25 ? -0.5 * (0.25 - (abs(scale(\"x\", datum[\"date_end\"]) - scale(\"x\", datum[\"date\"])))) : 0.5)" + } + }, + "x": { + "scale": "x", + "field": "date_offsetted_rect_end", + "offset": { + "signal": "0.5 + (abs(scale(\"x\", datum[\"date_end\"]) - scale(\"x\", datum[\"date\"])) < 0.25 ? 0.5 * (0.25 - (abs(scale(\"x\", datum[\"date_end\"]) - scale(\"x\", datum[\"date\"])))) : -0.5)" + } + }, + "y": {"scale": "y", "field": "price_end"}, + "y2": {"scale": "y", "field": "price_start"} + } + } + }, + { + "name": "layer_1_marks", + "type": "text", + "style": ["text"], + "from": {"data": "data_1"}, + "encode": { + "update": { + "baseline": {"value": "bottom"}, + "fill": {"value": "black"}, + "description": { + "signal": "\"date: \" + (timeFormat(datum[\"date\"], timeUnitSpecifier([\"year\",\"month\"], {\"year-month\":\"%b %Y \",\"year-month-date\":\"%b %d, %Y \"}))) + \"; price: \" + (format(datum[\"price\"], \"\"))" + }, + "x": {"scale": "x", "field": "date"}, + "y": {"scale": "y", "field": "price"}, + "text": {"signal": "format(datum[\"price\"], \"\")"}, + "align": {"value": "center"} + } + } + } + ], + "scales": [ + { + "name": "x", + "type": "time", + "domain": { + "fields": [ + {"data": "data_0", "field": "date_offsetted_rect_start"}, + {"data": "data_0", "field": "date_offsetted_rect_end"}, + {"data": "data_1", "field": "date"}, + {"data": "data_1", "field": "date_end"} + ] + }, + "range": [0, {"signal": "width"}] + }, + { + "name": "y", + "type": "linear", + "domain": { + "fields": [ + {"data": "data_0", "field": "price_start"}, + {"data": "data_0", "field": "price_end"}, + {"data": "data_1", "field": "price"} + ] + }, + "range": [{"signal": "height"}, 0], + "nice": true, + "zero": true + } + ], + "axes": [ + { + "scale": "x", + "orient": "bottom", + "gridScale": "y", + "grid": true, + "tickCount": {"signal": "ceil(width/40)"}, + "tickMinStep": { + "signal": "datetime(2001, 1, 1, 0, 0, 0, 0) - datetime(2001, 0, 1, 0, 0, 0, 0)" + }, + "domain": false, + "labels": false, + "aria": false, + "maxExtent": 0, + "minExtent": 0, + "ticks": false, + "zindex": 0 + }, + { + "scale": "y", + "orient": "left", + "gridScale": "x", + "grid": true, + "tickCount": {"signal": "ceil(height/40)"}, + "domain": false, + "labels": false, + "aria": false, + "maxExtent": 0, + "minExtent": 0, + "ticks": false, + "zindex": 0 + }, + { + "scale": "x", + "orient": "bottom", + "grid": false, + "title": "date", + "format": { + "signal": "timeUnitSpecifier([\"year\",\"month\"], {\"year-month\":\"%b %Y \",\"year-month-date\":\"%b %d, %Y \"})" + }, + "labelFlush": true, + "labelOverlap": true, + "tickCount": {"signal": "ceil(width/40)"}, + "tickMinStep": { + "signal": "datetime(2001, 1, 1, 0, 0, 0, 0) - datetime(2001, 0, 1, 0, 0, 0, 0)" + }, + "zindex": 0 + }, + { + "scale": "y", + "orient": "left", + "grid": false, + "title": "price", + "labelOverlap": true, + "tickCount": {"signal": "ceil(height/40)"}, + "zindex": 0 + } + ] +} diff --git a/examples/compiled/bar_month_temporal_band_center.png b/examples/compiled/bar_month_temporal_band_center.png new file mode 100644 index 0000000000..62203cbc3b Binary files /dev/null and b/examples/compiled/bar_month_temporal_band_center.png differ diff --git a/examples/compiled/bar_month_temporal_band_center.svg b/examples/compiled/bar_month_temporal_band_center.svg new file mode 100644 index 0000000000..4c5776ae76 --- /dev/null +++ b/examples/compiled/bar_month_temporal_band_center.svg @@ -0,0 +1 @@ +JanFebMarAprMayJunJulAugSepOctNovDecdate (month)012345Mean of precipitation \ No newline at end of file diff --git a/examples/compiled/bar_month_temporal_band_center.vg.json b/examples/compiled/bar_month_temporal_band_center.vg.json new file mode 100644 index 0000000000..597691718b --- /dev/null +++ b/examples/compiled/bar_month_temporal_band_center.vg.json @@ -0,0 +1,160 @@ +{ + "$schema": "https://vega.github.io/schema/vega/v5.json", + "background": "white", + "padding": 5, + "width": 200, + "height": 200, + "style": "cell", + "data": [ + { + "name": "source_0", + "url": "data/seattle-weather.csv", + "format": {"type": "csv", "parse": {"date": "date"}}, + "transform": [ + { + "field": "date", + "type": "timeunit", + "units": ["month"], + "as": ["month_date", "month_date_end"] + }, + { + "type": "formula", + "expr": "0.5 * timeOffset('month', datum['month_date'], -1) + 0.5 * datum['month_date']", + "as": "month_date_offsetted_rect_start" + }, + { + "type": "formula", + "expr": "0.5 * datum['month_date'] + 0.5 * datum['month_date_end']", + "as": "month_date_offsetted_rect_end" + }, + { + "type": "aggregate", + "groupby": [ + "month_date", + "month_date_end", + "month_date_offsetted_rect_start", + "month_date_offsetted_rect_end" + ], + "ops": ["mean"], + "fields": ["precipitation"], + "as": ["mean_precipitation"] + }, + { + "type": "filter", + "expr": "(isDate(datum[\"month_date\"]) || (isValid(datum[\"month_date\"]) && isFinite(+datum[\"month_date\"]))) && isValid(datum[\"mean_precipitation\"]) && isFinite(+datum[\"mean_precipitation\"])" + } + ] + } + ], + "marks": [ + { + "name": "marks", + "type": "rect", + "style": ["bar"], + "from": {"data": "source_0"}, + "encode": { + "update": { + "fill": {"value": "#4c78a8"}, + "ariaRoleDescription": {"value": "bar"}, + "description": { + "signal": "\"date (month): \" + (timeFormat(datum[\"month_date\"], timeUnitSpecifier([\"month\"], {\"year-month\":\"%b %Y \",\"year-month-date\":\"%b %d, %Y \"}))) + \"; Mean of precipitation: \" + (format(datum[\"mean_precipitation\"], \"\"))" + }, + "x2": { + "scale": "x", + "field": "month_date_offsetted_rect_start", + "offset": { + "signal": "0.5 + (abs(scale(\"x\", datum[\"month_date_end\"]) - scale(\"x\", datum[\"month_date\"])) < 0.25 ? -0.5 * (0.25 - (abs(scale(\"x\", datum[\"month_date_end\"]) - scale(\"x\", datum[\"month_date\"])))) : 0.5)" + } + }, + "x": { + "scale": "x", + "field": "month_date_offsetted_rect_end", + "offset": { + "signal": "0.5 + (abs(scale(\"x\", datum[\"month_date_end\"]) - scale(\"x\", datum[\"month_date\"])) < 0.25 ? 0.5 * (0.25 - (abs(scale(\"x\", datum[\"month_date_end\"]) - scale(\"x\", datum[\"month_date\"])))) : -0.5)" + } + }, + "y": {"scale": "y", "field": "mean_precipitation"}, + "y2": {"scale": "y", "value": 0} + } + } + } + ], + "scales": [ + { + "name": "x", + "type": "time", + "domain": { + "data": "source_0", + "fields": [ + "month_date_offsetted_rect_start", + "month_date_offsetted_rect_end" + ] + }, + "range": [0, {"signal": "width"}] + }, + { + "name": "y", + "type": "linear", + "domain": {"data": "source_0", "field": "mean_precipitation"}, + "range": [{"signal": "height"}, 0], + "nice": true, + "zero": true + } + ], + "axes": [ + { + "scale": "x", + "orient": "bottom", + "gridScale": "y", + "grid": true, + "tickMinStep": { + "signal": "datetime(2001, 1, 1, 0, 0, 0, 0) - datetime(2001, 0, 1, 0, 0, 0, 0)" + }, + "domain": false, + "labels": false, + "aria": false, + "maxExtent": 0, + "minExtent": 0, + "ticks": false, + "zindex": 0 + }, + { + "scale": "y", + "orient": "left", + "gridScale": "x", + "grid": true, + "tickCount": {"signal": "ceil(height/40)"}, + "domain": false, + "labels": false, + "aria": false, + "maxExtent": 0, + "minExtent": 0, + "ticks": false, + "zindex": 0 + }, + { + "scale": "x", + "orient": "bottom", + "grid": false, + "title": "date (month)", + "format": { + "signal": "timeUnitSpecifier([\"month\"], {\"year-month\":\"%b %Y \",\"year-month-date\":\"%b %d, %Y \"})" + }, + "labelFlush": true, + "labelOverlap": true, + "tickMinStep": { + "signal": "datetime(2001, 1, 1, 0, 0, 0, 0) - datetime(2001, 0, 1, 0, 0, 0, 0)" + }, + "zindex": 0 + }, + { + "scale": "y", + "orient": "left", + "grid": false, + "title": "Mean of precipitation", + "labelOverlap": true, + "tickCount": {"signal": "ceil(height/40)"}, + "zindex": 0 + } + ] +} diff --git a/examples/compiled/bar_month_temporal_band_center_config.png b/examples/compiled/bar_month_temporal_band_center_config.png new file mode 100644 index 0000000000..62203cbc3b Binary files /dev/null and b/examples/compiled/bar_month_temporal_band_center_config.png differ diff --git a/examples/compiled/bar_month_temporal_band_center_config.svg b/examples/compiled/bar_month_temporal_band_center_config.svg new file mode 100644 index 0000000000..4c5776ae76 --- /dev/null +++ b/examples/compiled/bar_month_temporal_band_center_config.svg @@ -0,0 +1 @@ +JanFebMarAprMayJunJulAugSepOctNovDecdate (month)012345Mean of precipitation \ No newline at end of file diff --git a/examples/compiled/bar_month_temporal_band_center_config.vg.json b/examples/compiled/bar_month_temporal_band_center_config.vg.json new file mode 100644 index 0000000000..597691718b --- /dev/null +++ b/examples/compiled/bar_month_temporal_band_center_config.vg.json @@ -0,0 +1,160 @@ +{ + "$schema": "https://vega.github.io/schema/vega/v5.json", + "background": "white", + "padding": 5, + "width": 200, + "height": 200, + "style": "cell", + "data": [ + { + "name": "source_0", + "url": "data/seattle-weather.csv", + "format": {"type": "csv", "parse": {"date": "date"}}, + "transform": [ + { + "field": "date", + "type": "timeunit", + "units": ["month"], + "as": ["month_date", "month_date_end"] + }, + { + "type": "formula", + "expr": "0.5 * timeOffset('month', datum['month_date'], -1) + 0.5 * datum['month_date']", + "as": "month_date_offsetted_rect_start" + }, + { + "type": "formula", + "expr": "0.5 * datum['month_date'] + 0.5 * datum['month_date_end']", + "as": "month_date_offsetted_rect_end" + }, + { + "type": "aggregate", + "groupby": [ + "month_date", + "month_date_end", + "month_date_offsetted_rect_start", + "month_date_offsetted_rect_end" + ], + "ops": ["mean"], + "fields": ["precipitation"], + "as": ["mean_precipitation"] + }, + { + "type": "filter", + "expr": "(isDate(datum[\"month_date\"]) || (isValid(datum[\"month_date\"]) && isFinite(+datum[\"month_date\"]))) && isValid(datum[\"mean_precipitation\"]) && isFinite(+datum[\"mean_precipitation\"])" + } + ] + } + ], + "marks": [ + { + "name": "marks", + "type": "rect", + "style": ["bar"], + "from": {"data": "source_0"}, + "encode": { + "update": { + "fill": {"value": "#4c78a8"}, + "ariaRoleDescription": {"value": "bar"}, + "description": { + "signal": "\"date (month): \" + (timeFormat(datum[\"month_date\"], timeUnitSpecifier([\"month\"], {\"year-month\":\"%b %Y \",\"year-month-date\":\"%b %d, %Y \"}))) + \"; Mean of precipitation: \" + (format(datum[\"mean_precipitation\"], \"\"))" + }, + "x2": { + "scale": "x", + "field": "month_date_offsetted_rect_start", + "offset": { + "signal": "0.5 + (abs(scale(\"x\", datum[\"month_date_end\"]) - scale(\"x\", datum[\"month_date\"])) < 0.25 ? -0.5 * (0.25 - (abs(scale(\"x\", datum[\"month_date_end\"]) - scale(\"x\", datum[\"month_date\"])))) : 0.5)" + } + }, + "x": { + "scale": "x", + "field": "month_date_offsetted_rect_end", + "offset": { + "signal": "0.5 + (abs(scale(\"x\", datum[\"month_date_end\"]) - scale(\"x\", datum[\"month_date\"])) < 0.25 ? 0.5 * (0.25 - (abs(scale(\"x\", datum[\"month_date_end\"]) - scale(\"x\", datum[\"month_date\"])))) : -0.5)" + } + }, + "y": {"scale": "y", "field": "mean_precipitation"}, + "y2": {"scale": "y", "value": 0} + } + } + } + ], + "scales": [ + { + "name": "x", + "type": "time", + "domain": { + "data": "source_0", + "fields": [ + "month_date_offsetted_rect_start", + "month_date_offsetted_rect_end" + ] + }, + "range": [0, {"signal": "width"}] + }, + { + "name": "y", + "type": "linear", + "domain": {"data": "source_0", "field": "mean_precipitation"}, + "range": [{"signal": "height"}, 0], + "nice": true, + "zero": true + } + ], + "axes": [ + { + "scale": "x", + "orient": "bottom", + "gridScale": "y", + "grid": true, + "tickMinStep": { + "signal": "datetime(2001, 1, 1, 0, 0, 0, 0) - datetime(2001, 0, 1, 0, 0, 0, 0)" + }, + "domain": false, + "labels": false, + "aria": false, + "maxExtent": 0, + "minExtent": 0, + "ticks": false, + "zindex": 0 + }, + { + "scale": "y", + "orient": "left", + "gridScale": "x", + "grid": true, + "tickCount": {"signal": "ceil(height/40)"}, + "domain": false, + "labels": false, + "aria": false, + "maxExtent": 0, + "minExtent": 0, + "ticks": false, + "zindex": 0 + }, + { + "scale": "x", + "orient": "bottom", + "grid": false, + "title": "date (month)", + "format": { + "signal": "timeUnitSpecifier([\"month\"], {\"year-month\":\"%b %Y \",\"year-month-date\":\"%b %d, %Y \"})" + }, + "labelFlush": true, + "labelOverlap": true, + "tickMinStep": { + "signal": "datetime(2001, 1, 1, 0, 0, 0, 0) - datetime(2001, 0, 1, 0, 0, 0, 0)" + }, + "zindex": 0 + }, + { + "scale": "y", + "orient": "left", + "grid": false, + "title": "Mean of precipitation", + "labelOverlap": true, + "tickCount": {"signal": "ceil(height/40)"}, + "zindex": 0 + } + ] +} diff --git a/examples/compiled/bar_simple_binned_timeunit_special_chars.vg.json b/examples/compiled/bar_simple_binned_timeunit_special_chars.vg.json index 191f43dd0d..2fb873170f 100644 --- a/examples/compiled/bar_simple_binned_timeunit_special_chars.vg.json +++ b/examples/compiled/bar_simple_binned_timeunit_special_chars.vg.json @@ -31,7 +31,7 @@ {"type": "formula", "expr": "toDate(datum[\"a.b\"])", "as": "a.b"}, { "type": "formula", - "expr": "timeOffset('date', datum['a.b'], 1)", + "expr": "utcOffset('date', datum['a.b'], 1)", "as": "a.b_end" }, { diff --git a/examples/compiled/bar_yearmonth_center_band.png b/examples/compiled/bar_yearmonth_center_band.png new file mode 100644 index 0000000000..4ac977a0c8 Binary files /dev/null and b/examples/compiled/bar_yearmonth_center_band.png differ diff --git a/examples/compiled/bar_yearmonth_center_band.svg b/examples/compiled/bar_yearmonth_center_band.svg new file mode 100644 index 0000000000..4e3fa4a8cc --- /dev/null +++ b/examples/compiled/bar_yearmonth_center_band.svg @@ -0,0 +1 @@ +Jan 2012Jan 2013Jan 2014Jan 2015date (year-month)051015202530Mean of temp_max \ No newline at end of file diff --git a/examples/compiled/bar_yearmonth_center_band.vg.json b/examples/compiled/bar_yearmonth_center_band.vg.json new file mode 100644 index 0000000000..4886b6c607 --- /dev/null +++ b/examples/compiled/bar_yearmonth_center_band.vg.json @@ -0,0 +1,163 @@ +{ + "$schema": "https://vega.github.io/schema/vega/v5.json", + "description": "Temperature in Seattle as a bar chart with yearmonth time unit.", + "background": "white", + "padding": 5, + "width": 200, + "height": 200, + "style": "cell", + "data": [ + { + "name": "source_0", + "url": "data/seattle-weather.csv", + "format": {"type": "csv", "parse": {"date": "date"}}, + "transform": [ + { + "field": "date", + "type": "timeunit", + "units": ["year", "month"], + "as": ["yearmonth_date", "yearmonth_date_end"] + }, + { + "type": "formula", + "expr": "0.5 * timeOffset('month', datum['yearmonth_date'], -1) + 0.5 * datum['yearmonth_date']", + "as": "yearmonth_date_offsetted_rect_start" + }, + { + "type": "formula", + "expr": "0.5 * datum['yearmonth_date'] + 0.5 * datum['yearmonth_date_end']", + "as": "yearmonth_date_offsetted_rect_end" + }, + { + "type": "aggregate", + "groupby": [ + "yearmonth_date", + "yearmonth_date_end", + "yearmonth_date_offsetted_rect_start", + "yearmonth_date_offsetted_rect_end" + ], + "ops": ["mean"], + "fields": ["temp_max"], + "as": ["mean_temp_max"] + }, + { + "type": "filter", + "expr": "(isDate(datum[\"yearmonth_date\"]) || (isValid(datum[\"yearmonth_date\"]) && isFinite(+datum[\"yearmonth_date\"]))) && isValid(datum[\"mean_temp_max\"]) && isFinite(+datum[\"mean_temp_max\"])" + } + ] + } + ], + "marks": [ + { + "name": "marks", + "type": "rect", + "style": ["bar"], + "from": {"data": "source_0"}, + "encode": { + "update": { + "fill": {"value": "#4c78a8"}, + "ariaRoleDescription": {"value": "bar"}, + "description": { + "signal": "\"date (year-month): \" + (timeFormat(datum[\"yearmonth_date\"], timeUnitSpecifier([\"year\",\"month\"], {\"year-month\":\"%b %Y \",\"year-month-date\":\"%b %d, %Y \"}))) + \"; Mean of temp_max: \" + (format(datum[\"mean_temp_max\"], \"\"))" + }, + "x2": { + "scale": "x", + "field": "yearmonth_date_offsetted_rect_start", + "offset": { + "signal": "0.5 + (abs(scale(\"x\", datum[\"yearmonth_date_end\"]) - scale(\"x\", datum[\"yearmonth_date\"])) < 0.25 ? -0.5 * (0.25 - (abs(scale(\"x\", datum[\"yearmonth_date_end\"]) - scale(\"x\", datum[\"yearmonth_date\"])))) : 0.5)" + } + }, + "x": { + "scale": "x", + "field": "yearmonth_date_offsetted_rect_end", + "offset": { + "signal": "0.5 + (abs(scale(\"x\", datum[\"yearmonth_date_end\"]) - scale(\"x\", datum[\"yearmonth_date\"])) < 0.25 ? 0.5 * (0.25 - (abs(scale(\"x\", datum[\"yearmonth_date_end\"]) - scale(\"x\", datum[\"yearmonth_date\"])))) : -0.5)" + } + }, + "y": {"scale": "y", "field": "mean_temp_max"}, + "y2": {"scale": "y", "value": 0} + } + } + } + ], + "scales": [ + { + "name": "x", + "type": "time", + "domain": { + "data": "source_0", + "fields": [ + "yearmonth_date_offsetted_rect_start", + "yearmonth_date_offsetted_rect_end" + ] + }, + "range": [0, {"signal": "width"}] + }, + { + "name": "y", + "type": "linear", + "domain": {"data": "source_0", "field": "mean_temp_max"}, + "range": [{"signal": "height"}, 0], + "nice": true, + "zero": true + } + ], + "axes": [ + { + "scale": "x", + "orient": "bottom", + "gridScale": "y", + "grid": true, + "tickCount": {"signal": "ceil(width/40)"}, + "tickMinStep": { + "signal": "datetime(2001, 1, 1, 0, 0, 0, 0) - datetime(2001, 0, 1, 0, 0, 0, 0)" + }, + "domain": false, + "labels": false, + "aria": false, + "maxExtent": 0, + "minExtent": 0, + "ticks": false, + "zindex": 0 + }, + { + "scale": "y", + "orient": "left", + "gridScale": "x", + "grid": true, + "tickCount": {"signal": "ceil(height/40)"}, + "domain": false, + "labels": false, + "aria": false, + "maxExtent": 0, + "minExtent": 0, + "ticks": false, + "zindex": 0 + }, + { + "scale": "x", + "orient": "bottom", + "grid": false, + "title": "date (year-month)", + "format": { + "signal": "timeUnitSpecifier([\"year\",\"month\"], {\"year-month\":\"%b %Y \",\"year-month-date\":\"%b %d, %Y \"})" + }, + "labelFlush": true, + "labelOverlap": true, + "tickCount": {"signal": "ceil(width/40)"}, + "tickMinStep": { + "signal": "datetime(2001, 1, 1, 0, 0, 0, 0) - datetime(2001, 0, 1, 0, 0, 0, 0)" + }, + "zindex": 0 + }, + { + "scale": "y", + "orient": "left", + "grid": false, + "title": "Mean of temp_max", + "labelOverlap": true, + "tickCount": {"signal": "ceil(height/40)"}, + "zindex": 0 + } + ] +} diff --git a/examples/compiled/rect_heatmap_weather_temporal_center_band.png b/examples/compiled/rect_heatmap_weather_temporal_center_band.png new file mode 100644 index 0000000000..1e05b81d86 Binary files /dev/null and b/examples/compiled/rect_heatmap_weather_temporal_center_band.png differ diff --git a/examples/compiled/rect_heatmap_weather_temporal_center_band.svg b/examples/compiled/rect_heatmap_weather_temporal_center_band.svg new file mode 100644 index 0000000000..52c09d0b29 --- /dev/null +++ b/examples/compiled/rect_heatmap_weather_temporal_center_band.svg @@ -0,0 +1 @@ +18152229DayJanFebMarAprMayJunJulAugSepOctNovDecMonth102030Daily Max Temperatures (C) in Seattle, WA \ No newline at end of file diff --git a/examples/compiled/rect_heatmap_weather_temporal_center_band.vg.json b/examples/compiled/rect_heatmap_weather_temporal_center_band.vg.json new file mode 100644 index 0000000000..ddafffe73d --- /dev/null +++ b/examples/compiled/rect_heatmap_weather_temporal_center_band.vg.json @@ -0,0 +1,223 @@ +{ + "$schema": "https://vega.github.io/schema/vega/v5.json", + "background": "white", + "padding": 5, + "width": 200, + "height": 200, + "title": { + "text": "Daily Max Temperatures (C) in Seattle, WA", + "frame": "group" + }, + "style": "cell", + "data": [ + { + "name": "source_0", + "url": "data/seattle-weather.csv", + "format": { + "type": "csv", + "parse": {"date": "date", "temp_max": "number"} + }, + "transform": [ + { + "field": "date", + "type": "timeunit", + "units": ["date"], + "as": ["date_date", "date_date_end"] + }, + { + "type": "formula", + "expr": "0.5 * timeOffset('date', datum['date_date'], -1) + 0.5 * datum['date_date']", + "as": "date_date_offsetted_rect_start" + }, + { + "type": "formula", + "expr": "0.5 * datum['date_date'] + 0.5 * datum['date_date_end']", + "as": "date_date_offsetted_rect_end" + }, + { + "field": "date", + "type": "timeunit", + "units": ["month"], + "as": ["month_date", "month_date_end"] + }, + { + "type": "formula", + "expr": "0.5 * timeOffset('month', datum['month_date'], -1) + 0.5 * datum['month_date']", + "as": "month_date_offsetted_rect_start" + }, + { + "type": "formula", + "expr": "0.5 * datum['month_date'] + 0.5 * datum['month_date_end']", + "as": "month_date_offsetted_rect_end" + }, + { + "type": "aggregate", + "groupby": [ + "date_date", + "date_date_end", + "date_date_offsetted_rect_start", + "date_date_offsetted_rect_end", + "month_date", + "month_date_end", + "month_date_offsetted_rect_start", + "month_date_offsetted_rect_end" + ], + "ops": ["max"], + "fields": ["temp_max"], + "as": ["max_temp_max"] + }, + { + "type": "filter", + "expr": "(isDate(datum[\"month_date\"]) || (isValid(datum[\"month_date\"]) && isFinite(+datum[\"month_date\"]))) && isValid(datum[\"max_temp_max\"]) && isFinite(+datum[\"max_temp_max\"])" + } + ] + } + ], + "marks": [ + { + "name": "marks", + "type": "rect", + "style": ["rect"], + "from": {"data": "source_0"}, + "encode": { + "update": { + "fill": {"scale": "color", "field": "max_temp_max"}, + "description": { + "signal": "\"Day: \" + (timeFormat(datum[\"date_date\"], '%e')) + \"; Month: \" + (timeFormat(datum[\"month_date\"], timeUnitSpecifier([\"month\"], {\"year-month\":\"%b %Y \",\"year-month-date\":\"%b %d, %Y \"}))) + \"; Max of temp_max: \" + (format(datum[\"max_temp_max\"], \"\"))" + }, + "x2": { + "scale": "x", + "field": "date_date_offsetted_rect_start", + "offset": { + "signal": "0.5 + (abs(scale(\"x\", datum[\"date_date_end\"]) - scale(\"x\", datum[\"date_date\"])) < 0.25 ? -0.5 * (0.25 - (abs(scale(\"x\", datum[\"date_date_end\"]) - scale(\"x\", datum[\"date_date\"])))) : 0)" + } + }, + "x": { + "scale": "x", + "field": "date_date_offsetted_rect_end", + "offset": { + "signal": "0.5 + (abs(scale(\"x\", datum[\"date_date_end\"]) - scale(\"x\", datum[\"date_date\"])) < 0.25 ? 0.5 * (0.25 - (abs(scale(\"x\", datum[\"date_date_end\"]) - scale(\"x\", datum[\"date_date\"])))) : 0)" + } + }, + "y2": { + "scale": "y", + "field": "month_date_offsetted_rect_start", + "offset": { + "signal": "0.5 + (abs(scale(\"y\", datum[\"month_date_end\"]) - scale(\"y\", datum[\"month_date\"])) < 0.25 ? 0.5 * (0.25 - (abs(scale(\"y\", datum[\"month_date_end\"]) - scale(\"y\", datum[\"month_date\"])))) : 0)" + } + }, + "y": { + "scale": "y", + "field": "month_date_offsetted_rect_end", + "offset": { + "signal": "0.5 + (abs(scale(\"y\", datum[\"month_date_end\"]) - scale(\"y\", datum[\"month_date\"])) < 0.25 ? -0.5 * (0.25 - (abs(scale(\"y\", datum[\"month_date_end\"]) - scale(\"y\", datum[\"month_date\"])))) : 0)" + } + } + } + } + } + ], + "scales": [ + { + "name": "x", + "type": "time", + "domain": { + "data": "source_0", + "fields": [ + "date_date_offsetted_rect_start", + "date_date_offsetted_rect_end" + ] + }, + "range": [0, {"signal": "width"}] + }, + { + "name": "y", + "type": "time", + "domain": { + "data": "source_0", + "fields": [ + "month_date_offsetted_rect_start", + "month_date_offsetted_rect_end" + ] + }, + "range": [{"signal": "height"}, 0] + }, + { + "name": "color", + "type": "linear", + "domain": {"data": "source_0", "field": "max_temp_max"}, + "range": "heatmap", + "interpolate": "hcl", + "zero": false + } + ], + "axes": [ + { + "scale": "x", + "orient": "bottom", + "gridScale": "y", + "grid": true, + "tickCount": {"signal": "ceil(width/40)"}, + "tickMinStep": { + "signal": "datetime(2001, 0, 2, 0, 0, 0, 0) - datetime(2001, 0, 1, 0, 0, 0, 0)" + }, + "domain": false, + "labels": false, + "aria": false, + "maxExtent": 0, + "minExtent": 0, + "ticks": false, + "zindex": 0 + }, + { + "scale": "y", + "orient": "left", + "gridScale": "x", + "grid": true, + "tickMinStep": { + "signal": "datetime(2001, 1, 1, 0, 0, 0, 0) - datetime(2001, 0, 1, 0, 0, 0, 0)" + }, + "domain": false, + "labels": false, + "aria": false, + "maxExtent": 0, + "minExtent": 0, + "ticks": false, + "zindex": 0 + }, + { + "scale": "x", + "orient": "bottom", + "grid": false, + "title": "Day", + "format": "%e", + "labelAngle": 0, + "labelBaseline": "top", + "labelFlush": true, + "labelOverlap": true, + "tickCount": {"signal": "ceil(width/40)"}, + "tickMinStep": { + "signal": "datetime(2001, 0, 2, 0, 0, 0, 0) - datetime(2001, 0, 1, 0, 0, 0, 0)" + }, + "zindex": 0 + }, + { + "scale": "y", + "orient": "left", + "grid": false, + "title": "Month", + "format": { + "signal": "timeUnitSpecifier([\"month\"], {\"year-month\":\"%b %Y \",\"year-month-date\":\"%b %d, %Y \"})" + }, + "labelOverlap": true, + "tickMinStep": { + "signal": "datetime(2001, 1, 1, 0, 0, 0, 0) - datetime(2001, 0, 1, 0, 0, 0, 0)" + }, + "zindex": 0 + } + ], + "legends": [ + {"fill": "color", "gradientLength": {"signal": "clamp(height, 64, 200)"}} + ], + "config": {"axis": {"domain": false}, "style": {"cell": {"strokeWidth": 0}}} +} diff --git a/examples/compiled/rect_heatmap_weather_temporal_center_band_config.png b/examples/compiled/rect_heatmap_weather_temporal_center_band_config.png new file mode 100644 index 0000000000..1e05b81d86 Binary files /dev/null and b/examples/compiled/rect_heatmap_weather_temporal_center_band_config.png differ diff --git a/examples/compiled/rect_heatmap_weather_temporal_center_band_config.svg b/examples/compiled/rect_heatmap_weather_temporal_center_band_config.svg new file mode 100644 index 0000000000..52c09d0b29 --- /dev/null +++ b/examples/compiled/rect_heatmap_weather_temporal_center_band_config.svg @@ -0,0 +1 @@ +18152229DayJanFebMarAprMayJunJulAugSepOctNovDecMonth102030Daily Max Temperatures (C) in Seattle, WA \ No newline at end of file diff --git a/examples/compiled/rect_heatmap_weather_temporal_center_band_config.vg.json b/examples/compiled/rect_heatmap_weather_temporal_center_band_config.vg.json new file mode 100644 index 0000000000..ddafffe73d --- /dev/null +++ b/examples/compiled/rect_heatmap_weather_temporal_center_band_config.vg.json @@ -0,0 +1,223 @@ +{ + "$schema": "https://vega.github.io/schema/vega/v5.json", + "background": "white", + "padding": 5, + "width": 200, + "height": 200, + "title": { + "text": "Daily Max Temperatures (C) in Seattle, WA", + "frame": "group" + }, + "style": "cell", + "data": [ + { + "name": "source_0", + "url": "data/seattle-weather.csv", + "format": { + "type": "csv", + "parse": {"date": "date", "temp_max": "number"} + }, + "transform": [ + { + "field": "date", + "type": "timeunit", + "units": ["date"], + "as": ["date_date", "date_date_end"] + }, + { + "type": "formula", + "expr": "0.5 * timeOffset('date', datum['date_date'], -1) + 0.5 * datum['date_date']", + "as": "date_date_offsetted_rect_start" + }, + { + "type": "formula", + "expr": "0.5 * datum['date_date'] + 0.5 * datum['date_date_end']", + "as": "date_date_offsetted_rect_end" + }, + { + "field": "date", + "type": "timeunit", + "units": ["month"], + "as": ["month_date", "month_date_end"] + }, + { + "type": "formula", + "expr": "0.5 * timeOffset('month', datum['month_date'], -1) + 0.5 * datum['month_date']", + "as": "month_date_offsetted_rect_start" + }, + { + "type": "formula", + "expr": "0.5 * datum['month_date'] + 0.5 * datum['month_date_end']", + "as": "month_date_offsetted_rect_end" + }, + { + "type": "aggregate", + "groupby": [ + "date_date", + "date_date_end", + "date_date_offsetted_rect_start", + "date_date_offsetted_rect_end", + "month_date", + "month_date_end", + "month_date_offsetted_rect_start", + "month_date_offsetted_rect_end" + ], + "ops": ["max"], + "fields": ["temp_max"], + "as": ["max_temp_max"] + }, + { + "type": "filter", + "expr": "(isDate(datum[\"month_date\"]) || (isValid(datum[\"month_date\"]) && isFinite(+datum[\"month_date\"]))) && isValid(datum[\"max_temp_max\"]) && isFinite(+datum[\"max_temp_max\"])" + } + ] + } + ], + "marks": [ + { + "name": "marks", + "type": "rect", + "style": ["rect"], + "from": {"data": "source_0"}, + "encode": { + "update": { + "fill": {"scale": "color", "field": "max_temp_max"}, + "description": { + "signal": "\"Day: \" + (timeFormat(datum[\"date_date\"], '%e')) + \"; Month: \" + (timeFormat(datum[\"month_date\"], timeUnitSpecifier([\"month\"], {\"year-month\":\"%b %Y \",\"year-month-date\":\"%b %d, %Y \"}))) + \"; Max of temp_max: \" + (format(datum[\"max_temp_max\"], \"\"))" + }, + "x2": { + "scale": "x", + "field": "date_date_offsetted_rect_start", + "offset": { + "signal": "0.5 + (abs(scale(\"x\", datum[\"date_date_end\"]) - scale(\"x\", datum[\"date_date\"])) < 0.25 ? -0.5 * (0.25 - (abs(scale(\"x\", datum[\"date_date_end\"]) - scale(\"x\", datum[\"date_date\"])))) : 0)" + } + }, + "x": { + "scale": "x", + "field": "date_date_offsetted_rect_end", + "offset": { + "signal": "0.5 + (abs(scale(\"x\", datum[\"date_date_end\"]) - scale(\"x\", datum[\"date_date\"])) < 0.25 ? 0.5 * (0.25 - (abs(scale(\"x\", datum[\"date_date_end\"]) - scale(\"x\", datum[\"date_date\"])))) : 0)" + } + }, + "y2": { + "scale": "y", + "field": "month_date_offsetted_rect_start", + "offset": { + "signal": "0.5 + (abs(scale(\"y\", datum[\"month_date_end\"]) - scale(\"y\", datum[\"month_date\"])) < 0.25 ? 0.5 * (0.25 - (abs(scale(\"y\", datum[\"month_date_end\"]) - scale(\"y\", datum[\"month_date\"])))) : 0)" + } + }, + "y": { + "scale": "y", + "field": "month_date_offsetted_rect_end", + "offset": { + "signal": "0.5 + (abs(scale(\"y\", datum[\"month_date_end\"]) - scale(\"y\", datum[\"month_date\"])) < 0.25 ? -0.5 * (0.25 - (abs(scale(\"y\", datum[\"month_date_end\"]) - scale(\"y\", datum[\"month_date\"])))) : 0)" + } + } + } + } + } + ], + "scales": [ + { + "name": "x", + "type": "time", + "domain": { + "data": "source_0", + "fields": [ + "date_date_offsetted_rect_start", + "date_date_offsetted_rect_end" + ] + }, + "range": [0, {"signal": "width"}] + }, + { + "name": "y", + "type": "time", + "domain": { + "data": "source_0", + "fields": [ + "month_date_offsetted_rect_start", + "month_date_offsetted_rect_end" + ] + }, + "range": [{"signal": "height"}, 0] + }, + { + "name": "color", + "type": "linear", + "domain": {"data": "source_0", "field": "max_temp_max"}, + "range": "heatmap", + "interpolate": "hcl", + "zero": false + } + ], + "axes": [ + { + "scale": "x", + "orient": "bottom", + "gridScale": "y", + "grid": true, + "tickCount": {"signal": "ceil(width/40)"}, + "tickMinStep": { + "signal": "datetime(2001, 0, 2, 0, 0, 0, 0) - datetime(2001, 0, 1, 0, 0, 0, 0)" + }, + "domain": false, + "labels": false, + "aria": false, + "maxExtent": 0, + "minExtent": 0, + "ticks": false, + "zindex": 0 + }, + { + "scale": "y", + "orient": "left", + "gridScale": "x", + "grid": true, + "tickMinStep": { + "signal": "datetime(2001, 1, 1, 0, 0, 0, 0) - datetime(2001, 0, 1, 0, 0, 0, 0)" + }, + "domain": false, + "labels": false, + "aria": false, + "maxExtent": 0, + "minExtent": 0, + "ticks": false, + "zindex": 0 + }, + { + "scale": "x", + "orient": "bottom", + "grid": false, + "title": "Day", + "format": "%e", + "labelAngle": 0, + "labelBaseline": "top", + "labelFlush": true, + "labelOverlap": true, + "tickCount": {"signal": "ceil(width/40)"}, + "tickMinStep": { + "signal": "datetime(2001, 0, 2, 0, 0, 0, 0) - datetime(2001, 0, 1, 0, 0, 0, 0)" + }, + "zindex": 0 + }, + { + "scale": "y", + "orient": "left", + "grid": false, + "title": "Month", + "format": { + "signal": "timeUnitSpecifier([\"month\"], {\"year-month\":\"%b %Y \",\"year-month-date\":\"%b %d, %Y \"})" + }, + "labelOverlap": true, + "tickMinStep": { + "signal": "datetime(2001, 1, 1, 0, 0, 0, 0) - datetime(2001, 0, 1, 0, 0, 0, 0)" + }, + "zindex": 0 + } + ], + "legends": [ + {"fill": "color", "gradientLength": {"signal": "clamp(height, 64, 200)"}} + ], + "config": {"axis": {"domain": false}, "style": {"cell": {"strokeWidth": 0}}} +} diff --git a/examples/compiled/time_parse_binnedutc.vg.json b/examples/compiled/time_parse_binnedutc.vg.json index a739f18cb5..55dc8f7d78 100644 --- a/examples/compiled/time_parse_binnedutc.vg.json +++ b/examples/compiled/time_parse_binnedutc.vg.json @@ -16,7 +16,7 @@ {"type": "formula", "expr": "toDate(datum[\"date\"])", "as": "date"}, { "type": "formula", - "expr": "timeOffset('hours', datum['date'], 1)", + "expr": "utcOffset('hours', datum['date'], 1)", "as": "date_end" } ] diff --git a/examples/specs/bar_binned_yearmonth_grouped_center_band.vl.json b/examples/specs/bar_binned_yearmonth_grouped_center_band.vl.json new file mode 100644 index 0000000000..56c854dc14 --- /dev/null +++ b/examples/specs/bar_binned_yearmonth_grouped_center_band.vl.json @@ -0,0 +1,15 @@ +{ + "$schema": "https://vega.github.io/schema/vega-lite/v5.json", + "description": "Stock price over time.", + "data": {"url": "data/stocks.csv"}, + "transform": [ + {"filter": {"timeUnit": "binnedyearmonth", "field": "date", "range": [{"year": 2005, "month": 1}, {"year": 2005, "month": 3}]}} + ], + "mark": "bar", + "encoding": { + "x": {"timeUnit": "binnedyearmonth", "field": "date", "type": "temporal", "bandPosition": 0}, + "xOffset": {"field": "symbol", "type": "nominal"}, + "color": {"field": "symbol", "type": "nominal"}, + "y": {"field": "price", "type": "quantitative"} + } +} diff --git a/examples/specs/bar_binned_yearmonth_label_band_center.vl.json b/examples/specs/bar_binned_yearmonth_label_band_center.vl.json new file mode 100644 index 0000000000..798a6d11ec --- /dev/null +++ b/examples/specs/bar_binned_yearmonth_label_band_center.vl.json @@ -0,0 +1,21 @@ +{ + "$schema": "https://vega.github.io/schema/vega-lite/v5.json", + "description": "Google's stock price over time.", + "data": {"url": "data/stocks.csv"}, + "transform": [ + {"filter": "datum.symbol==='GOOG'"}, + {"filter": {"timeUnit": "binnedyearmonth", "field": "date", "range": [{"year": 2005, "month": 1}, {"year": 2005, "month": 3}]}} + ], + "encoding": { + "x": {"timeUnit": "binnedyearmonth", "field": "date", "type": "temporal", "bandPosition": 0}, + "y": {"field": "price", "type": "quantitative"} + }, + "layer": [{ + "mark": "bar" + }, { + "mark": {"type": "text", "baseline": "bottom"}, + "encoding": { + "text": {"field": "price", "type": "quantitative"} + } + }] +} diff --git a/examples/specs/bar_month_temporal_band_center.vl.json b/examples/specs/bar_month_temporal_band_center.vl.json new file mode 100644 index 0000000000..d8ef4102c3 --- /dev/null +++ b/examples/specs/bar_month_temporal_band_center.vl.json @@ -0,0 +1,9 @@ +{ + "$schema": "https://vega.github.io/schema/vega-lite/v5.json", + "data": {"url": "data/seattle-weather.csv"}, + "mark": "bar", + "encoding": { + "x": {"timeUnit": "month", "field": "date", "type": "temporal", "bandPosition": 0}, + "y": {"aggregate": "mean", "field": "precipitation"} + } +} diff --git a/examples/specs/bar_month_temporal_band_center_config.vl.json b/examples/specs/bar_month_temporal_band_center_config.vl.json new file mode 100644 index 0000000000..7d8c05b428 --- /dev/null +++ b/examples/specs/bar_month_temporal_band_center_config.vl.json @@ -0,0 +1,12 @@ +{ + "$schema": "https://vega.github.io/schema/vega-lite/v5.json", + "data": {"url": "data/seattle-weather.csv"}, + "mark": "bar", + "encoding": { + "x": {"timeUnit": "month", "field": "date", "type": "temporal"}, + "y": {"aggregate": "mean", "field": "precipitation"} + }, + "config": { + "bar": {"timeUnitBandPosition": 0} + } +} diff --git a/examples/specs/bar_yearmonth_center_band.vl.json b/examples/specs/bar_yearmonth_center_band.vl.json new file mode 100644 index 0000000000..69128eb98b --- /dev/null +++ b/examples/specs/bar_yearmonth_center_band.vl.json @@ -0,0 +1,10 @@ +{ + "$schema": "https://vega.github.io/schema/vega-lite/v5.json", + "description": "Temperature in Seattle as a bar chart with yearmonth time unit.", + "data": {"url": "data/seattle-weather.csv"}, + "mark": "bar", + "encoding": { + "x": {"timeUnit": "yearmonth", "field": "date", "bandPosition": 0}, + "y": {"aggregate": "mean", "field": "temp_max"} + } +} diff --git a/examples/specs/normalized/bar_binned_yearmonth_label_band_center_normalized.vl.json b/examples/specs/normalized/bar_binned_yearmonth_label_band_center_normalized.vl.json new file mode 100644 index 0000000000..7ab0179750 --- /dev/null +++ b/examples/specs/normalized/bar_binned_yearmonth_label_band_center_normalized.vl.json @@ -0,0 +1,42 @@ +{ + "$schema": "https://vega.github.io/schema/vega-lite/v5.json", + "description": "Google's stock price over time.", + "data": {"url": "data/stocks.csv"}, + "layer": [ + { + "mark": "bar", + "encoding": { + "x": { + "timeUnit": "binnedyearmonth", + "field": "date", + "type": "temporal", + "bandPosition": 0 + }, + "y": {"field": "price", "type": "quantitative"} + } + }, + { + "mark": {"type": "text", "baseline": "bottom"}, + "encoding": { + "x": { + "timeUnit": "binnedyearmonth", + "field": "date", + "type": "temporal", + "bandPosition": 0 + }, + "y": {"field": "price", "type": "quantitative"}, + "text": {"field": "price", "type": "quantitative"} + } + } + ], + "transform": [ + {"filter": "datum.symbol==='GOOG'"}, + { + "filter": { + "timeUnit": "binnedyearmonth", + "field": "date", + "range": [{"year": 2005, "month": 1}, {"year": 2005, "month": 3}] + } + } + ] +} \ No newline at end of file diff --git a/examples/specs/rect_heatmap_weather_temporal_center_band.vl.json b/examples/specs/rect_heatmap_weather_temporal_center_band.vl.json new file mode 100644 index 0000000000..5697820e15 --- /dev/null +++ b/examples/specs/rect_heatmap_weather_temporal_center_band.vl.json @@ -0,0 +1,45 @@ +{ + "$schema": "https://vega.github.io/schema/vega-lite/v5.json", + "data": { + "url": "data/seattle-weather.csv" + }, + "title": "Daily Max Temperatures (C) in Seattle, WA", + "config": { + "view": { + "strokeWidth": 0, + "step": 13 + }, + "axis": { + "domain": false + } + }, + "mark": "rect", + "encoding": { + "x": { + "field": "date", + "timeUnit": "date", + "type": "temporal", + "title": "Day", + "bandPosition": 0, + "axis": { + "labelAngle": 0, + "format": "%e" + } + }, + "y": { + "field": "date", + "timeUnit": "month", + "bandPosition": 0, + "type": "temporal", + "title": "Month" + }, + "color": { + "field": "temp_max", + "aggregate": "max", + "type": "quantitative", + "legend": { + "title": null + } + } + } +} diff --git a/examples/specs/rect_heatmap_weather_temporal_center_band_config.vl.json b/examples/specs/rect_heatmap_weather_temporal_center_band_config.vl.json new file mode 100644 index 0000000000..d00e44abbf --- /dev/null +++ b/examples/specs/rect_heatmap_weather_temporal_center_band_config.vl.json @@ -0,0 +1,46 @@ +{ + "$schema": "https://vega.github.io/schema/vega-lite/v5.json", + "data": { + "url": "data/seattle-weather.csv" + }, + "title": "Daily Max Temperatures (C) in Seattle, WA", + "config": { + "view": { + "strokeWidth": 0, + "step": 13 + }, + "axis": { + "domain": false + }, + "rect": { + "timeUnitBandPosition": 0 + } + }, + "mark": "rect", + "encoding": { + "x": { + "field": "date", + "timeUnit": "date", + "type": "temporal", + "title": "Day", + "axis": { + "labelAngle": 0, + "format": "%e" + } + }, + "y": { + "field": "date", + "timeUnit": "month", + "type": "temporal", + "title": "Month" + }, + "color": { + "field": "temp_max", + "aggregate": "max", + "type": "quantitative", + "legend": { + "title": null + } + } + } +} diff --git a/site/_data/examples.json b/site/_data/examples.json index 94159777c3..a1fab1f006 100644 --- a/site/_data/examples.json +++ b/site/_data/examples.json @@ -84,6 +84,10 @@ "name": "bar_month_temporal_initial", "title": "Bar Chart showing Initials of Month Names" }, + { + "name": "bar_month_temporal_band_center", + "title": "Bar Chart with bars center-aligned with time unit ticks" + }, { "name": "bar_negative", "title": "Bar Chart with Negative Values and a Zero-Baseline" diff --git a/site/_includes/docs_toc.md b/site/_includes/docs_toc.md index 3e5e066297..c1d1174988 100644 --- a/site/_includes/docs_toc.md +++ b/site/_includes/docs_toc.md @@ -228,8 +228,8 @@ - [Documentation Overview]({{site.baseurl}}/docs/axis.html#documentation-overview) - [Axis Properties]({{site.baseurl}}/docs/axis.html#axis-properties) - [Axis Config]({{site.baseurl}}/docs/axis.html#config) - - [Band]({{site.baseurl}}/docs/band.html) - - [Examples]({{site.baseurl}}/docs/band.html#examples) + - [Band Position]({{site.baseurl}}/docs/bandposition.html) + - [Examples]({{site.baseurl}}/docs/bandposition.html#examples) - [Bin]({{site.baseurl}}/docs/bin.html) - [Documentation Overview]({{site.baseurl}}/docs/bin.html#documentation-overview) - [Binning in Encoding Field Definition]({{site.baseurl}}/docs/bin.html#encoding) diff --git a/site/_layouts/docs.html b/site/_layouts/docs.html index c46a28c6e0..5dc0b66848 100644 --- a/site/_layouts/docs.html +++ b/site/_layouts/docs.html @@ -98,8 +98,8 @@ url: aggregate - text: Axis url: axis - - text: Band - url: band + - text: Band Position + url: bandposition - text: Bin url: bin - text: Condition diff --git a/site/docs/encoding/band.md b/site/docs/encoding/band.md deleted file mode 100644 index e5d15520cd..0000000000 --- a/site/docs/encoding/band.md +++ /dev/null @@ -1,21 +0,0 @@ ---- -layout: docs -title: Band -permalink: /docs/band.html ---- - -Band properties can be used to adjust mark bandwidth or position for band scales, bin intervals, or time unit intervals. - -{% include table.html props="band" source="DatumDef" %} - -## Examples - -### Line Position - -By default, points in line marks are placed at the beginning of a time interval (e.g., "month"): - -
- -Setting `band` to `0.5` moves the points to the middle of the time interval. - -
diff --git a/site/docs/encoding/bandPosition.md b/site/docs/encoding/bandPosition.md new file mode 100644 index 0000000000..63a239c396 --- /dev/null +++ b/site/docs/encoding/bandPosition.md @@ -0,0 +1,31 @@ +--- +layout: docs +title: Band Position +permalink: /docs/bandposition.html +--- + +Band properties can be used to adjust mark bandwidth or position for band scales, bin intervals, or time unit intervals. + +{% include table.html props="bandPosition" source="DatumDef" %} + +## Examples + +### Line Position + +By default, points in line marks are placed at the beginning of a time interval (e.g., "month"): + +
+ +Setting `bandPosition` to `0.5` moves the points to the middle of the time interval. + +
+ +### Bar Position + +By default, bar marks are placed from the beginning of a time interval (e.g., "month") to the end of the interval: + +
+ +Setting `bandPosition` to `0` moves the bar to center-align with ticks. + +
diff --git a/site/docs/transform/timeunit.md b/site/docs/transform/timeunit.md index 7f6ef48e6b..1074cd956e 100644 --- a/site/docs/transform/timeunit.md +++ b/site/docs/transform/timeunit.md @@ -68,6 +68,12 @@ Using `timeUnit` with rect-based marks (including `bar`, `rect`, and `image`) wi You can also add `"binned"` prefix if your data has already been binned and want Vega-Lite to apply the right formatting, including the right bands for the interval, to your charts. +By default, bar marks are placed from the beginning of a time interval (e.g., "month") to the end of the interval. + +Setting [`bandPosition`](bandposition.html) to `0` moves the bar to center-align with ticks. + +
+ ### Time Unit's Band diff --git a/src/channeldef.ts b/src/channeldef.ts index 77ed5a2581..7f2ccb8eb7 100644 --- a/src/channeldef.ts +++ b/src/channeldef.ts @@ -534,7 +534,7 @@ export function getBandPosition({ if (isFieldDef(fieldDef)) { const {timeUnit, bin} = fieldDef; if (timeUnit && !fieldDef2) { - return isRectBasedMark(mark.type) ? 0 : getMarkConfig('timeUnitBandPosition', mark, config); + return getMarkConfig('timeUnitBandPosition', mark, config); } else if (isBinning(bin)) { return 0.5; } diff --git a/src/compile/data/aggregate.ts b/src/compile/data/aggregate.ts index b09238cf78..a8a9cefba1 100644 --- a/src/compile/data/aggregate.ts +++ b/src/compile/data/aggregate.ts @@ -5,9 +5,18 @@ import { getPositionChannelFromLatLong, getSecondaryRangeChannel, isGeoPositionChannel, - isScaleChannel + isScaleChannel, + isXorY } from '../../channel'; -import {binRequiresRange, FieldDef, hasBandEnd, isScaleFieldDef, isTypedFieldDef, vgField} from '../../channeldef'; +import { + binRequiresRange, + FieldDef, + getBandPosition, + hasBandEnd, + isScaleFieldDef, + isTypedFieldDef, + vgField +} from '../../channeldef'; import * as log from '../../log'; import {isFieldRange} from '../../scale'; import {AggregateTransform} from '../../transform'; @@ -15,6 +24,8 @@ import {Dict, duplicate, hash, keys, replacePathInField, setEqual} from '../../u import {isUnitModel, ModelWithField} from '../model'; import {UnitModel} from '../unit'; import {DataFlowNode} from './dataflow'; +import {isRectBasedMark} from '../../mark'; +import {OFFSETTED_RECT_END_SUFFIX, OFFSETTED_RECT_START_SUFFIX} from './timeunit'; type Measures = Dict>>>; @@ -29,6 +40,15 @@ function addDimension(dims: Set, channel: Channel, fieldDef: FieldDef { + const formula = model.reduceFieldDef((timeUnitComponent: TimeUnitComponent, fieldDef, channel) => { const {field, timeUnit} = fieldDef; if (timeUnit) { let component: TimeUnitComponent | undefined; + if (isBinnedTimeUnit(timeUnit)) { // For binned time unit, only produce end if the mark is a rect-based mark (rect, bar, image, arc), which needs "range". if (isUnitModel(model)) { - const {mark} = model; - if (isRectBasedMark(mark) || !!fieldDef.bandPosition) { + const {mark, markDef, config} = model; + const bandPosition = getBandPosition({fieldDef, markDef, config}); + if (isRectBasedMark(mark) || !!bandPosition) { component = { timeUnit: normalizeTimeUnit(timeUnit), field @@ -67,6 +72,15 @@ export class TimeUnitNode extends DataFlowNode { timeUnit }; } + + if (isUnitModel(model)) { + const {mark, markDef, config} = model; + const bandPosition = getBandPosition({fieldDef, markDef, config}); + if (isRectBasedMark(mark) && isXorY(channel) && bandPosition !== 0.5) { + component.rectBandPosition = bandPosition; + } + } + if (component) { timeUnitComponent[hash(component)] = component; } @@ -157,9 +171,14 @@ export class TimeUnitNode extends DataFlowNode { const transforms: (VgTimeUnitTransform | VgFormulaTransform)[] = []; for (const f of vals(this.timeUnits)) { + const {rectBandPosition} = f; + const normalizedTimeUnit = normalizeTimeUnit(f.timeUnit); + if (isTimeUnitTransformComponent(f)) { - const {field, as, timeUnit} = f; - const {unit, utc, ...params} = normalizeTimeUnit(timeUnit); + const {field, as} = f; + const {unit, utc, ...params} = normalizedTimeUnit; + + const startEnd: [string, string] = [as, `${as}_end`]; transforms.push({ field: replacePathInField(field), @@ -167,22 +186,76 @@ export class TimeUnitNode extends DataFlowNode { ...(unit ? {units: getTimeUnitParts(unit)} : {}), ...(utc ? {timezone: 'utc'} : {}), ...params, - as: [as, `${as}_end`] + as: startEnd }); + + transforms.push(...offsetedRectFormulas(startEnd, rectBandPosition, normalizedTimeUnit)); } else if (f) { - const {field: escapedField, timeUnit} = f; + const {field: escapedField} = f; // since this is a expression, we want the unescaped field name const field = escapedField.replaceAll('\\.', '.'); - const smallestUnit = getSmallestTimeUnitPart(timeUnit?.unit); - const {part, step} = getDateTimePartAndStep(smallestUnit, timeUnit.step); + const expr = offsetExpr({timeUnit: normalizedTimeUnit, field}); + const endAs = offsetAs(field); transforms.push({ type: 'formula', - expr: `timeOffset('${part}', datum['${field}'], ${step})`, - as: offsetAs(field) + expr, + as: endAs }); + + transforms.push(...offsetedRectFormulas([field, endAs], rectBandPosition, normalizedTimeUnit)); } } return transforms; } } + +export const OFFSETTED_RECT_START_SUFFIX = 'offsetted_rect_start'; +export const OFFSETTED_RECT_END_SUFFIX = 'offsetted_rect_end'; + +function offsetExpr({timeUnit, field, reverse}: {timeUnit: TimeUnitParams; field: string; reverse?: boolean}) { + const {unit, utc} = timeUnit; + const smallestUnit = getSmallestTimeUnitPart(unit); + const {part, step} = getDateTimePartAndStep(smallestUnit, timeUnit.step); + const offsetFn = utc ? 'utcOffset' : 'timeOffset'; + const expr = `${offsetFn}('${part}', datum['${field}'], ${reverse ? -step : step})`; + return expr; +} + +function offsetedRectFormulas( + [startField, endField]: [string, string], + rectBandPosition: number | undefined, + timeUnit: TimeUnitParams +): VgFormulaTransform[] { + if (rectBandPosition !== undefined && rectBandPosition !== 0.5) { + const startExpr = `datum['${startField}']`; + const endExpr = `datum['${endField}']`; + return [ + { + type: 'formula', + expr: interpolateExpr( + [ + offsetExpr({ + timeUnit, + field: startField, + reverse: true + }), + startExpr + ], + rectBandPosition + 0.5 + ), + as: `${startField}_${OFFSETTED_RECT_START_SUFFIX}` + }, + { + type: 'formula', + expr: interpolateExpr([startExpr, endExpr], rectBandPosition + 0.5), + as: `${startField}_${OFFSETTED_RECT_END_SUFFIX}` + } + ]; + } + return []; +} + +function interpolateExpr([start, end]: [string, string], fraction: number) { + return `${1 - fraction} * ${start} + ${fraction} * ${end}`; +} diff --git a/src/compile/mark/encode/position-rect.ts b/src/compile/mark/encode/position-rect.ts index c2da226aff..afbd417d48 100644 --- a/src/compile/mark/encode/position-rect.ts +++ b/src/compile/mark/encode/position-rect.ts @@ -11,7 +11,7 @@ import { PolarPositionChannel, PositionChannel } from '../../../channel'; -import {getBandSize, isFieldDef, isFieldOrDatumDef, TypedFieldDef, vgField} from '../../../channeldef'; +import {getBandPosition, getBandSize, isFieldDef, isFieldOrDatumDef, TypedFieldDef, vgField} from '../../../channeldef'; import {Config, getViewConfigDiscreteStep} from '../../../config'; import {Encoding} from '../../../encoding'; import * as log from '../../../log'; @@ -30,6 +30,7 @@ import * as ref from './valueref'; import {getOffsetScaleChannel} from '../../../channel'; import {getFirstDefined} from '../../../util'; import {Mark} from '../../../mark'; +import {OFFSETTED_RECT_END_SUFFIX, OFFSETTED_RECT_START_SUFFIX} from '../../data/timeunit'; export function rectPosition(model: UnitModel, channel: 'x' | 'y' | 'theta' | 'radius'): VgEncodeEntry { const {config, encoding, markDef} = model; @@ -331,13 +332,18 @@ function rectBinPosition({ ? (1 - bandSize.band) / 2 : 0.5; + const bandPosition = getBandPosition({fieldDef, fieldDef2, markDef, config}); + if (isBinning(fieldDef.bin) || fieldDef.timeUnit) { + const useRectOffsetField = fieldDef.timeUnit && bandPosition !== 0.5; + return { [vgChannel2]: rectBinRef({ fieldDef, scaleName, bandPosition: bandPositionForBandSize, - offset: binSpacingOffset2 + offset: binSpacingOffset2, + useRectOffsetField }), [vgChannel]: rectBinRef({ fieldDef, @@ -345,7 +351,8 @@ function rectBinPosition({ bandPosition: isSignalRef(bandPositionForBandSize) ? {signal: `1-${bandPositionForBandSize.signal}`} : 1 - bandPositionForBandSize, - offset: binSpacingOffset + offset: binSpacingOffset, + useRectOffsetField }) }; } else if (isBinned(fieldDef.bin)) { @@ -373,21 +380,29 @@ function rectBinPosition({ /** * Value Ref for binned fields */ -export function rectBinRef({ +function rectBinRef({ fieldDef, scaleName, bandPosition, - offset + offset, + useRectOffsetField }: { fieldDef: TypedFieldDef; scaleName: string; bandPosition: number | SignalRef; offset?: number | SignalRef; + useRectOffsetField: boolean; }) { return ref.interpolatedSignalRef({ scaleName, fieldOrDatumDef: fieldDef, bandPosition, - offset + offset, + ...(useRectOffsetField + ? { + startSuffix: OFFSETTED_RECT_START_SUFFIX, + endSuffix: OFFSETTED_RECT_END_SUFFIX + } + : {}) }); } diff --git a/src/compile/mark/encode/valueref.ts b/src/compile/mark/encode/valueref.ts index 2c427f54af..7416b18155 100644 --- a/src/compile/mark/encode/valueref.ts +++ b/src/compile/mark/encode/valueref.ts @@ -167,12 +167,14 @@ export function interpolatedSignalRef({ fieldOrDatumDef2, offset, startSuffix, + endSuffix = 'end', bandPosition = 0.5 }: { scaleName: string; fieldOrDatumDef: TypedFieldDef; fieldOrDatumDef2?: SecondaryFieldDef; startSuffix?: string; + endSuffix?: string; offset: number | SignalRef | VgValueRef; bandPosition: number | SignalRef; }): VgValueRef { @@ -181,7 +183,7 @@ export function interpolatedSignalRef({ const end = fieldOrDatumDef2 !== undefined ? vgField(fieldOrDatumDef2, {expr}) - : vgField(fieldOrDatumDef, {suffix: 'end', expr}); + : vgField(fieldOrDatumDef, {suffix: endSuffix, expr}); const ref: VgValueRef = {}; diff --git a/src/compile/scale/domain.ts b/src/compile/scale/domain.ts index dd92b60cd3..1fde82c326 100644 --- a/src/compile/scale/domain.ts +++ b/src/compile/scale/domain.ts @@ -13,6 +13,7 @@ import {isBinning, isBinParams, isParameterExtent} from '../../bin'; import {getSecondaryRangeChannel, isScaleChannel, ScaleChannel} from '../../channel'; import { binRequiresRange, + getBandPosition, getFieldOrDatumDef, hasBandEnd, isDatumDef, @@ -54,6 +55,8 @@ import {SignalRefWrapper} from '../signal'; import {Explicit, makeExplicit, makeImplicit, mergeValuesWithExplicit} from '../split'; import {UnitModel} from '../unit'; import {ScaleComponent, ScaleComponentIndex} from './component'; +import {isRectBasedMark} from '../../mark'; +import {OFFSETTED_RECT_END_SUFFIX, OFFSETTED_RECT_START_SUFFIX} from '../data/timeunit'; export function parseScaleDomain(model: Model) { if (isUnitModel(model)) { @@ -240,7 +243,7 @@ function parseSingleChannelDomain( model: UnitModel, channel: ScaleChannel | 'x2' | 'y2' ): Explicit { - const {encoding} = model; + const {encoding, markDef, mark, config, stack} = model; const fieldOrDatumDef = getFieldOrDatumDef(encoding[channel]) as ScaleDatumDef | ScaleFieldDef; const {type} = fieldOrDatumDef; @@ -258,7 +261,6 @@ function parseSingleChannelDomain( return makeExplicit(convertDomainIfItIsDateTime(domain, type, timeUnit)); } - const stack = model.stack; if (stack && channel === stack.fieldChannel) { if (stack.offset === 'normalize') { return makeImplicit([[0, 1]]); @@ -347,28 +349,27 @@ function parseSingleChannelDomain( ]); } } - } else if ( - fieldDef.timeUnit && - util.contains(['time', 'utc'], scaleType) && - hasBandEnd( - fieldDef, - isUnitModel(model) ? model.encoding[getSecondaryRangeChannel(channel)] : undefined, - model.markDef, - model.config - ) - ) { - const data = model.requestDataName(DataSourceType.Main); - return makeImplicit([ - { - data, - field: model.vgField(channel) - }, - { - data, - field: model.vgField(channel, {suffix: 'end'}) - } - ]); - } else if (sort) { + } else if (fieldDef.timeUnit && util.contains(['time', 'utc'], scaleType)) { + const fieldDef2 = encoding[getSecondaryRangeChannel(channel)]; + + if (hasBandEnd(fieldDef, fieldDef2, markDef, config)) { + const data = model.requestDataName(DataSourceType.Main); + + const bandPosition = getBandPosition({fieldDef, fieldDef2, markDef, config}); + const isRectWithOffset = isRectBasedMark(mark) && bandPosition !== 0.5; + return makeImplicit([ + { + data, + field: model.vgField(channel, isRectWithOffset ? {suffix: OFFSETTED_RECT_START_SUFFIX} : {}) + }, + { + data, + field: model.vgField(channel, {suffix: isRectWithOffset ? OFFSETTED_RECT_END_SUFFIX : 'end'}) + } + ]); + } + } + if (sort) { return makeImplicit([ { // If sort by aggregation of a specified sort field, we need to use RAW table, diff --git a/src/compile/scale/range.ts b/src/compile/scale/range.ts index 0cc8b0b364..a25d382470 100644 --- a/src/compile/scale/range.ts +++ b/src/compile/scale/range.ts @@ -27,7 +27,14 @@ import { Y, YOFFSET } from '../../channel'; -import {getFieldOrDatumDef, isFieldDef, isFieldOrDatumDef, ScaleDatumDef, ScaleFieldDef} from '../../channeldef'; +import { + getBandPosition, + getFieldOrDatumDef, + isFieldDef, + isFieldOrDatumDef, + ScaleDatumDef, + ScaleFieldDef +} from '../../channeldef'; import {Config, getViewConfigDiscreteSize, getViewConfigDiscreteStep, ViewConfig} from '../../config'; import {DataSourceType} from '../../data'; import {channelHasFieldOrDatum} from '../../encoding'; @@ -370,6 +377,8 @@ function getOffsetRange(channel: string, model: UnitModel, offsetScaleType: Scal const positionScaleType = positionScaleCmpt.get('type'); const positionScaleName = model.scaleName(positionChannel); + const {markDef, config} = model; + if (positionScaleType === 'band') { const size = getDiscretePositionSize(positionChannel, model.size, model.config.view); @@ -388,9 +397,20 @@ function getOffsetRange(channel: string, model: UnitModel, offsetScaleType: Scal if (isFieldDef(positionDef) && positionDef.timeUnit) { const duration = durationExpr(positionDef.timeUnit, expr => `scale('${positionScaleName}', ${expr})`); const padding = model.config.scale.bandWithNestedOffsetPaddingInner; + const bandPositionOffset = + getBandPosition({ + fieldDef: positionDef, + markDef, + config + }) - 0.5; + const bandPositionOffsetExpr = bandPositionOffset !== 0 ? ` + ${bandPositionOffset}` : ''; if (padding) { - const startRatio = isSignalRef(padding) ? `${padding.signal}/2` : `${padding / 2}`; - const endRatio = isSignalRef(padding) ? `(1 - ${padding.signal}/2)` : `${1 - padding / 2}`; + const startRatio = isSignalRef(padding) + ? `${padding.signal}/2` + bandPositionOffsetExpr + : `${padding / 2 + bandPositionOffset}`; + const endRatio = isSignalRef(padding) + ? `(1 - ${padding.signal}/2)` + bandPositionOffsetExpr + : `${1 - padding / 2 + bandPositionOffset}`; return [{signal: `${startRatio} * (${duration})`}, {signal: `${endRatio} * (${duration})`}]; } return [0, {signal: duration}]; diff --git a/test/compile/data/aggregate.test.ts b/test/compile/data/aggregate.test.ts index e3b883490b..b0d882152f 100644 --- a/test/compile/data/aggregate.test.ts +++ b/test/compile/data/aggregate.test.ts @@ -1,4 +1,5 @@ import {AggregateNode} from '../../../src/compile/data/aggregate'; +import {OFFSETTED_RECT_END_SUFFIX, OFFSETTED_RECT_START_SUFFIX} from '../../../src/compile/data/timeunit'; import {AggregateTransform} from '../../../src/transform'; import {internalField} from '../../../src/util'; import {parseUnitModel} from '../../util'; @@ -83,6 +84,39 @@ describe('compile/data/aggregate', () => { }); }); + it('should produce the correct summary component for timeBinWithOffset', () => { + const model = parseUnitModel({ + mark: 'bar', + encoding: { + y: { + aggregate: 'sum', + field: 'Acceleration', + type: 'quantitative' + }, + x: { + timeUnit: 'yearmonth', + field: 'date', + type: 'temporal', + bandPosition: 0 + } + } + }); + + const agg = AggregateNode.makeFromEncoding(null, model); + expect(agg.assemble()).toEqual({ + type: 'aggregate', + groupby: [ + 'yearmonth_date', + 'yearmonth_date_end', + `yearmonth_date_${OFFSETTED_RECT_START_SUFFIX}`, + `yearmonth_date_${OFFSETTED_RECT_END_SUFFIX}` + ], + ops: ['sum'], + fields: ['Acceleration'], + as: ['sum_Acceleration'] + }); + }); + it('should produce the correct aggregate component for maps', () => { const model = parseUnitModel({ mark: 'rule', diff --git a/test/compile/data/timeunit.test.ts b/test/compile/data/timeunit.test.ts index 93524e7496..85cce49580 100644 --- a/test/compile/data/timeunit.test.ts +++ b/test/compile/data/timeunit.test.ts @@ -1,4 +1,4 @@ -import {TimeUnitNode} from '../../../src/compile/data/timeunit'; +import {OFFSETTED_RECT_END_SUFFIX, OFFSETTED_RECT_START_SUFFIX, TimeUnitNode} from '../../../src/compile/data/timeunit'; import {ModelWithField} from '../../../src/compile/model'; import {TimeUnitTransform} from '../../../src/transform'; import {parseUnitModel} from '../../util'; @@ -52,6 +52,67 @@ describe('compile/data/timeunit', () => { ]); }); + it('should return a unit transform for bar with bandPosition=0', () => { + const model = parseUnitModel({ + data: {values: []}, + mark: 'bar', + encoding: { + x: {field: 'a', type: 'temporal', timeUnit: 'month', bandPosition: 0} + } + }); + + expect(assembleFromEncoding(model)).toEqual([ + { + type: 'timeunit', + field: 'a', + as: ['month_a', 'month_a_end'], + units: ['month'] + }, + { + type: 'formula', + expr: "0.5 * timeOffset('month', datum['month_a'], -1) + 0.5 * datum['month_a']", + as: `month_a_${OFFSETTED_RECT_START_SUFFIX}` + }, + + { + type: 'formula', + expr: "0.5 * datum['month_a'] + 0.5 * datum['month_a_end']", + as: `month_a_${OFFSETTED_RECT_END_SUFFIX}` + } + ]); + }); + + it('should return a timeunit transform with step for bar with bandPosition=0', () => { + const model = parseUnitModel({ + data: {values: []}, + mark: 'bar', + encoding: { + x: {field: 'a', type: 'temporal', timeUnit: {unit: 'month', step: 2}, bandPosition: 0} + } + }); + + expect(assembleFromEncoding(model)).toEqual([ + { + type: 'timeunit', + field: 'a', + as: ['month_step_2_a', 'month_step_2_a_end'], + units: ['month'], + step: 2 + }, + { + type: 'formula', + expr: "0.5 * timeOffset('month', datum['month_step_2_a'], -2) + 0.5 * datum['month_step_2_a']", + as: `month_step_2_a_${OFFSETTED_RECT_START_SUFFIX}` + }, + + { + type: 'formula', + expr: "0.5 * datum['month_step_2_a'] + 0.5 * datum['month_step_2_a_end']", + as: `month_step_2_a_${OFFSETTED_RECT_END_SUFFIX}` + } + ]); + }); + it('should return a unit offset transforms for bar', () => { const model = parseUnitModel({ data: {values: []}, @@ -70,6 +131,34 @@ describe('compile/data/timeunit', () => { ]); }); + it('should return a unit offset transforms for bar with bandPosition = 0', () => { + const model = parseUnitModel({ + data: {values: []}, + mark: 'bar', + encoding: { + x: {field: 'a', type: 'temporal', timeUnit: 'binnedyearmonth', bandPosition: 0} + } + }); + + expect(assembleFromEncoding(model)).toEqual([ + { + type: 'formula', + expr: `timeOffset('month', datum['a'], 1)`, + as: 'a_end' + }, + { + type: 'formula', + expr: "0.5 * timeOffset('month', datum['a'], -1) + 0.5 * datum['a']", + as: `a_${OFFSETTED_RECT_START_SUFFIX}` + }, + { + type: 'formula', + expr: "0.5 * datum['a'] + 0.5 * datum['a_end']", + as: `a_${OFFSETTED_RECT_END_SUFFIX}` + } + ]); + }); + it('should return the proper field escaping with binnedyearmonth', () => { const model = parseUnitModel({ data: {values: []}, diff --git a/test/compile/mark/encode/position-rect.test.ts b/test/compile/mark/encode/position-rect.test.ts index e5db350947..a053a2d4d3 100644 --- a/test/compile/mark/encode/position-rect.test.ts +++ b/test/compile/mark/encode/position-rect.test.ts @@ -1,3 +1,4 @@ +import {OFFSETTED_RECT_END_SUFFIX, OFFSETTED_RECT_START_SUFFIX} from '../../../../src/compile/data/timeunit'; import {rectPosition} from '../../../../src/compile/mark/encode/position-rect'; import * as log from '../../../../src/log'; import {parseUnitModelWithScaleAndLayoutSize} from '../../../util'; @@ -28,6 +29,25 @@ describe('compile/mark/encode/position-rect', () => { }); }); + it('produces correct x-mixins for timeUnit with bandPosition = 0', () => { + const model = parseUnitModelWithScaleAndLayoutSize({ + data: {values: []}, + mark: 'bar', + encoding: { + x: { + timeUnit: 'yearmonth', + field: 'date', + type: 'temporal', + bandPosition: 0 + } + } + }); + + const props = rectPosition(model, 'x'); + expect(props.x['field']).toBe(`yearmonth_date_${OFFSETTED_RECT_END_SUFFIX}`); + expect(props.x2['field']).toBe(`yearmonth_date_${OFFSETTED_RECT_START_SUFFIX}`); + }); + it('produces correct x-mixins for binned data with step and start field, without end field', () => { const model = parseUnitModelWithScaleAndLayoutSize({ data: {values: []}, diff --git a/test/compile/scale/domain.test.ts b/test/compile/scale/domain.test.ts index 4e06805636..afc4973fa8 100644 --- a/test/compile/scale/domain.test.ts +++ b/test/compile/scale/domain.test.ts @@ -8,6 +8,7 @@ import * as log from '../../../src/log'; import {ScaleType} from '../../../src/scale'; import {EncodingSortField} from '../../../src/sort'; import {parseUnitModel} from '../../util'; +import {OFFSETTED_RECT_END_SUFFIX, OFFSETTED_RECT_START_SUFFIX} from '../../../src/compile/data/timeunit'; describe('compile/scale', () => { describe('parseDomainForChannel()', () => { @@ -313,6 +314,43 @@ describe('compile/scale', () => { expect(_domain).toEqual([{data: 'main', field: 'month_origin'}]); }); + it('should return the correct bar domain for month T', () => { + const model = parseUnitModel({ + mark: 'bar', + encoding: { + y: { + field: 'origin', + type: 'temporal', + timeUnit: 'month' + } + } + }); + const _domain = testParseDomainForChannel(model, 'y'); + expect(_domain).toEqual([ + {data: 'main', field: 'month_origin'}, + {data: 'main', field: 'month_origin_end'} + ]); + }); + + it('should return the correct bar domain for month T with bandPosition = 0', () => { + const model = parseUnitModel({ + mark: 'bar', + encoding: { + y: { + field: 'origin', + type: 'temporal', + timeUnit: 'month', + bandPosition: 0 + } + } + }); + const _domain = testParseDomainForChannel(model, 'y'); + expect(_domain).toEqual([ + {data: 'main', field: `month_origin_${OFFSETTED_RECT_START_SUFFIX}`}, + {data: 'main', field: `month_origin_${OFFSETTED_RECT_END_SUFFIX}`} + ]); + }); + it('should return the correct domain for month O', () => { const model = parseUnitModel({ mark: 'point', diff --git a/test/compile/scale/range.test.ts b/test/compile/scale/range.test.ts index ed31590a8d..36cb40bec6 100644 --- a/test/compile/scale/range.test.ts +++ b/test/compile/scale/range.test.ts @@ -132,7 +132,7 @@ describe('compile/scale', () => { ); }); - it('should return padded duration range when there is a nested offset with year time scale and no padding', () => { + it('should return padded duration range when there is a nested offset with year time scale and default padding', () => { const model = parseUnitModelWithScaleExceptRange({ mark: 'bar', encoding: { @@ -155,8 +155,31 @@ describe('compile/scale', () => { ]) ); }); + it('should return padded duration range when there is a nested offset with year time scale, default padding, and bandPosition=0', () => { + const model = parseUnitModelWithScaleExceptRange({ + mark: 'bar', + encoding: { + x: {field: 'x', type: 'temporal', timeUnit: 'year', bandPosition: 0}, + xOffset: {field: 'xSub', type: 'nominal'}, + y: {field: 'y', type: 'nominal'} + } + }); + + expect(parseRangeForChannel('xOffset', model)).toEqual( + makeImplicit([ + { + signal: + "-0.4 * (scale('x', datetime(2002, 0, 1, 0, 0, 0, 0)) - scale('x', datetime(2001, 0, 1, 0, 0, 0, 0)))" + }, + { + signal: + "0.4 * (scale('x', datetime(2002, 0, 1, 0, 0, 0, 0)) - scale('x', datetime(2001, 0, 1, 0, 0, 0, 0)))" + } + ]) + ); + }); - it('should return padded duration range signal when there is a nested offset with year time scale and no padding', () => { + it('should return padded duration range signal when there is a nested offset with year time scale and default padding', () => { const model = parseUnitModelWithScaleExceptRange({ mark: 'bar', encoding: {