From 9b7944fb4eb404edbce4f88d7d3426bc455fbd3f Mon Sep 17 00:00:00 2001 From: Anton Nesterov Date: Wed, 25 Sep 2024 16:31:53 +0200 Subject: [PATCH] add varous charts --- expr.ts | 14 ++-- plots.ts | 217 +++++++++++++++++++++++++++++++++++++++++++++---------- 2 files changed, 186 insertions(+), 45 deletions(-) diff --git a/expr.ts b/expr.ts index 0d08113..e0f4aa7 100644 --- a/expr.ts +++ b/expr.ts @@ -1,9 +1,9 @@ -import pl from "npm:nodejs-polars" +import pl from "npm:nodejs-polars"; export function residuals(x: pl.Expr, y: pl.Expr): pl.Expr { - const xM = x.minus(x.mean()) - const yM = y.minus(y.mean()) - const xMSQ = xM.pow(2) - const beta = xM.dot(yM).div(xMSQ.sum()) - return yM.minus(beta.mul(xM)) -} \ No newline at end of file + const xM = x.minus(x.mean()); + const yM = y.minus(y.mean()); + const xMSQ = xM.pow(2); + const beta = xM.dot(yM).div(xMSQ.sum()); + return yM.minus(beta.mul(xM)); +} diff --git a/plots.ts b/plots.ts index 6d2e8d7..aeeaec3 100644 --- a/plots.ts +++ b/plots.ts @@ -1,28 +1,35 @@ import * as _Plot from "npm:@observablehq/plot"; import { DOMParser, SVGElement } from "npm:linkedom"; +import * as _vega from "npm:vega"; +import * as _lite from "npm:vega-lite"; const defaultPlotSettings = { - grid: true, - margin: 50, - style: { - backgroundColor: "#fff" - } -} + grid: true, + margin: 50, + style: { + backgroundColor: "#fff", + }, +}; /** * Configure default plot settings - * @param options + * @param options */ export function configurePlots(options: any) { - Object.assign(defaultPlotSettings, options) + Object.assign(defaultPlotSettings, options); } -export const document = new DOMParser().parseFromString(``, "text/html"); +export const document = new DOMParser().parseFromString( + ``, + "text/html", +); export const Plot = _Plot; +export const vega = _vega; +export const vegalite = _lite; /** - * Draw side-by-side plots + * Draw side-by-side plots * Example: * ```ts * const plt = sideBySidePlot({ @@ -39,34 +46,168 @@ export const Plot = _Plot; * @param marks List of plot callbacks * @param cols Number of columns */ -export function sideBySidePlot(opts: { - x: string[], - y: string[], - marks: any[] - cols: number, - options, +export function sideBySidePlot(opts: { + x: string[]; + y: string[]; + marks: any[]; + cols: number; + options; }) { - const imgTags: string[] = [] - for (const xTarget of opts.x) { - for (const yTarget of opts.y) { - const plt = Plot.plot({ - ...(opts.options ?? defaultPlotSettings), - marks: opts.marks.map(fn => fn(xTarget, yTarget)), - document - }) - plt.setAttribute('xmlns', 'http://www.w3.org/2000/svg') - const svgUrl = `data:image/svg+xml;base64,${btoa(unescape(encodeURIComponent(plt)))}` - imgTags.push(``) - } + const imgTags: string[] = []; + for (const xTarget of opts.x) { + for (const yTarget of opts.y) { + const plt = Plot.plot({ + ...(opts.options ?? defaultPlotSettings), + marks: opts.marks.map((fn) => fn(xTarget, yTarget)), + document, + }); + plt.setAttribute("xmlns", "http://www.w3.org/2000/svg"); + const svgUrl = `data:image/svg+xml;base64,${ + btoa(unescape(encodeURIComponent(plt))) + }`; + imgTags.push(``); } - const output = ` -
- ${imgTags.join('')} + } + const output = ` +
+ ${imgTags.join("")}
- ` - return { - [Symbol.for("Jupyter.display")]: () => ({ - "text/html": output - }) - } -} \ No newline at end of file + `; + return { + [Symbol.for("Jupyter.display")]: () => ({ + "text/html": output, + }), + }; +} + +/** + * Histogram plot + * + * @param data + * @param x + * @param opts + * @returns Plot + */ +export function histPlot( + data: any[], + x = "column", + opts = { options: null, fn: "proportion" }, +) { + return Plot.plot({ + ...(opts.options ?? defaultPlotSettings), + y: { grid: true }, + marks: [ + Plot.ruleY([0]), + Plot.ruleX([0]), + Plot.rectY(data, Plot.binX({ y: opts.fn ?? "count" }, { x: x })), + ], + document, + }); +} + +export function oneBoxPlot( + data: any[], + y = "column", + opts = { options: null, box: null }, +) { + return Plot.plot({ + ...(opts.options ?? defaultPlotSettings), + y: { grid: true }, + marks: [ + Plot.ruleY([0]), + Plot.boxY(data, { y, ...(opts.box ?? {}) }), + ], + document, + }); +} + +export async function quantilePlotSVG( + data: any[], + x = "column", + opts = { width: 500 }, +) { + const spec = { + data: { values: data }, + width: opts.width, + "transform": [ + { + "quantile": "price", + "as": ["prob", "value"], + }, + { + "calculate": "quantileNormal(datum.prob)", + "as": "norm", + }, + ], + "layer": [ + { + "mark": { type: "circle", size: 80 }, + "encoding": { + "x": { + "field": "norm", + "type": "quantitative", + "title": "Theoretical Quantiles→", + }, + "y": { + "field": "value", + "type": "quantitative", + "title": "Ordered Values→", + }, + }, + }, + { + mark: { type: "line", color: "red" }, + transform: [ + { + "regression": "value", + "on": "norm", + }, + ], + "encoding": { + "x": { "field": "norm", "type": "quantitative" }, + "y": { "field": "value", "type": "quantitative" }, + }, + }, + ], + }; + let vegaspec = vegalite.compile(spec).spec; + var view = new vega.View(vega.parse(vegaspec), { renderer: "none" }); + + return await view.toSVG(); +} + +export function quantilePlot(data: any[], x = "column", opts = { width: 500 }) { + return { + [Symbol.for("Jupyter.display")]: async () => ({ + "text/html": await quantilePlotSVG(data, x, opts), + }), + }; +} + +const svgDataUrl = (plt) => + `data:image/svg+xml;base64,${btoa(unescape(encodeURIComponent(plt)))}`; + +export function threeChart(data: any[], x = "column", opts = { width: 800 }) { + const hist = histPlot(data, x); + hist.setAttribute("xmlns", "http://www.w3.org/2000/svg"); + const box = oneBoxPlot(data, x); + box.setAttribute("xmlns", "http://www.w3.org/2000/svg"); + const qq = quantilePlotSVG(data, x); + return { + [Symbol.for("Jupyter.display")]: async () => ({ + "text/html": ` +
+
+ + +
+
+ +
+
+ `, + }), + }; +}