avatorl / Deneb-Vega-Help

Do you need help with Deneb custom visual for Power BI and/or Vega visualization grammar? Create an issue here to get assistance from Deneb community expert Andrzej Leszkiewicz.
3 stars 0 forks source link

Vega - Dynamic small multiples with dynamic size #9

Closed DW19904 closed 3 weeks ago

DW19904 commented 1 month ago

Hi, this is more of an open ended question in the art of the possible in Vega, as I have tried to create this in Vega-Lite and do not believe it is possible.

I am just wondering is is possible to create a faceted visual which was dynamic based on the number of categories within the small multiple.

So if only 1 category, the full container would be full of 1 visual, then the full container would dynamically split in size depending on the number of items within the faceted category. Always thought this must be possible in Vega but I am unsure how you would control the size of the visual based on the number of items in the category you want to do the small multiples on.

Any help or direction would be appreciated, thank you.

avatorl commented 1 month ago

That's possible.

1) use https://vega.github.io/vega/docs/transforms/aggregate/ transform to group your dataset into a list of items in the category

"data": [
...
    {
      "name": "list-of-items-in-the-category",
      "source": "dataset",
      "transform": [{"type": "aggregate", "groupby": ["category"]}]
    }
]

2) use length() function to get number of items in the list

"signals": [
...
    {
      "name": "CountOfItemsInTheCategory",
      "update": "length(data('list-of-items-in-the-category'))"
    }
]

3) use "height" and "weight" signals to calculate height and weight of a single visual depends on the number of items in the category

"signals": [ ... { "name": "height", "update": "" } ]

4) create "gridColumns" signal for "columns" property of a group layout and calculate number of columns in the layout

    {
      "type": "group",
      "layout": {
        "align": "all",
        "bounds": "full",
        "columns": {"signal": "gridColumns"},
        "padding": {"signal": "gridPadding"}
DW19904 commented 1 month ago

Hi,

I have followed the above instructions to create a visual that has dynamic small multiples, and I do believe I have all the relevant components in the code but the visual is not working as expected.

The intended output is the small multiples to be dynamic to the number of items in the category it is faceted by, but the key is the small visuals will fill the container regardless of how many items in the small multiple category.

The vega code is below, and the mockup data it is based upon is attached, any help and advice is much appreciated, thanks in advance.

{ "data": [ {"name": "dataset"}, { "name": "CountryList", "source": "dataset", "transform": [ { "type": "aggregate", "groupby": ["Country"] } ] }, { "name": "LengthCountryList", "source": "CountryList", "transform": [ { "type": "aggregate", "ops": ["count"], "as": ["CountryLength"] } ] } ], "signals": [ { "name": "columnsignal", "update": "data('LengthCountryList')[0].CountryLength" }, { "name": "paddingsignal", "update": "data('LengthCountryList')[0].CountryLength" }, { "name": "heightsignal", "update": "data('LengthCountryList')[0].CountryLength 50" }, { "name": "widthsignal", "update": "data('LengthCountryList')[0].CountryLength 80" } ], "width": {"signal": "widthsignal"}, "height": {"signal": "heightsignal"}, "scales": [ { "name": "xscale", "type": "time", "domain": { "data": "dataset", "field": "Date" }, "range": "width" }, { "name": "yscale", "type": "linear", "domain": { "data": "dataset", "field": "Sales" }, "range": "height" } ], "axes": [ { "orient": "bottom", "scale": "xscale", "title": null, "format": "%b %y" }, { "orient": "left", "scale": "yscale", "title": null } ], "marks": [ { "type": "group", "layout": { "align": "all", "bounds": "full", "columns": { "signal": "columnsignal" }, "padding": { "signal": "paddingsignal" } }, "marks": [ { "type": "line", "from": {"data": "dataset"}, "encode": { "enter": { "x": { "scale": "xscale", "field": "Date" }, "y": { "scale": "yscale", "field": "Sales" }, "stroke": { "value": "steelblue" }, "strokeWidth": { "value": 2 } } } } ] } ] } MockData.xlsx

avatorl commented 1 month ago

You need to put your "line" marks into the following:

"marks": { "type": "group", "from": { "facet": { "name": "data-group-country", "data": "dataset", "groupby": "Country" } },

then use 'data-group-country' as a data source for the "line" marks (instead of "dataset").

For example, see https://github.com/avatorl/Deneb-Vega/blob/main/small-multiple-line-chart/small-multiple-line-chart-bike.json

DW19904 commented 1 month ago

Thank you, i am getting closer to the desired affect, I have implemented the above and now there is a line for each country on the visual, but it is not in a small multiple format, and certainly not dynamic to the size of the container. Any advice on what i am missing? Thanks again. Updated code is below.

{ "data": [ {"name": "dataset"}, { "name": "CountryList", "source": "dataset", "transform": [ { "type": "aggregate", "groupby": ["Country"] } ] }, { "name": "LengthCountryList", "source": "CountryList", "transform": [ { "type": "aggregate", "ops": ["count"], "as": ["CountryLength"] } ] } ], "signals": [ { "name": "columnsignal", "update": "data('LengthCountryList')[0].CountryLength" }, { "name": "paddingsignal", "update": "data('LengthCountryList')[0].CountryLength" }, { "name": "heightsignal", "update": "data('LengthCountryList')[0].CountryLength 50" }, { "name": "widthsignal", "update": "data('LengthCountryList')[0].CountryLength 80" } ], "width": {"signal": "widthsignal"}, "height": {"signal": "heightsignal"}, "scales": [ { "name": "xscale", "type": "time", "domain": { "data": "dataset", "field": "Date" }, "range": "width" }, { "name": "yscale", "type": "linear", "domain": { "data": "dataset", "field": "Sales" }, "range": "height" } ], "axes": [ { "orient": "bottom", "scale": "xscale", "format": "%b %y" }, { "orient": "left", "scale": "yscale" } ], "marks": [ { "type": "group", "layout": { "align": "all", "bounds": "full", "columns": { "signal": "columnsignal" }, "padding": { "signal": "paddingsignal" } }, "from": { "facet": { "name": "data-group-country", "data": "dataset", "groupby": "Country" } }, "marks": [ { "type": "line", "from": {"data": "data-group-country"}, "encode": { "enter": { "x": { "scale": "xscale", "field": "Date" }, "y": { "scale": "yscale", "field": "Sales" }, "stroke": { "value": "steelblue" }, "strokeWidth": { "value": 2 } } } } ] } ] }

avatorl commented 4 weeks ago

1) to create a small multiple you need "layout" property ( https://vega.github.io/vega/docs/layout/ )

It can be placed at the top level of the specification (outside of the top level "marks" property) or within a "group" mark

2) then add a "group" mark with "from": { "facet" } property for data faceting. If there is already a group with "layout" property, faceting goes into a sub group inside of the top level group.

In you latest code "from": { "facet" } is in the same "group" mark with "layout". Put it into an a new "group" mark inside of the existing group.

3) Also you'll need to use "title" and "axes" properties inside of the group (the one with faceting) if you need them for each chart.

4) Only when you already have working small multiple chart start replacing hard coded values ("width", "height", "columns") with signals to achieve expected auto-resizing.

I suggest to use one of the existing examples of a small multiple, maybe this one https://github.com/avatorl/Deneb-Vega/blob/main/donut-chart-multi-rings/donut-chart-multi-rings.json as a starting point. Just modify it step by step.

DW19904 commented 4 weeks ago

Thanks for your assistance so far, i have the visual working correctly as a small multiple and have the signals set up i believe for the dynamic sizing, but i cannot seem to get the logic correct so the visual appropriately fills the size of the container each time, any advice appreciated.

{ "data": [ {"name": "dataset"}, { "name": "CountryList", "source": "dataset", "transform": [ { "type": "aggregate", "groupby": ["Country"] } ] }, { "name": "LengthCountryList", "source": "CountryList", "transform": [ { "type": "aggregate", "ops": ["count"], "as": ["CountryLength"] } ] } ], "signals": [ { "name": "columnsignal", "update": "data('LengthCountryList')[0].CountryLength" }, { "name": "paddingsignal", "update": "data('LengthCountryList')[0].CountryLength 10" }, { "name": "heightsignal", "update": "data('LengthCountryList')[0].CountryLength 75" }, { "name": "widthsignal", "update": "data('LengthCountryList')[0].CountryLength * 100" } ], "width": 450, "height": 200, "padding": 20, "autosize": "pad", "layout": { "align": "all", "bounds": "full", "columns": 2, "padding": 20 }, "scales": [ { "name": "xscale", "type": "time", "domain": { "data": "dataset", "field": "Date" }, "range": "width" }, { "name": "yscale", "type": "linear", "domain": { "data": "dataset", "field": "Sales" }, "range": "height" } ], "marks": [ { "type": "group", "from": { "facet": { "name": "data-group-country", "data": "dataset", "groupby": "Country" } }, "encode": { "update": { "width": {"signal": "width / columnsignal"}, "height": {"signal": "height"} } }, "title": { "text": {"signal": "parent.Country"}, "frame": "group", "anchor": "middle", "offset": 10 }, "axes": [ { "orient": "bottom", "scale": "xscale", "format": "%b %y", "title": "Date", "encode": { "title": { "update": { "fill": {"value": "white"} } } } }, { "orient": "left", "scale": "yscale", "title": "Sales", "format": "~s", "encode": { "title": { "update": { "fill": {"value": "white"} } }, "labels": { "update": { "fontSize": {"value": 10} } } } } ], "marks": [ { "type": "line", "from": {"data": "data-group-country"}, "encode": { "enter": { "x": { "scale": "xscale", "field": "Date" }, "y": { "scale": "yscale", "field": "Sales" }, "stroke": { "value": "steelblue" }, "strokeWidth": { "value": 2 } } } } ] } ] }

avatorl commented 4 weeks ago

In your code "width" and "height" is hardcoded and used to define the size of the individual cells in the grid (they are not for the entire grid size).

See https://github.com/avatorl/Deneb-Vega-Help/issues/9#issuecomment-2216839235

Delete hardcoded "height" and "weight" from the top-level of the configuration and add "height" and "weight" signals to calculate height and weight depends on the number of items in the category

{
"name": "height",
"update": "<your formula to calculate cell height depends on the number of cells in the grid>"
},
{
"name": "width",
"update": "<your formula to calculate cell width depends on the number of cells in the grid>"
}
DW19904 commented 3 weeks ago

Thank you for your assistance! Finally got the visual working as desired, much appreciated.