← All ivy-nodes
IVYXSTUDIO · IVY NODE
S
Social: a period report as chart rows and a cover
ivy.node.report-rows · v0.1.0
ivyx✓
Turns IVYX Social's period report (the pulse with its months, the plan kept per account, the best posts) into the chart rows the chart node draws and the cover card's spec. No model, no network.
#social#report#ivyx-social
Inputs
| Field | Type | Description |
|---|---|---|
| reportrequired | object | The report as GET …/report answers it. |
| client | string | Not declared |
| brand | string | The name on the cover. |
| color | string | The accent colour. |
Outputs
| Field | Type | Description |
|---|---|---|
| charts | array | Not declared |
| spec | object | Not declared |
| countrequired | integer | Not declared |
| client | string | Not declared |
Source
python
# Input preparation
inp = __ivy_ctx__["nodes"][__ivy_node_id__]["input"]
# Compute
# Shared by every social node (built into each by build.mjs): dates, blocks, merging.
import datetime, json, re, statistics
def day(s):
s = str(s or "")[:10]
return s if re.fullmatch(r"\d{4}-\d{2}-\d{2}", s) else None
def now_iso():
return datetime.datetime.utcnow().replace(microsecond=0).isoformat() + "Z"
def ms_of(s):
if s is None or s == "":
return None
if isinstance(s, (int, float)):
return int(s * 1000) if s < 1e12 else int(s)
t = str(s).replace("Z", "+00:00")
t = re.sub(r"([+-]\d{2})(\d{2})$", r"\1:\2", t)
try:
return int(datetime.datetime.fromisoformat(t).timestamp() * 1000)
except ValueError:
return None
def rows_of(block):
"""A {fields, rows} block as dicts."""
if not isinstance(block, dict) or not block.get("fields"):
return []
f = block["fields"]
return [dict(zip(f, r)) for r in block.get("rows") or []]
def block_of(dicts, first):
fields = list(first)
for d in dicts:
for k in d:
if k not in fields:
fields.append(k)
return {"fields": fields, "rows": [[d.get(k) for k in fields] for d in dicts]}
def listed(v):
return [x for x in (v if isinstance(v, list) else [v] if v else []) if x is not None]
def text_of(v, limit=4000):
s = " ".join(str(v).split()) if isinstance(v, (str, int, float)) else ""
return s[:limit]
class ReportRows:
"Social: a period report as chart rows and a cover"
def run(self, inp):
out = {}
# A period report from IVYX Social (its pulse, months, best posts, cadence) as the chart rows and the cover card the design nodes draw.
rep = inp.get("report") if isinstance(inp.get("report"), dict) else {}
pulse = rep.get("pulse") or {}
months = listed(pulse.get("months"))
charts = []
if months:
rows = [{"month": m.get("month"), "reach_per_day": m.get("reachPerDay") or 0, "followers": m.get("followers") or 0, "posts": m.get("posts") or 0} for m in months[-12:]]
charts.append({"title": "Reach per day by month", "rows": rows, "x": "month", "series": ["reach_per_day"], "kind": "bar"})
charts.append({"title": "Followers by month", "rows": rows, "x": "month", "series": ["followers"], "kind": "line"})
cad = listed(rep.get("cadence"))
if cad:
charts.append({"title": "Plan kept per account", "rows": [{"account": c.get("accountId"), "planned": c.get("planned") or 0, "published": c.get("published") or 0} for c in cad], "x": "account", "series": ["planned", "published"], "kind": "bar"})
top = listed(rep.get("topPosts"))[:8]
if top:
charts.append({"title": "Best posts by lift", "rows": [{"post": (p.get("title") or p.get("text") or p.get("key") or "")[:28], "lift": round(float(p.get("lift") or 0), 2)} for p in top], "x": "post", "series": ["lift"], "kind": "bar"})
client = (rep.get("client") or {}).get("name") or inp.get("client") or "the brand"
win = rep.get("window") or {}
f = lambda v: (v or {}).get("value")
sub = []
if f(pulse.get("followers")) is not None:
sub.append(f"{f(pulse['followers']):,} followers")
if f(pulse.get("reachPerDay")) is not None:
sub.append(f"{f(pulse['reachPerDay']):,} reach a day")
if f(pulse.get("engagementRatePct")) is not None:
sub.append(f"{f(pulse['engagementRatePct'])}% engagement")
out["charts"] = charts
out["spec"] = {"headline": f"{client}: {win.get('from', '')} to {win.get('to', '')}".strip(), "subline": " · ".join(sub)[:120], "cta": "The period", "palette": {"bg": "#1E1B4B", "accent": inp.get("color") or "#D6336C", "text": "#F8F7FF"}, "layout": "poster", "brand": inp.get("brand") or client, "caption": "The period's report cover."}
out["count"] = len(charts)
out["client"] = client
return out
result = ReportRows().run(inp)
# Output collection (the runner reads it here)
out = __ivy_ctx__["nodes"][__ivy_node_id__]["output"]
out.update(result)Tests
Requires: python:3.9
- rows
Two months, a cadence and a best post become four charts and a cover naming the brand.
- empty-report
A report with nothing in it draws no chart and a cover for the brand by its input name.