ViewChart component
The ViewChart component renders a time-series or categorical chart backed by Aidbox ViewDefinition or AidboxQuery resources. It fetches rows from the given data source, sorts them, turns them into a chart configuration, and renders the result — handling loading and error states along the way. Use it for patient dashboard widgets that chart lab results, questionnaire scores, or any other tabular data Aidbox can produce.
Source: src/uberComponents/ViewChart/index.tsx
Import:
import { ViewChart } from 'src/uberComponents/ViewChart';
import type { ReferenceChartRow, ViewChartConfig, ViewChartProps } from 'src/uberComponents/ViewChart';
What ViewChart does
- Calls
useViewChartRowsto fetch rows fromsource, passingparametersas run/query parameters. The fetch re-runs whensource.type,source.reference, orparameterschange. - Sorts the loaded rows with
sort(defaults tosortByAxisLabel, which orders byaxis_label). - Renders
RenderRemoteDataaround the result: a spinner while loading, anAlerton failure, and the chart once data has loaded. - Resolves
chartinto aViewChartConfig— either used as-is or computed by calling it with the loaded rows — and builds aChartelement fromconfig.transform(data). - Renders that element directly, or via
renderChartif provided, so callers can wrap the chart in their own card, row count, or layout.
Props
type ViewChartProps<TRow extends ReferenceChartRow> = {
source: ViewChartDataSource;
parameters?: ViewDefinitionRunParameter[];
sort?: (a: TRow, b: TRow) => number;
chart?: ViewChartConfig<TRow> | ((rows: TRow[]) => ViewChartConfig<TRow>);
onPointClick?: (datum: ChartDatumBase) => void;
renderChart?: (chart: ReactNode, config: ViewChartConfig<TRow>, data: TRow[]) => ReactNode;
};
| Prop | Required | Description |
|---|---|---|
source | Yes | { type: 'ViewDefinition' | 'AidboxQuery', reference: string } — which resource to run and its id, e.g. { type: 'ViewDefinition', reference: 'ViewDefinition/creatinine-observations' }. |
parameters | No | FHIR Parameters.parameter entries passed to $run (ViewDefinition) or mapped to query params (AidboxQuery). Typically includes the patient reference. |
sort | No | Client-side row comparator applied after fetch. Defaults to sortByAxisLabel. |
chart | No | A ViewChartConfig, or a function from loaded rows to one. Defaults to buildReferenceChart, which derives title, reference-range bands, and y-axis domain from the rows themselves. |
onPointClick | No | Called with the clicked datum — useful for navigating to the source document (see the HMB example below). |
renderChart | No | Wraps the rendered chart element. Receives the chart element, the resolved config (for config.title, etc.), and the raw rows (for counts or empty states). Defaults to rendering the chart unwrapped. |
ReferenceChartRow
Rows returned by the data source must match this shape — it's the common column set both ViewDefinition and AidboxQuery sources are expected to project:
interface ReferenceChartRow {
id: string;
axis_label: string;
title: string | null;
reference_range: (ObservationReferenceRange | string)[] | null;
value_code: string | null;
value_integer: number | null;
value_quantity: number | null;
}
id and axis_label drive default sorting and x-axis labels; value_code / value_integer / value_quantity are alternate value slots depending on the resource being charted; reference_range feeds the default reference-band shading in buildReferenceChart.
Examples
CreatinineDashboard — a single chart with a computed config
src/components/DashboardCard/creatinine.tsx renders one ViewChart against a fixed ViewDefinition, alongside a form for recording new creatinine readings:
export function CreatinineDashboard({ patient }: Props) {
const [refreshKey, setRefreshKey] = useState(0);
const chart = buildCreatinineChart(patient.gender);
return (
<S.Card>
{/* ...header omitted... */}
<ViewChart<ReferenceChartRow>
key={refreshKey}
source={{ type: 'ViewDefinition', reference: 'ViewDefinition/creatinine-observations' }}
parameters={
patient.id ? [{ name: 'patient', valueReference: { reference: `Patient/${patient.id}` } }] : []
}
chart={chart}
renderChart={(chartElement, _config, data) => (
<div style={{ flex: 1 }}>
{data.length > 0 ? <div>{t`Total ${data.length}`}</div> : null}
{chartElement}
</div>
)}
/>
<QuestionnaireResponseForm
initialQuestionnaireResponse={{
resourceType: 'QuestionnaireResponse',
questionnaire: 'creatinine',
subject: { reference: `Patient/${patient.id}` },
}}
questionnaireLoader={questionnaireIdLoader('creatinine')}
onSuccess={() => setRefreshKey((key) => key + 1)}
/>
</S.Card>
);
}
Key points:
chartis a function ofpatient.gender, not of the loaded rows — the normal creatinine range differs by gender, and gender isn't part of the fetched data.buildCreatinineChartcloses over it and returns aViewChartConfigfactory.renderChartis used only to prepend a row count above the chart; the chart itself is still rendered byViewChart.- Bumping
refreshKey(as a Reactkey) after the form submits re-mountsViewChart, forcing a re-fetch so the new reading appears immediately.
HMBDiagnosticDashboard — multiple charts from a shared config array
src/containers/PatientDetails/HMBDiagnostic/HMBDiagnosticDashboard.tsx drives several ViewChart instances from one config array, mixing ViewDefinition and AidboxQuery sources:
export function HMBDiagnosticDashboard({ patient }: { patient: Patient }) {
const navigate = useNavigate();
const [refreshKey, setRefreshKey] = useState(0);
const onPointClick = (datum: ChartDatumBase) => {
const { qrId } = datum as HMBChartDatum;
navigate(`/patients/${patient.id}/documents/${qrId}`);
};
return (
<S.Grid>
{getHMBCharts().map((entry) => (
<ViewChart<ReferenceChartRow>
key={`${entry.id}-${refreshKey}`}
source={entry.source}
parameters={patient.id ? entry.parameters(patient.id) : []}
chart={entry.config}
onPointClick={onPointClick}
renderChart={(chart, config) => (
<DashboardCard title={config.title} icon={entry.icon}>
{chart}
</DashboardCard>
)}
/>
))}
</S.Grid>
);
}
getHMBCharts() (in config.tsx) returns one entry per chart, each with its own source, parameters, icon, and config:
{
id: 'flow-volume',
source: { type: 'ViewDefinition', reference: 'ViewDefinition/hmb-flow-volume' },
icon: <BarChartOutlined />,
parameters: viewDefinitionPatientParameters,
config: { variant: 'bar', transform: toFlowVolumeWithAxis(flow) /* ... */ },
},
{
id: 'pain-severity-score',
source: { type: 'AidboxQuery', reference: 'AidboxQuery/hmb-pain-severity-score' },
icon: <HeartOutlined />,
parameters: aidboxQueryPatientParameters,
config: buildPainSeverityAndScoreChart, // config as a function of rows
},
Key points:
ViewDefinitionsources here are filtered server-side with apatientFHIR reference parameter; theAidboxQuerysource instead takes the bare patient id as avalueString, matching how its SQL query reads{{params.patient}}(see ViewDefinition and AidboxQuery below).onPointClickis used for navigation: each row'sidis carried through asqrIdin the transformed datum, so clicking a point opens theQuestionnaireResponsedocument that produced it.renderCharthere wraps every chart in the sharedDashboardCard, reading the title from the resolvedconfigrather than hardcoding it per entry.- As in the creatinine example,
key={entry.id}-${refreshKey}forces a re-fetch of all charts after a new questionnaire response is submitted.
Using ViewChart in a patient dashboard
The patient dashboard (src/containers/PatientDetails/Dashboard/config.ts) is a DashboardInstance — an object with top / left / right / bottom arrays of WidgetInfo, each { widget, query? }. ViewChart-based widgets slot in the same way as any other dashboard card, as long as they're wrapped in a component matching WidgetProps ({ patient, widgetInfo }).
CreatinineDashboardContainer is that wrapper for the creatinine example above — it just forwards patient to CreatinineDashboard:
export function CreatinineDashboardContainer({ patient }: ContainerProps) {
return <CreatinineDashboard patient={patient} />;
}
It's registered in the bottom area of the dashboard config (config.ts):
export const patientDashboardConfig: DashboardInstance = {
top: [
// AppointmentCardContainer, GeneralInformationDashboardContainer,
// StandardCardContainerFabric(prepareConditions), ... (query-driven cards)
],
left: [],
right: [],
bottom: [
{
widget: CreatinineDashboardContainer,
},
],
};
Unlike the query-driven top widgets (which declare a FHIR search and get pre-fetched resources), a ViewChart-based widget fetches its own data — it needs no query entry, only widget. Follow this pattern to add your own ViewChart dashboards: build a small WidgetProps-compatible container, place it in whichever dashboard area fits, and let ViewChart handle the fetch and render internally.
ViewDefinition and AidboxQuery
ViewChart's source.type selects which Aidbox operation supplies rows:
-
ViewDefinition— a SQL on FHIRViewDefinitionresource, projected into flat rows via the$runoperation (POST /ViewDefinition/:id/$run).ViewChartcalls it withparametersplus{ name: '_format', valueCode: 'json' }. Use it when the data lives directly on a FHIR resource and can be expressed as awhere/selectview. Example —ViewDefinition/creatinine-observations, which projects one row per creatinineObservation:resourceType: ViewDefinitionid: creatinine-observationsresource: Observationwhere:- path: code.coding.where(system = 'http://loinc.org' and code = '2160-0').exists()select:- column:- name: idpath: getResourceKey()type: string- name: axis_labelpath: effective.ofType(dateTime)type: dateTime- name: titlepath: "'Creatinine'"type: string- name: value_quantitypath: value.ofType(Quantity).value.first()type: decimal -
AidboxQuery— a custom parameterized SQL resource, run via the$queryendpoint (GET /$query/:id). Use it when a single FHIR resource type can't express the shape you need — e.g. joining rows from more than one source, as in the HMB example, where pain severity and pain score come from two differentViewDefinition-backed views (sof.hmb_pain_severity,sof.hmb_pain_score) and are combined byid. Parameters are declared on the resource and substituted with{{params.name}};ViewChartpassesparametersthrough as query-string params. Example —AidboxQuery/hmb-pain-severity-score:resourceType: AidboxQueryid: hmb-pain-severity-scorequery: >-SELECTCOALESCE(severity.id, score.id) AS id,COALESCE(severity.axis_label, score.axis_label) AS axis_label,'Period Pain Severity & Score' AS title,NULL AS reference_range,severity.value_code AS value_code,score.value_integer AS value_integer,NULL AS value_quantityFROM sof.hmb_pain_severity AS severityFULL JOIN sof.hmb_pain_score AS score ON severity.id = score.idWHERE COALESCE(severity.patient_id, score.patient_id) = {{params.patient}}params:patient:type: "string"isRequired: true
Whichever type you use, the returned columns must line up with ReferenceChartRow (or a superset of it, as HMBChartMeta/HMBChartDatum do) — id, axis_label, title, reference_range, and the relevant value_* column — since that's what ViewChart's default sort and buildReferenceChart config expect. See the SQL on FHIR reference and Aidbox custom search docs for the full ViewDefinition/AidboxQuery syntax.
In a custom EMR build, add your ViewDefinition/AidboxQuery YAML files to contrib/fhir-emr/resources/init-seeds/ so Aidbox loads them on startup, the same way questionnaire actions load Questionnaire/Mapping resources.
Related documentation
- Resource detail page — tabbed FHIR resource detail layout, another place chart-like widgets are commonly embedded
- Questionnaire actions — Questionnaire + Mapping pairs, including how init-seed resources are loaded
- Custom EMR build — project template, including the Aidbox init-seeds volume