Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
401 changes: 401 additions & 0 deletions mitosheet/css/mito.css

Large diffs are not rendered by default.

6 changes: 6 additions & 0 deletions mitosheet/mitosheet/api/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
from mitosheet.api.get_ai_completion import get_ai_completion
from mitosheet.api.get_chart_suggestions import get_chart_suggestions
from mitosheet.api.get_column_suggestions import get_column_suggestions
from mitosheet.api.get_card_template import get_card_template
from mitosheet.api.get_card_content import get_card_content
from mitosheet.api.get_data_alerts import get_data_alerts
from mitosheet.api.get_available_snowflake_options_and_defaults import \
get_available_snowflake_options_and_defaults
Expand Down Expand Up @@ -240,6 +242,10 @@ def handle_api_event(
result = get_chart_suggestions(params, steps_manager)
elif event["type"] == "get_column_suggestions":
result = get_column_suggestions(params, steps_manager)
elif event["type"] == "get_card_template":
result = get_card_template(params, steps_manager)
elif event["type"] == "get_card_content":
result = get_card_content(params, steps_manager)
elif event["type"] == "get_data_alerts":
result = get_data_alerts(params, steps_manager)
elif event["type"] == "get_parameterizable_params":
Expand Down
36 changes: 36 additions & 0 deletions mitosheet/mitosheet/api/card_content_utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
#!/usr/bin/env python
# coding: utf-8

# Copyright (c) Saga Inc.
# Distributed under the terms of the GNU Affero General Public License v3.0 License.

"""
Helpers for at-a-glance vs full card rendering.
"""

from __future__ import annotations

from typing import Any, Dict, List

_GLANCE_METRIC_LIMIT = 3


def filter_blocks_for_glance(blocks: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
"""Keep a compact subset of blocks for the floating at-a-glance card."""
out: List[Dict[str, Any]] = []
metric_count = 0
has_insight = False

for block in blocks:
block_type = block.get("type")
if block_type in ("header", "caption"):
out.append(block)
elif block_type == "metric":
if metric_count < _GLANCE_METRIC_LIMIT:
out.append(block)
metric_count += 1
elif block_type == "alert" and not has_insight:
out.append(block)
has_insight = True

return out
105 changes: 105 additions & 0 deletions mitosheet/mitosheet/api/card_storage.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
#!/usr/bin/env python
# coding: utf-8

# Copyright (c) Saga Inc.
# Distributed under the terms of the GNU Affero General Public License v3.0 License.

"""
Parse and serialize column card definitions (glance + full Streamlit code + explore links).
"""

from __future__ import annotations

import json
import re
from typing import Any, Dict, List, Tuple

import pandas as pd

_PLACEHOLDER_RE = re.compile(r"\{([^}]+)\}")


def parse_card_definition(stored: str) -> Tuple[str, str, List[Dict[str, Any]]]:
"""
Returns (glance_code, full_code, explore).
glance_code may be empty — caller should fall back to filtering full render.
"""
stripped = stored.strip()
if not stripped:
return "", "", []
try:
parsed = json.loads(stripped)
if isinstance(parsed, dict) and isinstance(parsed.get("code"), str):
full_code = parsed["code"]
explore = parsed.get("explore", [])
if not isinstance(explore, list):
explore = []
glance_raw = parsed.get("glance_code", "")
glance_code = glance_raw if isinstance(glance_raw, str) else ""
return glance_code.strip(), full_code, explore
except json.JSONDecodeError:
pass
return "", stripped, []


def serialize_card_definition(
code: str,
explore: List[Dict[str, Any]],
glance_code: str = "",
) -> str:
glance = glance_code.strip()
full = code.strip()
has_distinct_glance = glance != "" and glance != full
has_explore = len(explore) > 0

if not has_distinct_glance and not has_explore:
return code

payload: Dict[str, Any] = {"code": code}
if has_distinct_glance:
payload["glance_code"] = glance
if has_explore:
payload["explore"] = explore
return json.dumps(payload)


def _format_cell_value(value: Any) -> str:
if value is None or (isinstance(value, float) and pd.isna(value)):
return ""
if isinstance(value, (int, float)) and not isinstance(value, bool):
f = float(value)
if f == 0:
return "0"
af = abs(f)
if af >= 1_000_000:
return f"{f:,.0f}"
if af >= 10_000:
return f"{f:,.2f}".rstrip("0").rstrip(".")
if af >= 1:
rounded = round(f, 2)
return str(int(rounded)) if rounded == int(rounded) else str(rounded)
return str(round(f, 2))
return str(value)


def render_explore_labels(
explore: List[Dict[str, Any]], row: pd.Series
) -> List[Dict[str, str]]:
"""Fill {Column Name} placeholders in explore link labels from the selected row."""
out: List[Dict[str, str]] = []
for item in explore:
if not isinstance(item, dict):
continue
label = item.get("label")
view_code = item.get("view_code")
if not isinstance(label, str) or not isinstance(view_code, str):
continue
rendered_label = label
for match in _PLACEHOLDER_RE.finditer(label):
col_name = match.group(1)
if col_name in row.index:
rendered_label = rendered_label.replace(
match.group(0), _format_cell_value(row[col_name])
)
out.append({"label": rendered_label, "view_code": view_code})
return out
65 changes: 65 additions & 0 deletions mitosheet/mitosheet/api/get_card_content.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
#!/usr/bin/env python
# coding: utf-8

# Copyright (c) Saga Inc.
# Distributed under the terms of the GNU Affero General Public License v3.0 License.

from __future__ import annotations

from typing import Any, Dict

from mitosheet.api.card_content_utils import filter_blocks_for_glance
from mitosheet.api.card_storage import parse_card_definition, render_explore_labels
from mitosheet.api.streamlit_card_recorder import render_card
from mitosheet.types import StepsManagerType


def get_card_content(params: Dict[str, Any], steps_manager: StepsManagerType) -> Dict[str, Any]:
sheet_index = params.get("sheet_index")
row_index = params.get("row_index")
column_id = params.get("column_id")
mode = params.get("mode", "full")
if mode not in ("glance", "full"):
mode = "full"

if not isinstance(sheet_index, int) or not isinstance(row_index, int):
return {"error": "Invalid params"}

state = steps_manager.curr_step.final_defined_state
dfs = state.dfs

if sheet_index < 0 or sheet_index >= len(dfs):
return {"error": "Invalid sheet index"}

df = dfs[sheet_index]
if df is None or len(df) == 0:
return {"error": "No data in sheet"}

if row_index < 0 or row_index >= len(df):
return {"error": "Invalid row index"}

if sheet_index >= len(state.column_cards):
return {"blocks": [], "explore": []}

cards_for_sheet = state.column_cards[sheet_index]
if not isinstance(column_id, str) or column_id not in cards_for_sheet:
return {"blocks": [], "explore": []}

stored = cards_for_sheet[column_id]
if not isinstance(stored, str):
return {"blocks": [], "explore": []}

glance_code, full_code, explore = parse_card_definition(stored)
row = df.iloc[row_index]

if mode == "glance":
if glance_code:
blocks = render_card(glance_code, df, row_index)
else:
blocks = filter_blocks_for_glance(render_card(full_code, df, row_index))
return {"blocks": blocks, "explore": []}

return {
"blocks": render_card(full_code, df, row_index),
"explore": render_explore_labels(explore, row),
}
Loading
Loading