Forum Discussion
Model View layout bug?
- 1 year ago
Hi Ben1981
What you're experiencing with the Power BI Model View layout shifting dramatically toward the bottom-left over time—especially as more tables are added—is a known and increasingly reported issue among users. This behavior isn't due to any setting you've missed; rather, it's generally considered a layout bug or usability flaw within the Model View canvas of Power BI Desktop. As you add tables, Power BI sometimes auto-places them far from the existing layout, often above and to the far left of your current model, forcing you to scroll extensively or drag them manually into view. The “Fit to screen” or “Center diagram” button can temporarily help, but as you've noticed, it doesn't solve the core issue of erratic table placement.
This problem tends to get worse with larger data models and is not something you can permanently fix via settings. While recreating the layout view or organizing objects into separate diagram layers (which Power BI allows via the model view tabs) may help manage complexity, the erratic placement behavior remains. Microsoft hasn’t officially acknowledged a fix for this yet, but it's worth submitting feedback or upvoting similar issues on the Power BI Ideas forum, as this helps prioritize improvements. Until then, careful use of diagram layers and manually maintaining layout discipline is the most practical workaround.
So, out of frustration (and to see how far I can push copilot), I got copilot to write a pyton script for fix the position of the diagram. If copilot (chatGPT) can do this in a few minutes, I don't know why Microsoft can't have a button to do the same in Power BI!
Simply:
1. save and close the power BI
2. rename the <Report>.pbix to <Report>.pbix.zip and extract DiagramLayout file
3. run the python similar to the following:
python RepositionPowerBIModelView.py "DiagramLayout" -o "DiagramLayout" --MinX 100 --MinY 1004. Put the file back in <Report>.pbix.zip, overwriting the original
5. Rename the file back to <Report>.pbix
Python code (Provided with no promises or guarantees... USE AT YOUR OWN RISK!!!):
import json
import argparse
from pathlib import Path
from typing import Any, Tuple, Optional
# -----------------------------
# Encoding-aware JSON load/save
# -----------------------------
def detect_encoding(raw: bytes) -> str:
"""
Detect likely encoding for Power BI DiagramLayout files.
- BOM UTF-16 LE/BE -> 'utf-16' (Python handles BOM)
- NUL bytes early -> UTF-16LE without BOM (common for these files; your sample shows this) [1](https://openwayfood-my.sharepoint.com/personal/amay_openwayfood_com_au/Documents/Microsoft%20Copilot%20Chat%20Files/DiagramLayout.json?web=1)
- Otherwise assume UTF-8 (with optional BOM via utf-8-sig)
"""
if raw.startswith(b"\xff\xfe") or raw.startswith(b"\xfe\xff"):
return "utf-16"
if b"\x00" in raw[:200]:
return "utf-16-le"
return "utf-8-sig"
def load_json_file(path: Path) -> Tuple[dict[str, Any], str]:
"""
Returns (data_dict, detected_output_encoding).
detected_output_encoding is normalized to:
- 'utf-16-le' if input is UTF-16-ish
- 'utf-8' otherwise
"""
raw = path.read_bytes()
enc = detect_encoding(raw)
text = raw.decode(enc)
text = text.lstrip("\ufeff").lstrip() # remove BOM char + whitespace if present
data = json.loads(text)
if not isinstance(data, dict):
raise ValueError(f"Top-level JSON is not an object/dict. Got: {type(data)}")
# Normalize "match" output encoding to utf-16-le or utf-8
out_enc = "utf-16-le" if enc in ("utf-16", "utf-16-le") else "utf-8"
return data, out_enc
def save_json_file_minified(path: Path, data: dict[str, Any], encoding: str) -> None:
"""
Writes MINIFIED (compressed) JSON, no indentation.
separators=(',', ':') removes unnecessary whitespace.
ensure_ascii=False preserves any non-ASCII characters as-is.
"""
text = json.dumps(data, ensure_ascii=False, separators=(",", ":"))
path.write_text(text, encoding=encoding)
# -----------------------------
# Diagram operations
# -----------------------------
def list_diagram_names(data: dict[str, Any]) -> list[str]:
diagrams = data.get("diagrams")
if not isinstance(diagrams, list):
return []
return [str(d.get("name", "")) for d in diagrams]
def find_diagram(data: dict[str, Any], diagram_name: str) -> dict[str, Any]:
diagrams = data.get("diagrams")
if not isinstance(diagrams, list):
raise ValueError("JSON does not contain a 'diagrams' array.")
target = diagram_name.strip().lower()
for d in diagrams:
name = str(d.get("name", "")).strip().lower()
if name == target:
return d
available = list_diagram_names(data)
raise ValueError(f'No diagram named "{diagram_name}" found. Available: {available}')
def compute_min_xy(nodes: list[dict[str, Any]]) -> Tuple[float, float]:
xs: list[float] = []
ys: list[float] = []
for n in nodes:
loc = n.get("location", {})
x = loc.get("x", None)
y = loc.get("y", None)
if isinstance(x, (int, float)):
xs.append(float(x))
if isinstance(y, (int, float)):
ys.append(float(y))
if not xs:
raise ValueError("No numeric node.location.x values found in nodes.")
if not ys:
raise ValueError("No numeric node.location.y values found in nodes.")
return min(xs), min(ys)
def shift_nodes(nodes: list[dict[str, Any]], delta_x: float, delta_y: float) -> int:
changed = 0
for n in nodes:
loc = n.get("location")
if isinstance(loc, dict):
updated = False
if isinstance(loc.get("x", None), (int, float)):
loc["x"] = float(loc["x"]) + delta_x
updated = True
if isinstance(loc.get("y", None), (int, float)):
loc["y"] = float(loc["y"]) + delta_y
updated = True
if updated:
changed += 1
return changed
def process_file(
input_path: str,
output_path: Optional[str],
min_x: float,
min_y: float,
diagram_name: str,
shift_scroll: bool,
output_encoding_mode: str
) -> None:
input_file = Path(input_path)
if not input_file.exists():
raise FileNotFoundError(f"Input file not found: {input_file}")
output_file = Path(output_path) if output_path else input_file
data, detected_out_enc = load_json_file(input_file)
# Resolve output encoding
if output_encoding_mode == "match":
out_enc = detected_out_enc
elif output_encoding_mode == "utf-16-le":
out_enc = "utf-16-le"
else:
out_enc = "utf-8"
diagram = find_diagram(data, diagram_name)
nodes = diagram.get("nodes")
if not isinstance(nodes, list) or len(nodes) == 0:
raise ValueError(f'Diagram "{diagram_name}" has no nodes array (or it is empty).')
current_min_x, current_min_y = compute_min_xy(nodes)
# Shift so that new minimums become MinX and MinY:
delta_x = float(min_x) - current_min_x
delta_y = float(min_y) - current_min_y
changed_nodes = shift_nodes(nodes, delta_x, delta_y)
# Optional: shift scrollPosition too (keeps viewport aligned with moved nodes)
if shift_scroll:
sp = diagram.get("scrollPosition")
if isinstance(sp, dict):
if isinstance(sp.get("x", None), (int, float)):
sp["x"] = float(sp["x"]) + delta_x
if isinstance(sp.get("y", None), (int, float)):
sp["y"] = float(sp["y"]) + delta_y
# Write MINIFIED JSON
save_json_file_minified(output_file, data, encoding=out_enc)
print(f"Updated '{input_file}' -> '{output_file}'")
print(f'Diagram processed: "{diagram_name}"')
print(f" currentMinX={current_min_x:.6f}, currentMinY={current_min_y:.6f}")
print(f" targetMinX={min_x:.6f}, targetMinY={min_y:.6f}")
print(f" deltaX={delta_x:.6f}, deltaY={delta_y:.6f}")
print(f" nodes shifted={changed_nodes}")
print(f" output encoding: {out_enc}")
print(" output format: minified JSON")
if shift_scroll:
print(" scrollPosition shifted: yes")
def main():
parser = argparse.ArgumentParser(
description="Shift a Power BI diagram's node locations so minimum X/Y become specified values."
)
parser.add_argument("input", help="Input layout file path (e.g., DiagramLayout.json)")
parser.add_argument("-o", "--output", default=None,
help="Output file path (defaults to overwrite input)")
parser.add_argument("--MinX", type=float, required=False, help="New minimum X for nodes")
parser.add_argument("--MinY", type=float, required=False, help="New minimum Y for nodes")
parser.add_argument("--DiagramName", default="All tables",
help='Diagram name to process (default: "All tables")')
parser.add_argument("--shift-scroll", action="store_true",
help="Also shift diagram.scrollPosition by the same deltaX/deltaY")
parser.add_argument("--list-diagrams", action="store_true",
help="List diagram names in the file and exit")
parser.add_argument(
"--output-encoding",
default="match",
choices=["match", "utf-16-le", "utf-8"],
help="Output encoding: match (default), utf-16-le, or utf-8"
)
args = parser.parse_args()
if args.list_diagrams:
data, _ = load_json_file(Path(args.input))
names = list_diagram_names(data)
print("Diagrams found:")
for n in names:
print(f" - {n}")
return
if args.MinX is None or args.MinY is None:
parser.error("--MinX and --MinY are required unless --list-diagrams is used.")
process_file(
input_path=args.input,
output_path=args.output,
min_x=args.MinX,
min_y=args.MinY,
diagram_name=args.DiagramName,
shift_scroll=args.shift_scroll,
output_encoding_mode=args.output_encoding
)
if __name__ == "__main__":
main()
- Ben19817 months ago
Helper III
That is impressive but I doubt I'll be able to try it out on my work systems, also a bit scary with what AI can do these days haha.
The only solution I found for this was simply creating a new layout tab and re-creating the model in here and it doesn't seem to mess it up.