Envisioning LogoEnvisioning App (1.0.3)
Configuration

Schema

How to configure schemas for new projects

What is a Schema?

The Schema is the bridge between your raw data and the Envisioning App. It tells the app how to interpret your data by defining what each piece of data represents (entities, tags, clusters, metrics, or scopes) and how they relate to each other.

Think of it as a mapping layer that transforms arbitrary JSON data into a structured format the app understands.

Data Structure

Your data should be a JSON object where keys contain arrays of objects (entries):

{
  "allCountries": [
    { "id": "BRA", "title": "Brazil", "region": "Americas", "population": 212559409 },
    { "id": "USA", "title": "United States", "region": "Americas", "population": 331002651 }
  ],
  "allRegions": [
    { "id": "Americas", "title": "Americas" },
    { "id": "Europe", "title": "Europe" }
  ],
  "allLanguages": [
    { "id": "por", "title": "Portuguese" },
    { "id": "eng", "title": "English" }
  ]
}

The schema then describes which keys represent what type of data.

Schema Structure

const schema = {
  skipAutoJoin: false,  // Enable/disable automatic join creation
  objects: {
    // Define your objects here (entity, tag, cluster, metric, scope)
  }
}

Common Properties

All object types share these base properties:

PropertyTypeDescription
type"entity" | "tag" | "cluster" | "metric" | "scope"The object type
srcstringJSON path to the data array (e.g., "allCountries")
identifierstringPath to the unique ID field. Default: "id"
titlePathstringPath to the display title. Default: "title"
labelI18nDisplay label for the object type
singularI18nSingular form (e.g., "Country")
pluralI18nPlural form (e.g., "Countries")
descriptionI18nOptional description
filterobject | nullMongoDB-style filter to apply to the data
joinsRecord<string, Join>Relationships to other objects

I18n format: Can be a simple string "Country" or an object with language keys { en: "Country", pt: "País" }.


Object Types

Entity

The entity is your main data type - the primary items displayed in visualizations. You need exactly one entity type per project.

entities: {
  type: "entity",
  src: "allCountries",
  identifier: "id",        // Default: "id"
  titlePath: "title",
  label: { en: "Country" },
  singular: { en: "Country" },
  plural: { en: "Countries" },
  filter: null,
  joins: { /* ... */ }
}

Use case: Countries, technologies, companies, products - whatever your main dataset represents.


Tag

Tags are categories that can be assigned to entities in a many-to-many relationship. An entity can have multiple tags, and a tag can belong to multiple entities.

language: {
  type: "tag",
  src: "allLanguages",
  identifier: "id",
  titlePath: "title",
  label: { en: "Languages" },
  singular: { en: "Language" },
  plural: { en: "Languages" },
  filter: null,
  colorSpec: {             // Optional: color configuration
    type: "fixed",
    colors: ["#ff0000", "#00ff00"]
  }
}

Use case: Categories, skills, features, topics - any classification where items can have multiple values.


Cluster

Clusters group entities into mutually exclusive categories. Each entity belongs to exactly one cluster.

collection: {
  type: "cluster",
  src: "allRegions",
  identifier: "id",
  titlePath: "title",
  label: { en: "Regions" },
  singular: { en: "Region" },
  plural: { en: "Regions" },
  filter: null
}

Use case: Geographic regions, departments, status levels - any grouping where items belong to only one category.


Metric

Metrics are numerical values associated with entities. They define a measurable property with a specific domain (range of possible values).

population: {
  type: "metric",
  src: "self",           // "self" means metricResults is the source
  identifier: "value",   // Default for metrics: "value"
  titlePath: "title",
  label: { en: "Population" },
  singular: { en: "Population" },
  plural: { en: "Populations" },
  filter: null,
  metricDomain: [0, 10_000_000_000],  // [min, max] possible values
  metricRange: [0, 100],              // Optional: normalized range for display
  metricOrigin: 0,                    // Optional: zero point (useful for -10 to 10 scales)
  metricType: "CONTINUOUS",           // "CONTINUOUS" or "DISCRETE"
  unitLabel: { en: "people" },        // Optional: unit label
  metricResults: [                    // Optional: define value buckets
    { value: [0, 1_000_000], label: { en: "Small" } },
    { value: [1_000_000, 100_000_000], label: { en: "Medium" } },
    { value: [100_000_000, 10_000_000_000], label: { en: "Large" } }
  ]
}

Metric Types:

  • CONTINUOUS: Any number within the range (e.g., temperature, percentage)
  • DISCRETE: Distinct whole numbers (e.g., count, level 1-5)

src: "self": This value is exclusive to metrics. When src is "self", the metric doesn't fetch data from an external array in your data. Instead, the metricResults array becomes the metric's source. Each entity's raw value (e.g., population: 212559409) is matched against the metricResults[].value ranges to determine which bucket it belongs to.


Scope

Scopes are similar to clusters but are used for filtering the entire dataset. They provide a way to view subsets of data.

year: {
  type: "scope",
  src: "allYears",
  identifier: "id",
  titlePath: "title",
  label: { en: "Year" },
  singular: { en: "Year" },
  plural: { en: "Years" },
  filter: null,
  noScopeLabel: { en: "All Years" }  // Label when no scope is selected (null to hide option)
}

Use case: Time periods, data versions, geographic scopes - any dimension to filter the entire view.


Joins

Joins define relationships between objects. They tell the app how to connect entity data with tags, clusters, metrics, and scopes.

Join Properties

PropertyTypeDescription
fromstringSchema object key to join from
localPathstringPath in the entity where the relationship data lives
cardinality"single" | "multiple"One-to-one or one-to-many relationship
localIdentifierstringHow to extract the ID from localPath data

Understanding localIdentifier

  • "id" - The local data is an array of objects with id field: [{ id: "eng" }, { id: "por" }]
  • "value" - The local data contains the value directly (for metrics): { trl: 5 }
  • "." - The local data IS the identifier (primitive value): "Americas" or ["BRA", "ARG"]

Example Joins

joins: {
  // Single cluster relationship: entity.region = "Americas" -> cluster.id = "Americas"
  collection: {
    from: "collection",      // Schema key for regions cluster
    localPath: "region",     // Entity field containing the region
    cardinality: "single",   // Each entity has one region
    localIdentifier: "."     // The value itself is the identifier
  },

  // Multiple tags relationship: entity.languages = ["eng", "por"] -> tag.id
  language: {
    from: "language",
    localPath: "languages",
    cardinality: "multiple",
    localIdentifier: "."
  },

  // Metric relationship: entity.population = 212559409 -> metric bucket
  population: {
    from: "population",
    localPath: "population",
    cardinality: "single",
    localIdentifier: "."
  },

  // Self-referential join: entity.borders = ["ARG", "BOL"] -> other entities
  borders: {
    from: "entities",
    localPath: "borders",
    cardinality: "multiple",
    localIdentifier: "."
  }
}

Auto Join

When skipAutoJoin: false (default), the app automatically creates joins based on naming conventions:

  • If your entity has a field named language and you have a schema object with key language, a join is created automatically
  • Tags get cardinality: "multiple" with localIdentifier: "id"
  • Clusters and Scopes get cardinality: "single" with localIdentifier: "id"
  • Metrics get cardinality: "single" with localIdentifier: "value"

When to use manual joins:

  • Field names don't match schema keys (e.g., region field → collection schema)
  • You need localIdentifier: "." for primitive values
  • You want different join behavior than the defaults

Set skipAutoJoin: true if you want full control over all joins.


Complete Example

Here's a full schema for a countries database:

import type { Config } from "@envisioning/app";

export const schema = {
  skipAutoJoin: false,
  objects: {
    entities: {
      type: "entity",
      src: "allCountries",
      titlePath: "title",
      label: { en: "Country" },
      singular: { en: "Country" },
      plural: { en: "Countries" },
      filter: null,
      identifier: "id",
      joins: {
        collection: {
          from: "collection",
          localPath: "region",
          cardinality: "single",
          localIdentifier: ".",
        },
        population: {
          from: "population",
          localPath: "population",
          cardinality: "single",
          localIdentifier: ".",
        },
      },
    },
    language: {
      type: "tag",
      src: "allLanguages",
      titlePath: "title",
      label: { en: "Languages" },
      singular: { en: "Language" },
      plural: { en: "Languages" },
      filter: null,
      identifier: "id",
    },
    collection: {
      type: "cluster",
      src: "allRegions",
      titlePath: "title",
      label: { en: "Regions" },
      singular: { en: "Region" },
      plural: { en: "Regions" },
      filter: null,
      identifier: "id",
    },
    population: {
      type: "metric",
      src: "self",
      filter: null,
      titlePath: "title",
      label: { en: "Population" },
      singular: { en: "Population" },
      plural: { en: "Populations" },
      identifier: "value",
      metricDomain: [0, 10_000_000_000],
      metricResults: [
        { value: [0, 1_000_000], label: { en: "Small" } },
        { value: [1_000_000, 100_000_000], label: { en: "Medium" } },
        { value: [100_000_000, 10_000_000_000], label: { en: "Large" } },
      ],
    },
  },
} satisfies Config["schema"];

Data Transformation Tips

Your raw data often needs transformation before it matches the schema. Common patterns:

  1. Normalize IDs: Ensure all items have a consistent id field
  2. Extract relationships: Convert nested objects to arrays of IDs
  3. Create lookup tables: Generate separate arrays for tags, clusters from entity data if needed.
// Example: Transform raw country data
const transformedData = {
  allCountries: rawCountries.map(country => ({
    id: country.cca3,
    title: country.name.common,
    region: country.region,
    languages: Object.keys(country.languages),  // Extract IDs from object
    population: country.population,
  })),
  allRegions: [...new Set(rawCountries.map(c => c.region))]
    .map(region => ({ id: region, title: region })),
  allLanguages: extractUniqueLanguages(rawCountries),
};

On this page