|
| 1 | +# SQL summary table ([#23](https://github.com/observablehq/framework/issues/23)) |
| 2 | + |
| 3 | +```sql echo |
| 4 | +SELECT * FROM aapl; |
| 5 | +``` |
| 6 | + |
| 7 | +```sql echo |
| 8 | +SELECT * FROM gaia; |
| 9 | +``` |
| 10 | + |
| 11 | +```sql echo |
| 12 | +SELECT * FROM penguins; |
| 13 | +``` |
| 14 | + |
| 15 | +```js echo |
| 16 | +const sql = DuckDBClient.sql({aapl, penguins, gaia: FileAttachment("/lib/gaia-sample.parquet")}); |
| 17 | +``` |
| 18 | + |
| 19 | +```js echo |
| 20 | +import * as _Inputs from "npm:@observablehq/inputs" |
| 21 | +import * as Arrow from "npm:apache-arrow"; |
| 22 | +import * as d3 from "npm:d3"; |
| 23 | +import {html} from "npm:htl"; |
| 24 | + |
| 25 | +width; // refresh when resized |
| 26 | + |
| 27 | +const Inputs = ({..._Inputs, table}) |
| 28 | + |
| 29 | +function table(data, options = {}) { |
| 30 | + if (!data) return data; |
| 31 | + |
| 32 | + const container = document.createElement("div"); |
| 33 | + container.append(_Inputs.table(data, options)); |
| 34 | + |
| 35 | + // Duck typing Arrow table |
| 36 | + if (!Array.isArray(data?.schema?.fields)) return container; |
| 37 | + |
| 38 | + // Get the fields as described by Arrow, in the order given (potentially) by the options. |
| 39 | + const fields = (options.columns?.map(k => data.schema.find(({name}) => name === k)) ?? data.schema.fields).map(({name, type}) => ({name: String(name), type: String(type)})); |
| 40 | + |
| 41 | + const th = d3.select(container).select("thead").selectAll("th").data([{}, ...fields]); |
| 42 | + th.append("div").classed("type", true).html(({type}) => type); |
| 43 | + const summaries = th.append("div").classed("summary", true); |
| 44 | + const footer = html`<footer style="width: 100%; height: 1em;"> |
| 45 | + <div style="position: absolute; left: 0;"><!-- <input type="search" placeholder="Search text fields"> --></div> |
| 46 | + <div style="position: absolute; right: 0;">${data.numRows.toLocaleString("en-US")} rows</div> |
| 47 | + </footer>`; |
| 48 | + container.appendChild(footer); |
| 49 | + |
| 50 | + requestAnimationFrame(() => summaries |
| 51 | + .filter(({type}) => type) |
| 52 | + .append(function({name, type}) { |
| 53 | + return summary(data.getChild(name), type, this.getBoundingClientRect()); |
| 54 | + }) |
| 55 | + ); |
| 56 | + return container; |
| 57 | +} |
| 58 | + |
| 59 | +function summary(values, type, {width = 80, height = 33}) { |
| 60 | + let chart; |
| 61 | + if (type.startsWith("Float") || type.startsWith("Date")) { |
| 62 | + chart = Plot.plot({ |
| 63 | + width, |
| 64 | + height, |
| 65 | + style: "overflow: visible;", |
| 66 | + x: {round: true}, |
| 67 | + y: {axis: null}, |
| 68 | + marginLeft: 2, |
| 69 | + marginRight: 2, |
| 70 | + marginTop: 0, |
| 71 | + marginBottom: 13, |
| 72 | + marks: [ |
| 73 | + Plot.rectY(values, Plot.binX(undefined, {fill: "var(--theme-foreground-focus)", inset: -.3})), |
| 74 | + Plot.axisX({tickSpacing: 41, tickSize: 3, tickPadding: 2, fontSize: 8}), |
| 75 | + ] |
| 76 | + }); |
| 77 | + |
| 78 | + // restore insets where possible |
| 79 | + const rects = chart.querySelectorAll("rect"); |
| 80 | + if (rects.length < 100) { |
| 81 | + for (const rect of rects) { |
| 82 | + rect.setAttribute("x", Math.floor(rect.getAttribute("x"))); |
| 83 | + rect.setAttribute("width", Math.max(1, Math.floor(rect.getAttribute("width")) - 1)); |
| 84 | + } |
| 85 | + } |
| 86 | + } |
| 87 | + else if (type === "Utf8") { |
| 88 | + chart = Plot.plot({ |
| 89 | + width, |
| 90 | + height, |
| 91 | + style: "overflow: visible;", |
| 92 | + x: {axis: null}, |
| 93 | + y: {axis: null}, |
| 94 | + marginLeft: 2, |
| 95 | + marginRight: 2, |
| 96 | + marginTop: 0, |
| 97 | + marginBottom: 13, |
| 98 | + marks: [ |
| 99 | + Plot.barX(values, Plot.groupZ({x: "count"}, {z: Plot.identity, insetRight: 1, fill: "var(--theme-foreground-focus)"})), |
| 100 | + Plot.text(values, Plot.stackX(Plot.groupZ({x: "count", text: "first"}, {z: Plot.identity, fill: "var(--plot-background)"}))), |
| 101 | + ] |
| 102 | + }); |
| 103 | + } |
| 104 | + return chart ?? html`<span>Unknown type ${type}`; |
| 105 | +} |
| 106 | +``` |
| 107 | +
|
| 108 | +<style> |
| 109 | +
|
| 110 | + table .type {font-size: smaller; font-weight: normal; height: 1.35em;} |
| 111 | + table .summary {font-size: smaller; font-weight: normal; height: 33px;} |
| 112 | + footer {font-family: var(--sans-serif); font-size: small; color: var(--theme-foreground-faint)} |
| 113 | +
|
| 114 | +</style> |
0 commit comments