For the maps engineer

KG to Website: Frontend Integration Guide

Audience: the engineer wiring the knowledge graph into the maps and search on mgc-frontend. This is the website side only. No database/schema/loader content here.

1. TL;DR

2. Where to call the KG

The proxy at app/api/mgc/[...path]/route.ts allowlists a set of endpoint prefixes (viz/, country/, energy/, ... and kg/). Any client-side fetch('/api/mgc/kg/...') is forwarded server-side to the real mgc-api at ${MGC_API_BASE_URL}/kg/..., with the deployed API's HTTP Basic Auth credentials attached. This is the same proxy and the same env vars the /viz/* map data calls already use: there is no separate KG proxy and no new server-side work required.

Env vars the deployed app needs (server-side, no NEXT_PUBLIC_ prefix, read in src/lib/api/mgc-api/client.ts):

Var Purpose
MGC_API_BASE_URL Base URL of the deployed mgc-api (e.g. https://api.mapglobalchina.com). Falls back to that same production host with a logged warning if unset; do not rely on the fallback.
MGC_API_USERNAME / MGC_API_PASSWORD HTTP Basic Auth for the deployed API (it sits behind nginx Basic Auth). Optional only on hosts with no auth.

There is also a dev-only override, MGC_KG_API_BASE_URL, handled directly in the proxy route: when set, every kg/* request is forwarded to that URL instead of the live mgc-api, while everything else (viz/, country/, etc.) still goes to the live API. It exists purely so the panel can be developed against the local reference KG service before the real /kg/* endpoints are live in production (see .env.local, currently pointed at http://localhost:18010). It is inert when unset, but the code comment is explicit that it must not ship in the PR: do not deploy with it set.

3. The hooks and components you reuse

All hooks live in src/hooks/kg/ and are re-exported from src/hooks/kg/index.ts (import { useKgResolve, ... } from '@/hooks/kg'). Every hook follows the same shape: { data, loading, error } (or data, resolvedId, loading, error for resolve), backed by React Query, disabled until its required argument is present.

Hook / component File Signature What it does
useKgResolve src/hooks/kg/useKgResolve.ts useKgResolve({dataset, rowId}, {primaryOnly?}) -> {data, resolvedId, loading, error} Resolves a (dataset, row_id) map point to its KG node(s) via GET /kg/resolve?dataset=&row_id=. resolvedId is entity_id ?? node_id of the first hit, or undefined if the point has no KG link (not an error). This is the "first hop" from a map click into the graph.
useKgEntity src/hooks/kg/useKgEntity.ts useKgEntity(entityId?, {hops?, relationType?, tier?, limit?}) -> {data, loading, error} Neighborhood bundle for one entity: {center, edges, neighbors} via GET /kg/entity/{id}. Feeds EntityNetworkGraph.
useKgRelations src/hooks/kg/useKgRelations.ts useKgRelations(entityId?, {relationType?, direction?, tier?, minConfidence?, limit?, offset?}) -> {data, loading, error} Flat list of KG edges touching an entity, each with provenance, via GET /kg/entity/{id}/relations.
useKgClaims src/hooks/kg/useKgClaims.ts useKgClaims(aboutId?, {stance?, minConfidence?, limit?, offset?}) -> {data, loading, error} Stance-bearing claims about an entity via GET /kg/claims?about=.
useKgEvents src/hooks/kg/useKgEvents.ts useKgEvents(participantId?, {eventType?, yearFrom?, yearTo?, limit?, offset?}) -> {data, loading, error} Events the entity participated in via GET /kg/events?participant=.
useKgAsk src/hooks/kg/useKgAsk.ts useKgAsk(question?, {limit?}) -> {data, loading, error} The "Ask MGC" natural-language endpoint: GET /kg/ask?q=&limit=. Returns an interpretation line, matched entities, ranked cited results, and a focused graph.
useKgSearch src/hooks/kg/useKgSearch.ts useKgSearch(q?, {entityType?, limit?}) -> {data, loading, error} Entity name lookup via GET /kg/search?q=. Exists and works but is not currently mounted in any component; it's there if you want to add a typeahead.
useKgPath src/hooks/kg/useKgPath.ts useKgPath(fromId?, toId?, {maxHops?, minConfidence?}) -> {data, loading, error} Bounded shortest path between two entities via GET /kg/path. Not currently used by any component either, available if a future feature needs it.
deriveMapPoint src/components/maps/shared/kgMapPoint.ts deriveMapPoint(clicked: {id}, family?: MapPointFamily) -> {dataset, rowId} \| null Turns a clicked feature's /viz id into the {dataset, rowId} shape RelationshipsPanel's mapPoint prop (and useKgResolve) expect. dataset is built as `viz:${family}`. Pass family explicitly whenever you know it (recommended); it will only infer the family from the id's prefix (port:, plant:, asset:, investment:, bri-contract:, state-visit:) if you omit it.
RelationshipsPanel src/components/maps/shared/RelationshipsPanel.tsx <RelationshipsPanel entityId={string \| null} entityLabel={string} onClose={() => void} onFocusEntity?={(id) => void} accentColor? width?=440 mapPoint?={{dataset, rowId}} /> The sidebar/drawer. Two entry modes: pass mapPoint for a per-point layer (it resolves via useKgResolve internally, then falls through to the entity hooks), or pass entityId directly for a choropleth/country selection. mapPoint takes precedence over entityId when both are present. Renders Network / Relations / Claims / Events tabs, each using CitationBlock (also exported from this file) for provenance. Returns null (renders nothing) when neither entityId nor mapPoint is set.
EntityNetworkGraph src/components/charts/EntityNetworkGraph.tsx <EntityNetworkGraph nodes={KgGraphNode[]} links={KgGraphLink[]} width={number} height?=400 focusId? onNodeClick? onNodeHover? /> Pure presentational force-directed graph (d3-force, settles synchronously, plain SVG). No data fetching: RelationshipsPanel's Network tab and AskMgcAnswer's graph both build nodes/links from their own query data and pass them in.
AskMgcBox src/components/kg/AskMgcBox.tsx <AskMgcBox placeholder? onFocusEntity? graphWidth? elevated?=true /> The search input. Calls useKgAsk internally on submit and renders AskMgcAnswer below the field. elevated=false drops its own card surface for when the caller already supplies one (e.g. a map overlay Paper).
AskMgcAnswer src/components/kg/AskMgcAnswer.tsx <AskMgcAnswer answer={KgAskAnswer \| null} loading? error? onFocusEntity? graphWidth?=480 /> Presentational renderer for one /kg/ask answer: interpretation heading, fallback note, matched-entity chips, cited result rows (with CitationBlock), and a collapsed-by-default focused EntityNetworkGraph.

4. Joining KG onto existing map data

This is the core mechanic. Every feature on every MGC map already carries its /viz row id as the feature id (examples from kgMapPoint.ts: port:34 for ports, plant:<hash> for energy plants, investment:<n>, asset:<n> for infrastructure, bri-contract:<n>, state-visit:<n>). The backend's kg.map_link table is keyed on (viz:<family>, <that id>), so joining a map feature to the graph is just building that pair and resolving it.

Pattern A: per-point layer (a clicked marker/dot). On the layer's click handler, call deriveMapPoint(feature, family) and pass the result as mapPoint to RelationshipsPanel. Real example, the energy map, app/maps/energy/page.tsx:

// on a project point click
const handleProjectClick = useCallback((project: EnergyProject) => {
  const point = deriveMapPoint({ id: project.id }, 'energy-projects')
  if (!point) return
  setMapPointLabel(project.plant_name || project.unit_name || 'Energy project')
  setMapPoint(point)
}, [])

// ...

{!selectedCountryData && mapPoint && (
  <RelationshipsPanel
    entityId={null}
    entityLabel={mapPointLabel}
    mapPoint={mapPoint}
    onClose={() => setMapPoint(null)}
    onFocusEntity={(id) => store.set('selectedCountryIso3', id)}
  />
)}

Here project.id is already the /viz row id (plant:<hash>) as it comes straight off useEnergyData's /viz/energy-projects rows (src/hooks/energy/useEnergyData.ts), so no id translation is needed before calling deriveMapPoint.

Pattern B: country choropleth. Pass entityId={ country:${iso3} } directly, no deriveMapPoint/useKgResolve hop needed, because the country nodes are addressed by ISO3 without going through kg.map_link. Real example, the investment map, app/maps/investment/page.tsx:

// KG entity id fed to RelationshipsPanel: the clicked neighbor node when
// set, otherwise the selected country's KG node (`country:<iso3>`).
const relationshipsEntityId = relationshipsFocusId
  ?? (store.selectedCountryIso3 ? `country:${store.selectedCountryIso3}` : null)

// ...

<RelationshipsPanel
  entityId={relationshipsEntityId}
  entityLabel={selectedCountryData.name}
  onClose={() => setShowRelationships(false)}
  onFocusEntity={(id) => setRelationshipsFocusId(id)}
/>

The country: prefix is required: a bare ISO3 string will not resolve (see section 5).

5. The hard rule and gotchas

6. Search ("Ask MGC")

Mount AskMgcBox wherever a search entry point belongs: nav, hero, or a map overlay. It is already mounted in two places you can copy:

AskMgcBox calls useKgAsk, which hits GET /api/mgc/kg/ask?q=&limit=. There is also GET /api/mgc/kg/search?q= (via useKgSearch) for plain entity-name autocomplete, if you want a typeahead; it is not wired into AskMgcBox today.

Important caveat: /kg/ask (and the relevance filter it depends on, which hides low-information "hub" connections like a project's own country) currently exists only in the local reference service, kg-demo/local_kg_api.py (@app.get("/kg/ask")). It has not been confirmed present in the deployed mgc-api. That means search wiring works today against the local KG service (MGC_KG_API_BASE_URL pointed at local_kg_api.py) but will not work against the deployed API until /kg/ask is ported to production. That porting is the API person's task, not yours, but it blocks your search feature going live: flag it as a dependency rather than assuming it will just work once you set MGC_API_BASE_URL to the production host.

7. The bidirectional UX (optional, next step)

"View connections" (map point -> graph) is built: click a feature -> deriveMapPoint -> useKgResolve (inside RelationshipsPanel) -> the panel's tabs.

"Show on map" (a graph/panel node -> its map location) is the reverse direction and is not built yet. It would need a small addition: resolve a KG node id back to its (dataset, row_id) and fly the map to that feature's coordinates. Treat this as a follow-up enhancement, not a v1 requirement; nothing in the checklist below depends on it.

8. Your checklist

  1. Confirm the deployed mgc-api's /kg base URL and Basic Auth creds are set in the frontend's env (MGC_API_BASE_URL, MGC_API_USERNAME, MGC_API_PASSWORD), same vars the /viz/* map data already uses, no new proxy config needed. Make sure MGC_KG_API_BASE_URL is not set in the deployed environment (it's the local-dev-only override).
  2. For each map: attach the feature's click handler to deriveMapPoint(feature, family) (per-point layers) or build `country:${iso3}` (choropleths), and mount RelationshipsPanel with mapPoint/entityId respectively, copying the energy map's or the investment map's pattern from section 4.
  3. Repoint any layer whose feature ids are not already /viz/<family> ids before wiring it (see section 5's gotcha).
  4. Place AskMgcBox in the nav, a hero, or a map overlay, copying the /research page or the energy map's overlay pattern from section 6.
  5. Coordinate with whoever owns the backend to get /kg/ask and its hub relevance filter deployed to production mgc-api. Your search feature is blocked on that endpoint existing outside the local reference service.

Everything above is proven and working today against the local reference /kg service (kg-demo/local_kg_api.py). Going live is these wiring steps, plus the deployed API actually serving /kg/* (including /kg/ask).