add graphql2chartjs to community tools (#1669)

This commit is contained in:
Rishichandra Wawhal 2019-03-14 14:30:41 +05:30 committed by Shahidh K Muhammed
parent 75ec309e8a
commit a9cd694eff
52 changed files with 15370 additions and 0 deletions

View File

@ -0,0 +1,3 @@
node_modules
lib
bundle

View File

@ -0,0 +1,3 @@
src
example
bundle

View File

@ -0,0 +1,229 @@
# graphql2chartjs - Instant realtime charts using GraphQL
`graphql2chartjs` reshapes your GraphQL data as per the [ChartJS](https://chartjs.org) API. This makes it easy to query a GraphQL API and render the output as a ChartJS chart.
For example, if you're using Postgres and [Hasura](https://hasura.io), this is what using `graphql2chartjs` looks like:
![graphql2chartjs](https://storage.googleapis.com/graphql-engine-cdn.hasura.io/img/graphql2chartjs-explained.png)
## Demos & sandbox
We've set up a GraphQL server with continuously changing data, so that you can try graphql2chartjs out easily.
|[View live charts](https://graphql2chartjs-examples.herokuapp.com) | [Edit in sandbox](https://codesandbox.io/s/p2wpj1o8pj) | [Open GraphiQL](https://g2c-examples-graphiql.herokuapp.com/) |
|---|---|---|
![realtime chart with live data](https://storage.googleapis.com/graphql-engine-cdn.hasura.io/assets/graphql2chartjs/live-chart.gif)
The demo above cover the following types of charts: [basic](https://graphql2chartjs-examples.herokuapp.com/#bar), [multiple datasets](https://graphql2chartjs-examples.herokuapp.com/#multi-bar), [mixed chart-types](https://graphql2chartjs-examples.herokuapp.com/#mixed), [realtime chart with live data](https://graphql2chartjs-examples.herokuapp.com/#live-chart), [realtime time-series](https://graphql2chartjs-examples.herokuapp.com/#timeseries-chart)
## Usage with Hasura
Hasura gives you an instant realtime GraphQL API on an existing Postgres database. You can create views to capture analytics and aggregations on your database and instantly turn them into charts.
Watch this video below to see a demo/tutorial of using Hasura with an existing Postgres database, creating views and building charts.
<div style="text-align:center">
<a href="https://www.youtube.com/watch?v=153iv1-qFuc&feature=youtu.be" target="_blank">
<img src="https://storage.googleapis.com/graphql-engine-cdn.hasura.io/assets/graphql2chartjs/g2c-youtube-embed.png" width="1000px" alt="youtube video demo">
</a>
</div>
## Example usage with react, apollo and react-chartjs-2
```javascript
import {Query} from 'react-apollo';
import gql from 'graphql-tag';
import graphql2chartjs from 'graphql2chartjs';
import {Bar} from 'react-chartjs-2';
const Chart = () => (
<Query
query={gql`
query {
Articles: articleStats {
label: title
data: num_likes
}
}`}
}>
{({data} => {
if (data) {
const g2c = new graphql2chartjs(data, 'bar');
return (<Bar data={g2c.data} />);
}
return null;
}
</Query>
);
```
## Mapping GraphQL queries to ChartJS charts
Different types of charts need different structures in their datasets.
For example a bar chart dataset needs labels and data associated for each label; the ChartJS API refers to this as `label` and `data`. Once you alias fields in your graphql query to `label` and `data`, and pass the response through `graphql2chartjs`, your dataset is ready to be used by bar chart in chartjs.
### Bar / Line / Doughnut / Pie / Radar / Polar Area / Area
Charts of this type need 2 data inputs, `label` and `data`.
```graphql
query {
ArticleLikes : articles {
label: title
data: likes
}
}
```
### Scatter / Bubble
Charts of this type need 2 data inputs: `data_x`, `data_y` (and `data_r` for bubble).
```graphql
query {
ArticleLikesVsComments : articles {
data_x: num_likes
data_y: num_comments
}
}
```
### Time series (line / bar)
Charts of this type need 2 data inputs, `data_x` or `data_t` and `data_y`. Note that there is no `label`.
```graphql
query {
StockPrices : stockprice {
data_t: created
data_y: price
}
}
```
## graphql2chartjs usage
graphql2chartjs works in 3 steps:
1. Initialise graphql2chartjs: `const g2c = new graphql2chartjs()`
2. Add data from your graphql response: `g2c.add(graphqlResponse.data, 'line')`
3. Set your chart data to the data properly of the graphql2chartjs instance: `g2c.data`
### Step 1: Initialiase with data: `new graphql2chartjs()`
#### Option 1: Initialise with data and chart type
**`graphql2chartjs(data, chartType)`**
```javascript
const g2c = new graphql2chartjs(data, 'bar');
```
- `data`: This is your GraphQL response. This data should have fields `label`, `data` etc. as per the GraphQL querying described above.
- `chartType`: This is a string that represents valid values of what your chart type is. Valid values include `'line'`, `'bar'`, `'radar'`, `'doughnut'`, `'pie'`, `'polarArea'`, `'bubble'`, `'scatter'`.
**Notes:**
- This is the simplest way of using `graphql2chartjs`
- If you have multiple datasets, all of the datasets will be rendered automatically as the same type of chart
- To customise the UI options of the rendered chart like colors or to create a mixed type chart (one dataset is rendered as a line chart, another as a bar chart) use the next initialisation method instead of this one.
#### Option 2: Initialise with data and a transform function
**`graphql2chartjs(data, transform)`**
The transformation function can add chartjs dataset props or even modify the record data:
```javascript
const g2c = new graphql2chartjs(data, (datasetName, dataPoint) => {
return {
chartType: 'bar',
backgroundColor: 'yellow'
};
});
```
- `transform(datasetName, dataPoint)`: This function defined by you can take the name of the dataset and the data record that comes from the GraphQL response and returns an object that can should have the `chartType` key and optionally other keys that specify other dataset properties.
- The object returned by this function should look like the following:
```javascript
{
chartType: 'line', // Or 'line', 'bar', 'radar', 'doughnut', 'pie', 'polarArea', 'bubble', 'scatter'
<other keys as per the dataset properties per chart. Refer to the link below>
}
```
- `chartType`: This should be a string value, one of: `'line'`, `'bar'`, `'radar'`, `'doughnut'`, `'pie'`, `'polarArea'`, `'bubble'`, `'scatter'`
- Other keys in this object should be dataset properties. These properties are slightly different for different chart types.
- Line chart: https://www.chartjs.org/docs/latest/charts/line.html#dataset-properties
- Bar chart: https://www.chartjs.org/docs/latest/charts/bar.html#dataset-properties
- Radar chart: https://www.chartjs.org/docs/latest/charts/radar.html#dataset-properties
- Doughnut & Pie: https://www.chartjs.org/docs/latest/charts/doughnut.html#dataset-properties
- Polar: https://www.chartjs.org/docs/latest/charts/polar.html#dataset-properties
- Bubble: https://www.chartjs.org/docs/latest/charts/bubble.html#dataset-properties
- Scatter: https://www.chartjs.org/docs/latest/charts/scatter.html#dataset-properties
### Step 2: Now create your cchart with data - `g2c.data`
`g2c.data` gives you access to the latest ChartJS data that can be passed to your chart.
1. Javascript
```javascript
var myChart = new Chart(ctx, { data: g2c.data });
```
2. react-chartjs-2
```javascript
<Bar data={g2c.data} />
```
### Step 3: (optional) Incrementally add data for your chart
**`g2c.add()`**
Once you've initialised a `graphql2chartjs` object, you can use the `add` function to add data for the first time or incrementally:
```javascript
await data = runQuery(..);
// Add for a chart type
g2c.add(data, 'line');
// Add with a transformation function to change UI props for the new data added or udpated
g2c.add(data, (datasetName, dataPoint) => {
chartType: 'line',
pointBackgroundColor: 'yellow'
});
```
## Installation
### Via npm
```
npm install --save graphql2chartjs
```
### Use in a script tag
```html
<script src="https://storage.googleapis.com/graphql-engine-cdn.hasura.io/tools/graphql2chartjs/index.js" type="application/javascript"></script>
```
## Reforming the data
### `reform()`
You can reform the existing data in your `graphql2chartjs` instance using the reform function that takes a reformer function as an argument. This reformer function is run over every datapoint in every dataset. For instance, to scale the x and y coordinates, you would do something like:
```
g2c.reform((datasetName, dataPoint) => {
// scale the x, y coordinates
return {
data_x: scalingFactor(dataPoint.data_x),
data_y: scalingFactor(dataPoint.data_y)
}
})
```

View File

@ -0,0 +1,16 @@
const presets = [
[
"@babel/env",
{
targets: {
edge: "17",
firefox: "60",
chrome: "67",
safari: "11.1",
},
useBuiltIns: "usage",
},
],
];
module.exports = { presets };

View File

@ -0,0 +1,15 @@
# Example
This example app is live at https://graphql2chartjs-examples.herokuapp.com
## Client (React app)
The app directory has the React app that is already set up with a backend.
## Migrations (Hasura migrations)
The hasura directory contains the migrations for the Hasura GraphQL Engine backend that is setup at https://graphql2chartjs.hasura.app and has a GraphQL endpoint https://graphqlchartjs.hasura.app/v1alpha1/graphql
## Scripts
The scripts directory contains the scripts that are run to populate the backend with sample data.

View File

@ -0,0 +1,2 @@
node_modules
build

View File

@ -0,0 +1,6 @@
To run this example:
```
npm install
npm start
```

View File

@ -0,0 +1,39 @@
{
"name": "graphql2chartjs-examples",
"version": "0.1.0",
"private": true,
"dependencies": {
"apollo-boost": "^0.1.28",
"apollo-link-ws": "^1.0.14",
"bootstrap": "^4.3.1",
"chart.js": "^2.7.3",
"graphql": "^14.1.1",
"graphql2chartjs": "^0.2.1",
"react": "^16.8.3",
"react-apollo": "^2.4.1",
"react-bootstrap": "^1.0.0-beta.5",
"react-chartjs-2": "^2.7.4",
"react-dom": "^16.8.3",
"react-scripts": "2.1.5",
"react-syntax-highlighter": "^10.1.3",
"subscriptions-transport-ws": "^0.9.15"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"
},
"eslintConfig": {
"extends": "react-app"
},
"browserslist": [
">0.2%",
"not dead",
"not ie <= 11",
"not op_mini all"
],
"devDependencies": {
"@playlyfe/gql": "^2.6.1"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

View File

@ -0,0 +1,48 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico" />
<meta
name="viewport"
content="width=device-width, initial-scale=1, shrink-to-fit=no"
/>
<meta name="theme-color" content="#000000" />
<!--
manifest.json provides metadata used when your web app is installed on a
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
-->
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.7.2/css/all.css" integrity="sha384-fnmOCqbTlWIlj8LyTjo7mOUStjsKC4pOpQbqyi7RrhN7udi9RwhKkMHpvLbHG9Sr" crossorigin="anonymous"/>
<link
rel="stylesheet"
href="https://maxcdn.bootstrapcdn.com/bootstrap/4.2.1/css/bootstrap.min.css"
integrity="sha384-GJzZqFGwb1QTTN6wy59ffF1BuGJpLSa9DkKMp0DgiMDm4iYMj70gZWKYbI706tWS"
crossorigin="anonymous"
/>
<!--
Notice the use of %PUBLIC_URL% in the tags above.
It will be replaced with the URL of the `public` folder during the build.
Only files inside the `public` folder can be referenced from the HTML.
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<title>graphql2chartjs examples</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<!--
This HTML file is a template.
If you open it directly in the browser, you will see an empty page.
You can add webfonts, meta tags, or analytics to this file.
The build step will place the bundled scripts into the <body> tag.
To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`.
-->
</body>
</html>

View File

@ -0,0 +1,15 @@
{
"short_name": "React App",
"name": "Create React App Sample",
"icons": [
{
"src": "favicon.ico",
"sizes": "64x64 32x32 24x24 16x16",
"type": "image/x-icon"
}
],
"start_url": ".",
"display": "standalone",
"theme_color": "#000000",
"background_color": "#ffffff"
}

View File

@ -0,0 +1,65 @@
.App {
text-align: center;
}
.App-logo {
animation: App-logo-spin infinite 20s linear;
height: 40vmin;
pointer-events: none;
}
.App-header {
background-color: #282c34;
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
font-size: calc(10px + 2vmin);
color: white;
}
.App-link {
color: #61dafb;
}
@keyframes App-logo-spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
.half_screen {
width: 50%;
display: inline-block;
position: relative;
}
.chartWrapper {
display: flex;
}
.loadingIndicator {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
@media(max-width: 767px) {
.half_screen {
width: 100%;
display: block;
}
.chartWrapper {
display: block;
}
}
.fiftypercent {
width: 100px;
display: inline-block;
}

View File

@ -0,0 +1,28 @@
import React from 'react';
import './App.css';
import NavBar from './Navbar';
import {
BasicBarChart,
StyledBarChart,
MultiDatasetBarChart,
MixedLineBarChart,
LiveChart,
RealtimeTimeseriesChart
} from './charts';
const App = () => (
<div>
<NavBar />
<div style={{margin: '10px', paddingTop: '65px'}}>
<BasicBarChart/>
<StyledBarChart/>
<MultiDatasetBarChart />
<MixedLineBarChart />
<LiveChart />
<RealtimeTimeseriesChart />
</div>
</div>
);
export default App;

View File

@ -0,0 +1,9 @@
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
it('renders without crashing', () => {
const div = document.createElement('div');
ReactDOM.render(<App />, div);
ReactDOM.unmountComponentAtNode(div);
});

View File

@ -0,0 +1,33 @@
import React from 'react';
import { Navbar, Nav, NavDropdown, Button } from 'react-bootstrap';
const Bar = () => {
return (
<Navbar bg="light" fixed="top">
<Navbar.Brand href="/">graphql2chartjs examples</Navbar.Brand>
<Navbar.Collapse id="basic-navbar-nav">
<Nav>
<NavDropdown title="Jump" id="basic-nav-dropdown">
<NavDropdown.Item href="#bar">Basic bar chart</NavDropdown.Item>
<NavDropdown.Item href="#styled-bar">Styled bar chart</NavDropdown.Item>
<NavDropdown.Item href="#multi-bar">Bar (multiple datasets)</NavDropdown.Item>
<NavDropdown.Item href="#mixed">Mixed chart (bar and line)</NavDropdown.Item>
<NavDropdown.Item href="#live-chart">Live chart</NavDropdown.Item>
<NavDropdown.Item href="#timeseries-chart">Time series</NavDropdown.Item>
</NavDropdown>
</Nav>
<Nav>
<Nav.Link href="https://github.com/hasura/graphql-engine/tree/master/community/tools/graphql2chartjs">
<Button variant="dark" size="sm">GitHub</Button>
</Nav.Link>
<Nav.Link href="https://codesandbox.io/s/p2wpj1o8pj">
<Button variant="link" size="sm">Edit in sandbox</Button>
</Nav.Link>
</Nav>
</Navbar.Collapse>
</Navbar>
)
}
export default Bar;

View File

@ -0,0 +1,75 @@
import React from 'react';
import { Bar } from 'react-chartjs-2';
import { docco } from 'react-syntax-highlighter/dist/esm/styles/hljs';
import SyntaxHighlighter from 'react-syntax-highlighter';
import graphql2chartjs from 'graphql2chartjs';
import { Query } from 'react-apollo';
import gql from 'graphql-tag';
const query = `
query {
ArticleLikes: article_stats {
id
label: title
data: num_likes
}
}
`;
// Chart component
const Chart = ({ query }) => (
<Query
query={gql`${query}`}
>
{
({data, error, loading}) => {
if (loading || error) {
return <div className="loadingIndicator">Please wait </div>;
}
// create graphql2chartjs instance
const g2c = new graphql2chartjs();
// add graphql data to graphql2chartjs instance
g2c.add(data, 'bar');
// render chart with g2c data :)
return (
<Bar data={g2c.data} />
)
}
}
</Query>
)
/****************************************UTILS*****************************************/
const HighlightedQuery = ({ query }) => (
<SyntaxHighlighter
language="graphql"
style={docco}
>
{query}
</SyntaxHighlighter>
)
const BasicBarChart = ({ path }) => {
return (
<div style={{margin: '10px', paddingTop: '65px'}}>
<div key="bar">
<div style={{marginBottom: '20px'}} id="bar">
<h2 style={{margin: '10px', textAlign: 'center'}}>Basic bar chart</h2>
<div className="chartWrapper">
<div className="half_screen">
<HighlightedQuery query={query} />
</div>
<div className="half_screen">
<Chart query={query}/>
</div>
</div>
</div>
<a href="https://github.com/hasura/graphql-engine/tree/master/community/tools/graphql2chartjs/example/app/src/charts/BasicBarChart.js">View source </a>
<hr />
</div>
</div>
)
}
export { BasicBarChart };

View File

@ -0,0 +1,108 @@
import React from 'react';
import { Line } from 'react-chartjs-2';
import { docco } from 'react-syntax-highlighter/dist/esm/styles/hljs';
import SyntaxHighlighter from 'react-syntax-highlighter';
import graphql2chartjs from 'graphql2chartjs';
import { Subscription } from 'react-apollo';
import gql from 'graphql-tag';
const subscription = `
subscription {
stock_price_for_Amazon: stocks (
order_by: {
created: desc
}
where: {
ticker: {
_eq: "AMZN"
}
}
limit: 100
) {
data_t: created
data_y: price
}
}
`;
// Chart component
const Chart = () => (
<Subscription
subscription={gql`${subscription}`}
>
{
({data, error, loading}) => {
if (loading || error) {
console.error(error);
return <div className="loadingIndicator">Please wait </div>;
}
// create graphql2chartjs instance
const g2c = new graphql2chartjs();
// add graphql data to graphql2chartjs instance while adding different chart types and properties
g2c.add(data, (dataSetName, dataPoint) => {
return {
...dataPoint,
chartType: 'line',
borderColor: '#333538',
pointBackgroundColor: '#333538',
backgroundColor: '#333538',
fill: false
}
});
// render chart with g2c data :)
return (
<Line
data={g2c.data}
options={{
scales: {
xAxes: [{
type: 'time'
}]
},
animation: {
duration: 0, // general animation time
},
bezierCurve : false
}}
/>
)
}
}
</Subscription>
)
/****************************************UTILS*****************************************/
const HighlightedSubscription = () => (
<SyntaxHighlighter
language="graphql"
style={docco}
>
{subscription}
</SyntaxHighlighter>
)
const LiveChart = ({ path }) => {
return (
<div style={{margin: '10px', paddingTop: '65px'}}>
<div key="live-chart">
<div style={{marginBottom: '20px'}} id="live-chart">
<h2 style={{margin: '10px', textAlign: 'center'}}>Live chart (with mock data)</h2>
<div className="chartWrapper">
<div className="half_screen">
<HighlightedSubscription subscription={subscription} />
</div>
<div className="half_screen">
<Chart />
</div>
</div>
</div>
<a href="https://github.com/hasura/graphql-engine/tree/master/community/tools/graphql2chartjs/example/app/src/charts/LiveChart.js">View source </a>
<br/>
<hr />
</div>
</div>
)
}
export { LiveChart };

View File

@ -0,0 +1,99 @@
import React from 'react';
import { Bar } from 'react-chartjs-2';
import { docco } from 'react-syntax-highlighter/dist/esm/styles/hljs';
import SyntaxHighlighter from 'react-syntax-highlighter';
import graphql2chartjs from 'graphql2chartjs';
import { Query } from 'react-apollo';
import gql from 'graphql-tag';
const query = `
query {
ArticleComments: article_stats {
id
label: title
data: num_comments
}
ArticleLikes: article_stats {
id
label: title
data: num_likes
}
}
`;
// Chart component
const Chart = () => (
<Query
query={gql`${query}`}
>
{
({data, error, loading}) => {
if (loading || error) {
console.error(error);
return <div className="loadingIndicator">Please wait </div>;
}
// create graphql2chartjs instance
const g2c = new graphql2chartjs();
// add graphql data to graphql2chartjs instance while adding different chart types and properties
g2c.add(data, (dataSetName, dataPoint) => {
if (dataSetName === 'ArticleLikes') {
// return bar chart properties for article likes
return {
...dataPoint,
chartType: 'bar',
backgroundColor: '#44c0c1',
}
}
// return line chart properties for article comments
return {
...dataPoint,
chartType: 'line',
borderColor: '#ffce49',
pointBackgroundColor: '#ffce49',
backgroundColor: '#ffce49',
fill: false
}
});
// render chart with g2c data :)
return (
<Bar data={g2c.data} />
)
}
}
</Query>
)
/****************************************UTILS*****************************************/
const HighlightedQuery = ({ query }) => (
<SyntaxHighlighter
language="graphql"
style={docco}
>
{query}
</SyntaxHighlighter>
)
const MixedLineBarChart = ({ path }) => {
return (
<div style={{margin: '10px', paddingTop: '65px'}}>
<div key="mixed">
<div style={{marginBottom: '20px'}} id="mixed">
<h2 style={{margin: '10px', textAlign: 'center'}}>Mixed chart (line and bar)</h2>
<div className="chartWrapper">
<div className="half_screen">
<HighlightedQuery query={query} />
</div>
<div className="half_screen">
<Chart />
</div>
</div>
</div>
<a href="https://github.com/hasura/graphql-engine/tree/master/community/tools/graphql2chartjs/example/app/src/charts/MixedLineBarChart.js">View source </a>
<hr />
</div>
</div>
)
}
export { MixedLineBarChart };

View File

@ -0,0 +1,94 @@
import React from 'react';
import { Bar } from 'react-chartjs-2';
import { docco } from 'react-syntax-highlighter/dist/esm/styles/hljs';
import SyntaxHighlighter from 'react-syntax-highlighter';
import graphql2chartjs from 'graphql2chartjs';
import { Query } from 'react-apollo';
import gql from 'graphql-tag';
const query = `
query {
ArticleComments: article_stats {
id
label: title
data: num_comments
}
ArticleLikes: article_stats {
id
label: title
data: num_likes
}
}
`;
// Chart component
const Chart = () => (
<Query
query={gql`${query}`}
>
{
({data, error, loading}) => {
if (loading || error) {
console.error(error);
return <div className="loadingIndicator">Please wait </div>;
}
// create graphql2chartjs instance
const g2c = new graphql2chartjs();
// add graphql data to graphql2chartjs instance while adding the backgroundcolor property
g2c.add(data, (dataSetName, dataPoint) => {
if (dataSetName === 'ArticleLikes') {
return {
...dataPoint,
chartType: 'bar',
backgroundColor: '#44c0c1',
}
}
return {
...dataPoint,
chartType: 'bar',
backgroundColor: '#ffce49',
}
});
// render chart with g2c data :)
return (
<Bar data={g2c.data} />
)
}
}
</Query>
)
/****************************************UTILS*****************************************/
const HighlightedQuery = ({ query }) => (
<SyntaxHighlighter
language="graphql"
style={docco}
>
{query}
</SyntaxHighlighter>
)
const MultiDatasetBarChart = ({ path }) => {
return (
<div style={{margin: '10px', paddingTop: '65px'}}>
<div key="multi-bar">
<div style={{marginBottom: '20px'}} id="multi-bar">
<h2 style={{margin: '10px', textAlign: 'center'}}>Bar chart (multiple datasets)</h2>
<div className="chartWrapper">
<div className="half_screen">
<HighlightedQuery query={query} />
</div>
<div className="half_screen">
<Chart />
</div>
</div>
</div>
<a href="https://github.com/hasura/graphql-engine/tree/master/community/tools/graphql2chartjs/example/app/src/charts/MultiDatasetBarChart.js">View source </a>
<hr />
</div>
</div>
)
}
export { MultiDatasetBarChart };

View File

@ -0,0 +1,204 @@
import React, {useState, useEffect} from 'react';
import { Line } from 'react-chartjs-2';
import { docco } from 'react-syntax-highlighter/dist/esm/styles/hljs';
import SyntaxHighlighter from 'react-syntax-highlighter';
import graphql2chartjs from 'graphql2chartjs';
import { ApolloConsumer } from 'react-apollo';
import gql from 'graphql-tag';
// Chart component
const Chart = ({client}) => {
const [chartJsData, setChartjsData] = useState({});
useEffect(
() => {
// initialize g2c
const g2c = new graphql2chartjs();
let latestTime = null;
// load initial data
client.query({
query: gql`${firstQuery}`
}).then((resp) => {
// add the received data to g2c and update state
g2c.add(resp.data, () => lineChartOptions);
setChartjsData(g2c.data)
// update the timestamp of the last received entry
if (resp.data.StockPriceForAmazon.length) {
latestTime = resp.data.StockPriceForAmazon[0].data_t;
}
// subscribe to a notification with newest data in the database
client.subscribe({
query: gql`${lastEventSubscription}`
}).subscribe({
next(event) {
// if the data is not stale, fetch new data and add to g2c
if (event.data.StockPriceForAmazon.length) {
if (!latestTime || event.data.StockPriceForAmazon[0].data_t > latestTime) {
fetchMore()
}
}
},
error(err) {
console.error(err);
}
})
})
const fetchMore = () => {
client.query({
query: gql`${fetchMoreQuery}`,
variables: {
time: latestTime || "2019-03-12T19:16:45.640128+00:00"
}
}).then((resp) => {
if (resp.data.StockPriceForAmazon.length) {
g2c.add(resp.data, () => lineChartOptions);
latestTime = resp.data.StockPriceForAmazon[0].data_t;
setChartjsData(g2c.data);
}
})
}
},
[]
)
return (
<Line
data={chartJsData}
options={{
scales: {
xAxes: [{
type: 'time'
}]
},
animation: {
duration: 0, // general animation time
},
bezierCurve : false
}}
/>
)
}
/****************************************UTILS*****************************************/
const firstQuery = `
query {
StockPriceForAmazon: stocks (
order_by: {
created: desc
}
where: {
ticker: {
_eq: "AMZN"
}
}
limit: 1000
) {
data_t: created
data_y: price
ticker
}
}
`;
const lastEventSubscription = `
subscription {
StockPriceForAmazon: stocks (
order_by: {
created: desc
}
where: {
ticker: {
_eq: "AMZN"
}
}
limit: 1
) {
data_t: created
}
}
`
const fetchMoreQuery = `
query ($time: timestamptz) {
StockPriceForAmazon: stocks (
order_by: {
created: desc
}
where: {
_and: [
{
ticker: {
_eq: "AMZN"
}
},
{
created: {
_gt: $time
}
}
]
}
) {
data_t: created
data_y: price
ticker
}
}
`
const HighlightedSubscription = () => (
<SyntaxHighlighter
language="graphql"
style={docco}
>
{
`
# first query to load last 1000 data points${firstQuery}
# subscription to detect any change in the database${lastEventSubscription}
# fetch data newer than the locally existing data${fetchMoreQuery}
`
}
</SyntaxHighlighter>
)
const RealtimeTimeseriesChart = ({ path }) => {
return (
<div style={{margin: '10px', paddingTop: '65px'}}>
<div key="timeseries-chart">
<div style={{marginBottom: '20px'}} id="timeseries-chart">
<h2 style={{margin: '10px', textAlign: 'center'}}>Timeseries chart (with mock data)</h2>
<div className="chartWrapper">
<div className="half_screen">
<HighlightedSubscription/>
</div>
<div className="half_screen">
<ApolloConsumer>
{
client => <Chart client={client} />
}
</ApolloConsumer>
</div>
</div>
</div>
<a href="https://github.com/hasura/graphql-engine/tree/master/community/tools/graphql2chartjs/example/app/src/charts/RealtimeTimeseriesChart.js">View source </a>
<br/>
<hr />
</div>
</div>
)
}
const lineChartOptions = {
chartType: 'line',
fill: false,
borderColor: 'brown',
pointBackgroundColor: 'brown',
showLine: false
}
export { RealtimeTimeseriesChart };

View File

@ -0,0 +1,79 @@
import React from 'react';
import { Bar } from 'react-chartjs-2';
import { docco } from 'react-syntax-highlighter/dist/esm/styles/hljs';
import SyntaxHighlighter from 'react-syntax-highlighter';
import graphql2chartjs from 'graphql2chartjs';
import { Query } from 'react-apollo';
import gql from 'graphql-tag';
const query = `
query {
ArticleLikes: article_stats {
id
label: title
data: num_likes
}
}
`;
// Chart component
const Chart = ({ query }) => (
<Query
query={gql`${query}`}
>
{
({data, error, loading}) => {
if (loading || error) {
return <div className="loadingIndicator">Please wait </div>;
}
// create graphql2chartjs instance
const g2c = new graphql2chartjs();
// add graphql data to graphql2chartjs instance while adding the backgroundcolor property
g2c.add(data, (datasetName, dataPoint) => ({
...dataPoint,
chartType: 'bar',
backgroundColor: '#44c0c1',
}));
// render chart with g2c data :)
return (
<Bar data={g2c.data} />
)
}
}
</Query>
)
/****************************************UTILS*****************************************/
const HighlightedQuery = ({ query }) => (
<SyntaxHighlighter
language="graphql"
style={docco}
>
{query}
</SyntaxHighlighter>
)
const StyledBarChart = ({ path }) => {
return (
<div style={{margin: '10px', paddingTop: '65px'}}>
<div key="styled-bar">
<div style={{marginBottom: '20px'}} id="styled-bar">
<h2 style={{margin: '10px', textAlign: 'center'}}>Styled bar chart</h2>
<div className="chartWrapper">
<div className="half_screen">
<HighlightedQuery query={query} />
</div>
<div className="half_screen">
<Chart query={query}/>
</div>
</div>
</div>
<a href="https://github.com/hasura/graphql-engine/tree/master/community/tools/graphql2chartjs/example/app/src/charts/StyledBarChart.js">View source </a>
<hr />
</div>
</div>
)
}
export { StyledBarChart };

View File

@ -0,0 +1,6 @@
export { BasicBarChart } from './BasicBarChart';
export { StyledBarChart } from './StyledBarChart';
export { MultiDatasetBarChart } from './MultiDatasetBarChart';
export { MixedLineBarChart } from './MixedLineBarChart';
export { LiveChart } from './LiveChart';
export { RealtimeTimeseriesChart } from './RealtimeTimeseriesChart';

View File

@ -0,0 +1,14 @@
body {
margin: 0;
padding: 0;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen",
"Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue",
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
code {
font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New",
monospace;
}

View File

@ -0,0 +1,31 @@
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import { ApolloClient } from 'apollo-client';
import { ApolloProvider } from 'react-apollo';
import { WebSocketLink } from 'apollo-link-ws';
import { InMemoryCache } from 'apollo-cache-inmemory';
const link = new WebSocketLink({
uri: 'wss://graphql2chartjs.hasura.app/v1alpha1/graphql',
options: {
reconnect: true
}
})
const cache = new InMemoryCache();
const client = new ApolloClient({
link,
cache
});
ReactDOM.render(
(
<ApolloProvider client={client}>
<App />
</ApolloProvider>
),
document.getElementById('root')
);

View File

@ -0,0 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 841.9 595.3">
<g fill="#61DAFB">
<path d="M666.3 296.5c0-32.5-40.7-63.3-103.1-82.4 14.4-63.6 8-114.2-20.2-130.4-6.5-3.8-14.1-5.6-22.4-5.6v22.3c4.6 0 8.3.9 11.4 2.6 13.6 7.8 19.5 37.5 14.9 75.7-1.1 9.4-2.9 19.3-5.1 29.4-19.6-4.8-41-8.5-63.5-10.9-13.5-18.5-27.5-35.3-41.6-50 32.6-30.3 63.2-46.9 84-46.9V78c-27.5 0-63.5 19.6-99.9 53.6-36.4-33.8-72.4-53.2-99.9-53.2v22.3c20.7 0 51.4 16.5 84 46.6-14 14.7-28 31.4-41.3 49.9-22.6 2.4-44 6.1-63.6 11-2.3-10-4-19.7-5.2-29-4.7-38.2 1.1-67.9 14.6-75.8 3-1.8 6.9-2.6 11.5-2.6V78.5c-8.4 0-16 1.8-22.6 5.6-28.1 16.2-34.4 66.7-19.9 130.1-62.2 19.2-102.7 49.9-102.7 82.3 0 32.5 40.7 63.3 103.1 82.4-14.4 63.6-8 114.2 20.2 130.4 6.5 3.8 14.1 5.6 22.5 5.6 27.5 0 63.5-19.6 99.9-53.6 36.4 33.8 72.4 53.2 99.9 53.2 8.4 0 16-1.8 22.6-5.6 28.1-16.2 34.4-66.7 19.9-130.1 62-19.1 102.5-49.9 102.5-82.3zm-130.2-66.7c-3.7 12.9-8.3 26.2-13.5 39.5-4.1-8-8.4-16-13.1-24-4.6-8-9.5-15.8-14.4-23.4 14.2 2.1 27.9 4.7 41 7.9zm-45.8 106.5c-7.8 13.5-15.8 26.3-24.1 38.2-14.9 1.3-30 2-45.2 2-15.1 0-30.2-.7-45-1.9-8.3-11.9-16.4-24.6-24.2-38-7.6-13.1-14.5-26.4-20.8-39.8 6.2-13.4 13.2-26.8 20.7-39.9 7.8-13.5 15.8-26.3 24.1-38.2 14.9-1.3 30-2 45.2-2 15.1 0 30.2.7 45 1.9 8.3 11.9 16.4 24.6 24.2 38 7.6 13.1 14.5 26.4 20.8 39.8-6.3 13.4-13.2 26.8-20.7 39.9zm32.3-13c5.4 13.4 10 26.8 13.8 39.8-13.1 3.2-26.9 5.9-41.2 8 4.9-7.7 9.8-15.6 14.4-23.7 4.6-8 8.9-16.1 13-24.1zM421.2 430c-9.3-9.6-18.6-20.3-27.8-32 9 .4 18.2.7 27.5.7 9.4 0 18.7-.2 27.8-.7-9 11.7-18.3 22.4-27.5 32zm-74.4-58.9c-14.2-2.1-27.9-4.7-41-7.9 3.7-12.9 8.3-26.2 13.5-39.5 4.1 8 8.4 16 13.1 24 4.7 8 9.5 15.8 14.4 23.4zM420.7 163c9.3 9.6 18.6 20.3 27.8 32-9-.4-18.2-.7-27.5-.7-9.4 0-18.7.2-27.8.7 9-11.7 18.3-22.4 27.5-32zm-74 58.9c-4.9 7.7-9.8 15.6-14.4 23.7-4.6 8-8.9 16-13 24-5.4-13.4-10-26.8-13.8-39.8 13.1-3.1 26.9-5.8 41.2-7.9zm-90.5 125.2c-35.4-15.1-58.3-34.9-58.3-50.6 0-15.7 22.9-35.6 58.3-50.6 8.6-3.7 18-7 27.7-10.1 5.7 19.6 13.2 40 22.5 60.9-9.2 20.8-16.6 41.1-22.2 60.6-9.9-3.1-19.3-6.5-28-10.2zM310 490c-13.6-7.8-19.5-37.5-14.9-75.7 1.1-9.4 2.9-19.3 5.1-29.4 19.6 4.8 41 8.5 63.5 10.9 13.5 18.5 27.5 35.3 41.6 50-32.6 30.3-63.2 46.9-84 46.9-4.5-.1-8.3-1-11.3-2.7zm237.2-76.2c4.7 38.2-1.1 67.9-14.6 75.8-3 1.8-6.9 2.6-11.5 2.6-20.7 0-51.4-16.5-84-46.6 14-14.7 28-31.4 41.3-49.9 22.6-2.4 44-6.1 63.6-11 2.3 10.1 4.1 19.8 5.2 29.1zm38.5-66.7c-8.6 3.7-18 7-27.7 10.1-5.7-19.6-13.2-40-22.5-60.9 9.2-20.8 16.6-41.1 22.2-60.6 9.9 3.1 19.3 6.5 28.1 10.2 35.4 15.1 58.3 34.9 58.3 50.6-.1 15.7-23 35.6-58.4 50.6zM320.8 78.4z"/>
<circle cx="420.9" cy="296.5" r="45.7"/>
<path d="M520.5 78.1z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.6 KiB

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1 @@
endpoint: http://localhost:8080

View File

@ -0,0 +1,77 @@
CREATE TABLE public.articles (
id integer NOT NULL,
title text NOT NULL,
created timestamp with time zone DEFAULT now() NOT NULL,
length integer NOT NULL
);
CREATE SEQUENCE public.article_id_seq
AS integer
START WITH 1
INCREMENT BY 1
NO MINVALUE
NO MAXVALUE
CACHE 1;
CREATE TABLE public.comments (
uuid uuid DEFAULT public.gen_random_uuid() NOT NULL,
article_id integer NOT NULL,
comment text NOT NULL,
created timestamp with time zone DEFAULT now() NOT NULL
);
CREATE TABLE public.likes (
uuid uuid DEFAULT public.gen_random_uuid() NOT NULL,
article_id integer NOT NULL,
created timestamp with time zone DEFAULT now() NOT NULL
);
CREATE VIEW public.article_stats AS
SELECT a.id,
a.title,
a.length,
l.count AS num_likes,
c.count AS num_comments
FROM public.articles a,
( SELECT likes.article_id,
count(likes.uuid) AS count
FROM public.likes
GROUP BY likes.article_id) l,
( SELECT comments.article_id,
count(comments.uuid) AS count
FROM public.comments
GROUP BY comments.article_id) c
WHERE ((a.id = l.article_id) AND (a.id = c.article_id));
CREATE TABLE public.stocks (
ticker text NOT NULL,
price numeric NOT NULL,
created timestamp with time zone DEFAULT now() NOT NULL
);
ALTER TABLE ONLY public.articles ALTER COLUMN id SET DEFAULT nextval('public.article_id_seq'::regclass);
ALTER TABLE ONLY public.articles
ADD CONSTRAINT article_pkey PRIMARY KEY (id);
ALTER TABLE ONLY public.comments
ADD CONSTRAINT comments_pkey PRIMARY KEY (uuid);
ALTER TABLE ONLY public.likes
ADD CONSTRAINT likes_pkey PRIMARY KEY (uuid);
ALTER TABLE ONLY public.stocks
ADD CONSTRAINT stocks_pkey PRIMARY KEY (ticker);

View File

@ -0,0 +1,2 @@
- type: replace_metadata
args: {"functions":[],"remote_schemas":[],"tables":[{"table":"article_stats","object_relationships":[],"array_relationships":[],"insert_permissions":[],"select_permissions":[{"role":"anonymous","comment":null,"permission":{"allow_aggregations":false,"columns":["id","title","length","num_likes","num_comments"],"filter":{}}}],"update_permissions":[],"delete_permissions":[],"event_triggers":[]},{"table":"comments","object_relationships":[],"array_relationships":[],"insert_permissions":[],"select_permissions":[],"update_permissions":[],"delete_permissions":[],"event_triggers":[]},{"table":"stocks","object_relationships":[],"array_relationships":[],"insert_permissions":[],"select_permissions":[{"role":"anonymous","comment":null,"permission":{"allow_aggregations":false,"columns":["ticker","price","created"],"filter":{}}}],"update_permissions":[],"delete_permissions":[],"event_triggers":[]},{"table":"articles","object_relationships":[],"array_relationships":[],"insert_permissions":[],"select_permissions":[],"update_permissions":[],"delete_permissions":[],"event_triggers":[]},{"table":"likes","object_relationships":[],"array_relationships":[],"insert_permissions":[],"select_permissions":[],"update_permissions":[],"delete_permissions":[],"event_triggers":[]}],"query_templates":[]}

View File

@ -0,0 +1,6 @@
- args:
relationship: comments
table:
name: articles
schema: public
type: drop_relationship

View File

@ -0,0 +1,13 @@
- args:
name: comments
table:
name: articles
schema: public
using:
manual_configuration:
column_mapping:
id: article_id
remote_table:
name: comments
schema: public
type: create_array_relationship

View File

@ -0,0 +1,3 @@
- args:
sql: ALTER TABLE "public"."stocks" DROP COLUMN "uuid"
type: run_sql

View File

@ -0,0 +1,6 @@
- args:
sql: CREATE EXTENSION IF NOT EXISTS pgcrypto;
type: run_sql
- args:
sql: ALTER TABLE "public"."stocks" ADD COLUMN "uuid" uuid NOT NULL DEFAULT gen_random_uuid()
type: run_sql

View File

@ -0,0 +1,6 @@
- args:
cascade: false
sql: |-
alter table stocks drop constraint stocks_pkey;
alter table stocks add constraint stocks_pkey primary key (uuid)
type: run_sql

View File

@ -0,0 +1 @@
node_modules

View File

@ -0,0 +1 @@
.env

View File

@ -0,0 +1,6 @@
FROM node:8
WORKDIR /usr/src/app
COPY package*.json ./
RUN npm install
COPY . .
CMD [ "node", "index.js" ]

View File

@ -0,0 +1,10 @@
Scripts that run to keep changing the data in the database.
```
ADMIN_SECRET=<secret> node index.js
```
```
docker build -t g2c-stocks-script:v1 .
docker run -e ADMIN_SECRET=<secret> g2c-stocks-script:v1
```

View File

@ -0,0 +1,67 @@
require('dotenv').config();
const fetch = require('node-fetch');
const trendVars = {
minor: 1,
major: 10
};
let companies = [
{ticker: 'AMZN', price: 821.66},
{ticker: 'FB', price: 490.89},
{ticker: 'AAPL', price: 853.23},
{ticker: 'GOOG', price: 829.36},
{ticker: 'MSFT', price: 871.95}
];
// Set up minor trend (updates every half second)
const changeMinor = () => {
companies = companies.map((c) => {
return {
ticker: c.ticker,
price: c.price + (Math.random() * 2) - 1
};
});
};
setInterval(changeMinor, 500);
// Set up major trend (updates every 5seconds)
const changeMajor = () => {
companies = companies.map((c) => {
return {
ticker: c.ticker,
price: c.price + (Math.random() * 20) - 10
};
});
};
setInterval(changeMajor, 5000);
// Update stock prices
const updatePrices = () => {
fetch(
'https://graphql2chartjs.hasura.app/v1alpha1/graphql',
{
method: 'POST',
body: JSON.stringify({
query: `
mutation($data: [stocks_insert_input!]!) {
insert_stocks(objects: $data) {
affected_rows
}
}
`,
variables: {
data: companies
}
}),
headers: {
'x-hasura-admin-secret': process.env.ADMIN_SECRET
}
}
).then((resp) => resp.json()).then((r) => {
console.log(JSON.stringify(r, null, 2));
setTimeout(updatePrices, 1000);
});
};
updatePrices();

View File

@ -0,0 +1,18 @@
{
"name": "scripts",
"version": "1.0.0",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
"dotenv": {
"version": "6.2.0",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-6.2.0.tgz",
"integrity": "sha512-HygQCKUBSFl8wKQZBSemMywRWcEDNidvNbjGVyZu3nbZ8qq9ubiPoGLMdRDpfSrpkkm9BXYFkpKxxFX38o/76w=="
},
"node-fetch": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.3.0.tgz",
"integrity": "sha512-MOd8pV3fxENbryESLgVIeaGKrdl+uaYhCSSVkjeOb/31/njTpcis5aWfdqgNlHIrKOLRbMnfPINPOML2CIFeXA=="
}
}
}

View File

@ -0,0 +1,14 @@
{
"name": "scripts",
"version": "1.0.0",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC",
"dependencies": {
"dotenv": "^6.2.0",
"node-fetch": "^2.3.0"
}
}

View File

@ -0,0 +1,11 @@
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
# yarn lockfile v1
dotenv@^6.2.0:
version "6.2.0"
resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-6.2.0.tgz#941c0410535d942c8becf28d3f357dbd9d476064"
node-fetch@^2.3.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.3.0.tgz#1a1d940bbfb916a1d3e0219f037e89e71f8c5fa5"

View File

@ -0,0 +1,224 @@
## How it works
### Motivation
We started using ChartJS with GraphQL so that we could leverage GraphQL's realtime subscriptions to build realtime charts. Soon enough we realised that we can automate this tedious procedure of restructuring the GraphQL data to a form that ChartJS expects.
The idea behind this tool is to generate ChartJS compliant `data` object from your GraphQL response by simply adding a few aliases in your GraphQL query.
### GraphQL Aliasing
GraphQL gives you the power of aliasing the response fields with custom names. Lets look at a simple GraphQL query.
```gql
query {
rootField {
field1
field2
}
}
```
The response to this query would be of the form:
```json
{
"data": {
"rootField": [
{
"field1": "value 1",
"field2": "value 2"
}
]
}
}
```
Now, when we alias the above GraphQL query like so:
```gql
query {
aliasedRootField: rootField {
aliasedField1: field1
aliasedField2: field2
}
}
```
The response would be:
```
{
"data": {
"aliasedRootField": {
"aliasedField1": 'value 1',
"aliasedField2": 'value 2'
}
}
}
```
### ChartJS API
Most of the ChartJS charts expect a data object of the form:
```js
{
"labels": ["label1", "label2", ..., "label10"], // list of strings
"datasets": [ // list of custom datasets with their properties
{
"data": [1334, 4314, ..., 2356],
"backgroundColor": ['red', "blue", ..., "brown"],
"borderColor": ['red', "blue", ..., "brown"],
"fill": false
}
]
}
```
### The graphql2chartjs function
The `graphql2chartjs` function i.e. the default export of this library accepts two arguments:
1. **type**: (String) Type of the chart; Eg. `bar`, `line`, `pie`
2. **graphqlData**: [Object] This should be an object with each field having its value as a list of data points.
You can directly feed the output of the `graphql2chartjs` function to your ChartJS instance.
```js
const graphQLResponse = makeGraphQLQuery();
var chartType = 'bar';
var myChart = new Chart(ctx, {
type: chartType,
data: graphql2chartjs(chartType, graphQLResponse),
options: {...} //custom options
});
```
### How the restructuring works
The `graphql2chartjs` function understands the API for each kind of chart that it supports. It constructs appropriate arrays mapping the indices of labels with other dataset properties.
Lets consider this GraphQL response:
```json
{
"data": {
"VideoGameFollowers": [
{
"id": 1,
"label": "Dota",
"data": 427014,
"pointBackgroundColor": "red",
"fill": false
},
{
"id": 2,
"label": "CS:GO",
"data": 220006,
"pointBackgroundColor": "yellow",
"fill": false
},
{
"id": 3,
"label": "NFS",
"data": 71004,
"pointBackgroundColor": "#3366ff",
"fill": false
},
{
"id": 4,
"label": "PUBG",
"data": 129769,
"pointBackgroundColor": "#330000",
"fill": false
},
{
"id": 5,
"label": "Quake 3",
"data": 90808,
"pointBackgroundColor": "green",
"fill": false
}
]
}
}
```
The above GraphQL response is restructured to the ChartJS `data` as follows:
1. It starts with initializing the `data` object as:
```json
{
"labels": [],
"datasets": []
}
```
2. It pushes a dataset with label as `humanized(rootFieldName)`. In this case, the root field is `VideoGameFollowers`. After inserting this step, the `data` object looks like
```json
{
"labels": [],
"dataset": [
{
"label": "Video game followers"
}
]
}
```
3. It then iterates over the contents of this dataset. For each datapoint in the dataset, it pushes the label to the top level `labels` array and every other property to the dataset. So, after inserting the first data point, that is:
```json
{
"id": 1,
"name": "Dota",
"data": 427014,
"pointBackgroundColor": "red",
"fill": false
}
```
the `data` object looks like:
```json
{
"labels": ["Dota"],
"datasets": [
{
"data": [427014],
"pointBackgroundColor": ["red"],
"fill": false
}
]
}
```
As you see, `pointBackgroundColor` and `data` get pushed in an array while `fill` gets set as a top level field. This is because `graphql2chartjs` function understands that the ChartJS API expects `pointBackgroundColor` to be an array and `fill` to be a simple flag.
4. It repeats the step above for every data point. The final `data` object would be:
```json
{
"labels": [ "Dota", "Cs:go", "Nfs", "Pubg", "Quake 3"],
"datasets": [
{
"label": "Video game followers",
"id": 5,
"data": [ 427014, 220006, 71004, 129769, 90808 ],
"pointBackgroundColor": ["red", "yellow", "#3366ff", "#330000", "green"],
"fill": false
}
]
}
```
Now you can pass this data object to your ChartJS instance and you will have a chart like this:
![line-chart-example](assets/readme-line-chart-example.png)
#

View File

@ -0,0 +1,32 @@
{
"name": "graphql2chartjs",
"version": "0.2.1",
"description": "",
"main": "lib/index.js",
"scripts": {
"test": "node test/index.test.js",
"compile": "rm -rf lib && ./node_modules/.bin/babel src --out-dir lib",
"bundle": "rm -rf bundle/js && ./node_modules/.bin/rollup -c",
"launch": "yarn compile && yarn bundle && yarn publish"
},
"author": "Hasura",
"repository": "hasura/graphql-engine",
"license": "ISC",
"dependencies": {
"@babel/polyfill": "^7.2.5",
"inflection": "^1.12.0"
},
"devDependencies": {
"@babel/cli": "^7.2.3",
"@babel/core": "^7.3.4",
"@babel/preset-env": "^7.3.4",
"browserify": "^16.2.3",
"rollup": "^1.3.1",
"rollup-plugin-babel": "^4.3.2",
"rollup-plugin-commonjs": "^9.2.1",
"rollup-plugin-filesize": "^6.0.1",
"rollup-plugin-multi-entry": "^2.1.0",
"rollup-plugin-node-resolve": "^4.0.1",
"rollup-plugin-progress": "^1.0.0"
}
}

View File

@ -0,0 +1,34 @@
import babel from 'rollup-plugin-babel';
import resolve from 'rollup-plugin-node-resolve';
import multiEntry from 'rollup-plugin-multi-entry';
import filesize from 'rollup-plugin-filesize';
import commonjs from 'rollup-plugin-commonjs';
import progress from 'rollup-plugin-progress';
let pluginOptions = [
multiEntry(),
resolve({
module: true,
jsnext: true,
browser: true
}),
commonjs(),
progress(),
babel({
exclude: 'node_modules/**',
}),
filesize({
showGzippedSize: false,
})
];
export default [{
input: './src/index.js',
output: {
name: 'main', // for external calls (need exports)
file: 'bundle/js/index.js',
format: 'umd',
},
moduleName: 'graphql2chartjs',
plugins: pluginOptions,
}];

View File

@ -0,0 +1,101 @@
const line = {
data: true,
borderDash: true,
pointBackgroundColor: true,
pointBorderColor: true,
pointBorderWidth: true,
pointRadius: true,
pointStyle: true,
pointRotation: true,
pointHitRadius: true,
pointHoverBackgroundColor: true,
pointHoverBorderColor: true,
pointHoverBorderWidth: true,
pointHoverRadius: true,
};
const bar = {
data: true,
backgroundColor: true,
borderColor: true,
borderWidth: true,
hoverBackgroundColor: true,
hoverBorderColor: true,
hoverBorderWidth: true
};
const pie = {
data: true,
backgroundColor: true,
borderColor: true,
borderWidth: true,
hoverBackgroundColor: true,
hoverBorderColor: true,
hoverBorderWidth: true
}
const polar = {
data: true,
backgroundColor: true,
borderColor: true,
borderWidth: true,
hoverBackgroundColor: true,
hoverBorderColor: true,
hoverBorderWidth: true
}
const scatter = {
borderDash: true,
pointBackgroundColor: true,
pointBorderColor: true,
pointBorderWidth: true,
pointRadius: true,
pointStyle: true,
pointRotation: true,
pointHitRadius: true,
pointHoverBackgroundColor: true,
pointHoverBorderColor: true,
pointHoverBorderWidth: true,
pointHoverRadius: true,
};
const bubble = {
borderDash: true,
pointBackgroundColor: true,
pointBorderColor: true,
pointBorderWidth: true,
pointRadius: true,
pointStyle: true,
pointRotation: true,
pointHitRadius: true,
pointHoverBackgroundColor: true,
pointHoverBorderColor: true,
pointHoverBorderWidth: true,
pointHoverRadius: true,
};
const radar = {
data: true,
borderDash: true,
pointBackgroundColor: true,
pointBorderColor: true,
pointBorderWidth: true,
pointRadius: true,
pointStyle: true,
pointRotation: true,
pointHitRadius: true,
pointHoverBackgroundColor: true,
pointHoverBorderColor: true,
pointHoverBorderWidth: true,
pointHoverRadius: true,
}
module.exports = {
line,
bar,
scatter,
bubble,
pie,
polar,
radar
};

View File

@ -0,0 +1,78 @@
const inflection = require('inflection');
const arrayFields = require('./array-fields');
const chartTypeMap = {
'line': arrayFields.line,
'bar': arrayFields.bar,
'radar': arrayFields.radar,
'polarArea': arrayFields.polar,
'doughnut': arrayFields.pie,
'pie': arrayFields.pie,
'bubble': arrayFields.bubble,
'scatter': arrayFields.scatter
};
function convert(graphqlData, chartType) {
const data = {
labels: [],
datasets: [],
};
const dataSets = Object.keys(graphqlData);
const numDataSets = dataSets.length;
for (let i = 0; i < numDataSets; i++) {
const dataSetName = dataSets[i];
const dataSet = graphqlData[dataSetName];
data.datasets.push({
label: inflection.transform(dataSetName, ['underscore', 'humanize']),
data: []
});
const dataSetSize = dataSet.length;
for (let j = 0; j < dataSetSize; j++) {
const element = dataSet[j];
let isRadiusDefined = element.data_r !== undefined;
if (element.data_x !== undefined || element.data_t !== undefined) {
if (element.data_x) {
const dataPoint = {
x: element.data_x,
y: element.data_y,
}
if (isRadiusDefined) {
dataPoint.r = element.data_r;
}
data.datasets[i].data.push(dataPoint);
} else if (element.data_t !== undefined) {
data.datasets[i].data.push();
let dataPoint = {
t: element.data_t,
y: element.data_y
};
if (isRadiusDefined) {
dataPoint[r] = element.data_r;
}
data.datasets[i].data.push(dataPoint);
}
}
const arrayFieldsByType = element.chartType ? chartTypeMap[element.chartType] : chartTypeMap[chartType];
Object.keys(element).forEach(property => {
if (property === 'data_x' || property === 'data_t' || property === 'data_y' || property === 'data_r') {
return;
}
if (property === 'label') {
if (i === 0) {
data.labels.push(element[property]);
}
} else if (arrayFieldsByType[property]) {
if (!data.datasets[i][property]) {
data.datasets[i][property] = [];
}
data.datasets[i][property].push(element[property]);
} else {
data.datasets[i][property === 'chartType' ? 'type' : property] = element[property];
}
});
}
};
return data;
}
module.exports = convert;

View File

@ -0,0 +1,118 @@
const converter = require('./converter');
function convert (type, graphqlData) {
try {
return converter(graphqlData, type);
} catch (e) {
console.error('unexpected error in graphql2chartjs; please check your graphql response');
console.error(e);
}
}
class Graphql2Chartjs {
constructor() {
this.data = {}
}
handleInit (graphqlData, arg) {
this.data = {};
if (typeof arg === 'string') {
this.gqlData = graphqlData;
this.chartType = arg;
this.data = convert(arg, graphqlData);
} else if (typeof arg === 'function') {
this.transformer = arg;
this.gqlData = this.transformGqlData(graphqlData, arg);
this.data = convert(this.chartType, this.gqlData);
} else {
console.error('invalid second argument to graphql2chartjs');
}
}
transformGqlData(graphqlData, transformer) {
const transformedGqlData = {};
Object.keys(graphqlData).forEach(datasetName => {
transformedGqlData[datasetName] = graphqlData[datasetName].map((dataPoint) => {
return { ...dataPoint, ...transformer(datasetName, dataPoint) }
});
});
return transformedGqlData;
}
reset (graphqlData, arg) {
this.handleInit(graphqlData, arg);
}
add (graphqlData, arg) {
if (!graphqlData) {
console.warn('invalid graphql data provided to Graphql2Chartjs');
return;
}
if (!this.gqlData || (this.gqlData && Object.keys(this.gqlData).length === 0)) {
this.handleInit(graphqlData, arg);
return;
}
this.mergeData(
(typeof arg === 'function') ? this.chartType : arg,
(typeof arg === 'function') ? this.transformGqlData(graphqlData, arg) : graphqlData
);
}
reform (transformer) {
this.gqlData = this.transformGqlData(this.gqlData, transformer);
this.data = convert(this.chartType, this.gqlData);
}
mergeData(type, graphqlData) {
const oldGqlData = { ...this.gqlData };
Object.keys(graphqlData).forEach(dsName => {
if (oldGqlData[dsName]) {
graphqlData[dsName].forEach((dp) => {
const oldDs = oldGqlData[dsName];
let oldDsLength = oldGqlData[dsName].length;
let refIndex;
for (var _i = oldDs.length - 1; _i >= 0; _i--) {
let refDp = oldDs[_i];
if (refDp.label && refDp.label === dp.label) {
refIndex = _i;
break;
} else if (refDp.data_r !== undefined) {
if (refDp.data_x === dp.data_x && refDp.data_y === dp.data_y && refDp.data_r === dp.data_r) {
refIndex = _i;
break;
}
} else if (refDp.data_x !== undefined) {
if (refDp.data_x === dp.data_x && refDp.data_y === dp.data_y) {
refIndex = _i;
break;
}
} else if (refDp.data_t !== undefined) {
if (refDp.data_t === dp.data_t && refDp.data_y === dp.data_y) {
refIndex = _i;
break;
}
}
}
if (!refIndex) {
refIndex = oldDsLength;
oldDsLength++;
oldGqlData[dsName] = [...oldGqlData[dsName], { ...dp }]
} else {
oldGqlData[dsName][refIndex] = {...oldGqlData[dsName][refIndex], ...dp};
}
})
} else {
oldGqlData[dsName] = graphqlData[dsName];
}
})
this.gqlData = oldGqlData;
this.data = convert(type, oldGqlData);
}
}
if ((typeof window) != 'undefined') {
window.Graphql2Chartjs = Graphql2Chartjs;
window.graphql2chartjs = Graphql2Chartjs;
window.GraphQL2ChartJS = Graphql2Chartjs;
window.Graphql2chartjs = Graphql2Chartjs;
}
module.exports = Graphql2Chartjs;

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,178 @@
const Graphql2Chartjs = require('../src/index');
const { vg1, vg2, vg3, scatter1, scatter2 } = require('./data.test')
const logTestResult = (condition, message) => {
if (condition) {
console.log(`Passed: ${message}`);
} else {
console.log(`Failed: ${message}`);
process.exit(1);
}
}
const runTests = () => {
console.log('Running tests \n\n');
let g2c = new Graphql2Chartjs();
logTestResult((Object.keys(g2c.data).length === 0), 'Empty initialization');
g2c = new Graphql2Chartjs()
g2c.add(vg1.data, 'line')
logTestResult(
(
g2c.data.labels.length === 5 && g2c.data.datasets.length === 1,
g2c.data.datasets[0].fill === false &&
g2c.data.datasets[0].data[0] === 427014 && g2c.data.datasets[0].pointBackgroundColor[0] === "red" &&
g2c.data.datasets[0].data[1] === 220006 && g2c.data.datasets[0].pointBackgroundColor[1] === "yellow" &&
g2c.data.datasets[0].data[2] === 71004 && g2c.data.datasets[0].pointBackgroundColor[2] === "#3366ff" &&
g2c.data.datasets[0].data[3] === 129769 && g2c.data.datasets[0].pointBackgroundColor[3] === "#330000" &&
g2c.data.datasets[0].data[4] === 90808 && g2c.data.datasets[0].pointBackgroundColor[4] === "green" &&
true
),
'Initialization with data without transformer'
)
g2c = new Graphql2Chartjs();
g2c.add(vg2.data, (dsName, dp) => {
return {
...dp, fill: true, chartType: 'line'
}
})
logTestResult(
(
g2c.data.labels.length === 5 && g2c.data.datasets.length === 1,
g2c.data.datasets[0].fill === true &&
g2c.data.datasets[0].data[0] === 427014 && g2c.data.datasets[0].pointBackgroundColor[0] === "red" &&
g2c.data.datasets[0].data[1] === 220006 && g2c.data.datasets[0].pointBackgroundColor[1] === "yellow" &&
g2c.data.datasets[0].data[2] === 71004 && g2c.data.datasets[0].pointBackgroundColor[2] === "#3366ff" &&
g2c.data.datasets[0].data[3] === 129222 && g2c.data.datasets[0].pointBackgroundColor[3] === "#330000" &&
g2c.data.datasets[0].data[4] === 90808 && g2c.data.datasets[0].pointBackgroundColor[4] === "green"
),
'Initialization with data with transformer'
)
g2c.add({ "VideoGameFollowers": [{
"id": 4,
"label": "PUBG",
"data": 129769,
"pointBackgroundColor": "#333333",
}]}, 'line')
logTestResult(
(
g2c.data.labels.length === 5 && g2c.data.datasets.length === 1,
g2c.data.datasets[0].fill === true &&
g2c.data.datasets[0].data[0] === 427014 && g2c.data.datasets[0].pointBackgroundColor[0] === "red" &&
g2c.data.datasets[0].data[1] === 220006 && g2c.data.datasets[0].pointBackgroundColor[1] === "yellow" &&
g2c.data.datasets[0].data[2] === 71004 && g2c.data.datasets[0].pointBackgroundColor[2] === "#3366ff" &&
g2c.data.datasets[0].data[3] === 129769 && g2c.data.datasets[0].pointBackgroundColor[3] === "#333333" &&
g2c.data.datasets[0].data[4] === 90808 && g2c.data.datasets[0].pointBackgroundColor[4] === "green"
),
'Update without transformer'
)
g2c.add({ "VideoGameFollowers": [{
"id": 4,
"label": "PUBG",
"data": 129769,
"pointBackgroundColor": "#333333",
}]}, (ds, dp) => {
return {
pointBackgroundColor: "#111111",
data: 120000,
chartType: 'line'
}
})
logTestResult(
(
g2c.data.labels.length === 5 && g2c.data.datasets.length === 1 &&
g2c.data.datasets[0].fill === true &&
g2c.data.datasets[0].data[0] === 427014 && g2c.data.datasets[0].pointBackgroundColor[0] === "red" &&
g2c.data.datasets[0].data[1] === 220006 && g2c.data.datasets[0].pointBackgroundColor[1] === "yellow" &&
g2c.data.datasets[0].data[2] === 71004 && g2c.data.datasets[0].pointBackgroundColor[2] === "#3366ff" &&
g2c.data.datasets[0].data[3] === 120000 && g2c.data.datasets[0].pointBackgroundColor[3] === "#111111" &&
g2c.data.datasets[0].data[4] === 90808 && g2c.data.datasets[0].pointBackgroundColor[4] === "green"
),
'Update with transformer'
)
g2c.add(scatter1.data, 'line');
logTestResult(
(
g2c.data.labels.length === 5 && g2c.data.datasets.length === 3 &&
g2c.data.datasets[1].backgroundColor === "purple" &&
g2c.data.datasets[2].backgroundColor === "orange"
),
'Update by adding a new dataset'
)
g2c.reform((dp, ds) => {
return {
chartType: 'line'
}
})
g2c.add(scatter1.data, (ds, dp) => {
if (ds === 'DataSet2') {
return {
...dp,
backgroundColor: 'red',
chartType: 'line'
};
} else if (ds === 'DataSet1') {
return {
...dp,
backgroundColor: 'green',
chartType: 'line'
};
}
return dp;
});
logTestResult(
(
g2c.data.labels.length === 5 && g2c.data.datasets.length === 3 &&
g2c.data.datasets[1].backgroundColor === "green" &&
g2c.data.datasets[2].backgroundColor === "red"
),
'Update by adding a new dataset with transformer'
)
g2c.reset(scatter1.data, (ds, dp) => {
if (ds === 'DataSet2') {
return {
...dp,
backgroundColor: 'brown',
chartType: 'scatter'
};
} else if (ds === 'DataSet1') {
return {
...dp,
backgroundColor: 'blue',
chartType: 'bubble'
};
}
return dp;
});
logTestResult(
(
g2c.data.labels.length === 0 && g2c.data.datasets.length === 2 &&
g2c.data.datasets[1].backgroundColor === "brown" &&
g2c.data.datasets[0].backgroundColor === "blue"
),
'Reset scatter new data'
)
g2c.reset(vg3.data, (ds, db) => {
return {
chartType: 'bar'
}
})
logTestResult(
(
g2c.data.labels.length === 5 && g2c.data.datasets.length === 1 &&
g2c.data.datasets[0].backgroundColor[0] === "red" && g2c.data.datasets[0].data[0] === 427014 &&
g2c.data.datasets[0].backgroundColor[1] === "yellow" && g2c.data.datasets[0].data[1] === 220006 &&
g2c.data.datasets[0].backgroundColor[2] === "#3366ff" && g2c.data.datasets[0].data[2] === 71004 &&
g2c.data.datasets[0].backgroundColor[3] === "#330000" && g2c.data.datasets[0].data[3] === 129769 &&
g2c.data.datasets[0].backgroundColor[4] === "green" && g2c.data.datasets[0].data[4] === 90808
),
'Reset with bar data'
)
}
runTests();

File diff suppressed because it is too large Load Diff