Skip to main content

@ttoss/geovis-workspace

A React component that composes a slot-based workspace around a GeoVis map. Six named slots (map, legend, warnings, inspector, metadata, controls) each render a runtime-bound default panel, are configurable through a config object, and can be hidden or replaced with a custom component per slot; the map is rendered from a GeoVis visualizationSpec.

Installation

pnpm add @ttoss/geovis-workspace

@ttoss/geovis, @ttoss/ui, @ttoss/react-i18n and react are peer dependencies.

Storybook

Interactive examples are available on Storybook.

Usage

The parent owns the selection state and derives the next visualizationSpec from it, so picking a menu item recolors the map. Seed the initial selection with getInitialSelection (reads each menu's defaultValue).

import { type VisualizationSpec } from '@ttoss/geovis';
import {
type GeovisWorkspaceConfig,
GeovisWorkspace,
getInitialSelection,
} from '@ttoss/geovis-workspace';
import * as React from 'react';

const config: GeovisWorkspaceConfig = {
controls: {
menus: [
{
id: 'variable',
title: 'Variável',
defaultValue: 'rate',
items: [
{ value: 'rate', label: 'Taxa cumulativa' },
{ value: 'range', label: 'Faixa (% da pop 65+)' },
],
},
],
},
rightSidebar: { title: 'Details' },
};

// Maps the current selection to a GeoVis spec — your domain logic.
const buildSpec = (
selection: Record<string, string | undefined>
): VisualizationSpec => {
// ...
};

export const Example = () => {
const [selection, setSelection] = React.useState(() => {
return getInitialSelection({ config });
});

const visualizationSpec = React.useMemo(() => {
return buildSpec(selection);
}, [selection]);

return (
<GeovisWorkspace
config={config}
visualizationSpec={visualizationSpec}
variables={selection}
onVariableChange={setSelection}
/>
);
};

variables and onVariableChange are optional: omit both to let the workspace manage the selection internally (seeded from defaultValue). Provide them to control it from the parent — required when the selection must drive the visualizationSpec. Selection is per menu group: choosing an item only affects its own group. Read the current selection anywhere inside the workspace with useGeovisWorkspace().

Grouped menus

When a menu has many items, split them into a carousel instead of one long list: give the menu groups (each a labelled tab) in place of items. Only the open group's items show, and the open group follows the selection so the active item is never hidden. The selection stays a single value shared across groups.

const config: GeovisWorkspaceConfig = {
controls: {
menus: [
{
id: 'variable',
title: 'Variável',
defaultValue: 'rate',
groups: [
{
id: 'demografia',
label: 'Demografia',
items: [{ value: 'rate', label: 'Taxa cumulativa' }],
},
{
id: 'renda',
label: 'Renda',
items: [{ value: 'gini', label: 'Índice de Gini' }],
},
],
},
],
},
};

Slots

The workspace is built from six named slots. map fills the main area; controls renders in the left sidebar; legend, warnings, inspector, and metadata stack in that order in the right sidebar. Placement is fixed — only a slot's content is configurable:

SlotRegionDefault panel
mapMain areaThe GeoVis canvas.
controlsLeft sidebarMenu groups from config.controls.
legendRight sidebarDescription/sources from config.legend plus the spec's legends.
warningsRight sidebarIssues from useGeoVis().result — see Warnings and repair.
inspectorRight sidebarThe clicked feature from useGeoVisClick(), with a dismiss button.
metadataRight sidebarThe spec's mapType and source count — see Metadata.

A sidebar renders only when at least one of its slots has content — an override component, or (for controls/legend/metadata) non-empty config, a spec-resolved legend, or (for metadata) a spec with a mapType or at least one source. Use config.slots to hide a slot or replace its default panel with a custom component, which gets the same runtime access (useGeoVis(), useGeoVisClick(), useGeoVisHover()) as the default it replaces:

const config: GeovisWorkspaceConfig = {
slots: {
legend: { hidden: true },
controls: { component: MyCustomControls },
},
};

Layer control and the left sidebar

If the map spec declares a control (GeoVis's floating layer-toggle panel), it is auto-mounted by GeoVisProvider and anchored to a map corner — by default the bottom-left, the same corner the left sidebar opens over. To keep an opening sidebar from covering it, the workspace hands the map a larger control.offset.x while the left sidebar is open, sliding the control clear along the bottom edge; it snaps back when the sidebar closes. The shift is purely presentational (the workspace never mutates your spec's other fields) and only applies to a left-anchored control — a bottom-right/top-right control the sidebar never overlaps is left untouched. No configuration is needed; it follows the sidebar's open state automatically.

Warnings and repair

The warnings slot's default panel renders every issue on the current useGeoVis().result: resolved results show their (non-blocking) warnings; any other status shows its (blocking) issues. Each issue renders a translated message keyed by its code (falling back to the raw message for a code with no catalog entry yet), a monospace subject reference, and a button per repair candidate. Pass onRepair to GeovisWorkspace to apply one — omit it and repair buttons still render, disabled rather than hidden:

<GeovisWorkspace
config={config}
visualizationSpec={visualizationSpec}
onRepair={(repair) => {
// repair is always a `set-value` — for an `allowed-values` issue with
// several buttons, each one applies as a `set-value` for that one value.
setVisualizationSpec((spec) => applyRepair(spec, repair));
}}
/>

A failure with no prior successful resolve (cold start) renders a repair-affordance empty state in the map slot instead of an uninitialized canvas — the warnings panel stays empty in that case, since the empty state already shows the same issues. Once any resolve succeeds, later failures keep the last good map visible while the warnings panel lists the new issue, the same "nothing renders on failure" contract GeoVisProvider already has.

Inspector

The inspector slot's default panel shows the last clicked feature from useGeoVisClick() — its layerId, value, and featureId — with a dismiss button. That button (and pressing Escape, or clicking empty space on the map) all clear the same selection via useDismissGeoVisClick(), so the panel and the map's selection highlight always stay in sync. The panel renders nothing when no feature is selected.

For a richer, data-bound detail view, configure the imperative detail API on rightSidebar instead of overriding the slot. When onFeatureSelect (and/or renderDetails) is set, an accepted click opens the right sidebar, runs onFeatureSelect for the clicked feature, and hands its loading/error/ data state to renderDetails. shouldOpen gates which clicks are accepted — return false to ignore a click, keeping the current detail and open state. The workspace never fetches or caches: onFeatureSelect owns the request.

<GeovisWorkspace
config={{
rightSidebar: {
title: 'Details',
shouldOpen: (info) => info.layerId === 'kitchens',
onFeatureSelect: (info) =>
fetch(`/api/kitchens/${info.featureId}`).then((r) => r.json()),
renderDetails: ({ loading, error, data }) => {
if (loading) return <Spinner />;
if (error || !data) return null;
return <KitchenDetail kitchen={data as Kitchen} />;
},
},
}}
visualizationSpec={visualizationSpec}
/>

Metadata

The metadata slot's default panel needs no config: it reads the current visualizationSpec via useGeoVis() and shows the mapType, when set, and a pluralized source count. It renders nothing — and contributes no content toward showing the right sidebar — when the spec has neither, so it never appears as an always-on placeholder.

Layer list controls

LayerListControls is an opt-in, publicly exported alternative to the controls slot's default menu panel: one row per visualizationSpec.layers entry with a visibility checkbox and its activeLegendId, reading useGeoVis().spec.layers instead of config.controls.menus. Enable it via config.slots.controls, and rebuild visualizationSpec with the toggled layer's visible field from onLayerVisibilityChange — the workspace never mutates the spec itself, the same delegation onVariableChange and onRepair already use:

import { GeovisWorkspace, LayerListControls } from '@ttoss/geovis-workspace';

<GeovisWorkspace
config={{ slots: { controls: { component: LayerListControls } } }}
visualizationSpec={visualizationSpec}
onLayerVisibilityChange={(layerId, visible) => {
setVisualizationSpec((spec) => {
return {
...spec,
layers: spec.layers.map((layer) => {
return layer.id === layerId ? { ...layer, visible } : layer;
}),
};
});
}}
/>;

API

GeovisWorkspace props

PropTypeDescription
configGeovisWorkspaceConfigDescribes the slots. Required.
visualizationSpecVisualizationSpecGeoVis spec rendered in the main map area. Required.
variablesRecord<string, string | undefined>Controlled selection per menu group. Omit for uncontrolled.
onVariableChange(variables) => voidCalled with the full next selection when an item is picked.
onRepair(repair: RepairOption) => voidCalled with the chosen repair when a repair button is pressed. Omit to render repair buttons disabled.
onLayerVisibilityChange(layerId: string, visible: boolean) => voidCalled with a layer's id and its next visible value when LayerListControls toggles it.

GeovisWorkspaceConfig

PropertyTypeDescription
slotsPartial<Record<GeovisWorkspaceSlotName, GeovisWorkspaceSlotConfig>>Per-slot override/hide. Omit an entry for the default.
controlsGeovisWorkspaceControlsContent for the controls slot's default panel.
legendGeovisWorkspaceLegendConfigContent for the legend slot's default panel.
leftSidebarGeovisWorkspaceLeftSidebarStateLeft sidebar menus and open/closed state.
rightSidebarGeovisWorkspaceRightSidebarStateRight sidebar title, open/closed state, and detail API.

GeovisWorkspaceSlotName

'map' | 'legend' | 'warnings' | 'inspector' | 'metadata' | 'controls' — the closed, versioned slot vocabulary. Adding a name is additive; renaming one is breaking.

GeovisWorkspaceSlotConfig

PropertyTypeDescription
componentReact.ComponentTypeReplaces the slot's default panel. Gets the same runtime access.
hiddenbooleanHides the slot's region entirely instead of rendering its default.

GeovisWorkspaceControls

PropertyTypeDescription
menusGeovisWorkspaceMenu[]Menu groups rendered by the default panel.

GeovisWorkspaceMenu

A menu is either flat (a titled list of items) or grouped (its items split across a carousel of tabs, with only the open group's items shown). items and groups are mutually exclusive.

PropertyTypeDescription
idstringUnique menu identifier; keys this menu's selection.
defaultValuestringItem selected by default.
titlestringHeading above the items (flat) or above the tabs (grouped — optional here).
items{ value: string; label: string }[]Flat menu: selectable items.
groupsGeovisWorkspaceMenuGroup[]Grouped menu: carousel groups, one tab each.
defaultGroupIdstringGrouped menu: group open first. Defaults to the group holding defaultValue.

GeovisWorkspaceMenuGroup

PropertyTypeDescription
idstringUnique group id (tracks the open tab).
labelstringText on the group's tab.
items{ value: string; label: string }[]Items shown while the group is open.

GeovisWorkspaceSidebarState / GeovisWorkspaceLeftSidebarState / GeovisWorkspaceRightSidebarState

PropertyTypeDescription
initialState'open' | 'closed'Whether the sidebar starts open. Defaults to 'closed'.
menusGeovisWorkspaceMenu[]Left sidebar only: alias for controls.menus. controls.menus wins when both are set.
titlestringRight sidebar only: title shown at the top.
shouldOpen(info: MapClickInfo) => booleanRight sidebar only: gate deciding whether a click drives the inspector. Defaults to accepting.
onFeatureSelect(info: MapClickInfo) => Promise<unknown>Right sidebar only: fetches the clicked feature's detail; its promise drives renderDetails.
renderDetails(state: GeovisWorkspaceDetailState) => React.ReactNodeRight sidebar only: renders the inspector slot from the loading/error/data fetch state.

GeovisWorkspaceLegendConfig

A declarative description and a list of (optionally linked) data sources for the legend slot's default panel, plus the class swatches the map's own visualizationSpec.legends already resolves — there is no hand-authored swatch list to keep in sync with the map. Each block renders only when present.

PropertyTypeDescription
descriptionstringParagraph above the legend swatches.
sources{ title?: string; items: { label; href? }[] }Data sources; href adds a link.
const config: GeovisWorkspaceConfig = {
rightSidebar: { title: 'POPULAÇÃO 65+ COMO % DA POPULAÇÃO TOTAL' },
legend: {
description: 'Proporção da população total com 65 anos ou mais.',
sources: {
title: 'Fonte dos dados:',
items: [
{ label: 'SEADE (2025)', href: 'https://repositorio.seade.gov.br' },
{ label: 'Geometria: Distritos Municipais de São Paulo.' },
],
},
},
};