Frequently Asked Questions

Getting Started & Implementation

What is the "Start to Finish: Wine DB with Hygraph" guide about?

This guide walks you through building a complete wine and food pairing database using Hygraph, covering content modeling, data migration, API usage, and data visualization. It is designed to help both technical and non-technical users understand how to structure, import, and present content using Hygraph's features. Read the full guide.

How do I create a new project in Hygraph?

You can create a new project in Hygraph by using the user interface to define your content models and enumerations, or programmatically via the Management API. The guide provides step-by-step instructions for both approaches, including how to set up models, enums, and relationships. See the tutorial.

How long does it take to implement a project with Hygraph?

Implementation time varies by project complexity. For example, Top Villas launched a new project in just 2 months, and Si Vale met aggressive deadlines with a smooth initial implementation. Hygraph's onboarding resources, free API playground, and developer account help teams get started quickly. See Top Villas case study.

What onboarding resources are available for new Hygraph users?

Hygraph offers a structured onboarding process with introduction calls, account provisioning, business and technical kickoffs, and content schema planning. Users also have access to webinars, live streams, how-to videos, extensive documentation, and a community Slack channel. Explore documentation.

How do I import data into Hygraph?

You can import data into Hygraph using scripts (e.g., Node.js with csvtojson and isomorphic-fetch) that interact with the Hygraph API. The guide provides sample scripts for importing wines, food classes, and foods from CSV files, including authentication and mutation examples. See import scripts in the guide.

How do I authenticate API requests when importing data?

To authenticate API requests, you need to create a Permanent Auth Token in your Hygraph project dashboard. This token is used in your API requests to authorize data imports and other operations. See the guide for details.

How do I test my imported data in Hygraph?

You can use the API Explorer to run GraphQL queries and verify that your data has been imported and connected correctly. The guide provides example queries for checking wines, food classes, and their relationships. See example queries.

How do I enable public API access for my Hygraph project?

To enable public API access, you need to allow public read permissions in your project settings. This is useful for sharing your data with others or for public-facing applications, but be mindful of rate limits for free projects. See the guide for instructions.

Content Modeling & Schema Design

How do I define content models and enumerations in Hygraph?

You can define content models (for complex data structures) and enumerations (for lists of values) using the Hygraph UI or API. The guide explains how to choose between models and enums, and how to set up relationships and fields for each. Learn more in the guide.

What is the difference between a model and an enumeration in Hygraph?

In Hygraph, a model represents a complex data structure with multiple fields and relationships, while an enumeration is a defined list of singular values (like a dropdown). The guide provides examples of when to use each for food categories, wine classes, and more. See examples in the guide.

How do I create relationships between models in Hygraph?

You can create relationships (references) between models using the UI or API. For example, a wine can reference a wine class, and a food can reference a food class. The guide explains how to set up one-to-many and many-to-many relationships for flexible data modeling. See relationship setup.

Can I add unique fields like slugs to my models?

Yes, you can add unique fields such as slugs to your models for use cases like URL routing or display names. The guide provides mutation examples for adding unique string fields to models via the API. See mutation examples.

How do I structure data for visualizations in Hygraph?

You can structure your data using models, enums, and relationships to support complex visualizations. The guide demonstrates how to group and query data for D3 visualizations, including using GraphQL aliases and client-side data transformations. See visualization tips.

What are references in Hygraph and why are they important?

References in Hygraph are relationships between models that enable powerful graph connections and detailed queries. They allow you to link related content, such as pairing foods with wines, and are essential for building flexible, connected data structures. Learn more about references.

API & Technical Documentation

What APIs does Hygraph provide?

Hygraph offers multiple APIs: Content API (read/write), High Performance Content API (low latency, high throughput), MCP Server API (for AI assistants), Asset Upload API, and Management API. Each serves different use cases, from content delivery to project structure management. See API Reference.

Where can I find technical documentation for Hygraph?

Comprehensive technical documentation is available at hygraph.com/docs, including API references, schema components, webhooks, and AI integrations. This resource supports both developers and non-technical users.

How do I use the API Playground in Hygraph?

The API Playground allows you to test queries and mutations against your Hygraph project. You can use it to explore your schema, run sample queries, and debug data imports or content retrieval. The guide provides examples of using the playground for project setup and data validation. See API Playground usage.

What integrations are available with Hygraph?

Hygraph integrates with Digital Asset Management systems (e.g., Aprimo, AWS S3, Bynder, Cloudinary, Imgix, Mux, Scaleflex Filerobot), Adminix, Plasmic, and supports custom integrations via SDKs and APIs. Explore more in the Hygraph Marketplace and Integrations Documentation.

Features & Capabilities

What are the key features of Hygraph?

Hygraph offers a GraphQL-native architecture, content federation, scalability, enterprise-grade security and compliance, user-friendly tools, Smart Edge Cache, localization, asset management, and cost efficiency. These features support modern content management at scale. See all features.

How does Hygraph support high-performance content delivery?

Hygraph provides high-performance endpoints designed for low latency and high read-throughput. Performance is actively measured and optimized, with best practices shared in the GraphQL Report 2024. Read about performance improvements.

How easy is Hygraph to use for non-technical users?

Hygraph is frequently praised for its intuitive user interface, ease of setup, and ability to empower non-technical users to manage content independently. Customers highlight the clear editor UI and real-time content updates. See user feedback.

What security and compliance certifications does Hygraph have?

Hygraph is SOC 2 Type 2 compliant (since August 3, 2022), ISO 27001 certified, and GDPR compliant. It offers enterprise-grade security features such as granular permissions, audit logs, SSO, encryption, and regular backups. See security features.

Pricing & Plans

What pricing plans does Hygraph offer?

Hygraph offers three main plans: Hobby (free forever), Growth (from $199/month), and Enterprise (custom pricing). Each plan includes different features, user limits, and support options. See pricing details.

What features are included in the Hobby plan?

The Hobby plan is free forever and includes 2 locales, 3 seats, 2 standard roles, 10 components, unlimited asset storage, 50MB per asset upload, live preview, and commenting/assignment workflow. Sign up for free.

What features are included in the Growth plan?

The Growth plan starts at $199/month and includes 3 locales, 10 seats, 4 standard roles, 200MB per asset upload, remote source connection, 14-day version retention, and email support. Get started with Growth.

What features are included in the Enterprise plan?

The Enterprise plan offers custom limits, advanced governance, scheduled publishing, dedicated infrastructure, global CDN, SSO, multitenancy, instant backup recovery, custom workflows, and dedicated support. Try Enterprise or request a demo.

Use Cases & Success Stories

What are common use cases for Hygraph?

Hygraph is used for building content-rich applications, data visualizations, eCommerce platforms, global marketing sites, and more. It supports developers, product managers, marketers, and content creators across industries like SaaS, media, healthcare, and eCommerce. See case studies.

What industries are represented in Hygraph's customer base?

Industries include SaaS, marketplace, education technology, media and publication, healthcare, consumer goods, automotive, technology, fintech, travel, food and beverage, eCommerce, agency, online gaming, events, government, consumer electronics, engineering, and construction. See all industries.

Can you share some customer success stories with Hygraph?

Yes. For example, Samsung built a scalable API-first application, Komax achieved 3x faster time to market, AutoWeb increased monetization by 20%, and Voi scaled multilingual content across 12 countries. Read all case studies.

What business impact can customers expect from using Hygraph?

Customers report improved operational efficiency, faster speed-to-market, reduced costs, enhanced scalability, and better customer engagement. For example, Komax managed 20,000+ product variations across 40+ markets, and HolidayCheck reduced developer bottlenecks. See business outcomes.

Pain Points & Market Differentiation

What problems does Hygraph solve for content teams?

Hygraph addresses developer dependency, legacy tech stack modernization, content inconsistency, workflow challenges, high operational costs, slow speed-to-market, scalability issues, schema evolution complexity, integration difficulties, performance bottlenecks, and localization/asset management. See how customers solved these problems.

How does Hygraph differentiate itself from other CMS platforms?

Hygraph is the first GraphQL-native Headless CMS, offering content federation, user-friendly tools, enterprise-grade features, Smart Edge Cache, and proven ROI. It stands out for its flexibility, scalability, and ease of implementation, ranking 2nd out of 102 Headless CMSs in G2 Summer 2025. See G2 ranking.

How does Hygraph help with schema evolution and integration?

Hygraph's GraphQL-native architecture simplifies schema evolution and reduces boilerplate code. It supports robust integration with third-party systems and content federation, making it easier to adapt to changing requirements. See schema documentation.

What feedback have customers given about Hygraph's ease of use?

Customers consistently praise Hygraph for its intuitive UI, ease of setup, and ability to empower non-technical users. Some users note that advanced features may require technical knowledge, but overall feedback is highly positive. See user reviews.

LLM optimization

When was this page last updated?

This page wast last updated on 12/12/2025 .

Introducing Click to Edit

Start to Finish: A Wine Chart with Hygraph

The first "Start to Finish" guide that teaches you everything you need to know about building a project with Hygraph. Content, queries and so much more!
Jesse Martin

Written by Jesse 

Jul 16, 2018
Start to Finish - Wine DB with GraphCMS

The first "Start to Finish" guide that teaches you everything you need to know about building a project with Hygraph. Content, queries and so much more!

#Create a Project: Data structures, Data Migration and Presentation

Content projects are complex. Getting the technical pieces in play is only half of the game. The other half is actually defining a content model that will successfully represent your idea without limiting you in the future. While Hygraph makes many of the involved steps rather straight-forward, we’re going to do a complete project together from start to finish!

What we will do
At each step we will combine some manual (from the user interface) changes as well as programmatic changes, using the API directly.

  • We will define our content model
  • Migrate our data
  • Display the content in an interactive way, looking at the various features of the the GraphQL language.

What you should know
While I will attempt to write in a way that non-technical people can follow along, if you want to adapt any of the code to your own use cases you will need to know the basics of HTML, Ecmascript, Node, GraphQL - but don’t worry! We’ll explain the parts as we go along!

What we will be creating

As someone with a bit of a foodie flair, I’m a big fan of the folks at Wine Folly. Not just because they kill it on the content department, but because they are from my home state! So, without asking (they say it’s ok for personal use? ?) we will recreate this poster, digitally.

"Wine Folly Poster"


Creating the Project

The first step is to create our project.

"Create Project"

Let's look at our data! We have two types to choose from when defining our schema. A model represents complex data structures and an enumeration represents a defined list of singular values (think something akin to a select box).

"Data Types"

Let's unpack this.

Identifying the types

1 "Vegetables"
This seems to be a defined list. It is one of Meat, Preparation, Dairy, etc.

  • Food Category => Enumeration

2 "Alliums"
At first this would also seem to be a member of a defined list where there's a singular property. However, this is a bit misleading. Take a look at 1 and 5? Our value assignment (pairing, perfect pairing) as well as vegetable belong to this entry! It's a model!

This is helpful for us for additional reasons. Imagine if we wanted to do a little more with our content later? Right now we only borrow the category name, but what if we wanted to include a flag for known allergens such as on Nightshades? Now we have a fourth value to include.

Why not make Vegetables a model as well? At some point you have to draw a line. Technically we could add additional data to Vegetables as well, however, a helpful mental model to structure our method with is reducing the amount of fields the higher up the classification goes. Vegetable is the top classification in our case, so we'll limit it's property set. Keeping this as an enum also has benefits later which we'll see.

  • Food Class => Model

3 "onion", "shallot", etc
This is a member of Alliums, amd there can be many members, so it's a Model. Models can only have a single entry from a selected Enumeration.

  • Food => Model

4 & 8 "Bold Red"
I labeled this twice, but they are technically the same. Bold Red is the top classification in our wine analogy, and so we would normally give it an Enumeration. However, number 2 owns several instances of these classifications through the edges of pairing and perfect pairing. With an Enumeration you can have exactly one selection. Further, in my case, I want to track the pairings based on type, not by adding all wines of a type directly to the food pairing. That gives us more flexibility in the future. For example, adding a new white wine would only require choosing the correct Wine Class and it will be instantly connected throughout our graph.

  • Wine Class => Model

5, 6 & 7
This becomes a bit more complex. 5 is actually a reference to 4 - which is our Wine Class, such as Bold Red. The rating actually exists as a member of Food Class. Written out, Nuts & Seeds has a collection of Wine Classes that are food pairings. It also has a collection of Wine Classes that are perfect pairings.

Side note, a perfect pairing is a subjective (and arguably universal in many cases) judgement about how well the drink pairs.

In this case, I am going to add 6 and 7 as two fields onto our 2 model as a relationship comprised of 5 – our Wine Class. In this case, 5 is a presentational artifact, something that appears as a default of the relationship between 2, 4, 6 and 7. These will be "field definitions" on our Food Class model.

Tongue-tied yet? Keep going, it will become clearer when we actually create the models!

8 (See 4)

9 Malbec
Since this belongs to a category and has at least one additional field (the label), it's a Model.

  • Wine => Model

Identifying the fields

From our above analysis, we are left with 1 Enumeration for the top level Food Category. Since they are simple lists of values, there's no fields to identify.

On our Models, however, the fields are everything. After we broke everything down, we were left with 4 Models. One for the Food entry, one for the Food Class, one for the Wine entry and one for the Wine Class.

Each of these need their own set of fields to help reconstruct our visual later and add the magical powers to our graph.

Wine Model

  • Name => String
  • Category => Enumeration

Wine Classification

  • Name => String
  • Wine => Relation to Wine Model

Food Classification

  • Name => String
  • Food Category => Enumeration
  • Pairing => Relation to Wine Classification Model
  • WF Perfect Pairing => Relation to Wine Classification Model

Food

  • Name => String
  • Food Classification => Relationship to Food Classification Model

And with that, our content analysis is complete! We can move on to defining the models and enumerations in our schema!

A Note about Display Names: In the future, we'll offer a field type that is simply a dropdown of strings which will allow better handling of display names. In my project I could also use all Models to be able to control my display names since Enumerations have very strict rules about their composition. For my purposes, I will format those to desired values on the client later on.


Defining the schema

We are going to edit our schema in two ways. Manually and with the API.

Here's how we create an enum with the UI.

Note: In an earlier iteration of this post, I had the wine category as an Enum. This video is not actually part of the tutorial any more, but is helpful to show how how you could create an enum manually.

"Create Enum"

Totally easy! But if we have several models to define or lots of options to add to our enums, it's by far easier to do this in code!

Enter, the API Playground

"Api Playground"

We need to get the stageId of our project, so we'll use this query to accomplish that.

Note that we are on the Management API. The Management API allows us to change all aspect of the project, where as the Project API let's us change all aspects of the content.

Double Note: The management API is still being developed, it's possible there will be some breaking changes. We will update this post accordingly, but if things aren't working, check out the documentation.

{
viewer {
projects {
name
stages {
name
id
}
}
}
}

Let's create our last enum with the following query and variable input:

Query

mutation createEnum($kinds: [String!]!) {
createEnumeration(data: {
stageId: "YOUR_STAGE_ID_FROM_THE_FIRST_QUERY",
apiId: "FoodCategory",
displayName: "Food Categories"
values: $kinds
}) {
values
}
}

Variables

{
"kinds": [
"Herbs_Spices",
"Dairy",
"Meat",
"Preparation",
"Starch",
"Sweet",
"Vegetables"
]
}

You can imagine the time savings this creates if you are provided with a large list of enum values that you otherwise have to copy and paste! Now it's time to move on to creating some models. Of course, this is quite easy in the GUI.

"Create Model"

And, of course, it's also quite easy from the API as well. First we need to create a model and get it's ID on return.

mutation {
createModel(data: {
stageId:"YOUR_STAGE_ID",
apiId: "Wine",
displayName: "Wine"
}) {
model {
id
}
}
}

Using the id from the previous query we can create the fields.

Mutation

mutation addFields($modelID: ID!) {
createStringField(data: {
modelId: $modelID,
apiId: "name",
displayName: "Name",
isRequired: true,
isUnique: true,
isList: false,
formConfig: {
renderer: "GCMS_SINGLE_LINE"
},
tableConfig: {
renderer: "GCMS_SINGLE_LINE"
}
}) {
field {
displayName
}
}

This part is no-longer relevant but is interesting to see for sake of reference!

createEnumerationField(data: {
modelId: $modelID,
enumerationId: "YOUR_ENUM_ID",
apiId: "category",
displayName: "Category",
isRequired: true,
isUnique: false,
isList: false,
formConfig: {
renderer: "GCMS"
},
tableConfig: {
renderer: "GCMS"
}
}) {
field {
displayName
}
}
}

Variables

{
"modelID": "YOUR_MODEL_ID_FROM_PREVIOUS_MUTATION"
}

One of the selling points for Hygraph is that we abstract way some of the pain of writing these queries when you don't want to. In the case of adding fields, it's not actually easier to use the API. We'll only create one model with fields via the API.

This model, and most of the other models, will incorporate a Reference - before we explain that, see if you can get a new model to look like this!

"Wine Class Base"

References are powerful in a graph database, they're what drive the magic connections that allow for highly detailed queries. You can read more about them in our documentation here.

In our case, we are going to create a one to many relationship between our wine model and our wine classification model. The reason is that our wine classification can have many wines, but for our current content needs, a wine can have only one wine classification. Drag the reference field onto the wine model and arrange the settings like this:

"Wine Class Relationship"

Moving on we can create our food model, which will also need a relationship. Start by creating a model that looks like this:

"Base Food Model

And then add a relationship with these settings. Similar to wine, a food can have one Food Class but a Food Class can have many foods.

"Food Class"

Now that we've gotten comfortable in our "Relationship" abilities. We're going to revisit our Food Class model and customize some relationship properties on there.

Our pairings and our perfect pairings are both relationships to the Wine Class model. In one example, a sub-selection of wine classes are pairs, and in the other a sub-selection of wine classes are perfect pairs. In this case, we also don't want the default API ID, we want to customize it to something we can reason about when looking at our data response from the server.

Add two more reference fields to the Wine Class model, and match these configurations.

Pairings

"Pairings"

Perfect Pairs

"Perfect Pairings"

Here we have two relationships that point to the same underlying model but are customized in naming to help us identify their purpose later on.

Adding a second unique field to a model

Warning: Deep API usage coming up

By the time you are reading this, it's quite likely that adding extra unique fields to a model will be supported from the user interface. Until then, we will do this via the API Explorer. We will be adding a second field to track a difference between our display name and a second "slug" field. For example, if we wanted to create a navigable route in our action to Rosé, a type of wine, the slug will need to be stripped of any special characters to just Rose.

Here's the mutation.

mutation {
createStringField(data: {
modelId: "YOUR_WINECLASS_MODEL_ID"
isUnique: true
isRequired: false
isList: false
apiId: "slug"
displayName: "Slug"
formConfig: {
renderer: "GCMS_SINGLE_LINE"
},
tableConfig: {
renderer: "GCMS_SINGLE_LINE"
}
}) {
field {
displayName
}
}
}

and we'll add a slug to our FoodClass model for the same reason.

mutation {
createStringField(data: {
modelId: "YOUR_FOODCLASS_MODEL_ID"
isUnique: true
isRequired: false
isList: false
apiId: "slug"
displayName: "Slug"
formConfig: {
renderer: "GCMS_SINGLE_LINE"
},
tableConfig: {
renderer: "GCMS_SINGLE_LINE"
}
}) {
field {
displayName
}
}
}

And with that, our content models are completed! Now we can move on to migrating data!

Importing the data

The next step will be writing our import script. Here's where some knowledge of Node.js will be helpful and/or required.

Creating our script project

Let's create a new script project. Create a new directory on your computer, navigate inside from the terminal and run the following series of commands;

# Init a project
$ npm inif -f
# Add dependencies
$ yarn add csvtojson isomorphic-fetch
# Create our script file
$ touch importWine.js importWineClass.js importFood.js importFoodClass.js
# Make a directory called data
$ mkdir data
# Download our data sets
# Wine
$ curl https://gist.githubusercontent.com/motleydev/689ac7b59fdecf5f70579e700ffb9524/raw/65db79a2b50a42716e2123334337f0058cc1372c/wine.csv > ./data/wine.csv
# Food
$ curl https://gist.githubusercontent.com/motleydev/0d32837213431fd7889d1f029ca58897/raw/9799c0e68f46fb05280d79fbb004777e89d5ca9e/food.csv > ./data/food.csv
# Food Classes
$ curl https://gist.githubusercontent.com/motleydev/709d3f8e1ea0aef6af67f613dfb8b635/raw/d3127a05520c72a89d498491ff078042b0936d8c/foodClasses.csv > ./data/foodClasses.csv

Creating a token for our API

By default, our api is closed to the public. We'll need to authenticate ourselves somehow. To do that, we can create a Permanent Auth Token (Yes, they can be deleted later!)

Here's a quick animation on how to do that.

"Create PAT"

Coding

Importing the Wine Classes

Now let's open up importWineClass.js in a code editor. It's always a good idea to create the dependent pieces of data before bringing in your primary content model. In our case, both our data models are rather simple, and so we're going to jump right into the deep-end and show you how to import BOTH at once, and connect them! Hold on!

Here's the body of our script.

const csv = require('csvtojson')
const fetch = require('isomorphic-fetch')
// Our endpoint, which we can get from the project dashboard
const endpoint = "YOUR_API_ENDPOINT"
// Typically you'd never want to put a token here
// in plain text, but for our little script, it's ok.
const token = "YOUR_TOKEN"
// Our mutation to write data to our database
const mutation = `mutation CreateWineClass(
$kind: String,
$wines: [WineCreateWithoutWineClassInput!]
){
createWineClass(data: {
name: $kind
slug: $kind
wines: {
create: $wines
}
}) {
name
id
}
}`;
// Our script to import the data
csv()
.fromFile('./data/wines.csv')
.then( wines => {
// Sets allow us to force unique entries,
// so we have just a set of our wine classes
// from our larger data set.
const wineClasses = new Set()
// Pushing our wine classes into our set.
for (const wine of wines) {
wineClasses.add(wine.kind)
}
// The [...wineClasses] allows us to
// convert the set to an array, so we
// can use map, so we can make async
// asynchronous calls, so, yah…
const promises = [...wineClasses].map(async wineClass => {
try {
const formattedWine = ({
kind: wineClass, // The Class
wines: wines.filter(
// Filter our wines by the current class
wine => wine.kind == wineClass
).map(
// Format our data in a way our API likes,
// see the video to explain how I figured that part out.
wine => ({name: wine.value, status: 'PUBLISHED'})
)})
// The Fetch statement to send the data for each
const resp = await fetch(endpoint, {
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`},
method: 'POST',
body: JSON.stringify({
query: mutation,
variables: formattedWine
})
})
// Parse the response to verify success
const body = await resp.json()
const data = await body.data
console.log('Uploaded', data)
return
} catch (error) {
console.log("Error!", error)
}
})
Promise.all(promises).then(()=> console.log("Done"))
})

The astute will notice that I've forgotten to add the PUBLISHED status to my wine class. Well, my mistake is your win, here's an example query in the API Explorer that demonstrates how easy bulk updates are.

mutation {
updateManyWineClasses(where: {
status: DRAFT
}, data: {
status: PUBLISHED
}) {
count
}
}

The power of the API Explorer cannot be underestimated!

I want to explain the mutation statement from our create function above. There's a lot going on here, so I've recorded a short video to explain the parts.

Go back to your content window and see if we have our data. Let's do a test query in the API Explorer to make sur everything looks like we'd expect!

{
wines {
name
wineClass {
name
}
}
}

And here's another query that will start to reveal some of the power behind the graph connections.

{
wines(where: {
wineClass: {
name_in: ["Bold_red", "Medium_Red"]
}
}, orderBy: name_ASC) {
name
}
}

Good job! Now we will repeat the process to import our food classifications. Here's what our foodClasses.csv data set looks like.

classification, kind, pairing, wfpp
Meat, RED MEAT, Bold_Red;Medium_Red, Bold_Red
Meat, CURED MEAT, Bold_Red;Medium_Red;Light_Red;Rose;Sparkling;Sweet_White;Dessert, Light_Red;Sweet_White
Meat, PORK, Bold_Red;Medium_Red;Rose;Sparkling, Medium_Red
Meat, POULTRY, Medium_Red;Light_Red;Rose;Rich_White;Light_White;Sparkling, Light_Red;Rich_White
Meat, MOLLUSK, Light_White;Sparkling, Sparkling
...

This would be quite typical for how we might get data from a third-party data source. I had the benefit of being able to hand craft the data, but tried to replicate standard csv export behavior.

Note: The author has done some additional work to ensure the integrity of the data, hopefully you won't have any issues, but since this was a hand generated data-set, some fat-finger issues were bound to happen!

Back to the scripting! Let's open up importFoodClass.js and paste in this script.

const csv = require('csvtojson')
const fetch = require('isomorphic-fetch')
// Our endpoint, which we can get from the project dashboard
const endpoint = "YOUR_API_ENDPOINT"
// Typically you'd never want to put a token here
// in plain text, but for our little script, it's ok.
const token = "YOUR_TOKEN"
// Our mutation to write data to our database
const mutation = `mutation CreateFoodClass(
$kind: String!,
$classification: FoodCategory!,
$pairing: [WineClassWhereUniqueInput!],
$wfpp: [WineClassWhereUniqueInput!],
){
createFoodClass(data: {
name: $kind,
slug: $kind,
status: PUBLISHED
foodCategory: $classification,
wineClassWFPPs: {
connect: $wfpp
},
wineClassPairings: {
connect: $pairing
}
}) {
name
}
}`;
// Our script to import the data
csv()
.fromFile('./data/foodClasses.csv')
.then( foodClasses => {
// Format our data and provide null value for missing data
// in the API, a [Type!] can be null, but can't be an array
// with null values. This would cause an import error.
const createFormattedArray = arr =>
arr.length >= 1
? arr.split(';')
.map(item => ({slug: item}))
: null
const promises = foodClasses.map(async foodClass => {
// Parse our 'item;item;item' string into an array
// with the proper shape for the API, which we find in
// the api explorer.
foodClass.pairing = createFormattedArray(foodClass.pairing)
foodClass.wfpp = createFormattedArray(foodClass.wfpp)
try {
// The Fetch statement to send the data for each
const resp = await fetch(endpoint, {
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`},
method: 'POST',
body: JSON.stringify({query: mutation, variables: foodClass})
})
// Parse the response to verify success
const body = await resp.json()
// I introduced an error catcher which wasn't in the previous
// import scripts. It's a good idea.
// As a spec rule, error will always be 'errors' which
// is a one or more array of errors and some diagnostic data.
// I just wanted the error message.
if (body.errors) {
console.log("Error", body.errors.map(error => error.message))
}
const data = await body.data
console.log('Uploaded', data)
return
} catch (error) {
console.log("Error!", error)
}
})
Promise.all(promises).then(()=> console.log("Done"))
})

Let's test our imported data so far. We're going to write a query that combines multiple values.

{
foodClasses {
name
wineClassWFPPs {
name
wines {
name
}
}
wineClassPairings {
name
wines {
name
}
}
}
}

Hopefully you're starting to get excited! Look at all that ~wine data! Now for our last import! Food!

Open importFood.js and paste in this script:

const csv = require('csvtojson')
const fetch = require('isomorphic-fetch')
// Our endpoint, which we can get from the project dashboard
const endpoint = "YOUR_API_ENDPOINT"
// Typically you'd never want to put a token here
// in plain text, but for our little script, it's ok.
const token = "YOUR_TOKEN"
// Our mutation to write data to our database
const mutation = `mutation CreateFood(
$value:String!,
$kind:String!
) {
createFood(
data: {
name: $value,
status: PUBLISHED,
foodClass: {
connect: {
slug: $kind
}
}
}
){
name
}
}`;
// Our script to import the data
csv()
.fromFile('./data/food.csv')
.then( foods => {
const promises = foods.map(async food => {
try {
// The Fetch statement to send the data for each
const resp = await fetch(endpoint, {
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`},
method: 'POST',
body: JSON.stringify({query: mutation, variables: food})
})
// Parse the response to verify success
const body = await resp.json()
// Catch Errors
if (body.errors) {
console.log("Error", body.errors.map(error => error.message))
}
const data = await body.data
console.log('Uploaded', data)
return
} catch (error) {
console.log("Error!", error)
}
})
Promise.all(promises).then(()=> console.log("Done"))
})

And our content is imported! Let's run one last query that will check if all our data is present and connected.

{
wines {
name
wineClass {
foodClassPairings {
name
foods{
name
}
}
}
}
}

How cool is that?!

Now you can go through and clean up some of the imported data. You can either roll your own script like we've done above or you can use the interface.

With the exception of the WineClass model, the display name is correct for the visualization we will be creating. And with only 9 entries, we'll just clean those up quickly by hand.


Adding the design

Let's look at the chart again. What's the best way to ask for this data in a meaningful and structured way? It appears that our Y Axis will be comprised of Food Classes and our X Axis will be comprised of Wine Classes. Where the two meet we have a variable for either No Pairing, Pairing or Perfect Pairing.

So, for each Food Class I want the Wine Class pairings and the nested food. I want to group the Food Classes by Food Category. Here's how that Looks:

{
foodClasses(orderBy: foodCategory_ASC) {
name
foodCategory
foods {
name
}
wineClassPairings(orderBy: slug_ASC) {
name
slug
wines {
name
wineClass {
name
}
}
}
wineClassWFPPs(orderBy: slug_ASC) {
name
slug
wines {
name
wineClass {
name
}
}
}
}
}

Because this is an inforgraphic, we are going to use the defacto D3 library. It's relatively straight forward and quite powerful. I'll be providing all code if you don't know D3, but the project itself will be heavily documented if you'd like to learn more about what's going on!

We will be using the awesome online service Observable HQ for our visualization. It allows us to put together code and text in a meaningful way without having to set up our own servers. Think of it as a code playground.

One thing to note right away is that the infographic was not programmatically generated. The data PRESENTATION is not structured according to any kind of easily derived logic. Wine Classes are sorted according to taste and Food Categories seem to be sorted more or less at random. The Food Classes themselves don't follow any logic except for perhaps the more common items are at the top of the list. To start with, we will use simple logic, so our order of the data will be misaligned with the provided image, but structurally it should look quite similar.

I've also manipulated the data provided through various transforms when I needed to. This allows us to work with a single data query. The reality is, if you are ok making a handful of extra queries, GraphQL is perfectly suited for data visualizations!

With D3, we have access to scale functions that allow us to map data to defined outputs. As an example, I've mapped the discrete input of our Food Classes (Red Meat, Cured Meat, etc) to the numeric range of 0 through ~1200. That means that if I pass a value of Alliums into the scale function, I'd get a heigh readout roughly half way down the infographic! That's really cool!

The problem arises with our Food Category, such as Meat. We need a way to tell the rectangle to start at a given height and end at a given height, but our data isn't formatted for that! What we need is a new shape of our data, an array of objects that are bucketed by Food Category (so we can iterate over our roughly 7 Categories) but with access to their "contained" Food Classes so we can use our scale function to fetch the first and last item in the respective grouping and their respective positions. Confused yet?

Here's how our master query from above provided the data:

"foodClasses": [
{
"name": "HARD CHEESE",
"foodCategory": "Dairy",
"foods": [Object, Object, Object, Object, Object],
"wineClassPairings": [Object, Object, Object, Object, Object],
"wineClassWFPPs": [Object],
},
{
"name": "PUNGENT CHEESE",
"foodCategory": "Dairy",
"foods": [Object, Object, Object, Object],
"wineClassPairings": [Object, Object, Object, Object, Object, Object, Object],
"wineClassWFPPs": [Object, Object],
}
... ]

But for the labels I need data shaped more like this:

{
"Dairy": [
{
"name": "HARD CHEESE"
}
{
"name": "PUNGENT CHEESE"
}
... ]}

I used this function to format the data how I need:

foodCategoryBuckets = () => {
// foodCategoryReduce is an already
// reduced array of Food Category Strings
const data = foodCategoryReduce.reduce(
(result, item, index) => {
result[item] = [];
return result
}, {})
// wineIsServed.foodClasses is the fetched master data
for (let key of Object.keys(data)) {
data[key] = wineIsServed.foodClasses.filter(d => d.foodCategory === key)
}
return data
}

I could have also just written another GraphQL query like this:

query getFoodPairing() {
Vegetables: foodClasses (
where:{
foodCategory: Vegetables
}) {
name
}
Dairy: foodClasses(
where: {
foodCategory: Dairy
}) {
name
}
Meat: foodClasses(
where: {
foodCategory: Meat
}) {
name
}
...
}

This uses a feature called "Aliases" which allow us to map the data to new keys on the response. The flexibility of GraphQL really shines here! Because the size of our data is relatively small, it was a better cost-benefit to simply modify my data on the client. For more complex data munging, I recommend looking at libraries like Crossfilter which are tailor made for these kinds of issues.

Open the API Gates!

The last thing we need to do is open up our API for public consumption. To do this, we need to enable the API for public read.

NOTE: You have limits on the number of READS for your project! If you turn this on, and share it with a bunch of friends, it's possible you'll run into rate limitation issues! For a small test, it's likely not a problem, but this is not a good production pattern!"

"Read Permissions"

And with that, we can move on to the project! The remainder of this tutorial can be found at Observable HQ.

For reference, this is the core of what we'll be creating, plus some extras and few interactive charts as well!

"Chart"

You can find the whole project here.

See you over at Observable HQ!

Blog Author

Jesse Martin

Jesse Martin

Share with others

Sign up for our newsletter!

Be the first to know about releases and industry news and insights.