API Reference
Complete type definitions and manifest schema for custom visuals
Complete TypeScript types and manifest schema for building custom visuals and filters.
Visual Props
SingleInputVisualProps
Props passed to single-input custom visual components.
type SingleInputVisualProps = {
/** Query results from the card's data source */
data: Record<string, string | number | boolean>[];
/** Component settings defined in manifest */
settings?: Record<string, string | number | boolean>;
/** Card metadata including title, type, and formatting */
cardMetadata?: CardMetadata;
/** Dashboard theme configuration */
theme?: CustomCardTheme;
/** The dashboard filters applied to this card, each with its value */
filterInputs?: CustomVisualFilterInput[];
/** @deprecated Read `filterInputs`. */
filters?: DashboardFilter[];
/** @deprecated Read `filterInputs`. */
filterValues?: ActiveFilterValue[];
/**
* Pre-rendered inline filter components.
* Render these wherever you want in your layout.
* Always an array (empty if no inline filters configured).
*/
inlineFilters?: ReactNode[];
};filterInputs lists every dashboard filter that applies to the card and has a
value, on regular sheets and Document sheets alike. A filter whose
card mapping is set to Don't apply this
filter for this card is left out. See
CustomVisualFilterInput.
filters and filterValues still work, so existing plugins keep running. New
plugins should read filterInputs.
MultiInputVisualProps
Props passed to multi-input custom visual components.
type MultiInputVisualProps = {
/** Array of datasets, one per slot (indexed by tab position) */
data: Record<string, string | number | boolean>[][];
/** Global settings from the config card */
settings?: Record<string, string | number | boolean>;
/** Per-slot settings (aligned with data array) */
slotSettings?: Array<Record<string, string | number | boolean> | undefined>;
/** Dashboard theme configuration */
theme?: CustomCardTheme;
/** The dashboard filters applied to this card, each with its value */
filterInputs?: CustomVisualFilterInput[];
/** @deprecated Read `filterInputs`. */
filters?: DashboardFilter[];
/** @deprecated Read `filterInputs`. */
filterValues?: ActiveFilterValue[];
/**
* Pre-rendered inline filter components.
* Render these wherever you want in your layout.
*/
inlineFilters?: ReactNode[];
/** Tab metadata (titles, types, IDs) */
tabMetadata?: TabMetadata;
/** Rich metadata for each slot */
cardMetadata?: CardMetadata[];
};Filter Props
CustomFilterProps
Props passed to custom filter components for text and select filters.
type CustomFilterProps = {
/** Available filter options from the query */
options: TSelectedRecord[];
/** Loading state for initial data fetch */
isLoading: boolean;
/** Fetching state for refetches */
isFetching: boolean;
/** Error state */
isError: boolean;
/** Currently selected values */
selectedValues: TSelectedRecord[];
/** Called when selection changes */
onChange: (records: TSelectedRecord[]) => void;
/** Called to clear all selections */
onClear: () => void;
/** Called to toggle select all (if supported) */
onSelectAll?: (checked: boolean) => void;
/** Current search query (for searchable filters) */
searchQuery?: string;
/** Called when search query changes */
onSearchChange?: (query: string) => void;
/** True when search is in progress */
isSearching?: boolean;
/** Dashboard theme configuration */
theme?: CustomCardTheme;
/** Plugin settings from manifest */
settings?: Record<string, string | number | boolean>;
/** True if filter is single-select mode */
isSingleSelect?: boolean;
/** True if all options are selected */
allSelected?: boolean;
};CustomDateFilterProps
Props passed to custom filter components for date filters.
type CustomDateFilterProps = {
/** Current date range selection */
dateRange: DateRange | undefined;
/** Initial date range (for reset) */
initialDateRange: DateRange | undefined;
/** Loading state for initial data fetch */
isLoading: boolean;
/** Fetching state for refetches */
isFetching: boolean;
/** Error state */
isError: boolean;
/** Called when date range changes */
onDateChange: (range: DateRange | undefined) => void;
/** Called when relative date changes (e.g., "Last 7 days") */
onRelativeDateChange?: (
range: DateRange | undefined,
meta: RelativeDateFilter
) => void;
/** Called to clear the date selection */
onClear: () => void;
/** Called to reset to initial values */
onReset?: () => void;
/** Dashboard theme configuration */
theme?: CustomCardTheme;
/** Plugin settings from manifest */
settings?: Record<string, string | number | boolean>;
};TSelectedRecord
A selected record in a filter (value from filter options).
type TSelectedRecord = {
value: string | number | boolean;
label?: string;
};DateRange
Date range type for date filters.
type DateRange = {
from: Date | undefined;
to?: Date | undefined;
};RelativeDateFilter
Relative date filter configuration (e.g., "Last 7 days").
type RelativeDateFilter = {
mode: 'last' | 'this' | 'previous' | 'between';
unit: 'day' | 'week' | 'month' | 'quarter' | 'year';
n?: number;
complete?: boolean;
toDate?: boolean;
from?: number;
to?: number;
};Metadata Types
CardMetadata
Rich metadata about a card's configuration.
type CardMetadata = {
/** Card type (kpi, bar, table, custom, etc.) */
cardType: string;
/** Card title */
title: string;
/** Card description */
description?: string;
/** KPI-specific configuration */
kpiConfig?: {
/** Comparison period metadata */
comparisonMetadata?: ComparisonMetadataMap;
/** KPI display options */
options?: {
/** Favorable-direction policy for comparison evaluation */
comparisonPolarity?: 'higher_is_better' | 'lower_is_better' | 'neutral';
/** Comparison presentation settings */
comparisonPresentation?: ComparisonPresentationSpec;
/** Show sparkline trendline */
showTrendline?: boolean;
/** Show comparison value */
showComparison?: boolean;
};
/** Legacy number formatting */
formatNumber?: LegacyFormatNumber;
};
/** Number formatting configuration */
formatConfig?: CustomVisualFormatConfig;
};TabMetadata
Lightweight metadata about tabs in a multi-input visual.
type TabMetadata = {
/** Tab titles (labels) */
titles: string[];
/** Card types per tab */
cardTypes: string[];
/** Card IDs per tab */
cardIds: string[];
};CustomVisualFormatConfig
Number formatting configuration for custom visuals.
type CustomVisualFormatConfig = {
/** KPI number formatting */
kpi?: {
/** Primary value format */
primary?: FormatOptions;
/** Comparison value format */
comparison?: FormatOptions;
/** Conditional color ranges */
colorRanges?: ColorRange[];
};
/** Chart axis formatting */
axes?: {
xAxis?: FormatOptions;
yAxis?: FormatOptions;
secondaryYAxis?: FormatOptions;
};
/** Data label formatting */
dataLabels?: FormatOptions;
/** Table column formatting */
tables?: {
/** Per-column format settings */
columns?: Array<{
id?: string;
label?: string;
position?: number;
numberFormat?: ColumnNumberFormat | FormatOptions;
}>;
/** Column format lookup by ID */
columnMap?: Record<string, { numberFormat?: ColumnNumberFormat | FormatOptions }>;
/** Comparison column format */
comparison?: FormatOptions;
/** Default format for numeric columns */
defaultNumberFormat?: FormatOptions;
};
/** Legacy format fields (for backward compatibility) */
legacy?: {
formatNumber?: LegacyFormatNumber;
numberAxisFormat?: {
decimalPlaces?: number;
suffix?: string;
currency?: string;
locale?: string;
};
};
};FormatOptions
Number formatting options.
type FormatOptions = {
/** Format type */
type?: 'auto' | 'number' | 'currency' | 'percent' | 'scientific' | 'date';
/** Decimal places to display */
decimalPlaces?: number;
/** Currency code (e.g., 'USD', 'EUR') */
currency?: string;
/** Locale for formatting (e.g., 'en-US', 'de-DE') */
locale?: string;
/** Text prepended to value */
prefix?: string;
/** Text appended to value */
suffix?: string;
/** Enable automatic suffix (K, M, B) */
useSuffix?: boolean;
/** Show negative numbers in parentheses */
negativeInParentheses?: boolean;
/** Multiply value before display (e.g., 100 for percentages) */
multiplyBy?: number;
/** Date format string (e.g., 'MM/DD/YYYY') */
dateFormat?: string;
};ColorRange
Conditional color range for values.
type ColorRange = {
/** Start of range (inclusive) */
start: number;
/** End of range (inclusive) */
end: number;
/** Color to apply (hex or CSS color) */
color: string;
};ColumnNumberFormat
Table column number format options.
type ColumnNumberFormat = {
/** Number style */
style: 'decimal' | 'currency' | 'percent';
/** Currency code */
currency: string;
/** Locale for formatting */
locale: string;
/** Minimum decimal places */
minimumFractionDigits: number;
/** Maximum decimal places */
maximumFractionDigits: number;
/** Show data bar visualization */
showDataBar: boolean;
/** Data bar color */
dataBarColor: string;
/** Data bar minimum value (for scaling) */
dataBarMinValue?: number;
/** Data bar maximum value (for scaling) */
dataBarMaxValue?: number;
};LegacyFormatNumber
Legacy number format (for backward compatibility).
type LegacyFormatNumber = {
decimalPlaces?: number;
currency?: string;
locale?: string;
suffix?: string;
enabled?: boolean | string;
colorRanges?: ColorRange[];
[key: string]: unknown; // Allows additional legacy fields
};ComparisonMetadataEntry
Metadata for one metric comparison output. Import this type from
react-semaphor/analytics-protocol when possible.
type ComparisonMetadataEntry = {
type: 'previous_period' | 'previous_year' | 'target';
displayName?: string;
displayLabel?: string;
output: MetricComparisonOutput;
};output carries stable column keys, the explicit or inherited request, typed
availability, and optional half-open currentWindow and comparisonWindow
values using { startInclusive, endExclusive, timezone }.
ComparisonMetadataMap
Map keyed by MetricComparisonOutput.outputId. Display labels and column-name
prefixes are not metric identity.
type ComparisonMetadataMap = Record<string, ComparisonMetadataEntry>;Theme Types
CustomCardTheme
Theme configuration passed to custom visuals.
type CustomCardTheme = {
/** Color palette from dashboard theme */
colors?: string[];
/** Color mode */
mode?: 'light' | 'dark' | 'system';
};Filter Types
FilterOperation
Filter operation types.
type FilterOperation =
| '='
| '!='
| '>'
| '<'
| '>='
| '<='
| 'in'
| 'not in'
| 'like'
| 'not like'
| 'between'
| 'not between';CustomVisualFilterInput
One dashboard filter applied to the card, with its current value. Visuals
receive these in filterInputs.
type CustomVisualFilterInput = {
inputId: string; // Stable id for this filter on the dashboard
label: string; // Display name, as viewers see it
variableName: string; // The filter's Variable name (Advanced tab)
filterValue: ActiveFilterValue; // The value as this card's query applies it
};When the filter's Cards mapping points this card at a different field, filterValue
names that field (name and fieldMeta), so it describes the condition your
data was filtered by. filterId stays the filter's own id.
For example, a visual can show which filters shaped its numbers:
export function RevenueSummary({ data, filterInputs = [] }: SingleInputVisualProps) {
return (
<div>
<p>{data.length} rows</p>
{filterInputs.map((input) => (
<span key={input.inputId}>
{input.label}: {input.filterValue.values.join(', ')}
</span>
))}
</div>
);
}DashboardFilter
Simplified filter definition (metadata about a dashboard filter). Used by the
deprecated filters prop.
type DashboardFilter = {
id: string;
title: string; // Display name
column: string; // Column being filtered
table: string; // Table containing the column
dataType: string; // 'text', 'number', 'date', etc.
operation: FilterOperation;
};ActiveFilterValue
Active filter value (current selection).
type ActiveFilterValue = {
filterId: string; // References DashboardFilter.id
name: string; // Display name
operation: FilterOperation;
valueType: 'string' | 'number' | 'date' | 'boolean';
values: (string | number | boolean)[];
relativeMeta?: RelativeDateFilter;
};Manifest Schema
The components.config.ts file defines your plugin's components.
ComponentsConfig
Top-level configuration structure.
type ComponentsConfig = {
/** Visual components (charts, KPIs, tables) */
visuals: VisualConfig[];
/** Filter components (optional) */
filters?: FilterConfig[];
};VisualConfig
Configuration for a visual component.
type VisualConfig = {
/** Display name in the chart selector */
name: string;
/** React component export name */
component: string;
/** Component type (always 'chart' for visuals) */
componentType: 'chart';
/** Unique identifier for this visual */
chartType?: string;
/** Icon name from lucide-react */
icon?: string;
/** 'single' or 'multiple' data inputs */
visualType?: 'single' | 'multiple';
/** Minimum required inputs (multi-input only) */
minInputs?: number;
/** Maximum allowed inputs (multi-input only) */
maxInputs?: number;
/** Slot definitions (multi-input only) */
slots?: SlotDefinition[];
/** Global settings schema */
settings?: Record<string, SettingConfig>;
/** Per-slot settings schema (multi-input only) */
slotSettings?: Record<string, SettingConfig>;
/** Documentation for the visual */
docs?: VisualDocumentation;
};FilterConfig
Configuration for a filter component.
type FilterConfig = {
/** Display name (must be unique) */
name: string;
/** React component export name */
component: string;
/** Unique filter type identifier */
filterType?: string;
/** Icon name from lucide-react */
icon?: string;
/** Supported data types (e.g., ['text', 'varchar']) */
supportedDataTypes?: string[];
/** Settings schema */
settings?: Record<string, SettingConfig>;
/** Documentation for the filter */
docs?: FilterDocumentation;
};SlotDefinition
Slot definition for multi-input visuals.
type SlotDefinition = {
/**
* Position identifier:
* - number: exact position (0, 1, 2)
* - "N+": position N and all subsequent
* - "N-M": positions N through M
*/
position: number | string;
/** Tab label shown in editor */
label: string;
/** Tooltip description */
description?: string;
/** Expected card type(s) for this slot */
expectedType?: string | string[];
/** True if slot must have data */
required?: boolean;
};Position format examples:
| Position | Meaning |
|---|---|
0 | Exactly position 0 (first tab) |
1 | Exactly position 1 (second tab) |
"0+" | All positions starting from 0 |
"1+" | Position 1 and all subsequent |
"0-2" | Positions 0, 1, and 2 only |
SettingConfig
Configuration for a single setting.
type SettingConfig = {
/** Display title in the settings panel */
title: string;
/** Default value */
defaultValue: string;
/** UI control type */
ui: 'input' | 'select';
/** Options for select UI */
options?: { label: string; value: string }[];
/** Setting documentation */
docs?: SettingDocumentation;
};VisualDocumentation
Documentation for a visual component.
type VisualDocumentation = {
/** What this visual does */
description: string;
/** Markdown describing expected data format */
dataSchema: string;
/** When to use this visual */
useCases?: string[];
/** Preview image (base64 or URL) */
thumbnail?: string;
};FilterDocumentation
Documentation for a filter component.
type FilterDocumentation = {
/** What this filter does */
description: string;
/** When to use this filter */
useCases?: string[];
};SettingDocumentation
Documentation for a setting.
type SettingDocumentation = {
/** What this setting controls */
description?: string;
};Helper Functions
Utility functions available in the plugin quickstart template.
parseKPIData
Parse KPI data using the segment convention.
type Data = Record<string, string | number | boolean>[];
function parseKPIData(data: Data): {
currentValue: string | number | boolean | null;
comparisonValue: string | number | boolean | null;
trendlineData: Data;
valueKey: string | undefined;
};Usage:
const { currentValue, comparisonValue, trendlineData } = parseKPIData(data);
// currentValue: value from row where segment === 'current'
// comparisonValue: value from row where segment === 'comparison'
// trendlineData: rows where segment === 'trendline'evaluateMetricComparison
Evaluate comparison values through the shared analytics contract. Import this
function from react-semaphor/analytics-protocol.
const evaluation = evaluateMetricComparison({
currentValue: 125000,
comparisonValue: 98000,
polarity: 'higher_is_better',
});The result includes delta, percentChange, ratio, direction, sentiment,
and availability status. percentChange is a decimal ratio; multiply it by 100
only when rendering percentage points.
evaluation.percentChange;
// 0.275510...
evaluateMetricComparison({
currentValue: 80000,
comparisonValue: 100000,
polarity: 'lower_is_better',
}).sentiment;
// "favorable"formatKPIValue
Format a number using KPI format configuration.
function formatKPIValue(
value: number | undefined,
formatConfig?: FormatOptions | LegacyFormatNumber
): string;Usage:
formatKPIValue(125000, { currency: 'USD', decimalPlaces: 0 });
// "$125,000"
formatKPIValue(0.2755, { type: 'percent', decimalPlaces: 1 });
// "27.6%"
formatKPIValue(1500000, { suffix: 'M', multiplyBy: 0.000001, decimalPlaces: 1 });
// "1.5M"getComparisonLabel
Get display label for a comparison type.
function getComparisonLabel(metadata?: {
type?: string;
displayLabel?: string;
}): string;Usage:
getComparisonLabel({ type: 'previous_period' });
// "vs Previous Period"
getComparisonLabel({ type: 'previous_year' });
// "vs Previous Year"
getComparisonLabel({ displayLabel: 'vs Budget' });
// "vs Budget"Comparison types:
| Type | Default Label |
|---|---|
previous_period | vs Previous Period |
previous_year | vs Previous Year |
target | vs Target |
formatDateRange
Format a date range for display.
function formatDateRange(
start: string,
end: string,
displayTimezone?: string
): string;Usage:
formatDateRange('2024-01-01', '2024-01-31');
// "Jan 1, 2024 - Jan 31, 2024"formatDateShort
Format a date range with year de-duplication.
function formatDateShort(
start: string,
end: string,
displayTimezone?: string
): string;Usage:
formatDateShort('2024-01-01', '2024-01-31');
// "Jan 1 - Jan 31, 2024"
formatDateShort('2023-12-01', '2024-01-31');
// "Dec 1, 2023 - Jan 31, 2024"Related Topics
- Single-Input Visuals - Building standard visuals
- Multi-Input Visuals - Combining multiple data sources
- Custom Filters - Building filter components