| sidebar_position | 2 |
|---|
Reactodia defines a contract (DataProvider interface) to query a subset of data from external source (data graph) to provide means for incremental data loading when exploring the graph.
Reactodia uses RDF (Resource Description Framework) as a representation format for the graph data. The core concepts of RDF are:
- IRI (Internationalized Resource Identifier) — basically a URI but not limited to ASCII and may contain most unicode characters.
- resource — a graph node (element) represented by an IRI (in which case it is a named node) or a anonymous dataset-local identifier (it which case it is a blank node).
- literal — a simple value represented by a string with a datatype or a language tag.
- triple — an expressions of the form subject–predicate–object to represent a graph edge of type predicate (link type) between source resource and target resource or literal.
- quad — a triple with an additional associated graph IRI.
For interoperability with other RDF-based libraries for JavaScript, the property values for entities and relations are stored as either named node or literal values using commonly used RDF/JS representation.
To provide improved type-safety with TypeScript when dealing with various kinds of IRIs from the data graph, the library uses the following branded string types:
| Type | Description |
|---|---|
ElementIri |
IRI of a entity (resource). |
ElementTypeIri |
IRI of a entity type (resource). |
LinkTypeIri |
IRI of a link type, i.e. triple predicate when the object is a resource (the predicate is always a named node). |
PropertyTypeIri |
IRI of a property type, i.e. triple predicate when the object is a literal (the predicate is always a named node). |
The library provides a number of built-in DataProvider interface implementations for various scenarios:
| Provider | Description |
|---|---|
EmptyDataProvider |
An empty provider which returns nothing from all query methods. |
RdfDataProvider |
Provides graph data from an in-memory RDF/JS-compatible graph dataset. |
SparqlDataProvider |
Provides graph data by requesting it from a SPARQL endpoint. |
CompositeDataProvider |
Provides graph data by combining results from multiple other data providers. |
DecoratedDataProvider |
Generically wraps over another provider to modify how the requests are made or alter the results. |
IndexedDbCachedProvider |
Caches graph data returned from another data provider using browser's built-in IndexedDB storage. |
:::tip
It is recommended to extend EmptyDataProvider when implementing a data provider: this way methods can be implemented one-by-one as needed and no changes will be necessary if DataProvider will gain additional methods in the future.
:::
In this example Reactodia is initialized with RdfDataProvider which is provisioned with graph data in JSON Graph Format.
As a first step, the data in converted into RDF graph (triples), next the graph is added to the provider, finally all the nodes are added tot the diagram:
function ExampleRdfProviderProvisionFromJGF() {
const {defaultLayout} = Reactodia.useWorker(Layouts);
const {onMount} = Reactodia.useLoadedWorkspace(async ({context, signal}) => {
const {model, performLayout} = context;
// Example graph data based on JSON graph documentation:
const jsonGraph = {
"graph": {
"nodes": {
"alice": {
"label": "Alice",
"metadata": {
"type": "Person",
"birthDate": "1990-01-01"
}
},
"bob": {
"label": "Bob",
"metadata": {
"type": "Person",
"birthDate": "1990-02-02"
}
}
},
"edges": [
{
"source": "alice",
"relation": "isFriendOf",
"target": "bob",
"metadata": {
"since": "2000-03-03"
}
}
]
}
} as const;
const factory = Reactodia.Rdf.DefaultDataFactory;
const hasType = factory.namedNode(Reactodia.rdf.type);
const hasLabel = factory.namedNode(Reactodia.rdfs.label);
const triples: Reactodia.Rdf.Quad[] = [];
for (const [id, node] of Object.entries(jsonGraph.graph.nodes)) {
const iri = factory.namedNode(`graph:node:${id}`);
const {type, ...otherProperties} = node.metadata;
triples.push(
factory.quad(iri, hasType, factory.namedNode(`graph:type:${type}`)),
factory.quad(iri, hasLabel, factory.literal(node.label))
);
for (const [property, value] of Object.entries(otherProperties)) {
const propertyIri = factory.namedNode(`graph:property:${property}`);
triples.push(factory.quad(iri, propertyIri, factory.literal(value)));
}
}
for (const edge of jsonGraph.graph.edges) {
const source = factory.namedNode(`graph:node:${edge.source}`);
const target = factory.namedNode(`graph:node:${edge.target}`);
const predicate = factory.namedNode(`graph:node:${edge.relation}`);
const edgeTriple = factory.quad(source, predicate, target);
triples.push(edgeTriple);
for (const [property, value] of Object.entries(edge.metadata)) {
const propertyIri = factory.namedNode(`graph:property:${property}`);
triples.push(factory.quad(edgeTriple, propertyIri, factory.literal(value)));
}
}
const dataProvider = new Reactodia.RdfDataProvider();
dataProvider.addGraph(triples);
await model.createNewDiagram({dataProvider, signal});
for (const {element} of await dataProvider.lookup({elementTypeId: 'graph:type:Person'})) {
model.createElement(element.id);
}
await model.requestData();
await performLayout({signal});
}, []);
return (
<div className='reactodia-live-editor'>
<Reactodia.Workspace ref={onMount}
defaultLayout={defaultLayout}>
<Reactodia.DefaultWorkspace />
</Reactodia.Workspace>
</div>
);
}When exploring the graph data, Reactodia components track which data needs to be loaded and requests to fetch it based on currently displayed diagram content. For example, when an EntityElement is added to the canvas and rendered with thedefault template, the library will load corresponding entity types to display correct labels.
The library includes a number of hooks and methods to simplify data loading from a custom component which are listed below.
After adding one or more EntityElement elements to the canvas e.g. with model.createElement() (see Manipulating the diagram), it is necessary to call one or several of the following methods to initiate loading entity data and relations between them:
| Method | Description |
|---|---|
model.requestData() |
Requests to load all non-loaded (placeholder) entity elements and links connected to them. |
model.requestElementData() |
Requests to load (or reload) data for the specified set of entities. |
model.requestLinks() |
Requests to load (or reload) all relations connected to the specified sets of entities. |
It is also possible to use requestElementData() and restoreLinksBetweenElements() command effects to re-request the data on undo/redo if needed.
In some cases it is easier to manually trigger a request to load data for an entity, relation or property type:
| Method | Description |
|---|---|
model.createElementType() |
Requests to load an entity type if it has not been loaded yet. model.getElementType() can be used to get the placeholder or loaded data. |
model.createLinkType() |
Requests to load a relation type if it has not been loaded yet. model.getLinkType() can be used to get the placeholder or loaded data. |
model.createPropertyType() |
Requests to load a property type if it has not been loaded yet. model.getPropertyType() can be used to get the placeholder or loaded data. |
function MyElementTypeBadge(props: { elementTypeIri }) {
const {elementTypeIri} = props;
const {model} = Reactodia.useWorkspace();
const t = Reactodia.useTranslation();
const language = Reactodia.useObservedProperty(
model.events, 'changeLanguage', () => model.language
);
const [elementType, setElementType] = React.useState<Reactodia.ElementType>();
React.useEffect(() => {
setElementType(model.createElementType(elementTypeIri));
}, [elementTypeIri]);
const data = Reactodia.useSyncStore(
Reactodia.useEventStore(elementType?.events, 'changeData'),
() => elementType?.data
);
return (
<div className="my-badge">
{t.formatLabel(data?.label, elementTypeIri, language)}
</div>
);
}:::note
When requesting the data manually, make sure to subscribe to created instances to re-render when the data loads via useObservedProperty(), useEventStore() or manual event subscription.
:::
useKeyedSyncStore hook allows to subscribe to a set of targets and fetch the data for each:
| Store | Description |
|---|---|
subscribeElementTypes |
Subscribe and fetch entity types. |
subscribeLinkTypes |
Subscribe and fetch relation types. |
subscribeElementTypes |
Subscribe and fetch property types. |
Example: subscribe to property types from an element template
function MyElement(props: Reactodia.TemplateProps) {
const {model} = Reactodia.useWorkspace();
const t = Reactodia.useTranslation();
const language = Reactodia.useObservedProperty(
model.events, 'changeLanguage', () => model.language
);
const data = props.element instanceof Reactodia.EntityElement
? props.element.data : undefined;
// Select only properties with at least one value
const properties = Object.entries(data?.properties ?? {})
.filter(([iri, values]) => values.length > 0);
// Subscribe and fetch property types
Reactodia.useKeyedSyncStore(
Reactodia.subscribePropertyTypes,
properties.map(([iri]) => iri),
model
);
return (
<ul>
{properties.map(([iri, values])) => {
// Get property type to display
const property = model.getPropertyType(iri);
return (
<li>
{t.formatLabel(property?.data?.label, iri, language)}{': '}
{values.map(v => v.value).join(', ')}
</li>
);
}}
</ul>
);
}useProvidedEntities hook allows to loads entity data for a target set of IRIs even when the entities are not displayed on the canvas at all.
Example: load entity variants for a select input
function MyInputForShape(props: Forms.InputSingleProps) {
const {factory} = props;
const {model} = Reactodia.useWorkspace();
const {data: entities} = Reactodia.useProvidedEntities(
model.dataProvider,
[shapes.Square, shapes.Circle, shapes.Triangle]
);
const language = Reactodia.useObservedProperty(
model.events, 'changeLanguage', () => model.language
);
const variants = React.useMemo(
() => Array.from(entities.values(), (item): Forms.InputSelectVariant => ({
value: factory.namedNode(item.id),
label: model.locale.formatEntityLabel(item, language),
})),
[entities, language, factory]
);
return (
<Forms.InputSelect {...props} variants={variants} />
);
}It is possible to customize how library components display graph data by supplying a custom DataLocaleProvider when calling model.importLayout().
Data locale provider can be used to alter the following behavior:
- locale.selectEntityLabel() and locale.formatEntityLabel() to select or format default entity label from its properties (by default it looks for
rdfs:labelproperty values); - locale.selectEntityImageUrl() to select default entity thumbnail image IRI from its properties (by default it looks for
schema:thumbnailUrlproperty value); - locale.prepareAnchor() to provide props for an anchor (
<a>link) to a resource IRI; - locale.resolveAssetUrl() to resolve an IRI/URL to referenced data asset for display or download, e.g. an image (thumbnail) or a downloadable file.
:::tip
It is possible to extend DefaultDataLocaleProvider to slightly alter its behavior instead of implementing the full DataLocaleProvider interface.
:::