diff --git a/playground/index.html b/playground/index.html index 0597f9f2..62e78e60 100644 --- a/playground/index.html +++ b/playground/index.html @@ -449,6 +449,11 @@

assertSeriesEqual / assertFrameEqual / assertIndexEqual โ€” rich assertion helpers for use in test suites. Numeric tolerance, checkLike column-order mode, dtype checks, AssertionError with detailed diff messages. Mirrors pandas.testing.

โœ… Complete
+
+

๐ŸŽจ Styler โ€” DataFrame Style API

+

dataFrameStyle(df) ยท highlightMax / highlightMin / highlightNull / highlightBetween ยท backgroundGradient / textGradient ยท barChart ยท format / formatIndex ยท apply / applymap / map ยท setCaption / setTableStyles / hide ยท toHtml / toLatex. Mirrors pandas.DataFrame.style (Styler).

+
โœ… Complete
+
diff --git a/playground/style.html b/playground/style.html new file mode 100644 index 00000000..cbe1d2a3 --- /dev/null +++ b/playground/style.html @@ -0,0 +1,416 @@ + + + + + + tsb โ€” DataFrame Styler + + + +
+

tsb โ€” Styler

+

dataFrameStyle ยท highlightMax ยท backgroundGradient ยท barChart ยท toHtml ยท toLatex ยท mirrors pandas.DataFrame.style

+
+ +
+ +
+

Overview

+

+ The Styler class provides a fluent API for applying CSS styles to a + DataFrame and rendering the result as styled HTML โ€” directly analogous to + pandas.DataFrame.style (the pandas.io.formats.style.Styler + class). +

+

+ All methods return this, so you can chain them: +

+
import { DataFrame, dataFrameStyle } from "tsb";
+
+const df = DataFrame.fromColumns({
+  product: ["Widget A", "Widget B", "Widget C"],
+  sales:   [120, 340, 85],
+  profit:  [30, 95, -10],
+});
+
+const html = dataFrameStyle(df)
+  .highlightMax({ color: "lightgreen", subset: ["sales", "profit"] })
+  .highlightMin({ color: "salmon",     subset: ["sales", "profit"] })
+  .highlightNull("orange")
+  .backgroundGradient({ cmap: "Blues", subset: ["sales"] })
+  .format((v) => typeof v === "number" ? v.toLocaleString() : String(v))
+  .setCaption("Q3 Product Summary")
+  .setTableStyles([
+    { selector: "th", props: { "background-color": "#343a40", color: "#fff" } },
+  ])
+  .toHtml();
+
+ +
+

Import

+
import {
+  Styler,
+  dataFrameStyle,
+} from "tsb";
+
+// Type imports
+import type {
+  CellProps,
+  TableStyle,
+  StyleRecord,
+  ValueFormatter,
+  ColSubset,
+  AxisStyleFn,
+  ElementStyleFn,
+  HighlightOptions,
+  HighlightBetweenOptions,
+  GradientOptions,
+  BarOptions,
+} from "tsb";
+
+ +
+

Factory function

+
const styler = dataFrameStyle(df);
+// Equivalent to: new Styler(df)
+// Equivalent to: df.style  (in pandas)
+

The styler.data property returns the underlying DataFrame.

+
+ +
+

Formatting

+ +

.format(formatter, subset?, naRep?)

+
// Function formatter
+dataFrameStyle(df).format((v) => typeof v === "number" ? `$${v.toFixed(2)}` : String(v));
+
+// Template string (replaces {v} with the cell value)
+dataFrameStyle(df).format("{v}%", ["pct_col"]);
+
+// Only format specific columns
+dataFrameStyle(df).format(
+  (v) => typeof v === "number" ? v.toFixed(1) : String(v),
+  ["a", "b"],   // ColSubset
+);
+
+// null formatter resets to default
+dataFrameStyle(df).format(null);
+ +

.formatIndex(formatter)

+
// Format the row index labels
+dataFrameStyle(df).formatIndex((v) => `[${String(v)}]`);
+ +

.setPrecision(n)

+
// Show 2 decimal places by default
+dataFrameStyle(df).setPrecision(2);
+ +

.setNaRep(str)

+
// Show "N/A" for missing values
+dataFrameStyle(df).setNaRep("N/A");
+
+ +
+

Custom CSS styles

+ +

.apply(fn, axis?, subset?)

+

+ Apply a column-wise (axis=0) or row-wise (axis=1) function. + The function receives an array of values and must return an array of CSS strings of the + same length. +

+
// Highlight values > 100 in each column
+dataFrameStyle(df).apply(
+  (vals) => vals.map((v) => typeof v === "number" && v > 100 ? "color: red;" : ""),
+  0,         // axis=0 (per-column); use 1 for per-row
+  ["sales"], // optional column subset
+);
+
+// String axis aliases
+dataFrameStyle(df).apply(fn, "index");   // same as axis=0
+dataFrameStyle(df).apply(fn, "columns"); // same as axis=1
+ +

.applymap(fn, subset?) / .map(fn, subset?)

+

Apply an element-wise function (pandas โ‰ฅ 2.1 renamed applymap โ†’ map; both are supported).

+
dataFrameStyle(df).applymap((v) =>
+  typeof v === "number" && v < 0 ? "color: red; font-weight: bold;" : ""
+);
+
+// .map() is an alias
+dataFrameStyle(df).map((v) => v === null ? "background-color: #ffeeee;" : "");
+ +

.setProperties(props, subset?)

+
// Apply CSS to all cells (or a subset)
+dataFrameStyle(df).setProperties({ "font-weight": "bold", color: "navy" }, ["important"]);
+
+ +
+

Built-in highlights

+ +
+
+

.highlightMax(options?)

+
dataFrameStyle(df).highlightMax({
+  color: "lightgreen",  // CSS color
+  subset: ["a", "b"],   // columns to consider
+  axis: 0,              // 0=per-col, 1=per-row, null=table-wide
+});
+
+
+

.highlightMin(options?)

+
dataFrameStyle(df).highlightMin({
+  color: "salmon",
+  axis: 1,  // highlight min in each row
+});
+
+
+ +

.highlightNull(color?, subset?)

+
dataFrameStyle(df).highlightNull("orange");    // default color: "red"
+ +

.highlightBetween(options?)

+
dataFrameStyle(df).highlightBetween({
+  left: 0,       // lower bound (null = no lower bound)
+  right: 100,    // upper bound (null = no upper bound)
+  inclusive: "both",  // "both" | "left" | "right" | "neither"
+  color: "lightyellow",
+  subset: ["score"],
+});
+
+ +
+

Color gradients

+ +

.backgroundGradient(options?)

+
dataFrameStyle(df).backgroundGradient({
+  cmap: "Blues",    // colormap name or "colorA:colorB" shorthand
+  low: 0,           // clip low fraction  (default: 0)
+  high: 1,          // clip high fraction (default: 1)
+  axis: 0,          // 0=per-col, 1=per-row, null=table-wide
+  subset: ["a"],    // column subset
+  textColor: true,  // auto-choose dark/light text for contrast
+  vmin: null,       // override minimum for normalization
+  vmax: null,       // override maximum
+});
+ +

Available colormaps:

+

+ Blues ยท Greens ยท Reds ยท Oranges ยท + Purples ยท PuBu ยท YlOrRd ยท RdYlGn ยท + coolwarm ยท "#hex1:#hex2" (custom) +

+ +

.textGradient(options?)

+
// Same as backgroundGradient but applies to text color instead
+dataFrameStyle(df).textGradient({ cmap: "RdYlGn" });
+
+ +
+

Bar charts

+ +

.barChart(options?)

+

Renders inline bar charts using CSS linear-gradient.

+
dataFrameStyle(df).barChart({
+  color: "#4285f4",            // single color or [negColor, posColor]
+  width: 100,                  // bar cell width %
+  align: "left",               // "left" | "mid" | "zero"
+  subset: ["sales"],           // columns
+  vmin: null, vmax: null,      // normalization bounds
+});
+
+ +
+

Table decoration

+ +

.setCaption(text)

+
dataFrameStyle(df).setCaption("Q3 Sales Report");
+ +

.setTableStyles(styles)

+
dataFrameStyle(df).setTableStyles([
+  { selector: "table",        props: { border: "2px solid #999" } },
+  { selector: "th",           props: { "background-color": "#343a40", color: "#fff" } },
+  { selector: "tr:hover td",  props: { "background-color": "#f5f5f5" } },
+  // props as array of pairs is also supported:
+  { selector: "td",           props: [["padding", "6px 12px"], ["font-size", "13px"]] },
+]);
+ +

.setTableAttributes(attrs)

+
dataFrameStyle(df).setTableAttributes('class="report-table" id="q3-report"');
+ +

.hide(axis?, subset?)

+
dataFrameStyle(df).hide(0);                // hide row index
+dataFrameStyle(df).hide("index");           // same
+dataFrameStyle(df).hide(1, ["col_a"]);      // hide specific columns
+dataFrameStyle(df).hide("columns", ["b"]); // same
+
+ +
+

Rendering

+ +

.toHtml(uuid?) / .render(uuid?)

+
const html: string = dataFrameStyle(df)
+  .highlightMax()
+  .toHtml();         // .render() is an alias
+
+// Inject into a page
+document.getElementById("output").innerHTML = html;
+ +

.toLatex(environment?, hrules?)

+
const latex: string = dataFrameStyle(df)
+  .setCaption("Results")
+  .toLatex("tabular", true);  // environment, hrules
+

+ Note: CSS styles are not translated to LaTeX โ€” only the data and caption are rendered. +

+
+ +
+

Utilities

+ +

.exportStyles()

+
const records: StyleRecord[] = dataFrameStyle(df)
+  .highlightMax()
+  .exportStyles();
+// [{ row: 0, col: 1, css: "background-color: yellow;" }, ...]
+ +

.clearStyles()

+
// Remove all accumulated styles (keeps caption, hide state, precision)
+styler.clearStyles();
+
+ +
+

API reference

+
+
dataFrameStyle(df)
+
Create a new Styler wrapping df. Equivalent to df.style in pandas.
+ +
.format(formatter, subset?, naRep?)
+
Format cell values with a function or template string.
+ +
.formatIndex(formatter)
+
Format row index labels.
+ +
.setPrecision(n)
+
Set default float precision (default: 6).
+ +
.setNaRep(str)
+
Set representation for null/NaN values (default: "").
+ +
.apply(fn, axis?, subset?)
+
Column-wise or row-wise CSS function. Returns array of CSS strings.
+ +
.applymap(fn, subset?) / .map(fn, subset?)
+
Element-wise CSS function. Returns a single CSS string per value.
+ +
.setProperties(props, subset?)
+
Set CSS properties for all cells or a subset.
+ +
.highlightMax(options?)
+
Highlight maximum values. Options: color, subset, axis.
+ +
.highlightMin(options?)
+
Highlight minimum values.
+ +
.highlightNull(color?, subset?)
+
Highlight null/undefined/NaN values.
+ +
.highlightBetween(options?)
+
Highlight values in a numeric range. Options: left, right, inclusive, color, subset.
+ +
.backgroundGradient(options?)
+
Background color gradient. Options: cmap, low, high, axis, subset, textColor, vmin, vmax.
+ +
.textGradient(options?)
+
Text color gradient (same options as backgroundGradient).
+ +
.barChart(options?)
+
Inline bar chart. Options: color, width, align, subset, vmin, vmax.
+ +
.setCaption(text)
+
Set a <caption> on the rendered table.
+ +
.setTableStyles(styles)
+
Set CSS selector rules via a <style> block prepended to the output.
+ +
.setTableAttributes(attrs)
+
Set extra HTML attributes on the <table> tag.
+ +
.hide(axis?, subset?)
+
Hide the index (axis=0) or specific columns (axis=1, subset).
+ +
.clearStyles()
+
Reset all style applications, formatters, and table styles.
+ +
.exportStyles()
+
Return the accumulated per-cell styles as StyleRecord[].
+ +
.toHtml(uuid?) / .render(uuid?)
+
Render the styled DataFrame as an HTML string.
+ +
.toLatex(environment?, hrules?)
+
Render as a LaTeX tabular environment.
+
+
+ +
+

Pandas equivalence table

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
tsbpandas equivalent
dataFrameStyle(df)df.style
.format(fn).format(fn)
.formatIndex(fn).format_index(fn)
.apply(fn, axis).apply(fn, axis=0)
.applymap(fn).applymap(fn) / .map(fn)
.map(fn).map(fn) (โ‰ฅ pandas 2.1)
.highlightMax(opts).highlight_max()
.highlightMin(opts).highlight_min()
.highlightNull(color).highlight_null()
.highlightBetween(opts).highlight_between()
.backgroundGradient(opts).background_gradient()
.textGradient(opts).text_gradient()
.barChart(opts).bar()
.setCaption(text).set_caption(text)
.setProperties(props).set_properties(**kwargs)
.setTableStyles(styles).set_table_styles(styles)
.setTableAttributes(attrs).set_table_attributes(attrs)
.hide(0).hide(axis="index")
.hide(1, subset).hide(subset, axis="columns")
.exportStyles().export()
.clearStyles().clear()
.toHtml() / .render().to_html() / .render()
.toLatex().to_latex()
+
+ +
+ + diff --git a/src/index.ts b/src/index.ts index 572ed113..411cb787 100644 --- a/src/index.ts +++ b/src/index.ts @@ -668,3 +668,17 @@ export type { AssertFrameEqualOptions, AssertIndexEqualOptions, } from "./testing/index.ts"; +export { Styler, dataFrameStyle } from "./stats/index.ts"; +export type { + CellProps, + TableStyle, + StyleRecord, + ValueFormatter, + ColSubset, + AxisStyleFn, + ElementStyleFn, + HighlightOptions, + HighlightBetweenOptions, + GradientOptions, + BarOptions, +} from "./stats/index.ts"; diff --git a/src/stats/index.ts b/src/stats/index.ts index d5426bd1..cd2c46fe 100644 --- a/src/stats/index.ts +++ b/src/stats/index.ts @@ -486,3 +486,17 @@ export type { ResampleAggFn, ResampleOptions, } from "./resample.ts"; +export { Styler, dataFrameStyle } from "./style.ts"; +export type { + CellProps, + TableStyle, + StyleRecord, + ValueFormatter, + ColSubset, + AxisStyleFn, + ElementStyleFn, + HighlightOptions, + HighlightBetweenOptions, + GradientOptions, + BarOptions, +} from "./style.ts"; diff --git a/src/stats/resample.ts b/src/stats/resample.ts index e493c493..dbb49477 100644 --- a/src/stats/resample.ts +++ b/src/stats/resample.ts @@ -108,7 +108,9 @@ function isMissing(v: Scalar): boolean { // โ”€โ”€โ”€ helpers: date coercion โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ function toDate(v: Label): Date | null { - if (v instanceof Date) return v; + if (v instanceof Date) { + return v; + } if (typeof v === "string" || typeof v === "number") { const d = new Date(v as string | number); return Number.isNaN(d.getTime()) ? null : d; @@ -246,7 +248,9 @@ function nextGroupKey(ts: number, freq: string): number { */ function keyToLabel(key: number, freq: string, label: ResampleLabel): number { const dflt = freqDefaultLabel(freq); - if (label === dflt) return key; + if (label === dflt) { + return key; + } if (label === "right") { // User wants right label on a left-default freq โ†’ next bin start @@ -254,7 +258,9 @@ function keyToLabel(key: number, freq: string, label: ResampleLabel): number { } // User wants left label on a right-default freq (W*, ME, QE, YE/AE) - if (freq.startsWith("W")) return key - 6 * MS_D; // anchor โ†’ Mon/+1 + if (freq.startsWith("W")) { + return key - 6 * MS_D; // anchor โ†’ Mon/+1 + } if (freq === "ME") { const d = new Date(key); return Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), 1); @@ -283,7 +289,9 @@ function buildGroups(index: Index