← 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

FieldTypeDescription
reportrequiredobjectThe report as GET …/report answers it.
clientstringNot declared
brandstringThe name on the cover.
colorstringThe accent colour.

Outputs

FieldTypeDescription
chartsarrayNot declared
specobjectNot declared
countrequiredintegerNot declared
clientstringNot 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.