deneb-viz / deneb

Deneb is a custom visual for Microsoft Power BI, which allows developers to use the declarative JSON syntax of the Vega or Vega-Lite languages to create their own data visualizations.
https://deneb-viz.github.io
MIT License
193 stars 15 forks source link

Deneb Vega Inconsistency #270

Open Giammaria opened 1 year ago

Giammaria commented 1 year ago

Hi Daniel, I hope all is well. I have a pretty nasty Vega spec that I built in the online Vega editor. I'm in a bit of a bind because I cannot get it to display correctly when moving it over to Deneb. Specifically, things display incorrectly when I use a Power BI dataset as the source as opposed to a url pointing to a JSON file. I have an example .pbix here with dummy data that has two pages. I am using the standalone version (necessary to publish in my environment), but I noticed the same behavior in the certified version of Deneb. The first page shows a Deneb visual that is displaying correctly and is pointing directly to a JSON file as its source. The second page is displaying incorrectly and is also using that same JSON file, but there is a connection to that JSON file as a data source in Power Query, and then Deneb is using the resulting query for its dataset. The only difference between the two specs is on line 290; one has the url property (pointing to the json file directly), and the other does not. I'm not seeing anything obvious with datatype mismatches between the two datasets, and I even made sure to bring in the fields in the same order that the JSON has them in (not that it should matter). I know that you're insanely busy but if there is any chance you could take a look, it would be GREATLY appreciated. Like I said, I'm in a bit of a bind.

Additionally, below is the spec created in the online editor (same exact spec being used in the first page of the .pbix).

Thank you so much for your time.

{ "$schema": "https://vega.github.io/schema/vega/v5.json", "padding": 5, "signals": [ {"name": "background_padding", "value": 25}, {"name": "x_step", "value": 85}, {"name": "y_step", "value": 75}, {"name": "max_Columns", "value": 2}, {"name": "icon_size", "update": "2"}, {"name": "group_Column_Padding", "value": 15}, { "name": "grid_height", "update": "(data('background_bands')[0] ? data('background_bands')[0]['Height'] : 0)+(data('background_bands')[1] ? data('background_bands')[1]['Height'] : 0)+(data('background_bands')[2] ? data('background_bands')[2]['Height'] : 0)" }, { "name": "total_columns", "update": "data('dividing_lines')[0]['Total Column Count']" }, { "name": "child_child_width", "update": "bandspace(domain('child_x').length, 0, 0) * x_step" } ], "marks": [ { "name": "Background", "type": "rect", "encode": { "enter": { "fill": {"value": "#fff"}, "x": {"signal": "-1*background_padding"}, "y": {"signal": "-3*background_padding"}, "width": { "signal": "(total_columns*x_step)+(group_Column_Padding*length(data('group_column_domain')))+(background_padding*2)" }, "height": {"signal": "grid_height+(background_padding*4)"}, "cornerRadius": {"signal": "background_padding"} } } }, { "name": "Background Bands", "type": "group", "layout": {"columns": 1}, "from": {"data": "background_bands"}, "encode": { "update": { "x": {"field": "X"}, "y": {"field": "Y"}, "width": {"field": "Width"}, "height": {"field": "Height"}, "fill": {"field": "Background Hex"} } } }, { "type": "group", "layout": { "padding": {"column": {"signal": "group_Column_Padding"}}, "headerBand": {"column": 0.5}, "columns": {"signal": "length(data('column_domain'))"}, "bounds": "full", "align": "each" }, "marks": [ { "name": "column_header", "type": "group", "role": "column-header", "from": {"data": "column_domain"}, "sort": {"field": "datum[\"Phase Sort\"]", "order": "ascending"}, "title": { "text": { "signal": "isValid(parent[\"Phase\"]) ? parent[\"Phase\"] : \"\"+parent[\"Phase\"]" }, "style": "guide-label", "frame": "group", "offset": 10, "fontSize": 20 } }, { "name": "cell", "type": "group", "from": { "facet": { "name": "facet", "data": "data_2", "groupby": ["Phase"], "aggregate": { "fields": ["Group Name", "Phase Sort"], "ops": ["distinct", "min"], "as": ["distinct_Group Name", "Phase Sort_by_Phase"] } } }, "sort": { "field": ["datum[\"Phase Sort_by_Phase\"]"], "order": ["ascending"] }, "data": [ { "name": "child_column_domain", "source": "facet", "transform": [ { "type": "aggregate", "groupby": ["Group Name"], "fields": ["Group Name"], "ops": ["min"], "as": ["Group Name"] } ] } ], "encode": {"update": {"columns": {"field": "distinct_Group Name"}}}, "layout": { "padding": {"column": {"signal": "group_Column_Padding"}}, "bounds": "full", "align": "each", "headerBand": 0.5 }, "marks": [ { "name": "child_column_header", "type": "group", "role": "column-header", "from": {"data": "child_column_domain"}, "sort": {"field": "datum[\"Group Name\"]", "order": "ascending"}, "title": { "text": { "signal": "isValid(parent[\"Group Name\"]) ? parent[\"Group Name\"] : \"\"+parent[\"Group Name\"]" }, "style": "guide-label", "frame": "group", "offset": 10, "fontSize": 12 }, "encode": {"update": {"width": {"signal": "child_child_width"}}} }, { "name": "child_cell", "type": "group", "from": { "facet": { "name": "child_facet", "data": "facet", "groupby": ["Group Name"], "aggregate": { "fields": ["Row For Scale", "Group Name"], "ops": ["distinct", "min"], "as": ["distinct_Row", "Group Name_by_Group Name"] } } }, "sort": { "field": ["datum[\"Group Name_by_Group Name\"]"], "order": ["ascending"] }, "encode": { "update": { "width": {"signal": "child_child_width"}, "height": { "signal": "bandspace(datum[\"distinct_Row\"], 0, 0) * y_step" } } }, "marks": [ { "name": "child_child_layer_0_marks", "type": "rect", "style": ["rect"], "from": {"data": "child_facet"}, "encode": { "update": { "fill": {"signal": "datum['Background Hex']"}, "description": { "signal": "\"Column: \" + (isValid(datum[\"Column\"]) ? datum[\"Column\"] : \"\"+datum[\"Column\"]) + \"; Row: \" + (isValid(datum[\"Row\"]) ? datum[\"Row\"] : \"\"+datum[\"Row\"])" }, "x": {"scale": "child_x", "field": "Column"}, "width": {"signal": "max(0.25, bandwidth('child_x'))"}, "y": {"scale": "child_child_y", "field": "Row For Scale"}, "height": { "signal": "max(0.25, bandwidth('child_child_y'))" } } } }, { "name": "child_child_layer_1_marks", "type": "text", "style": ["text"], "from": {"data": "child_facet"}, "encode": { "update": { "fill": {"signal": "datum['Foreground Hex']"}, "x": {"scale": "child_x", "field": "Column", "band": 0.5}, "y": { "scale": "child_child_y", "field": "Row For Scale", "band": 0.5 }, "text": { "signal": "isValid(datum[\"Label\"]) ? datum[\"Label\"] : \"\"+datum[\"Label\"]" }, "align": {"value": "center"}, "baseline": {"value": "middle"} } } }, { "type": "symbol", "from": {"data": "child_facet"}, "transform": [ { "type": "formula", "as": "Icon yOffset", "expr": "datum['Icon ViewBox Height']" } ], "encode": { "update": { "shape": {"field": "Icon Path"}, "fill": {"field": "Foreground Hex"}, "x": { "scale": "child_x", "field": "Column", "offset": { "signal": "(x_step/2-((datum['Icon ViewBox Width']*(1+(icon_size*0.25))))/4)" } }, "y": { "scale": "child_child_y", "field": "Row For Scale", "offset": { "signal": "(y_step/5-((datum['Icon ViewBox Height']*(1+(icon_size*0.25))))/4)" } }, "size": {"signal": "icon_size"} } } } ] } ], "scales": [ { "name": "child_x", "type": "band", "domain": {"data": "data_2", "field": "Column", "sort": true}, "range": {"step": {"signal": "x_step"}}, "paddingInner": 0, "paddingOuter": 0 }, { "name": "child_child_y", "type": "band", "domain": { "data": "data_2", "field": "Row For Scale", "sort": true }, "range": {"step": {"signal": "y_step"}}, "paddingInner": 0, "paddingOuter": 0 } ] } ] }, { "name": "Group Dividing Lines", "type": "rect", "from": {"data": "dividing_lines"}, "encode": { "update": { "x": {"field": "X"}, "y": {"signal": "datum['New Phase'] ? -50 : -30"}, "height": {"signal": "grid_height+(datum['New Phase'] ? 50 : 30)"}, "fill": {"signal": "datum['New Phase'] ? '#000' : '#000'"}, "width": {"signal": "datum['New Phase'] ? 3 : 2"}, "fillOpacity": {"signal": "datum['New Phase'] ? 0.5 : 0.15"} } } } ], "data": [ { "name": "dataset", "url": "https://raw.githubusercontent.com/Giammaria/PublicFiles/master/n1_OFRP_dataset.json", "format": {"type": "json"}, "transform": [ { "type": "formula", "expr": "datum['Group Name'] === 'Independent' ? 'IND' : datum['Group Name']", "as": "Group Name" }, {"type": "filter", "expr": "datum['Phase']"}, {"type": "filter", "expr": "datum['Coast']==='LANT'"}, {"type": "filter", "expr": "datum['Trigraph']"} ] }, { "name": "column_domain", "source": "dataset", "transform": [ { "type": "aggregate", "groupby": ["Phase"], "fields": ["Column", "Phase Sort"], "ops": ["distinct", "min"], "as": ["distinct_Column", "Phase Sort"] } ] }, { "name": "group_column_domain", "source": "dataset", "transform": [ { "type": "aggregate", "ops": ["count"], "fields": ["Group Name"], "groupby": ["Phase", "Group Name"], "as": ["Count"] } ] }, { "name": "data_2", "source": "dataset", "transform": [ { "type": "formula", "expr": "[datum['Trigraph'], datum['Fit Fill Label'], datum['AMEX']]", "as": "Label" }, { "type": "window", "params": [null], "as": ["Group Member Ordinal"], "ops": ["row_number"], "fields": [null], "sort": {"field": ["Band Sort"], "order": ["ascending"]}, "groupby": ["Group Name", "Phase", "Band Sort"], "frame": [null, null] }, { "type": "formula", "as": "Group Member Ordinal", "expr": "datum['Group Member Ordinal']" }, { "type": "formula", "expr": "datum['Count']=== 1 ? 1 : datum['Group Member Ordinal']%max_Columns === 0 ? max_Columns : datum['Group Member Ordinal']%max_Columns", "as": "Column" }, { "type": "window", "params": [null], "as": ["Row"], "ops": ["row_number"], "fields": [null], "sort": {"field": ["Band Sort"], "order": ["ascending"]}, "groupby": ["Group Name", "Column", "Band Sort", "Phase"], "frame": [null, null] }, { "type": "formula", "as": "Row For Scale", "expr": "parseFloat(toString(datum['Band Sort'])+'.'+toString(datum['Row']))" }, {"type": "collect", "sort": {"field": ["Row", "Column"]}}, {"type": "filter", "expr": "datum['Trigraph']"} ] }, { "name": "dividing_lines", "source": "data_2", "transform": [ { "type": "aggregate", "as": ["Column Count"], "ops": ["max"], "fields": ["Column"], "groupby": ["Phase", "Phase Sort", "Group Name"] }, {"type": "collect", "sort": {"field": "Phase Sort"}}, { "type": "joinaggregate", "as": ["Total Column Count"], "ops": ["sum"], "fields": ["Column Count"] }, { "type": "formula", "as": "Width", "expr": "x_step*datum['Column Count']+group_Column_Padding" }, {"type": "window", "ops": ["lag"], "fields": ["Width"], "as": ["X"]}, { "type": "window", "frame": [null, 0], "ops": ["sum"], "fields": ["X"], "as": ["Xlag"] }, { "type": "window", "frame": [null, null], "ops": ["row_number"], "groupby": ["Phase", "Phase Sort"], "as": ["New Phase"] }, { "type": "formula", "as": "New Phase", "expr": "(datum['X'] !== 0 && datum['New Phase']===1 ? 1 : 0)" }, {"type": "filter", "expr": "datum['X']>0"}, { "type": "formula", "as": "X", "expr": "datum['Xlag']-group_Column_Padding/2" } ] }, { "name": "background_bands", "source": "data_2", "transform": [ {"type": "collect", "sort": {"field": "Band Sort"}}, { "type": "formula", "as": "Width", "expr": "(total_columns*x_step)+(group_Column_Padding*length(data('group_column_domain')))" }, {"type": "formula", "as": "X", "expr": "0"}, { "type": "aggregate", "as": ["Height"], "ops": ["max"], "fields": ["Row"], "groupby": ["Band Sort", "Background Hex", "X", "Width"] }, {"type": "formula", "as": "Height", "expr": "datum['Height']*y_step"}, {"type": "window", "ops": ["lag"], "fields": ["Height"], "as": ["Y"]}, { "type": "window", "frame": [null, 0], "ops": ["sum"], "fields": ["Y"], "as": ["Y"] } ] } ], "config": { "view": {"stroke": "transparent"}, "background": "transparent", "font": "Segoe UI", "header": { "titleFont": "wf_standard-font, helvetica, arial, sans-serif", "titleFontSize": 16, "titleColor": "#252423", "labelFont": "Segoe UI", "labelFontSize": 13.333333333333332, "labelColor": "#605E5C" }, "axis": { "ticks": false, "grid": false, "domain": false, "labelColor": "#605E5C", "labelFontSize": 12, "titleFont": "wf_standard-font, helvetica, arial, sans-serif", "titleColor": "#252423", "titleFontSize": 16, "titleFontWeight": "normal" }, "axisQuantitative": { "tickCount": 3, "grid": true, "gridColor": "#C8C6C4", "gridDash": [1, 5], "labelFlush": false }, "axisBand": {"tickExtra": true}, "axisX": {"labelPadding": 5}, "axisY": {"labelPadding": 10}, "bar": {"fill": "#118DFF"}, "line": { "stroke": "#118DFF", "strokeWidth": 3, "strokeCap": "round", "strokeJoin": "round" }, "text": {"font": "Segoe UI", "fontSize": 12, "fill": "#605E5C"}, "arc": {"fill": "#118DFF"}, "area": {"fill": "#118DFF", "line": true, "opacity": 0.6}, "path": {"stroke": "#118DFF"}, "rect": {"fill": "#118DFF"}, "point": {"fill": "#118DFF", "filled": true, "size": 75}, "shape": {"stroke": "#118DFF"}, "symbol": {"fill": "#118DFF", "strokeWidth": 1.5, "size": 50}, "legend": { "titleFont": "Segoe UI", "titleFontWeight": "bold", "titleColor": "#605E5C", "labelFont": "Segoe UI", "labelFontSize": 13.333333333333332, "labelColor": "#605E5C", "symbolType": "circle", "symbolSize": 75 }, "range": { "category": [ "#118DFF", "#12239E", "#E66C37", "#6B007B", "#E044A7", "#744EC2", "#D9B300", "#D64550" ], "diverging": ["#DEEFFF", "#118DFF"], "heatmap": ["#DEEFFF", "#118DFF"], "ordinal": [ "#DEEFFF", "#c7e4ff", "#b0d9ff", "#9aceff", "#83c3ff", "#6cb9ff", "#55aeff", "#3fa3ff", "#2898ff", "#118DFF" ] } } }

dm-p commented 1 year ago

Thanks for raising - I've spent a couple of hours looking at this but am out of time for the moment. The only conclusion I can draw is that it is something to do with Power BI data vs. JSON data (which is currently not very insightful). My own notes for another time, or for anyone else's benefit if they can/want to pick up:

PBI-David commented 1 year ago

The only difference I can see is the width and height inbuilt signals are both 0 on the broken chart which in turns throws off the entire layout. I do not know why this is though.

This Vega looks generated. Is it and if so, how was it generated?

Giammaria commented 1 year ago

Thanks Daniel - all of your notes are helpful and align with what I have found. I was also thinking that there must be something different between the JSON data vs. Power BI data, but nothing stands out. Not being able to see the lower level data streams makes it challenging to investigate, because that is exactly where things start go awry with the layout. I'm going to continue to investigate on my end and see if I can find anything. As a last resort, and as much as I don't want to, I'm wondering if I may need to forgo with facet and figure out the layout with scales instead. Going that route will definitely require some thought.

Again, I really appreciate you taking the time. Also, if anyone else is available to take a look as well, I won't fight it :)

The more the merrier!

Giammaria commented 1 year ago

The only difference I can see is the width and height inbuilt signals are both 0 on the broken chart. I do not know why this is though.

This Vega looks generated. Is it and if so, how was it generated?

I noticed that with the width and height signals as well, but like you said, it's unclear why and it seems to only be impacting the lower level objects. Part of the spec is generated. I conducted the faceting in Vega-Lite and got as far as I could, knowing that I'd eventually need to switch over to Vega. I took the compiled Vega spec and continued from there. I realize that I need to clean up the names of objects to be more descriptive/consistent. Thanks for taking a look!

Giammaria commented 1 year ago

I ended up getting it to work by ditching the facet and calculating all the horizontal coordinates. As much as I would really like to understand why this was failing with my original implementation, I'm glad to have a workaround. I will come back and update this comment with an updated url to the working .pbix once I've cleaned up the spec some.

Update: Workaround solution can be found here. I will continue to look at the original implementation as well to see if I can find out what's causing the issue.

dm-p commented 1 year ago

Glad you have a solution, but it still concerns me that it will work with the JSON data, but not from the model, as they should be the same. I've tested this in the current v2 codebase, and the issue is the same here also, so I may still need to carve out some time to figure out what's happening.