<bmx-lineage-graph>
Data lineage you can explore, for data catalogues, data platforms and governance tools: where a figure comes from, what a change would break, and where personal data ends up.
14 properties · 2 events · 11 methods · 5 parts
Example
The data platform of a fictional shop: sources on the left, dbt models in the middle, dashboards, a churn model and a marketing audience on the right. Choose an asset, then a column to trace it; PII is highlighted wherever it lands.
Show markup
<bmx-lineage-graph id="ex-lg" highlight="PII" style="--bmx-lineage-graph-height: 32rem"></bmx-lineage-graph>
<p class="note" id="ex-lg-out" role="status" style="margin: 12px 0 0">The data platform of a fictional shop: sources on the left, dbt models in the middle, dashboards, a churn model and a marketing audience on the right. Choose an asset, then a column to trace it; PII is highlighted wherever it lands.</p>
<script type="module">
await customElements.whenDefined('bmx-lineage-graph');
const lg = document.getElementById('ex-lg');
const out = document.getElementById('ex-lg-out');
// Harbour & Pine's (fictional) data platform in the element's own { nodes, edges } shape; it reads
// OpenLineage events and dbt manifests just as well.
const col = (name, type, extra = {}) => ({ name, type, ...extra });
const pii = { classification: 'PII' };
const map = (to, from, transform) => ({ to, from: Array.isArray(from) ? from : [from], ...(transform ? { transform } : {}) });
lg.data = {
nodes: [
{ id: 'shop.customers', name: 'customers', kind: 'source', system: 'Postgres', namespace: 'shop.public', owner: 'Web team', columns: [col('id', 'bigint'), col('email', 'text', pii), col('full_name', 'text', pii), col('phone', 'text', pii), col('country', 'char(2)'), col('created_at', 'timestamptz')] },
{ id: 'shop.orders', name: 'orders', kind: 'source', system: 'Postgres', namespace: 'shop.public', owner: 'Web team', columns: [col('id', 'bigint'), col('customer_id', 'bigint'), col('placed_at', 'timestamptz'), col('status', 'text'), col('total_pence', 'integer')] },
{ id: 'shop.order_lines', name: 'order_lines', kind: 'source', system: 'Postgres', namespace: 'shop.public', owner: 'Web team', columns: [col('order_id', 'bigint'), col('sku', 'text'), col('quantity', 'integer'), col('price_pence', 'integer')] },
{ id: 'stripe.charges', name: 'charges', kind: 'api', system: 'Stripe', owner: 'Payments', columns: [col('id', 'text'), col('order_ref', 'text'), col('amount', 'integer'), col('card_last4', 'text', { classification: 'Confidential' }), col('status', 'text')] },
{ id: 'crm.contacts', name: 'contacts', kind: 'source', system: 'Salesforce', owner: 'Sales ops', status: 'stale', updated: '2026-10-11T02:00:00Z', columns: [col('email', 'text', pii), col('segment', 'text'), col('opt_in', 'boolean')] },
{ id: 'stg.customers', name: 'stg_customers', kind: 'view', system: 'dbt', namespace: 'analytics.staging', owner: 'Data engineering', columns: [col('customer_id', 'bigint'), col('email', 'text'), col('name', 'text'), col('country', 'text'), col('signed_up_at', 'timestamp')] },
{ id: 'stg.orders', name: 'stg_orders', kind: 'view', system: 'dbt', namespace: 'analytics.staging', owner: 'Data engineering', columns: [col('order_id', 'bigint'), col('customer_id', 'bigint'), col('ordered_at', 'timestamp'), col('status', 'text'), col('amount_gbp', 'numeric')] },
{ id: 'stg.payments', name: 'stg_payments', kind: 'view', system: 'dbt', namespace: 'analytics.staging', owner: 'Data engineering', columns: [col('payment_id', 'text'), col('order_id', 'bigint'), col('amount_gbp', 'numeric'), col('succeeded', 'boolean')] },
{ id: 'int.order_items', name: 'int_order_items', kind: 'model', system: 'dbt', namespace: 'analytics.intermediate', owner: 'Data engineering', columns: [col('order_id', 'bigint'), col('items', 'integer'), col('basket_gbp', 'numeric')] },
{ id: 'mart.fct_orders', name: 'fct_orders', kind: 'model', system: 'dbt', namespace: 'analytics.marts', owner: 'Analytics', tags: ['finance', 'certified'], description: 'One row per order, with what was paid and what was in the basket.', columns: [col('order_id', 'bigint'), col('customer_id', 'bigint'), col('ordered_at', 'timestamp'), col('items', 'integer'), col('revenue_gbp', 'numeric'), col('paid', 'boolean')] },
{ id: 'mart.dim_customers', name: 'dim_customers', kind: 'model', system: 'dbt', namespace: 'analytics.marts', owner: 'Analytics', tags: ['certified'], columns: [col('customer_id', 'bigint'), col('email', 'text'), col('name', 'text'), col('country', 'text'), col('segment', 'text'), col('marketing_opt_in', 'boolean')] },
{ id: 'mart.customer_ltv', name: 'customer_ltv', kind: 'model', system: 'dbt', namespace: 'analytics.marts', owner: 'Analytics', status: 'failed', updated: '2026-10-14T03:10:00Z', columns: [col('customer_id', 'bigint'), col('orders', 'integer'), col('lifetime_gbp', 'numeric'), col('first_order_at', 'timestamp')] },
{ id: 'bi.revenue', name: 'Revenue dashboard', kind: 'dashboard', system: 'Tableau', owner: 'Finance', url: 'https://example.com/tableau/revenue', columns: [col('day'), col('revenue'), col('orders'), col('country'), col('margin')] },
{ id: 'bi.customer360', name: 'Customer 360', kind: 'dashboard', system: 'Looker', owner: 'Customer service', columns: [col('customer'), col('email'), col('lifetime_value'), col('segment')] },
{ id: 'ml.churn', name: 'churn_features', kind: 'feature', system: 'Feast', owner: 'Data science', columns: [col('customer_id'), col('days_since_order'), col('lifetime_gbp'), col('orders_90d')] },
{ id: 'braze.audience', name: 'Win-back audience', kind: 'application', system: 'Braze', owner: 'Marketing', columns: [col('external_id'), col('email'), col('first_name')] },
],
edges: [
{ from: 'shop.customers', to: 'stg.customers', via: 'dbt run', columns: [map('customer_id', 'id'), map('email', 'email', 'lower(trim(email))'), map('name', 'full_name'), map('country', 'country'), map('signed_up_at', 'created_at')] },
{ from: 'shop.orders', to: 'stg.orders', via: 'dbt run', columns: [map('order_id', 'id'), map('customer_id', 'customer_id'), map('ordered_at', 'placed_at'), map('status', 'status'), map('amount_gbp', 'total_pence', 'total_pence / 100.0')] },
{ from: 'stripe.charges', to: 'stg.payments', via: 'Fivetran + dbt', columns: [map('payment_id', 'id'), map('order_id', 'order_ref', 'cast(order_ref as bigint)'), map('amount_gbp', 'amount', 'amount / 100.0'), map('succeeded', 'status', "status = 'succeeded'")] },
{ from: 'shop.order_lines', to: 'int.order_items', via: 'dbt run', columns: [map('order_id', 'order_id'), map('items', 'quantity', 'SUM'), map('basket_gbp', ['quantity', 'price_pence'], 'SUM(quantity * price_pence) / 100.0')] },
{ from: 'stg.orders', to: 'mart.fct_orders', via: 'dbt run', columns: [map('order_id', 'order_id'), map('customer_id', 'customer_id'), map('ordered_at', 'ordered_at')] },
{ from: 'stg.payments', to: 'mart.fct_orders', via: 'dbt run', columns: [map('revenue_gbp', 'amount_gbp', 'SUM where succeeded'), map('paid', 'succeeded', 'bool_or')] },
{ from: 'int.order_items', to: 'mart.fct_orders', via: 'dbt run', columns: [map('items', 'items')] },
{ from: 'stg.customers', to: 'mart.dim_customers', via: 'dbt run', columns: [map('customer_id', 'customer_id'), map('email', 'email'), map('name', 'name'), map('country', 'country')] },
{ from: 'crm.contacts', to: 'mart.dim_customers', via: 'dbt run', columns: [map('segment', 'segment'), map('marketing_opt_in', 'opt_in')] },
{ from: 'mart.fct_orders', to: 'mart.customer_ltv', via: 'dbt run', columns: [map('customer_id', 'customer_id'), map('orders', 'order_id', 'COUNT'), map('lifetime_gbp', 'revenue_gbp', 'SUM'), map('first_order_at', 'ordered_at', 'MIN')] },
{ from: 'mart.fct_orders', to: 'bi.revenue', via: 'Tableau extract', columns: [map('day', 'ordered_at', 'DATE'), map('revenue', 'revenue_gbp', 'SUM'), map('orders', 'order_id', 'COUNT')] },
{ from: 'mart.dim_customers', to: 'bi.revenue', via: 'Tableau extract', columns: [map('country', 'country')] },
{ from: 'mart.dim_customers', to: 'bi.customer360', via: 'Looker', columns: [map('customer', 'name'), map('email', 'email'), map('segment', 'segment')] },
{ from: 'mart.customer_ltv', to: 'bi.customer360', via: 'Looker', columns: [map('lifetime_value', 'lifetime_gbp')] },
{ from: 'mart.customer_ltv', to: 'ml.churn', via: 'feature pipeline', columns: [map('customer_id', 'customer_id'), map('lifetime_gbp', 'lifetime_gbp'), map('orders_90d', 'orders', 'windowed COUNT')] },
{ from: 'mart.fct_orders', to: 'ml.churn', via: 'feature pipeline', columns: [map('days_since_order', 'ordered_at', 'now() - MAX')] },
{ from: 'mart.dim_customers', to: 'braze.audience', via: 'Census sync', columns: [map('external_id', 'customer_id'), map('email', 'email'), map('first_name', 'name', 'split_part(name, \' \', 1)')] },
{ from: 'ml.churn', to: 'braze.audience', via: 'Census sync' },
],
};
lg.addEventListener('bmxSelect', async e => {
const { node, column, asset } = e.detail;
const impact = await lg.getImpact(node, column);
out.textContent = `bmxSelect: ${asset.name}${column ? `.${column}` : ''} - a change here reaches ${impact.items.length} assets owned by ${impact.owners.length} teams.`;
});
</script>
- assets (sources, tables, models, dashboards, ML features, applications)
as boxes from left to right, each with its columns;
- choose an asset to light up everything upstream and downstream of it; choose a column to trace it, column by column, both ways;
- focus on one asset and show only a number of hops either side;
- impact analysis: everything downstream of a change, the columns affected and the owners to tell, as a report and CSV;
- classifications (PII, Confidential...) carried downstream through every column built from a classified one, and highlighted where they land;
- search across assets, columns, owners and tags; checks for cycles and gaps in the lineage itself; SVG, CSV and JSON out.
<bmx-lineage-graph id="lg"></bmx-lineage-graph>
<script>
lg.data = await (await fetch('target/manifest.json')).json(); // or OpenLineage events, or { nodes, edges }
</script>
It reads OpenLineage run events (with the column-lineage facet), a dbt
manifest.json, or its own { nodes, edges } JSON. Everything runs in the
page; the engine is also in dist/node/bmx-lineage-graph.mjs.
Keyboard: each asset's name is a button (one tab stop for the graph): the arrow keys move to the nearest asset in that direction, Enter chooses it, Enter again goes into its columns (Up and Down move, Enter traces, Escape goes back). Every relationship drawn is also listed as text in the details panel and the Impact and Assets views.
Properties
| Property | Attribute | Type | Default | Description |
|---|---|---|---|---|
allowOpen |
allow-open |
boolean |
true |
Offer the "Open lineage" button. |
data |
property only | unknown |
'' |
The lineage: OpenLineage events (an array, { events } or JSON lines), a dbt manifest, or { nodes, edges } - text or parsed. Mutable (opening a file sets it). |
downstreamDepth |
downstream-depth |
number |
2 |
Hops shown downstream of the focus (0 = all). Mutable. |
fileName |
file-name |
string |
'lineage' |
The name downloads are given (without the extension). |
focusAsset |
focus-asset |
string |
'' |
Show only this asset and its neighbourhood (upstream-depth, downstream-depth). Mutable. |
highlight |
highlight |
string |
'' |
A classification to highlight wherever it lands ("PII"); empty for none. Mutable. |
label |
label |
string |
— | The element's accessible name. Default: "Lineage graph". |
maxColumns |
max-columns |
number |
8 |
At most this many columns a box shows before "+n more". |
selected |
selected |
string |
'' |
The chosen asset's id. Mutable. |
selectedColumn |
selected-column |
string |
'' |
The chosen column of the chosen asset (traced both ways). Mutable. |
showColumns |
show-columns |
boolean |
true |
Show columns in the boxes. Mutable. |
strings |
strings |
Partial<BmxLineageGraphStrings> | string |
— | Words to show instead of the English ones, as an object or JSON. |
upstreamDepth |
upstream-depth |
number |
2 |
Hops shown upstream of the focus (0 = all). Mutable. |
view |
view |
BmxLineageView |
'graph' |
The view shown. Mutable. |
Events
| Event | Detail | Description |
|---|---|---|
bmxLoad |
BmxLineageLoadDetail |
Fired when lineage has been read. |
bmxSelect |
BmxLineageSelectDetail |
Fired when an asset or a column is chosen. |
Methods
| Method | Signature | Description |
|---|---|---|
download |
download(format?: BmxLineageExportFormat, fileName?: string) => Promise<void> |
Saves an export as a file. |
exportCsv |
exportCsv(kind?: "lineage" | "impact") => Promise<string> |
Column lineage as CSV (lineage), or the impact report of the chosen asset or column (impact). |
exportSvg |
exportSvg() => Promise<string> |
The graph as shown, as a stand-alone SVG picture. |
fit |
fit() => Promise<void> |
Fits the graph to the view. |
focusOn |
focusOn(node?: string, upstream?: number, downstream?: number) => Promise<void> |
Shows only an asset and the hops either side (0 = all); no id shows everything. |
getGraph |
getGraph() => Promise<BmxLineageData> |
The lineage as read (nodes and merged edges). |
getImpact |
getImpact(node?: string, column?: string) => Promise<BmxImpact | undefined> |
What a change to an asset, or one column of it, affects downstream (default the chosen one). |
getProblems |
getProblems() => Promise<BmxLineageProblem[]> |
Problems in the lineage itself: cycles, missing or unexplained columns, isolated assets. |
search |
search(query: string) => Promise<BmxLineageMatch[]> |
Assets and columns matching the text. |
selectAsset |
selectAsset(node: string, column?: string) => Promise<void> |
Chooses an asset (and a column of it, to trace) and brings it into view. |
traceColumn |
traceColumn(node: string, column: string, direction?: "up" | "down") => Promise<{ column: string; hops: number; }[]> |
The columns a column is made from (up) or feeds (down), as node::column with hops. |
CSS shadow parts
| Part | Description |
|---|---|
asset |
An asset's box; also asset-selected. |
details |
The details panel. |
table |
A table (impact, assets). |
tabs |
The row of views. |
viewport |
The area the graph is drawn in. |
CSS custom properties
| Property | Description |
|---|---|
--bmx-lineage-graph-background |
The graph's background. |
--bmx-lineage-graph-details-width |
The details panel's width. |
--bmx-lineage-graph-downstream |
The colour of what it feeds. |
--bmx-lineage-graph-height |
The graph's height. |
--bmx-lineage-graph-radius |
Corner radius of the element. |
--bmx-lineage-graph-upstream |
The colour of what the chosen asset comes from. |