npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@naviary-sanctuary/kinship-graph

v0.3.1

Published

A deterministic D3 pedigree graph renderer for kinship schema v1.

Readme

kinship-graph

example

Framework-independent D3 renderer for the graph produced by @naviary-sanctuary/kinship.

kinship owns pedigree topology. kinship-graph turns that topology into a couple-first, generation-based SVG family tree for browsers or a deterministic layout for server-side work.

Features

  • Partners stay on the same visual generation, even when their source lineageDepth values differ, in the default pedigree layout.
  • An optional generation-row layout aligns all components by source lineage depth and groups founders under presentation-only origin headers.
  • A family is drawn as one familiar connector: couple line, downward trunk, sibling bus, and child stems.
  • focusId and depth project a smaller family view without changing source data.
  • Missing images can use a configurable defaultImage.
  • D3 pan and zoom, keyboard navigation, accessible names, and responsive fit are included.
  • Nodes, family markers, and lines can be styled with CSS variables or DOM callbacks.
  • The layout entrypoint has no DOM dependency and works in Node.js.

Install

This README documents the kinship-graph 0.2.0 API, which consumes the schema version 1 produced by kinship 0.2.x. The legacy kinship 0.1.x API is incompatible with these examples.

Applications that receive schema version 1 JSON from a backend need only the renderer:

npm install @naviary-sanctuary/kinship-graph@^0.2.0

d3 is a runtime dependency and is installed automatically with the renderer. Applications upgrading from 0.1.0 can remove their direct d3 dependency if they do not use D3 themselves. The package is ESM-only, uses import exports, and supports Node.js 20.19 or newer. Browser rendering requires a DOM; server code should use the /layout entrypoint.

Install kinship separately only when the same runtime also builds topology:

npm install @naviary-sanctuary/kinship@^0.2.0

kinship is not a runtime or peer dependency of kinship-graph. A normal frontend receives result.graph from its backend rather than bundling the topology engine.

End-to-end quick start

This same-runtime example shows the complete topology-to-renderer handoff:

import { Kinship } from '@naviary-sanctuary/kinship';
import { KinshipGraph } from '@naviary-sanctuary/kinship-graph';

const result = new Kinship({
  parrots: [
    { id: 'father', gender: 'male' },
    { id: 'mother', gender: 'female' },
    {
      id: 'child',
      fatherId: 'father',
      motherId: 'mother',
      gender: 'unknown',
    },
  ],
  pairs: [
    {
      id: 'pair-1',
      maleId: 'father',
      femaleId: 'mother',
    },
  ],
}).build();

if (!result.ok) {
  // Map the structured issues to the application's validation contract.
  throw new Error('Pedigree input is invalid.');
}

const graph = new KinshipGraph('#pedigree', {
  height: 680,
  defaultImage: '/images/default-parrot.svg',
  onNodeActivate: ({ layoutNode }) => {
    console.log(layoutNode.node.individualId);
  },
});

graph.render({
  graph: result.graph,
  individuals: [
    { id: 'father', name: 'Atlas', image: '/images/atlas.webp' },
    { id: 'mother', name: 'Iris', image: '/images/iris.webp' },
    { id: 'child', name: 'Nova' },
  ],
});

Successful builds may still include warnings. Preserve result.issues in the API response or application state and handle them separately; kinship-graph accepts only result.graph and neither displays nor discards build issues.

The presentation records are joined by individualId. They may contain any additional application fields.

Input contract

The renderer accepts the JSON-safe schema version 1 emitted by kinship:

interface KinshipGraphRenderInput<TIndividual> {
  readonly graph: KinshipGraphData;
  readonly individuals?: readonly TIndividual[];
  readonly focusId?: string;
  readonly depth?: number;
  readonly includePartners?: boolean;
  readonly includeSiblings?: boolean;
}

Opaque graph node IDs are never parsed. focusId and presentation record IDs always use the domain individualId.

In the default pedigree layout, lineageDepth is validated as source topology metadata but is deliberately not used as a screen row. An external partner may have depth 0 while their partner has depth 3; the renderer still places the couple together.

Focus and depth

| Input | Result | | ------------------------- | ---------------------------------------------- | | No focusId | All graph components | | focusId only | The focus person's complete component | | focusId, depth: 0 | Focus and enabled partners and siblings | | focusId, depth: n | Relatives within n hops plus enabled context | | depth without focusId | Throws DEPTH_REQUIRES_FOCUS_ID |

Family junction nodes do not consume depth. Partners are same-generation context and do not recursively pull in their other relatives. Siblings are included only when their shared parent connector is visible.

includePartners and includeSiblings default to true for a depth-limited view. Both options require focusId and depth; supplying either option without depth throws INVALID_OPTIONS.

Layout model

The default layoutMode: 'pedigree' normalizes the graph into people and families.

  1. Every visible partner relationship forms a same-generation unit.
  2. Parent-family to child-unit relationships determine visual generation.
  3. Units are ordered to keep partners adjacent and siblings together.
  4. A family junction is placed between its partners.
  5. Children share one orthogonal trunk and sibling bus.

One biological individual normally has one layout occurrence. When a third partner, a partner cycle, a cross-generation relationship, or pedigree collapse makes one position geometrically impossible, the layout emits another occurrence with the same source node and a distinct occurrenceId. This keeps every couple adjacent and every biological child below its family without inventing a new individual. In a depth-limited view, generationOffset belongs to the occurrence so repeated appearances can accurately occupy different generations.

The default layout never routes ordinary relationships around the outside edge of the canvas and does not use background-colored line halos.

Generation rows and founder origins

Use layoutMode: 'generations' to put each individual on exactly one row, numbered lineageDepth + 1, across all connected and disconnected families. Each connected family occupies its own horizontal block, with couples and siblings kept together. Biological connectors stay within each family block. Each origin appears once in a shared row above the families and branches to its founders, including founders in different family blocks. Founders remain in generation 1 even when paired with a later generation. Cross-generation partner connections use outer routing lanes so they do not pass through intervening portraits. Focus and depth still select biological relatives; rows retain their absolute generation numbers.

const renderer = new KinshipGraph('#pedigree', {
  layoutMode: 'generations',
  getGenerationLabel: (generation) => `${generation}세대`,
});

renderer.render({
  graph: result.graph,
  founderGroups: [
    { id: 'origin-aa', label: 'aa', individualIds: ['A', 'B'] },
    { id: 'origin-bb', label: 'bb', individualIds: ['C', 'D', 'E'] },
  ],
});

founderGroups is display metadata for this layout mode. Supply only resolved individuals without a parent family in the full source graph. A parent outside the focused view, or a referenced parent without a record, does not make a child a founder. Group IDs and member IDs must be unique; the layout rejects unknown members and non-founders. Group order and member order are deterministic. Visible layout origins retain the supplied group IDs, regardless of how many family blocks contain their founders.

Origin headers and dashed connectors never become pedigree nodes or edges and never imply shared ancestry. Applications decide how to obtain and label origin metadata, including missing origins. getLayout() exposes founderGroups and generationRows in this mode; visible headers and origin connectors count toward the rendering safety limits.

Browser API

new KinshipGraph(container, options?)

container is an HTMLElement or selector for an existing HTML element.

render(input)

Validates, lays out, and renders the input. Returns the serializable KinshipGraphLayout.

getLayout()

Returns the last layout or null before the first render.

fit(options?)

fit() fits the entire pedigree inside the viewport, respecting minZoom. Use fit({ minScale: 0.8 }) for a readable starting view without restricting later manual zooming. minScale must be finite and within minZoom and maxZoom; the last fit choice is preserved when the viewport resizes. With scroll: true, a graph larger than the viewport starts at the top left, or centers the focused individual.

For example, set minZoom: 0.0001 and initialFit: false, then call fit({ minScale: 0.8 }) after rendering. The user can zoom out freely or call fit() to see the whole graph.

zoomBy(factor)

Changes the current scale by a positive factor. It validates the factor and does nothing when the renderer was created with zoom: false.

destroy()

Disconnects observers and event listeners and removes only renderer-owned DOM. The instance cannot be reused.

Options

Layout

| Option | Default | Meaning | | ----------------- | ---------: | ------------------------------------------- | | layoutMode | pedigree | Default couple layout or generations rows | | nodeWidth | 112 | Horizontal node footprint | | nodeHeight | 144 | Vertical node footprint | | horizontalGap | 52 | Space between people and family units | | generationGap | 60 | Space between generation footprints | | componentGap | 96 | Space between disconnected components | | padding | 48 | Layout padding | | maxVisibleNodes | 2000 | Projected node safety limit | | maxVisibleEdges | 6000 | Projected edge safety limit |

The effective horizontalGap is never less than 12 pixels. Generation rows expand beyond generationGap when connector lanes need more clearance to stay outside person footprints.

Browser

| Option | Default | Meaning | | -------------- | -----------------------: | ------------------------------------------------- | | height | 640 | SVG viewport height | | minZoom | 0.2 | Minimum scale | | maxZoom | 4 | Maximum scale | | zoom | true | Enable interaction and zoomBy() | | scroll | false | Native scrollbars and wheel/trackpad panning | | initialFit | true | Fit after rendering and container resize | | ariaLabel | Kinship pedigree graph | Accessible graph name | | defaultImage | null | Image used when a record image is absent or fails |

onNodeActivate runs for click, Enter, or Space activation. onNodeFocus runs whenever browser focus enters a person node; both callbacks receive the layout node, presentation record, and originating event. getGenerationLabel(generation) customizes generation-row labels, starting at 1; the default is Generation 1, Generation 2, and so on. All getter, render, and interaction callbacks are synchronous; returned promises are ignored.

With scroll: true, normal wheel and trackpad gestures move through the graph; Shift + wheel moves horizontally. Native scrollbars stay in sync with dragging and zooming, and keyboard navigation reveals the focused individual. Without this option, normal wheel scrolling is left to the page. Use pinch or Ctrl/Command + wheel to zoom the graph.

Presentation getters

import { KinshipGraph, type KinshipGraphIndividual } from '@naviary-sanctuary/kinship-graph';

interface MyIndividual extends KinshipGraphIndividual {
  readonly displayName?: string;
  readonly bandNumber?: string;
  readonly portraitUrl?: string;
}

const graph = new KinshipGraph<MyIndividual>('#pedigree', {
  getLabel: (individual, node) => individual?.displayName ?? node.individualId,
  getDescription: (individual) => individual?.bandNumber,
  getImage: (individual) => individual?.portraitUrl,
  getAriaLabel: (individual, layoutNode) =>
    `${individual?.displayName ?? layoutNode.node.individualId}, ${layoutNode.relation}`,
});

getLabel falls back to the presentation record and then the individual ID when it returns a blank or non-string value. Returning undefined from getDescription or getImage delegates to the presentation record. A blank or non-string description clears the description; a blank or non-string image uses defaultImage. Invalid getAriaLabel results use the generated accessible label.

DOM customization

Callbacks run after the default SVG content is updated:

import { KinshipGraph, type KinshipGraphIndividual } from '@naviary-sanctuary/kinship-graph';

interface TaggedIndividual extends KinshipGraphIndividual {
  readonly tags?: readonly string[];
}

const graph = new KinshipGraph<TaggedIndividual>('#pedigree', {
  renderNode: ({ element, layoutNode, individual }) => {
    element.classList.toggle('is-founder', individual?.tags?.includes('founder') ?? false);
    element.dataset.appRelation = layoutNode.relation;
  },
  renderFamily: ({ element, layoutFamily }) => {
    element.dataset.origin = layoutFamily.node.origin;
  },
  renderEdge: ({ element, layoutEdge }) => {
    element.style.strokeWidth = layoutEdge.edge.kind === 'partner' ? '3' : '2.5';
  },
});

These callbacks receive live renderer-owned elements. Treat them as trusted application code and remove only content that your callback owns.

CSS custom properties

Set variables on the graph container:

.pedigree {
  --kinship-graph-background: #e7f1ed;
  --kinship-graph-line: #3e7779;
  --kinship-graph-partner-line: var(--kinship-graph-line);
  --kinship-graph-child-line: var(--kinship-graph-line);
  --kinship-graph-focus: #bf5134;
  --kinship-graph-node-fill: #fffdfa;
  --kinship-graph-node-stroke: #ffffff;
  --kinship-graph-male: #dcecf5;
  --kinship-graph-female: #f2e1ec;
  --kinship-graph-unknown: #e1ebe7;
  --kinship-graph-text: #17342d;
  --kinship-graph-muted-text: #61736d;
  --kinship-graph-generation-line: #dce6e2;
  --kinship-graph-component-fill: #f8faf9;
  --kinship-graph-component-stroke: #e4ebe7;
  --kinship-graph-origin-fill: #eaf4ef;
  --kinship-graph-origin-stroke: #9fb9ad;
  --kinship-graph-origin-text: #245844;
  --kinship-graph-origin-line: #9fb9ad;
  --kinship-graph-font: ui-sans-serif, system-ui, sans-serif;
}

Stable class names include:

  • .kinship-graph
  • .kinship-graph__viewport
  • .kinship-graph__edge
  • .kinship-graph__edge--partner
  • .kinship-graph__edge--child
  • .kinship-graph__family
  • .kinship-graph__node
  • .kinship-graph__avatar
  • .kinship-graph__node-label
  • .kinship-graph__node-description
  • .kinship-graph__generation
  • .kinship-graph__component
  • .kinship-graph__origin
  • .kinship-graph__origin-connector

React

Create one renderer for the mounted container, render when data changes, and destroy it during effect cleanup:

import { useEffect, useRef } from 'react';
import { KinshipGraph, type KinshipGraphData, type KinshipGraphIndividual } from '@naviary-sanctuary/kinship-graph';

interface PedigreeProps {
  readonly graph: KinshipGraphData;
  readonly individuals: readonly KinshipGraphIndividual[];
  readonly focusId?: string;
}

export function Pedigree({ graph, individuals, focusId }: PedigreeProps) {
  const containerRef = useRef<HTMLDivElement>(null);
  const rendererRef = useRef<KinshipGraph<KinshipGraphIndividual> | null>(null);

  useEffect(() => {
    if (!containerRef.current) return;

    const renderer = new KinshipGraph<KinshipGraphIndividual>(containerRef.current, {
      defaultImage: '/default-parrot.svg',
    });
    rendererRef.current = renderer;

    return () => {
      renderer.destroy();
      rendererRef.current = null;
    };
  }, []);

  useEffect(() => {
    rendererRef.current?.render({
      graph,
      individuals,
      ...(focusId ? { focusId, depth: 3 } : {}),
    });
  }, [graph, individuals, focusId]);

  return <div ref={containerRef} />;
}

Static layout and SSR

Use the DOM-free entrypoint:

import { KinshipGraphLayoutEngine } from '@naviary-sanctuary/kinship-graph/layout';

const layout = new KinshipGraphLayoutEngine().layout({
  graph,
  individuals,
  focusId: 'NAV-001',
  depth: 3,
});

KinshipGraph.layout(input, options) provides the same calculation from the main browser entrypoint.

Accessibility

  • The SVG has a configurable accessible name and an updated structural description.
  • Every person is keyboard focusable through a roving tab stop.
  • Arrow keys move spatially; Home and End move to the first and last person.
  • Enter and Space call onNodeActivate.
  • Focus is shown with a high-contrast ring, not color alone.
  • Default rendering uses SVG text nodes and never injects presentation strings as HTML.

Errors and limits

Expected failures throw KinshipGraphError with a stable code and structured details. Important codes include:

  • DEPTH_REQUIRES_FOCUS_ID
  • FOCUS_NOT_FOUND
  • INVALID_GRAPH
  • INVALID_EDGE
  • INVALID_COMPONENT
  • INVALID_OPTIONS
  • VIEW_LIMIT_EXCEEDED
  • DESTROYED

Project large datasets with focusId and depth, and set explicit safety limits appropriate for the host application.

Development

bun install
bun run check
bunx playwright install chromium
bun run check:full

See CONTRIBUTING.md for the complete contributor workflow.

License

MIT