Посетете оригиналната публикация на адрес https://graphcms.com/blog/wine-db/ за по-добра четимост на кода/осветяване на синтаксиса.

Проектите със съдържание са сложни. Вкарването на техническите части в играта е само половината от играта. Другата половина всъщност е дефинирането на модел на съдържание, който успешно ще представи вашата идея, без да ви ограничава в бъдеще. Докато GraphCMS прави много от включените стъпки доста ясни, ние ще направим цялостен проект заедно от началото до края!

Какво ще правим
На всяка стъпка ще комбинираме някои ръчни (от потребителския интерфейс) промени, както и програмни промени, използвайки директно API.

  • Ние ще определим нашия модел на съдържание
  • Мигрирайте нашите данни
  • Показвайте съдържанието по интерактивен начин, разглеждайки различните характеристики на езика GraphQL.

Какво трябва да знаете
Въпреки че ще се опитам да пиша по начин, по който хората без технически познания могат да ме следват, ако искате да адаптирате някой от кода към собствените си случаи на употреба, ще ви трябва да знаете основите на HTML, Ecmascript, Node, GraphQL - но не се притеснявайте! Ще обясним частите, докато вървим!

Какво ще създаваме

Като човек с нюх към кулинарията, аз съм голям фен на хората от Wine Folly. Не само защото го убиват в отдела за съдържание, но защото са от родния ми щат! Така че, без да питаме (те казват, че е добре за лична употреба? 🤔), ние ще пресъздадем този плакат, цифрово.

Създаване на проекта

Първата стъпка е да създадем нашия проект.

Нека да разгледаме нашите данни! Имаме две types, от които да избираме, когато дефинираме нашата схема. model представлява сложни структури от данни, а enumeration представлява определен списък от единични стойности (помислете за нещо подобно на поле за избор).

Нека разопаковаме това.

Идентифициране на видовете

1 „Зеленчуци“
Това изглежда е определен списък. Това е one of Месо, Приготвяне, Млечни продукти и др.

  • Категория храна =› Enumeration

2 „Alliums“
Първоначално това също би изглеждало като член на определен списък, където има единствено свойство. Това обаче е малко подвеждащо. Погледнете 1 и 5? Нашата присвоена стойност (pairing, perfect pairing), както и vegetable принадлежат към този запис! Това е модел!

Това е полезно за нас по допълнителни причини. Представете си, ако искаме да направим малко повече с нашето съдържание по-късно? В момента заимстваме само името на категорията, но какво ще стане, ако искаме да включим флаг за известни алергени, като например на Nightshades? Сега имаме четвърта стойност за включване.

Защо да не направите и Vegetables модел? В един момент трябва да теглиш черта. Технически бихме могли да добавим и допълнителни данни към Vegetables, но полезен мисловен модел, с който да структурираме нашия метод, е намаляването на броя на полетата, колкото по-високо се класифицира. Vegetable е най-добрата класификация в нашия случай, така че ще ограничим набора от свойства. Запазването на това като enum също има предимства по-късно, които ще видим.

3 „лук“, „шалот“ и т.н.
Това е член на Alliums, освен това може да има много членове, така че е модел. Моделите могат да имат само един запис от избрано изброяване.

4 & 8 „Удебелено червено“
Означих това два пъти, но технически са еднакви. Bold Red е най-високата класификация в нашата аналогия с вино и затова обикновено бихме му дали Enumeration. Въпреки това, номер 2 притежава няколко екземпляра на тези класификации през краищата на pairing и perfect pairing. С изброяване можете да имате точно една селекция. Освен това, в моя случай, искам да проследя сдвояванията въз основа на типа, а не чрез добавяне на всички вина от даден тип директно към съчетаването на храната. Това ни дава повече гъвкавост в бъдеще. Например, добавянето на ново бяло вино ще изисква само избор на правилния клас вино и то ще бъде незабавно свързано в нашата графика.

5, 6 & 7
Това става малко по-сложно. 5 всъщност е препратка към 4 - което е нашият клас вино, като Bold Red. Рейтингът действително съществува като член на Food Class. Написано, Nuts & Seeds има колекция от Wine Classes, които са хранителни двойки. Освен това има колекция от Wine Classes, които са перфектни двойки.

Странична бележка, перфектното сдвояване е субективна (и може би универсална в много случаи) преценка за това колко добре се съчетава напитката.

В този случай ще добавя 6 и 7 като две полета към нашия 2 модел като връзка, съставена от 5 – нашето Wine Class. В този случай 5 е презентационен артефакт, нещо, което се явява по подразбиране на връзката между 2, 4, 6 и 7. Това ще бъдат „дефиниции на полета“ в нашия модел на хранителен клас.

Завързан език още? Продължавайте, ще стане по-ясно, когато всъщност създадем моделите!

8 (Вижте 4)

9 Malbec
Тъй като това принадлежи към категория и има поне едно допълнително поле (етикета), това е Model.

Идентифициране на полетата

От нашия горен анализ ни остава 1 Enumeration за най-високо ниво на хранителна категория. Тъй като те са прости списъци със стойности, няма полета за идентифициране.

На нашия Models обаче нивите са всичко. След като разбихме всичко, останахме с 4 Models. Един за запис за храна, един за клас храна, един за запис за вино и един за клас вино.

Всяко от тях се нуждае от собствен набор от полета, за да помогне за възстановяването на нашата визуализация по-късно и за добавяне на магически сили към нашата графика.

Модел за вино

  • Име =› String
  • Категория =› Enumeration

Класификация на виното

  • Име =› String
  • Вино =› Relation to Wine Model

Класификация на храните

  • Име =› String
  • Категория храна =› Enumeration
  • Сдвояване =› Relation to Wine Classification Model
  • WF Perfect Pairing =› Relation to Wine Classification Model

Храна

  • Име =› String
  • Класификация на храните =› Relationship to Food Classification Model

И с това нашият анализ на съдържанието е завършен! Можем да преминем към дефиниране на моделите и изброяванията в нашата схема!

Забележка относно екранните имена: В бъдеще ще предложим тип поле, което е просто падащо меню от низове, което ще позволи по-добра обработка на екранните имена. В моя проект бих могъл също да използвам всички модели, за да мога да контролирам моите показвани имена, тъй като изброяванията имат много строги правила за техния състав. За моите цели по-късно ще ги форматирам до желаните стойности на клиента.

Дефиниране на схемата

Ще редактираме нашата схема по два начина. Ръчно и с API.

Ето как създаваме enum с потребителския интерфейс.

Забележка: В по-ранна итерация на тази публикация имах категорията вино като Enum. Това видео всъщност вече не е част от урока, но е полезно да покаже как можете да създадете enum ръчно.

Напълно лесно! Но ако имаме няколко модела за дефиниране или много опции за добавяне към нашите изброявания, много по-лесно е да направим това в код!

Влезте в API Playground

Трябва да получим stageId на нашия проект, така че ще използваме тази заявка, за да постигнем това.

Имайте предвид, че ние сме в API за управление. Приложният програмен интерфейс (API) за управление ни позволява да променяме всички аспекти на проекта, а API на проекта ни позволява да променяме всички аспекти на съдържанието.

Двойна забележка: API за управление все още се разработва, възможно е да има някои критични промени. Ще актуализираме съответно тази публикация, но ако нещата не работят, вижте документацията.

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

Нека създадем последното ни enum със следната заявка и въвеждане на променлива:

Заявка

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

Променливи

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

Можете да си представите какво спестяване на време създава това, ако ви бъде предоставен голям списък от enum стойности, които иначе трябва да копирате и поставите! Сега е време да преминем към създаването на някои модели. Разбира се, това е доста лесно в GUI.

И, разбира се, също е доста лесно от API. Първо трябва да създадем модел и да получим неговия идентификатор при връщане.

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

Използвайки id от предишната заявка, можем да създадем полетата.

Мутация

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
#     }
#   }
}

Променливи

{ "modelID": "YOUR_MODEL_ID_FROM_PREVIOUS_MUTATION" }

Една от предимствата на GraphCMS е, че ние абстрахираме част от болката от писането на тези заявки, когато не искате. В случай на добавяне на полета, всъщност не е по-лесно да използвате API. Ще създадем само един модел с полета чрез API.

Този модел, както и повечето други модели, ще включват Reference - преди да обясним това, вижте дали можете да получите нов модел, който да изглежда така!

Препратките са мощни в графична база данни, те са това, което задвижва магическите връзки, които позволяват много подробни заявки. Можете да прочетете повече за тях в нашата документация тук.

В нашия случай ще създадем one to many връзка между нашия модел на вино и нашия модел за класификация на виното. Причината е, че нашата класификация на вина може да има many вина, но за настоящите ни нужди от съдържание едно вино може да има само one класификация на вина. Плъзнете референтното поле върху модела на вино и подредете настройките по следния начин:

Продължавайки напред, можем да създадем нашия хранителен модел, който също ще се нуждае от връзка. Започнете, като създадете модел, който изглежда така:

И след това добавете връзка с тези настройки. Подобно на виното, една храна може да има one хранителен клас, но хранителен клас може да има many храни.

Сега, когато се настанихме комфортно в нашите способности за „Връзка“. Ще преразгледаме нашия модел на хранителен клас и ще персонализираме някои свойства на връзката там.

Нашите съчетания и нашите перфектни съчетания са свързани с модела Wine Class. В един пример подселекция от класове вина са двойки, а в другия подселекция от класове вина са перфектни двойки. В този случай ние също не искаме API ID по подразбиране, искаме да го персонализираме към нещо, за което можем да разсъждаваме, когато разглеждаме нашия отговор на данни от сървъра.

Добавете още две reference полета към модела Wine Class и съпоставете тези конфигурации.

Сдвояване

Перфектни двойки

Тук имаме две релации, които сочат към един и същ основен модел, но са персонализирани в именуването, за да ни помогнат да идентифицираме тяхната цел по-късно.

Добавяне на второ уникално поле към модел

Предупреждение: Предстои задълбочено използване на API

Докато четете това, е много вероятно добавянето на допълнителни уникални полета към модел да се поддържа от потребителския интерфейс. Дотогава ще правим това чрез API Explorer. Ще добавим второ поле, за да проследим разликата между нашето екранно име и второ поле „slug“. Например, ако искаме да създадем навигационен маршрут в нашето действие до Rosé, вид вино, охлювът ще трябва да бъде лишен от всякакви специални знаци само до Rose.

Ето мутацията.

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
    }
  }
}

и ще добавим охлюв към нашия модел FoodClass по същата причина.

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
    }
  }
}

И с това нашите модели на съдържание са завършени! Сега можем да преминем към мигриране на данни!

Импортиране на данните

Следващата стъпка ще бъде писането на нашия скрипт за импортиране. Ето къде някои познания за Node.js ще бъдат полезни и/или необходими.

Създаване на нашия скриптов проект

Нека създадем нов скриптов проект. Създайте нова директория на вашия компютър, навигирайте вътре от терминала и изпълнете следните серии от команди;

# 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

Създаване на токен за нашия API

По подразбиране нашият API е затворен за обществеността. Ще трябва да се удостоверим по някакъв начин. За да направим това, можем да създадем постоянен токен за удостоверяване (Да, те могат да бъдат изтрити по-късно!)

Ето бърза анимация как да направите това.

Кодиране

Импортиране на класове за вино

Сега нека отворим importWineClass.js в редактор на код. Винаги е добра идея да създадете зависимите части от данни, преди да въведете основния си модел на съдържание. В нашия случай и двата ни модела на данни са доста прости и затова ще преминем направо в дълбокия край и ще ви покажем как да импортирате И ДВАТА наведнъж и да ги свържете! Дръж се!

Ето тялото на нашия скрипт.

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"))
    
})

Проницателният ще забележи, че съм забравил да добавя статуса PUBLISHED към моя клас вино. Е, моята грешка е вашата победа, ето примерна заявка в API Explorer, която демонстрира колко лесни са груповите актуализации.

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

Силата на API Explorer не може да бъде подценена!

Искам да обясня израза за мутация от нашата функция за създаване по-горе. Тук се случва много, така че записах кратко видео, за да обясня частите.

Върнете се в прозореца на вашето съдържание и вижте дали разполагаме с нашите данни. Нека направим тестова заявка в API Explorer, за да се уверим, че всичко изглежда така, както бихме очаквали!

{
 wines {
  name
  wineClass {
    name
  }
 } 
}

И ето още една заявка, която ще започне да разкрива част от силата зад графичните връзки.

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

Добра работа! Сега ще повторим процеса, за да импортираме нашите класификации на храните. Ето как изглежда нашият набор от данни foodClasses.csv.

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

Това би било доста типично за начина, по който можем да получим данни от източник на данни на трета страна. Имах предимството да мога да изработя ръчно данните, но се опитах да копирам стандартно поведение при експортиране на csv.

Забележка: Авторът е свършил малко допълнителна работа, за да гарантира целостта на данните, да се надяваме, че няма да имате никакви проблеми, но тъй като това беше ръчно генериран набор от данни, трябваше да се случат някои проблеми с дебелия пръст!

Обратно към сценария! Нека отворим importFoodClass.js и да поставим този скрипт.

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"))
})

Нека тестваме нашите импортирани данни досега. Ще напишем заявка, която комбинира множество стойности.

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

Надяваме се, че започвате да се вълнувате! Вижте цялото това вино, искам да кажа, данни! Сега за последния внос! Храна!

Отворете importFood.js и поставете този скрипт:

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")) 
})

И нашето съдържание е внесено! Нека изпълним една последна заявка, която ще провери дали всички наши данни присъстват и са свързани.

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

Колко готино е това?!

Сега можете да прегледате и да почистите някои от импортираните данни. Можете или да навиете свой собствен скрипт, както направихме по-горе, или можете да използвате интерфейса. Ето GIF, показващ някои основни редактиране на данни.

С изключение на модела WineClass, показваното име е правилно за визуализацията, която ще създаваме. И само с 9 записа, ние просто ще ги изчистим бързо на ръка.

Добавяне на дизайна

Нека отново да погледнем диаграмата. Какъв е най-добрият начин да поискате тези данни по смислен и структуриран начин? Изглежда, че нашата ос Y ще се състои от класове храни, а нашата ос X ще се състои от класове вино. Там, където двете се срещат, имаме променлива за No Pairing, Pairing или Perfect Pairing.

И така, за всеки клас храна искам сдвояването на класа вино и вложената храна. Искам да групирам класовете храни по категория храни. Ето как изглежда това:

{
  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
        }
      }
    }
  }
}

Тъй като това е инфографика, ние ще използваме дефакто D3 библиотеката. Той е сравнително прав и доста мощен. Ще предоставя целия код, ако не знаете D3, но самият проект ще бъде подробно документиран, ако искате да научите повече за случващото се!

Ще използваме страхотната онлайн услуга Observable HQ за нашата визуализация. Това ни позволява да комбинираме код и текст по смислен начин, без да се налага да настройваме собствени сървъри. Мислете за това като за игра на кодове.

Едно нещо, което трябва да се отбележи веднага е, че инфографиката не е генерирана програмно. ПРЕДСТАВЯНЕТО на данните не е структурирано според никаква лесно изведена логика. Класовете вина са сортирани според вкуса, а категориите храни изглеждат сортирани повече или по-малко на случаен принцип. Самите класове храни не следват никаква логика, освен че може би по-често срещаните елементи са в горната част на списъка. Като начало ще използваме проста логика, така че нашият ред на данните ще бъде неправилно подравнен с предоставеното изображение, но структурно трябва да изглежда доста подобно.

Също така манипулирах предоставените данни чрез различни трансформации, когато имах нужда. Това ни позволява да работим с една заявка за данни. Реалността е, че ако сте добре да правите няколко допълнителни заявки, GraphQL е идеално подходящ за визуализации на данни!

С D3 имаме достъп до scale функции, които ни позволяват да картографираме данни към дефинирани изходи. Като пример, съпоставих discrete входа на нашите класове храни (Red Meat, Cured Meat и т.н.) към числовия диапазон от 0 до ~1200. Това означава, че ако предам стойност Alliums във функцията за мащабиране, ще получа показание за височина приблизително по средата на инфографиката! Това е наистина страхотно!

Проблемът възниква с нашата категория храни, като Meat. Имаме нужда от начин да кажем на правоъгълника да започва от дадена височина и да завършва на дадена височина, но нашите данни не са форматирани за това! Това, от което се нуждаем, е нова форма на нашите данни, масив от обекти, които са групирани по категории храни (така че можем да обхождаме нашите приблизително 7 категории), но с достъп до техните „съдържащи се“ класове храни, така че да можем да използваме нашата функция за мащабиране, за да извличане на първия и последния елемент в съответното групиране и съответните им позиции. Още ли сте объркани?

Ето как нашата главна заявка от по-горе предостави данните:

"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],
}
... ]

Но за етикетите имам нужда от данни, оформени по-скоро така:

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

Използвах тази функция, за да форматирам данните според нуждите ми:

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
}

Можех също така просто да напиша друга GraphQL заявка като тази:

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

Това използва функция, наречена „Псевдоними“, която ни позволява да картографираме данните към нови ключове в отговора. Гъвкавостта на GraphQL наистина блести тук! Тъй като размерът на нашите данни е сравнително малък, беше по-изгодно просто да променя данните си на клиента. За по-сложно събиране на данни препоръчвам да разгледате библиотеки като Crossfilter, които са специално създадени за подобни проблеми.

Отворете портите на API!

Последното нещо, което трябва да направим, е да отворим нашия API за публична употреба. За да направим това, трябва да активираме API за публично четене.

ЗАБЕЛЕЖКА: Имате ограничения за броя на ЧЕТЕНЕТА за вашия проект! Ако включите това и го споделите с куп приятели, е възможно да се сблъскате с проблеми с ограничаване на скоростта! За малък тест това вероятно не е проблем, но това не е добър производствен модел!“

И с това можем да продължим към проекта! Останалата част от този урок може да бъде намерена в Observable HQ.

За справка, това е същността на това, което ще създаваме, плюс някои екстри и няколко интерактивни диаграми!

„Можете да намерите целия проект тук.“

Ще се видим в Observable HQ!

Първоначално публикувано в graphcms.com.