Skip to content

Commit 7488d8d

Browse files
authored
Merge pull request #299 from githubnext/copilot/add-more-complex-end-to-end-examples
Add complex end-to-end playground examples
2 parents 484da13 + 95eb5c1 commit 7488d8d

6 files changed

Lines changed: 968 additions & 1 deletion

File tree

Lines changed: 318 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,318 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8" />
5+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
6+
<title>tsb — Inventory Replenishment Planning — Examples</title>
7+
<style>
8+
:root {
9+
--bg: #0d1117;
10+
--surface: #161b22;
11+
--border: #30363d;
12+
--text: #e6edf3;
13+
--accent: #58a6ff;
14+
--green: #3fb950;
15+
--orange: #d29922;
16+
--red: #f85149;
17+
--font-mono: "Cascadia Code", "Fira Code", "JetBrains Mono", monospace;
18+
}
19+
* { box-sizing: border-box; margin: 0; padding: 0; }
20+
body {
21+
background: var(--bg);
22+
color: var(--text);
23+
font-family: system-ui, -apple-system, sans-serif;
24+
line-height: 1.6;
25+
padding: 2rem;
26+
max-width: 920px;
27+
margin: 0 auto;
28+
}
29+
a { color: var(--accent); }
30+
h1 { color: var(--accent); margin-bottom: 0.5rem; }
31+
h2 { margin-top: 0; margin-bottom: 0.5rem; font-size: 1.25rem; }
32+
h3 { font-size: 1.05rem; margin: 1rem 0 0.4rem; }
33+
p { color: #8b949e; margin-bottom: 1rem; }
34+
ul { color: #8b949e; margin-left: 1.25rem; margin-bottom: 1rem; }
35+
li { margin-bottom: 0.25rem; }
36+
code {
37+
font-family: var(--font-mono);
38+
font-size: 0.875em;
39+
background: var(--surface);
40+
border: 1px solid var(--border);
41+
border-radius: 0.3rem;
42+
padding: 0.1rem 0.4rem;
43+
}
44+
.back { margin-bottom: 1.25rem; display: inline-block; }
45+
.scenario {
46+
background: rgba(88, 166, 255, 0.06);
47+
border-left: 3px solid var(--accent);
48+
padding: 0.75rem 1rem;
49+
border-radius: 0 0.4rem 0.4rem 0;
50+
margin: 1rem 0 1.5rem;
51+
color: #c9d1d9;
52+
}
53+
.scenario strong { color: var(--accent); }
54+
#playground-loading {
55+
position: fixed; inset: 0;
56+
background: rgba(13, 17, 23, 0.92);
57+
display: flex; flex-direction: column;
58+
align-items: center; justify-content: center;
59+
z-index: 1000; gap: 1rem;
60+
}
61+
.spinner {
62+
width: 40px; height: 40px;
63+
border: 3px solid var(--border);
64+
border-top-color: var(--accent);
65+
border-radius: 50%;
66+
animation: spin 0.8s linear infinite;
67+
}
68+
@keyframes spin { to { transform: rotate(360deg); } }
69+
#playground-status { color: #8b949e; font-size: 0.95rem; }
70+
.section {
71+
background: var(--surface);
72+
border: 1px solid var(--border);
73+
border-radius: 0.75rem;
74+
padding: 1.5rem;
75+
margin-bottom: 1.5rem;
76+
}
77+
.section p { margin-bottom: 0.75rem; }
78+
.playground-block { margin-top: 0.75rem; }
79+
.playground-header {
80+
display: flex; align-items: center; justify-content: space-between;
81+
background: #1c2128; border: 1px solid var(--border);
82+
border-bottom: none; border-radius: 0.5rem 0.5rem 0 0;
83+
padding: 0.4rem 0.75rem;
84+
}
85+
.playground-label {
86+
font-size: 0.75rem; color: #8b949e;
87+
text-transform: uppercase; letter-spacing: 0.05em;
88+
}
89+
.playground-actions { display: flex; gap: 0.5rem; }
90+
.playground-actions button {
91+
background: transparent; color: var(--accent);
92+
border: 1px solid var(--border); border-radius: 0.35rem;
93+
padding: 0.25rem 0.7rem; font-size: 0.8rem;
94+
cursor: pointer; font-family: system-ui, sans-serif;
95+
transition: background 0.15s, border-color 0.15s;
96+
}
97+
.playground-actions button:hover:not(:disabled) {
98+
background: rgba(88, 166, 255, 0.1);
99+
border-color: var(--accent);
100+
}
101+
.playground-actions button:disabled { opacity: 0.4; cursor: not-allowed; }
102+
.playground-run { font-weight: 600; }
103+
.playground-editor {
104+
display: block; width: 100%; min-height: 80px;
105+
background: #0d1117; color: var(--text);
106+
border: 1px solid var(--border);
107+
border-top: none; border-bottom: none;
108+
padding: 1rem;
109+
font-family: var(--font-mono);
110+
font-size: 0.875rem; line-height: 1.55;
111+
resize: vertical; outline: none;
112+
tab-size: 2; white-space: pre; overflow-x: auto;
113+
}
114+
.playground-editor:focus {
115+
border-color: var(--accent);
116+
box-shadow: inset 0 0 0 1px var(--accent);
117+
}
118+
.playground-output {
119+
background: #1c2333;
120+
border: 1px solid var(--border);
121+
border-radius: 0 0 0.5rem 0.5rem;
122+
padding: 0.75rem 1rem;
123+
font-family: var(--font-mono);
124+
font-size: 0.85rem; color: #8b949e;
125+
white-space: pre-wrap; min-height: 2rem;
126+
word-break: break-word;
127+
}
128+
.playground-output.active { color: var(--green); border-color: var(--green); }
129+
.playground-output.error { color: var(--red); border-color: var(--red); }
130+
.playground-hint {
131+
font-size: 0.75rem; color: #484f58;
132+
margin-top: 0.35rem; text-align: right;
133+
}
134+
footer {
135+
text-align: center; padding: 2rem 0;
136+
color: #8b949e; font-size: 0.85rem;
137+
border-top: 1px solid var(--border);
138+
margin-top: 2rem;
139+
}
140+
</style>
141+
</head>
142+
<body>
143+
<div id="playground-loading">
144+
<div class="spinner"></div>
145+
<div id="playground-status">Initializing playground…</div>
146+
</div>
147+
148+
<a class="back" href="examples.html">← Back to examples</a>
149+
<h1>📦 Inventory Replenishment Planning</h1>
150+
<div class="scenario"><strong>Scenario:</strong> A retail operations analyst combines daily sell-through with SKU master data to find categories moving fastest and flag items whose on-hand stock is below lead-time demand.</div>
151+
<p>Skills you'll use: <code>readCsv</code>, <code>merge</code>, function-valued <code>assign</code>, rolling-style demand features, filters, and pivot tables.</p>
152+
153+
<div class="section">
154+
<h2>1 · Enrich sales with product master data</h2>
155+
<p>Parse store/SKU sales, join attributes, and compute inventory value and sell-through.</p>
156+
<div class="playground-block">
157+
<div class="playground-header">
158+
<span class="playground-label">TypeScript</span>
159+
<div class="playground-actions">
160+
<button class="playground-run" disabled>▶ Run</button>
161+
<button class="playground-reset">↺ Reset</button>
162+
</div>
163+
</div>
164+
<textarea class="playground-editor" spellcheck="false">import { DataFrame, merge, readCsv } from "tsb";
165+
166+
const csv = `date,sku,store,units_sold,on_hand,unit_cost
167+
2024-04-01,A100,Downtown,8,40,12
168+
2024-04-02,A100,Downtown,11,31,12
169+
2024-04-03,A100,Downtown,9,22,12
170+
2024-04-04,A100,Downtown,10,14,12
171+
2024-04-01,B200,Downtown,4,28,22
172+
2024-04-02,B200,Downtown,5,23,22
173+
2024-04-03,B200,Downtown,7,16,22
174+
2024-04-04,B200,Downtown,6,10,22
175+
2024-04-01,C300,Uptown,3,18,35
176+
2024-04-02,C300,Uptown,2,16,35
177+
2024-04-03,C300,Uptown,4,12,35
178+
2024-04-04,C300,Uptown,5,7,35`;
179+
const sales = readCsv(csv);
180+
const master = DataFrame.fromRecords([
181+
{ sku: "A100", category: "accessories", supplier: "Acme", lead_days: 5 },
182+
{ sku: "B200", category: "apparel", supplier: "Northwind", lead_days: 8 },
183+
{ sku: "C300", category: "home", supplier: "Acme", lead_days: 6 },
184+
]);
185+
186+
const enriched = merge(sales, master, { on: "sku", how: "left" }).assign({
187+
inventory_value: (df) =&gt; df.col("on_hand").mul(df.col("unit_cost")),
188+
sell_through: (df) =&gt; df.col("units_sold").div(df.col("on_hand").add(df.col("units_sold"))),
189+
});
190+
console.log(enriched.select(["date", "sku", "category", "units_sold", "on_hand", "sell_through", "inventory_value"]).head(8).toString());
191+
192+
const byCategory = enriched
193+
.groupby("category")
194+
.agg({ units_sold: "sum", inventory_value: "sum", sell_through: "mean" }, false)
195+
.sortValues("units_sold", false);
196+
console.log("\nCategory summary:");
197+
console.log(byCategory.toString());</textarea>
198+
<textarea class="playground-python" style="display:none">from io import StringIO
199+
import pandas as pd
200+
201+
csv = """date,sku,store,units_sold,on_hand,unit_cost
202+
2024-04-01,A100,Downtown,8,40,12
203+
2024-04-02,A100,Downtown,11,31,12
204+
2024-04-03,A100,Downtown,9,22,12
205+
2024-04-04,A100,Downtown,10,14,12
206+
2024-04-01,B200,Downtown,4,28,22
207+
2024-04-02,B200,Downtown,5,23,22
208+
2024-04-03,B200,Downtown,7,16,22
209+
2024-04-04,B200,Downtown,6,10,22
210+
2024-04-01,C300,Uptown,3,18,35
211+
2024-04-02,C300,Uptown,2,16,35
212+
2024-04-03,C300,Uptown,4,12,35
213+
2024-04-04,C300,Uptown,5,7,35"""
214+
sales = pd.read_csv(StringIO(csv))
215+
master = pd.DataFrame([
216+
{"sku": "A100", "category": "accessories", "supplier": "Acme", "lead_days": 5},
217+
{"sku": "B200", "category": "apparel", "supplier": "Northwind", "lead_days": 8},
218+
{"sku": "C300", "category": "home", "supplier": "Acme", "lead_days": 6},
219+
])
220+
enriched = sales.merge(master, on="sku", how="left")
221+
enriched["inventory_value"] = enriched["on_hand"] * enriched["unit_cost"]
222+
enriched["sell_through"] = enriched["units_sold"] / (enriched["on_hand"] + enriched["units_sold"])
223+
print(enriched.head(8))
224+
print(enriched.groupby("category", as_index=False).agg({"units_sold":"sum", "inventory_value":"sum", "sell_through":"mean"}))</textarea>
225+
<div class="playground-output">Click ▶ Run to execute</div>
226+
<div class="playground-hint">Ctrl+Enter to run · Tab to indent</div>
227+
</div>
228+
</div>
229+
<div class="section">
230+
<h2>2 · Calculate reorder points from recent demand</h2>
231+
<p>Estimate rolling demand per SKU, compare it to on-hand stock, and summarize reorder status.</p>
232+
<div class="playground-block">
233+
<div class="playground-header">
234+
<span class="playground-label">TypeScript</span>
235+
<div class="playground-actions">
236+
<button class="playground-run" disabled>▶ Run</button>
237+
<button class="playground-reset">↺ Reset</button>
238+
</div>
239+
</div>
240+
<textarea class="playground-editor" spellcheck="false">import { DataFrame, pivotTableFull } from "tsb";
241+
242+
const rows = [
243+
{ date: "2024-04-01", sku: "A100", units_sold: 8, on_hand: 40, lead_days: 5 },
244+
{ date: "2024-04-02", sku: "A100", units_sold: 11, on_hand: 31, lead_days: 5 },
245+
{ date: "2024-04-03", sku: "A100", units_sold: 9, on_hand: 22, lead_days: 5 },
246+
{ date: "2024-04-04", sku: "A100", units_sold: 10, on_hand: 14, lead_days: 5 },
247+
{ date: "2024-04-01", sku: "B200", units_sold: 4, on_hand: 28, lead_days: 8 },
248+
{ date: "2024-04-02", sku: "B200", units_sold: 5, on_hand: 23, lead_days: 8 },
249+
{ date: "2024-04-03", sku: "B200", units_sold: 7, on_hand: 16, lead_days: 8 },
250+
{ date: "2024-04-04", sku: "B200", units_sold: 6, on_hand: 10, lead_days: 8 },
251+
{ date: "2024-04-01", sku: "C300", units_sold: 3, on_hand: 18, lead_days: 6 },
252+
{ date: "2024-04-02", sku: "C300", units_sold: 2, on_hand: 16, lead_days: 6 },
253+
{ date: "2024-04-03", sku: "C300", units_sold: 4, on_hand: 12, lead_days: 6 },
254+
{ date: "2024-04-04", sku: "C300", units_sold: 5, on_hand: 7, lead_days: 6 },
255+
];
256+
const rollingDemand = rows.map((row, i) =&gt; {
257+
const peers = rows.slice(0, i + 1).filter((r) =&gt; r.sku === row.sku).slice(-3);
258+
return peers.reduce((sum, r) =&gt; sum + r.units_sold, 0) / peers.length;
259+
});
260+
const reorderPoint = rows.map((row, i) =&gt; rollingDemand[i]! * row.lead_days);
261+
const status = rows.map((row, i) =&gt; row.on_hand &lt;= reorderPoint[i]! ? "reorder" : "ok");
262+
const plan = DataFrame.fromRecords(rows).assign({ rolling_3d_demand: rollingDemand, reorder_point: reorderPoint, status });
263+
264+
console.log(plan.select(["date", "sku", "on_hand", "rolling_3d_demand", "reorder_point", "status"]).tail(6).toString());
265+
266+
const openOrders = plan.filter(plan.col("status").eq("reorder"));
267+
console.log("\nReplenishment queue:");
268+
console.log(openOrders.select(["date", "sku", "on_hand", "reorder_point"]).toString());
269+
270+
const heatmap = pivotTableFull(plan, {
271+
index: "sku",
272+
columns: "status",
273+
values: "units_sold",
274+
aggfunc: "sum",
275+
fill_value: 0,
276+
margins: true,
277+
});
278+
console.log("\nDemand by reorder status:");
279+
console.log(heatmap.toString());</textarea>
280+
<textarea class="playground-python" style="display:none">import numpy as np
281+
import pandas as pd
282+
283+
rows = [
284+
{"date": "2024-04-01", "sku": "A100", "units_sold": 8, "on_hand": 40, "lead_days": 5},
285+
{"date": "2024-04-02", "sku": "A100", "units_sold": 11, "on_hand": 31, "lead_days": 5},
286+
{"date": "2024-04-03", "sku": "A100", "units_sold": 9, "on_hand": 22, "lead_days": 5},
287+
{"date": "2024-04-04", "sku": "A100", "units_sold": 10, "on_hand": 14, "lead_days": 5},
288+
{"date": "2024-04-01", "sku": "B200", "units_sold": 4, "on_hand": 28, "lead_days": 8},
289+
{"date": "2024-04-02", "sku": "B200", "units_sold": 5, "on_hand": 23, "lead_days": 8},
290+
{"date": "2024-04-03", "sku": "B200", "units_sold": 7, "on_hand": 16, "lead_days": 8},
291+
{"date": "2024-04-04", "sku": "B200", "units_sold": 6, "on_hand": 10, "lead_days": 8},
292+
{"date": "2024-04-01", "sku": "C300", "units_sold": 3, "on_hand": 18, "lead_days": 6},
293+
{"date": "2024-04-02", "sku": "C300", "units_sold": 2, "on_hand": 16, "lead_days": 6},
294+
{"date": "2024-04-03", "sku": "C300", "units_sold": 4, "on_hand": 12, "lead_days": 6},
295+
{"date": "2024-04-04", "sku": "C300", "units_sold": 5, "on_hand": 7, "lead_days": 6},
296+
]
297+
plan = pd.DataFrame(rows).sort_values(["sku", "date"])
298+
plan["rolling_3d_demand"] = plan.groupby("sku")["units_sold"].transform(lambda s: s.rolling(3, min_periods=1).mean())
299+
plan["reorder_point"] = plan["rolling_3d_demand"] * plan["lead_days"]
300+
plan["status"] = np.where(plan["on_hand"] &lt;= plan["reorder_point"], "reorder", "ok")
301+
print(plan.tail(6))
302+
print(plan[plan["status"] == "reorder"])
303+
print(pd.pivot_table(plan, index="sku", columns="status", values="units_sold", aggfunc="sum", fill_value=0, margins=True))</textarea>
304+
<div class="playground-output">Click ▶ Run to execute</div>
305+
<div class="playground-hint">Ctrl+Enter to run · Tab to indent</div>
306+
</div>
307+
</div>
308+
309+
<footer>
310+
<p>
311+
<a href="examples.html">All examples</a> ·
312+
<a href="index.html">tsb playground</a> ·
313+
Built by <a href="https://github.com/githubnext/autoloop">Autoloop</a>
314+
</p>
315+
</footer>
316+
<script type="module" src="playground-runtime.js"></script>
317+
</body>
318+
</html>

0 commit comments

Comments
 (0)