Skip to content

Commit

Permalink
feat(react): add bar chart
Browse files Browse the repository at this point in the history
  • Loading branch information
jrea committed Nov 2, 2022
1 parent 4a2c8cd commit 9e17eab
Show file tree
Hide file tree
Showing 24 changed files with 1,772 additions and 983 deletions.
6 changes: 5 additions & 1 deletion packages/react/.storybook/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,11 @@ var path = require('path');

module.exports = {
stories: ['../stories/**/*.stories.@(ts|tsx|js|jsx)'],
addons: ['@storybook/addon-links', '@storybook/addon-essentials'],
addons: [
'@storybook/addon-links',
'@storybook/addon-essentials',
'storybook-addon-mock',
],
// https://storybook.js.org/docs/react/configure/typescript#mainjs-configuration
typescript: {
check: true, // type-check stories during Storybook build
Expand Down
2 changes: 2 additions & 0 deletions packages/react/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@
"eslint-plugin-storybook": "^0.6.7",
"husky": "^8.0.1",
"size-limit": "^8.1.0",
"storybook-addon-mock": "^3.2.0",
"tsdx": "^0.14.1",
"tslib": "^2.4.1",
"typescript": "^4.8.4"
Expand All @@ -88,6 +89,7 @@
"@tanstack/react-query": "^4.14.1",
"@theniledev/js": "^0.28.2",
"chart.js": "^3.9.1",
"chartjs-adapter-date-fns": "^2.0.0",
"date-fns": "^2.29.3",
"react": "^18.2.0",
"react-chartjs-2": "^4.3.1",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import React from 'react';
import { Bar } from 'react-chartjs-2';

import {
AggregateMetricsRequest,
AggregationType,
MetricsBarChartComponentProps,
} from '../types';

import { useAggregation } from './hooks';

type LabelAndData = { x: string; y: number }[];
export default function AggregateLineChart(
props: {
aggregation: AggregateMetricsRequest;
} & MetricsBarChartComponentProps
) {
const { chartOptions, dataset } = props;

const aggregationType: AggregationType = props.aggregation.aggregationType;
const { isLoading, buckets } = useAggregation(props);

const data = React.useMemo((): LabelAndData => {
if (!buckets) {
return [];
}
return buckets
.map((bucket) => {
return {
y: Number(bucket[aggregationType]),
x: bucket.timestamp?.toISOString() as string,
};
})
.filter(Boolean);
}, [aggregationType, buckets]);

if (isLoading) {
return null;
}

return (
<Bar
options={{
...chartOptions,
plugins: {
legend: {
display: false,
},
},
}}
data={{
datasets: [
{
label: props.aggregation.metricName,
data,
backgroundColor: 'rgb(111 226 255)',
borderColor: 'rgb(77, 158, 178)',
borderSkipped: true,
barPercentage: 0.9,
...dataset,
},
],
}}
/>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,17 @@ import React from 'react';
import { Line } from 'react-chartjs-2';
import { format } from 'date-fns';

import {
AggregateMetricsRequest,
MetricsLineChartComponentProps,
} from '../types';

import { useAggregation } from './hooks';
import { AggregateMetricsRequest, MetricsComponentProps } from './types';

export default function AggregateLineChart(
props: { aggregation: AggregateMetricsRequest } & MetricsComponentProps
props: {
aggregation: AggregateMetricsRequest;
} & MetricsLineChartComponentProps
) {
type LabelAndData = {
labels: string[];
Expand Down Expand Up @@ -54,8 +60,8 @@ export default function AggregateLineChart(
{
label: props.aggregation.metricName,
data,
borderColor: 'rgb(111 226 255)',
backgroundColor: 'rgb(77, 158, 178)',
backgroundColor: 'rgb(111 226 255)',
borderColor: 'rgb(77, 158, 178)',
pointRadius: 0,
borderWidth: 6,
cubicInterpolationMode: 'monotone',
Expand Down
55 changes: 55 additions & 0 deletions packages/react/src/components/Metrics/Aggregate/hooks.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { useQuery } from '@tanstack/react-query';

import { UseAggreationReturn, UseAggregationProps } from '../types';
import { useInterval } from '../../../lib/hooks/useInterval';
import { useNile } from '../../../context';
import Queries from '../../../lib/queries';

/**
* @example
* import { useAggregation } from '@theniledev/react';
* function MyChart() {
* const { buckets } = useAggregation({
* aggregation: {
* aggregationRequest: {
* startTime: new Date(),
* organizationId: 'myOrganization',
* bucketSize: '1h',
* },
* metricName: 'my.metric',
* },
* updateInterval: 4000
* });
* // rest of component
* }
*
* @param props config object for hook and metrics request
* @returns a boolean for the loading state and an array of Bucket objects
*/

export const useAggregation = (
props: UseAggregationProps
): UseAggreationReturn => {
const nile = useNile();

const { updateInterval, queryKey, aggregation } = props;
const qKey = queryKey || `aggregation:${aggregation.metricName}`;

const {
data = [],
isLoading,
refetch,
} = useQuery(
[Queries.FilterMetrics(qKey)],
() => {
return nile.metrics.aggregateMetrics(aggregation);
},
{ enabled: Boolean(nile.workspace) }
);

useInterval(() => {
refetch();
}, updateInterval);

return { isLoading, buckets: data };
};
90 changes: 90 additions & 0 deletions packages/react/src/components/Metrics/Filter/FilterBarChart.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import React from 'react';
import { Bar } from 'react-chartjs-2';
import { FilterMetricsRequest } from '@theniledev/js';
import {
Chart,
CategoryScale,
BarController,
Title,
Tooltip,
Legend,
TimeScale,
ChartDataset,
ChartOptions,
BarElement,
} from 'chart.js';

import { useFilter } from './hooks';

Chart.register(
CategoryScale,
BarController,
BarElement,
Title,
Tooltip,
Legend,
TimeScale
);

export type MetricsComponentProps = {
timeFormat?: string;
dataset?: Omit<ChartDataset<'bar', number[]>, 'data'>;
updateInterval?: number;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
chartOptions?: ChartOptions<any>;
queryKey?: string;
};
type LabelAndData = { x: string; y: number }[];

export default function FilterBarChart(
props: FilterMetricsRequest & MetricsComponentProps
) {
const { filter, chartOptions, dataset } = props;

const { isLoading, metrics } = useFilter(props);
const metricName = filter.metricName;

const data = React.useMemo<LabelAndData>((): LabelAndData => {
if (!metrics) {
return [];
}
return metrics.map((metric) => {
return { y: metric.value, x: metric.timestamp.toISOString() };
});
}, [metrics]);

const sets = React.useMemo(() => {
return data.map((datum) => {
return {
label: metricName,
data: [datum],
borderColor: 'rgb(77, 158, 178)',
borderSkipped: true,
backgroundColor: 'rgb(77, 158, 178)',
barPercentage: 0.9,
grouped: true,
...dataset,
};
});
}, [data, dataset, metricName]);

if (isLoading) {
return null;
}

return (
<Bar
options={{
...chartOptions,
plugins: {
legend: {
display: false,
},
},
}}
data={{
datasets: sets,
}}
/>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,17 @@ import { Line } from 'react-chartjs-2';
import { format } from 'date-fns';
import { FilterMetricsRequest } from '@theniledev/js';

import { MetricsLineChartComponentProps } from '../types';

import { useFilter } from './hooks';
import { MetricsComponentProps } from './types';

type LabelAndData = {
labels: string[];
data: number[];
};

export default function FilterLineChart(
props: FilterMetricsRequest & MetricsComponentProps
props: FilterMetricsRequest & MetricsLineChartComponentProps
) {
const { filter, chartOptions, timeFormat = 'HH:mm:ss', dataset } = props;

Expand All @@ -38,7 +39,6 @@ export default function FilterLineChart(
if (isLoading) {
return null;
}

return (
<Line
options={{
Expand Down
75 changes: 75 additions & 0 deletions packages/react/src/components/Metrics/Filter/hooks.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import React from 'react';
import { FilterMetricsRequest, Metric } from '@theniledev/js';
import { useQuery } from '@tanstack/react-query';

import { useNile } from '../../../context';
import Queries from '../../../lib/queries';
import { useInterval } from '../../../lib/hooks/useInterval';
import { UseMetricsProps, UseMetricsReturn } from '../types';

/**
* @example
* ```typescript
* import { useFilter } from '@theniledev/react';
* function MyChart() {
* const filter = {
* entityName: "clusters",
* metric: "my.metric"
* startTime: new Date(),
* duration: 60 * 1000,
* };
*
* const { isLoading, metrics } = useFilter({
* filter,
* });
* // rest of component
* }
* ```
* @param props config object for a metrics request
* @returns a boolean for the loading state and metrics flattened into measurements
*/

export const useFilter = (props: UseMetricsProps): UseMetricsReturn => {
const nile = useNile();

const updateInterval = props?.updateInterval;
const { filter } = props;

const payload = React.useMemo<FilterMetricsRequest>(() => {
// API does not like this currently
if (filter && filter.metricName === '') {
delete filter.metricName;
}
return {
...props,
filter: filter ? filter : {},
} as FilterMetricsRequest;
}, [filter, props]);

const queryKey = React.useMemo(
() => props.queryKey || `filter:${filter.metricName}`,
[filter.metricName, props.queryKey]
);

const { data, isLoading, refetch } = useQuery(
[Queries.FilterMetrics(queryKey)],
() => {
return nile.metrics.filterMetrics(payload);
},
{ enabled: Boolean(nile.workspace) }
);

const flatMetrics = React.useMemo(
() =>
data?.flatMap((metric: Metric) => {
return metric.measurements;
}),
[data]
);

useInterval(() => {
refetch();
}, updateInterval);

return { isLoading, metrics: flatMetrics };
};
Loading

2 comments on commit 9e17eab

@vercel
Copy link

@vercel vercel bot commented on 9e17eab Nov 2, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

nile-js – ./

nile-js-git-master-theniledev.vercel.app
nile-js-theniledev.vercel.app
nile-js.vercel.app

@vercel
Copy link

@vercel vercel bot commented on 9e17eab Nov 2, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.