Skip to content

Latest commit

 

History

History
296 lines (241 loc) · 16 KB

File metadata and controls

296 lines (241 loc) · 16 KB
sidebar_position 2

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:

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

Data Providers

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

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:

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:

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.

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:

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.

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:

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

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