|
| 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 — attrs: user-defined metadata</title> |
| 7 | + <style> |
| 8 | + body { font-family: system-ui, sans-serif; max-width: 860px; margin: 2rem auto; padding: 0 1rem; line-height: 1.6; color: #1a1a1a; } |
| 9 | + h1 { color: #0066cc; } |
| 10 | + h2 { color: #333; border-bottom: 1px solid #eee; padding-bottom: 0.3em; } |
| 11 | + pre { background: #f6f8fa; border-radius: 6px; padding: 1rem; overflow-x: auto; font-size: 0.9em; } |
| 12 | + code { font-family: 'Fira Code', 'Cascadia Code', monospace; } |
| 13 | + .note { background: #fffbea; border-left: 4px solid #f5a623; padding: 0.7rem 1rem; border-radius: 0 6px 6px 0; margin: 1rem 0; } |
| 14 | + table { border-collapse: collapse; width: 100%; margin: 1rem 0; } |
| 15 | + th, td { border: 1px solid #ddd; padding: 0.5rem 0.75rem; text-align: left; } |
| 16 | + th { background: #f0f4f8; } |
| 17 | + a { color: #0066cc; } |
| 18 | + .api-table td:first-child { font-family: monospace; white-space: nowrap; } |
| 19 | + </style> |
| 20 | +</head> |
| 21 | +<body> |
| 22 | + <p><a href="index.html">← tsb playground</a></p> |
| 23 | + |
| 24 | + <h1><code>attrs</code> — User-Defined Metadata</h1> |
| 25 | + <p> |
| 26 | + Attach arbitrary key→value metadata to any <code>Series</code> or <code>DataFrame</code> |
| 27 | + — mirrors |
| 28 | + <a href="https://pandas.pydata.org/docs/reference/api/pandas.DataFrame.attrs.html" target="_blank"> |
| 29 | + <code>pandas.DataFrame.attrs</code></a> and |
| 30 | + <a href="https://pandas.pydata.org/docs/reference/api/pandas.Series.attrs.html" target="_blank"> |
| 31 | + <code>pandas.Series.attrs</code></a>. |
| 32 | + </p> |
| 33 | + |
| 34 | + <div class="note"> |
| 35 | + <strong>Design note:</strong> Because <code>tsb</code> objects are immutable (their data, index, |
| 36 | + and dtype are frozen), attrs are stored in a <strong>WeakMap registry</strong> rather than as |
| 37 | + instance properties. This means attrs are attached & detached without touching the object |
| 38 | + itself, and garbage-collected automatically when the object is collected. |
| 39 | + </div> |
| 40 | + |
| 41 | + <h2>Basic usage</h2> |
| 42 | + |
| 43 | + <pre><code>import { |
| 44 | + getAttrs, setAttrs, updateAttrs, copyAttrs, withAttrs, |
| 45 | + clearAttrs, hasAttrs, getAttr, setAttr, deleteAttr, |
| 46 | + attrsCount, attrsKeys, mergeAttrs, |
| 47 | +} from "tsb"; |
| 48 | +import { DataFrame, Series } from "tsb"; |
| 49 | + |
| 50 | +// ─── annotate a DataFrame ───────────────────────────────────────────────── |
| 51 | +const df = DataFrame.fromColumns({ |
| 52 | + temperature: [22.1, 23.5, 21.8], |
| 53 | + humidity: [55, 60, 58], |
| 54 | +}); |
| 55 | + |
| 56 | +setAttrs(df, { |
| 57 | + source: "weather_station_42", |
| 58 | + unit: "Celsius", |
| 59 | + notes: "Morning readings", |
| 60 | +}); |
| 61 | + |
| 62 | +getAttrs(df); |
| 63 | +// → { source: "weather_station_42", unit: "Celsius", notes: "Morning readings" } |
| 64 | + |
| 65 | +getAttr(df, "unit"); // → "Celsius" |
| 66 | +getAttr(df, "missing"); // → undefined |
| 67 | +attrsCount(df); // → 3 |
| 68 | +attrsKeys(df); // → ["source", "unit", "notes"] |
| 69 | +hasAttrs(df); // → true |
| 70 | +</code></pre> |
| 71 | + |
| 72 | + <h2>Merging and updating</h2> |
| 73 | + |
| 74 | + <pre><code>// updateAttrs merges new keys, preserves existing |
| 75 | +updateAttrs(df, { version: 2, notes: "Updated notes" }); |
| 76 | +getAttrs(df); |
| 77 | +// → { source: "weather_station_42", unit: "Celsius", notes: "Updated notes", version: 2 } |
| 78 | + |
| 79 | +// setAttr / deleteAttr for single keys |
| 80 | +setAttr(df, "sensor_id", "WS-042"); |
| 81 | +deleteAttr(df, "notes"); |
| 82 | +getAttrs(df); |
| 83 | +// → { source: "weather_station_42", unit: "Celsius", version: 2, sensor_id: "WS-042" } |
| 84 | +</code></pre> |
| 85 | + |
| 86 | + <h2>Propagating metadata to derived objects</h2> |
| 87 | + |
| 88 | + <pre><code>// copyAttrs: copy all attrs from one object to another |
| 89 | +const s = new Series({ data: [22.1, 23.5, 21.8], name: "temperature" }); |
| 90 | +setAttrs(s, { unit: "Celsius", source: "sensor_A" }); |
| 91 | + |
| 92 | +const derived = new Series({ data: [71.8, 74.3, 71.2], name: "fahrenheit" }); |
| 93 | +copyAttrs(s, derived); |
| 94 | +getAttrs(derived); |
| 95 | +// → { unit: "Celsius", source: "sensor_A" } |
| 96 | + |
| 97 | +// Then update the copy |
| 98 | +setAttr(derived, "unit", "Fahrenheit"); |
| 99 | +getAttrs(derived); // → { unit: "Fahrenheit", source: "sensor_A" } |
| 100 | +getAttrs(s); // → { unit: "Celsius", source: "sensor_A" } ← unchanged |
| 101 | +</code></pre> |
| 102 | + |
| 103 | + <h2>Fluent helper — <code>withAttrs</code></h2> |
| 104 | + |
| 105 | + <pre><code>// withAttrs sets attrs and returns the same object reference |
| 106 | +// Handy for inline annotation |
| 107 | +const annotated = withAttrs( |
| 108 | + DataFrame.fromColumns({ x: [1, 2, 3] }), |
| 109 | + { source: "lab_experiment", date: "2026-04-09" }, |
| 110 | +); |
| 111 | + |
| 112 | +annotated === annotated; // true — same reference, not a copy |
| 113 | +getAttrs(annotated); |
| 114 | +// → { source: "lab_experiment", date: "2026-04-09" } |
| 115 | +</code></pre> |
| 116 | + |
| 117 | + <h2>Merging from multiple sources</h2> |
| 118 | + |
| 119 | + <pre><code>// mergeAttrs: combine attrs from multiple objects into a target |
| 120 | +const s1 = new Series({ data: [1, 2, 3], name: "a" }); |
| 121 | +const s2 = new Series({ data: [4, 5, 6], name: "b" }); |
| 122 | +setAttrs(s1, { source: "sensor_A", unit: "kg" }); |
| 123 | +setAttrs(s2, { source: "sensor_B", scale: 2.5 }); |
| 124 | + |
| 125 | +const combined = DataFrame.fromColumns({ a: [1, 2, 3], b: [4, 5, 6] }); |
| 126 | +mergeAttrs([s1, s2], combined); |
| 127 | +// Later sources win on conflicts: source="sensor_B" |
| 128 | +getAttrs(combined); |
| 129 | +// → { source: "sensor_B", unit: "kg", scale: 2.5 } |
| 130 | +</code></pre> |
| 131 | + |
| 132 | + <h2>Clearing metadata</h2> |
| 133 | + |
| 134 | + <pre><code>setAttrs(df, { x: 1, y: 2 }); |
| 135 | +hasAttrs(df); // → true |
| 136 | +attrsCount(df); // → 2 |
| 137 | + |
| 138 | +clearAttrs(df); |
| 139 | +hasAttrs(df); // → false |
| 140 | +getAttrs(df); // → {} |
| 141 | +</code></pre> |
| 142 | + |
| 143 | + <h2>API reference</h2> |
| 144 | + |
| 145 | + <table class="api-table"> |
| 146 | + <thead> |
| 147 | + <tr><th>Function</th><th>Description</th></tr> |
| 148 | + </thead> |
| 149 | + <tbody> |
| 150 | + <tr><td>getAttrs(obj)</td><td>Return a shallow copy of all stored attrs (empty {} if none)</td></tr> |
| 151 | + <tr><td>setAttrs(obj, attrs)</td><td>Overwrite attrs completely with the given record</td></tr> |
| 152 | + <tr><td>updateAttrs(obj, updates)</td><td>Merge updates into existing attrs (existing keys preserved)</td></tr> |
| 153 | + <tr><td>withAttrs(obj, attrs)</td><td>Fluent: set attrs and return the same object</td></tr> |
| 154 | + <tr><td>copyAttrs(source, target)</td><td>Copy all attrs from source to target</td></tr> |
| 155 | + <tr><td>mergeAttrs(sources[], target)</td><td>Merge attrs from multiple sources; later sources win</td></tr> |
| 156 | + <tr><td>clearAttrs(obj)</td><td>Remove all attrs from obj</td></tr> |
| 157 | + <tr><td>hasAttrs(obj)</td><td>Return true if any attrs are set</td></tr> |
| 158 | + <tr><td>getAttr(obj, key)</td><td>Get a single attr value (undefined if missing)</td></tr> |
| 159 | + <tr><td>setAttr(obj, key, value)</td><td>Set a single attr, preserving other keys</td></tr> |
| 160 | + <tr><td>deleteAttr(obj, key)</td><td>Delete a single attr key</td></tr> |
| 161 | + <tr><td>attrsCount(obj)</td><td>Number of stored attr keys</td></tr> |
| 162 | + <tr><td>attrsKeys(obj)</td><td>Array of stored attr key names</td></tr> |
| 163 | + </tbody> |
| 164 | + </table> |
| 165 | + |
| 166 | + <h2>Comparison with pandas</h2> |
| 167 | + |
| 168 | + <table> |
| 169 | + <thead> |
| 170 | + <tr><th>pandas</th><th>tsb</th></tr> |
| 171 | + </thead> |
| 172 | + <tbody> |
| 173 | + <tr><td><code>df.attrs</code></td><td><code>getAttrs(df)</code></td></tr> |
| 174 | + <tr><td><code>df.attrs = {"k": "v"}</code></td><td><code>setAttrs(df, { k: "v" })</code></td></tr> |
| 175 | + <tr><td><code>df.attrs["k"] = "v"</code></td><td><code>setAttr(df, "k", "v")</code></td></tr> |
| 176 | + <tr><td><code>df.attrs["k"]</code></td><td><code>getAttr(df, "k")</code></td></tr> |
| 177 | + <tr><td><code>del df.attrs["k"]</code></td><td><code>deleteAttr(df, "k")</code></td></tr> |
| 178 | + <tr><td><code>df.attrs.update(d)</code></td><td><code>updateAttrs(df, d)</code></td></tr> |
| 179 | + <tr><td><code>df.attrs.clear()</code></td><td><code>clearAttrs(df)</code></td></tr> |
| 180 | + </tbody> |
| 181 | + </table> |
| 182 | +</body> |
| 183 | +</html> |
0 commit comments