v2.6.0

<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

PropertyAttributeTypeDefaultDescription
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

EventDetailDescription
bmxLoad BmxLineageLoadDetail Fired when lineage has been read.
bmxSelect BmxLineageSelectDetail Fired when an asset or a column is chosen.

Methods

MethodSignatureDescription
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

PartDescription
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

PropertyDescription
--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.