Skip to main content

Data Provider

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.

IRI and RDF

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 subjectpredicateobject 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:

TypeDescription
ElementIriIRI of a entity (resource).
ElementTypeIriIRI of a entity type (resource).
LinkTypeIriIRI of a link type, i.e. triple predicate when the object is a resource (the predicate is always a named node).
PropertyTypeIriIRI of a property type, i.e. triple predicate when the object is a literal (the predicate is always a named node).

Data Providers

The library provides a number of built-in DataProvider interface implementations for various scenarios:

ProviderDescription
EmptyDataProviderAn empty provider which returns nothing from all query methods.
RdfDataProviderProvides graph data from an in-memory RDF/JS-compatible graph dataset.
SparqlDataProviderProvides graph data by requesting it from a SPARQL endpoint.
CompositeDataProviderProvides graph data by combining results from multiple other data providers.
DecoratedDataProviderGenerically wraps over another provider to modify how the requests are made or alter the results.
IndexedDbCachedProviderCaches 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.

Example: provisioning an RdfDataProvider from a graph data in JSON Graph Format

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:

Result
Loading...
Live Editor
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>
  );
}

Loading data from a data provider

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.

Request data for entities and/or relations on the canvas

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:

MethodDescription
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.

Manually request data for entity, relation or property types

In some cases it is easier to manually trigger a request to load data for an entity, relation or property type:

MethodDescription
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.

Example: manual request and subscription for an entity type

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()

useKeyedSyncStore hook allows to subscribe to a set of targets and fetch the data for each:

StoreDescription
subscribeElementTypesSubscribe and fetch entity types.
subscribeLinkTypesSubscribe and fetch relation types.
subscribeElementTypesSubscribe 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()

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} />
);
}

Data Locale

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:

tip

It is possible to extend DefaultDataLocaleProvider to slightly alter its behavior instead of implementing the full DataLocaleProvider interface.