Skip to content

Commit 99b8c6d

Browse files
Merge pull request #1722 from exogee-technology/EXOGW-430-Graphing-Library
`EXOGW-430` - Add custom dashboard to sqlite example with sales and genre popularity components
2 parents 1058043 + f42cc0a commit 99b8c6d

36 files changed

+2809
-1922
lines changed
Binary file not shown.
17.9 MB
Binary file not shown.

src/examples/sqlite/package.json

+9
Original file line numberDiff line numberDiff line change
@@ -12,22 +12,31 @@
1212
"version": "npm version --no-git-tag-version"
1313
},
1414
"dependencies": {
15+
"@apollo/client": "3.13.4",
1516
"@as-integrations/aws-lambda": "3.1.0",
1617
"@exogee/graphweaver": "workspace:*",
1718
"@exogee/graphweaver-admin-ui-components": "workspace:*",
1819
"@exogee/graphweaver-mikroorm": "workspace:*",
1920
"@exogee/graphweaver-scalars": "workspace:*",
2021
"@exogee/graphweaver-server": "workspace:*",
22+
"@nivo/bar": "0.88.0",
23+
"@nivo/core": "0.88.0",
24+
"@nivo/line": "0.88.0",
25+
"@nivo/pie": "0.88.0",
26+
"clsx": "2.1.1",
2127
"@mikro-orm/core": "6.4.11",
2228
"@mikro-orm/knex": "6.4.11",
2329
"@mikro-orm/sqlite": "6.4.11",
2430
"graphql": "16.10.0",
31+
"luxon": "3.5.0",
2532
"mikro-orm-sqlite-wasm": "workspace:*",
2633
"react": "19.0.0",
2734
"react-dom": "19.0.0"
2835
},
2936
"devDependencies": {
37+
"@types/luxon": "3.4.2",
3038
"@types/node": "22.13.14",
39+
"@types/react": "19.0.12",
3140
"graphweaver": "workspace:*",
3241
"typescript": "5.8.2"
3342
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { DefaultLayout } from '@exogee/graphweaver-admin-ui-components';
2+
import { SalesOverTimePerEmployee } from './sales-over-time-per-employee';
3+
import { GenrePopularity } from './genre-popularity';
4+
import styles from './styles.module.css';
5+
6+
export const Dashboard = () => {
7+
return (
8+
<DefaultLayout>
9+
<div className={styles.container}>
10+
<h1>Sales per employee over time</h1>
11+
<SalesOverTimePerEmployee />
12+
<h1>Customer preferences by genre</h1>
13+
<GenrePopularity />
14+
</div>
15+
</DefaultLayout>
16+
);
17+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
import { useMemo, useState } from 'react';
2+
import { ResponsivePie } from '@nivo/pie';
3+
import { Checkbox, ComboBox, SelectMode } from '@exogee/graphweaver-admin-ui-components';
4+
import { ChartColorScheme, theme } from '../utils';
5+
import { useGenrePopularityQuery } from './graphql.generated';
6+
import { defaultPieChartControls, getPieData, PieChartControls } from './utils';
7+
import styles from './styles.module.css';
8+
9+
export const GenrePopularity = () => {
10+
const { data: queryData, loading, error } = useGenrePopularityQuery();
11+
const [controls, setControls] = useState(defaultPieChartControls);
12+
const [genresRange, setGenresRange] = useState(10);
13+
14+
const data = useMemo(() => getPieData(queryData), [queryData]);
15+
16+
const rangeData = useMemo(() => data.slice(0, genresRange), [data, genresRange]);
17+
18+
if (loading) return <p>Loading...</p>;
19+
if (error) return <p>Error: {error.message}</p>;
20+
21+
const handleChange = (key: keyof PieChartControls, value: any) => {
22+
setControls((prevControls) => ({
23+
...prevControls,
24+
[key]: value,
25+
}));
26+
};
27+
28+
return (
29+
<div className={styles.container}>
30+
<div className={styles.controlsContainer}>
31+
<div className={styles.controlItem}>
32+
<label>Data points: </label>
33+
<input
34+
className={styles.rangeInput}
35+
type="range"
36+
min={0}
37+
max={data.length - 1}
38+
value={genresRange}
39+
onChange={(e) => setGenresRange(parseInt(e.target.value))}
40+
/>
41+
</div>
42+
<div className={styles.controlItem}>
43+
<label>Color scheme: </label>
44+
<ComboBox
45+
mode={SelectMode.SINGLE}
46+
options={Object.values(ChartColorScheme).map((value) => ({ value, label: value }))}
47+
onChange={(selected) => handleChange('colorScheme', selected[0].value)}
48+
value={{ value: controls.colorScheme }}
49+
placeholder="Select"
50+
/>
51+
</div>
52+
<div className={styles.controlItem}>
53+
<label>Angle: </label>
54+
<input
55+
className={styles.rangeInput}
56+
type="range"
57+
min={0}
58+
max={360}
59+
value={controls.angle}
60+
onChange={(e) => handleChange('angle', parseInt(e.target.value))}
61+
/>
62+
</div>
63+
<div className={styles.controlItem}>
64+
<label>Inner radius: </label>
65+
<input
66+
className={styles.rangeInput}
67+
type="range"
68+
min={0}
69+
max={95}
70+
value={controls.innerRadius * 100}
71+
onChange={(e) => handleChange('innerRadius', parseInt(e.target.value) / 100)}
72+
/>
73+
</div>
74+
<div className={styles.controlItem}>
75+
<label>Pad angle: </label>
76+
<input
77+
className={styles.rangeInput}
78+
type="range"
79+
min={0}
80+
max={45}
81+
value={controls.padAngle}
82+
onChange={(e) => handleChange('padAngle', parseInt(e.target.value))}
83+
/>
84+
</div>
85+
<div className={styles.controlItem}>
86+
<label>Corner radius: </label>
87+
<input
88+
className={styles.rangeInput}
89+
type="range"
90+
min={0}
91+
max={45}
92+
value={controls.cornerRadius}
93+
onChange={(e) => handleChange('cornerRadius', parseInt(e.target.value))}
94+
/>
95+
</div>
96+
<div className={styles.checkBoxContainer}>
97+
<Checkbox
98+
id="sortByValue"
99+
checked={controls.sortByValue}
100+
onChange={(e) => handleChange('sortByValue', (e.target as any).checked)}
101+
/>
102+
<label htmlFor="sortByValue">Sort by value</label>
103+
</div>
104+
<div className={styles.checkBoxContainer}>
105+
<Checkbox
106+
id="enableArcLabels"
107+
checked={controls.enableArcLabels}
108+
onChange={(e) => handleChange('enableArcLabels', (e.target as any).checked)}
109+
/>
110+
<label htmlFor="enableArcLabels">Enable arc labels</label>
111+
</div>
112+
<div className={styles.checkBoxContainer}>
113+
<Checkbox
114+
id="enableArcLinkLabels"
115+
checked={controls.enableArcLinkLabels}
116+
onChange={(e) => handleChange('enableArcLinkLabels', (e.target as any).checked)}
117+
/>
118+
<label htmlFor="enableArcLinkLabels">Enable arc link labels</label>
119+
</div>
120+
</div>
121+
<div className={styles.chartContainer}>
122+
<ResponsivePie
123+
data={rangeData}
124+
margin={{ top: 30, right: 150, bottom: 70, left: 60 }}
125+
theme={theme}
126+
colors={{ scheme: controls.colorScheme }}
127+
startAngle={controls.angle}
128+
innerRadius={controls.innerRadius}
129+
padAngle={controls.padAngle}
130+
cornerRadius={controls.cornerRadius}
131+
sortByValue={controls.sortByValue}
132+
enableArcLabels={controls.enableArcLabels}
133+
enableArcLinkLabels={controls.enableArcLinkLabels}
134+
activeOuterRadiusOffset={8}
135+
borderWidth={1}
136+
animate
137+
borderColor={{
138+
from: 'color',
139+
modifiers: [['darker', 0.2]],
140+
}}
141+
arcLinkLabelsSkipAngle={10}
142+
arcLinkLabelsThickness={2}
143+
arcLinkLabelsColor={{ from: 'color' }}
144+
arcLinkLabel="label"
145+
arcLabelsSkipAngle={10}
146+
arcLabelsTextColor={{
147+
from: 'color',
148+
modifiers: [['darker', 2]],
149+
}}
150+
legends={[
151+
{
152+
anchor: 'bottom-right',
153+
direction: 'column',
154+
justify: false,
155+
translateX: 100,
156+
translateY: 0,
157+
itemsSpacing: 0,
158+
itemDirection: 'left-to-right',
159+
itemWidth: 80,
160+
itemHeight: 20,
161+
itemOpacity: 0.75,
162+
symbolSize: 12,
163+
symbolShape: 'circle',
164+
symbolBorderColor: 'rgba(0, 0, 0, .5)',
165+
effects: [
166+
{
167+
on: 'hover',
168+
style: {
169+
itemBackground: 'rgba(0, 0, 0, .03)',
170+
itemOpacity: 1,
171+
},
172+
},
173+
],
174+
},
175+
]}
176+
/>
177+
</div>
178+
</div>
179+
);
180+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
/*
2+
* This file is auto-generated by Graphweaver.
3+
* Please do not edit it directly.
4+
*/
5+
import * as Types from '../../../../types.generated';
6+
7+
import { gql } from '@apollo/client';
8+
import * as Apollo from '@apollo/client';
9+
const defaultOptions = {} as const;
10+
export type GenrePopularityQueryVariables = Types.Exact<{ [key: string]: never; }>;
11+
12+
13+
export type GenrePopularityQuery = { __typename?: 'Query', genres?: Array<{ __typename?: 'Genre', genreId: string, name?: string | null, tracks: Array<{ __typename?: 'Track', trackId: string, invoiceLines: Array<{ __typename?: 'InvoiceLine', invoiceLineId: string, quantity: number }> }>, tracks_aggregate?: { __typename?: 'AggregationResult', count: number } | null } | null> | null };
14+
15+
16+
export const GenrePopularityDocument = gql`
17+
query genrePopularity {
18+
genres {
19+
genreId
20+
name
21+
tracks {
22+
trackId
23+
invoiceLines {
24+
invoiceLineId
25+
quantity
26+
}
27+
}
28+
tracks_aggregate {
29+
count
30+
}
31+
}
32+
}
33+
`;
34+
35+
/**
36+
* __useGenrePopularityQuery__
37+
*
38+
* To run a query within a React component, call `useGenrePopularityQuery` and pass it any options that fit your needs.
39+
* When your component renders, `useGenrePopularityQuery` returns an object from Apollo Client that contains loading, error, and data properties
40+
* you can use to render your UI.
41+
*
42+
* @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
43+
*
44+
* @example
45+
* const { data, loading, error } = useGenrePopularityQuery({
46+
* variables: {
47+
* },
48+
* });
49+
*/
50+
export function useGenrePopularityQuery(baseOptions?: Apollo.QueryHookOptions<GenrePopularityQuery, GenrePopularityQueryVariables>) {
51+
const options = {...defaultOptions, ...baseOptions}
52+
return Apollo.useQuery<GenrePopularityQuery, GenrePopularityQueryVariables>(GenrePopularityDocument, options);
53+
}
54+
export function useGenrePopularityLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<GenrePopularityQuery, GenrePopularityQueryVariables>) {
55+
const options = {...defaultOptions, ...baseOptions}
56+
return Apollo.useLazyQuery<GenrePopularityQuery, GenrePopularityQueryVariables>(GenrePopularityDocument, options);
57+
}
58+
export function useGenrePopularitySuspenseQuery(baseOptions?: Apollo.SkipToken | Apollo.SuspenseQueryHookOptions<GenrePopularityQuery, GenrePopularityQueryVariables>) {
59+
const options = baseOptions === Apollo.skipToken ? baseOptions : {...defaultOptions, ...baseOptions}
60+
return Apollo.useSuspenseQuery<GenrePopularityQuery, GenrePopularityQueryVariables>(GenrePopularityDocument, options);
61+
}
62+
export type GenrePopularityQueryHookResult = ReturnType<typeof useGenrePopularityQuery>;
63+
export type GenrePopularityLazyQueryHookResult = ReturnType<typeof useGenrePopularityLazyQuery>;
64+
export type GenrePopularitySuspenseQueryHookResult = ReturnType<typeof useGenrePopularitySuspenseQuery>;
65+
export type GenrePopularityQueryResult = Apollo.QueryResult<GenrePopularityQuery, GenrePopularityQueryVariables>;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { gql } from '@exogee/graphweaver-admin-ui-components';
2+
3+
export const genrePopularity = gql`
4+
query genrePopularity {
5+
genres {
6+
genreId
7+
name
8+
tracks {
9+
trackId
10+
invoiceLines {
11+
invoiceLineId
12+
quantity
13+
}
14+
}
15+
tracks_aggregate {
16+
count
17+
}
18+
}
19+
}
20+
`;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from './component';
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
.container {
2+
display: flex;
3+
}
4+
5+
.controlsContainer {
6+
display: flex;
7+
flex-direction: column;
8+
gap: 20px;
9+
min-width: 200px;
10+
}
11+
12+
.chartContainer {
13+
flex: 1;
14+
height: 35vh;
15+
}
16+
17+
.controlItem {
18+
display: flex;
19+
flex-direction: column;
20+
}
21+
22+
.checkBoxContainer {
23+
display: flex;
24+
flex-direction: row;
25+
gap: 10px;
26+
align-items: center;
27+
}

0 commit comments

Comments
 (0)