- {toArray(data[key]).map(({ name, value }) => {
- return (
-
- );
- })}
+
+
+
{key}
+
({
+ x: name,
+ y: value,
+ z: (value / total) * 100,
+ }))}
+ />
+
+
);
diff --git a/src/components/charts/BarChart.tsx b/src/components/charts/BarChart.tsx
new file mode 100644
index 00000000..09ff5154
--- /dev/null
+++ b/src/components/charts/BarChart.tsx
@@ -0,0 +1,85 @@
+import { useTheme } from 'components/hooks';
+import Chart, { ChartProps } from 'components/charts/Chart';
+import { renderNumberLabels } from 'lib/charts';
+import { useState } from 'react';
+import BarChartTooltip from 'components/charts/BarChartTooltip';
+
+export interface BarChartProps extends ChartProps {
+ unit: string;
+ stacked?: boolean;
+ renderXLabel?: (label: string, index: number, values: any[]) => string;
+ renderYLabel?: (label: string, index: number, values: any[]) => string;
+ XAxisType?: string;
+ YAxisType?: string;
+}
+
+export function BarChart(props: BarChartProps) {
+ const [tooltip, setTooltip] = useState(null);
+ const { colors } = useTheme();
+ const {
+ renderXLabel,
+ renderYLabel,
+ unit,
+ XAxisType = 'time',
+ YAxisType = 'linear',
+ stacked = false,
+ } = props;
+
+ const options = {
+ scales: {
+ x: {
+ type: XAxisType,
+ stacked: true,
+ time: {
+ unit,
+ },
+ grid: {
+ display: false,
+ },
+ border: {
+ color: colors.chart.line,
+ },
+ ticks: {
+ color: colors.chart.text,
+ autoSkip: false,
+ maxRotation: 0,
+ callback: renderXLabel,
+ },
+ },
+ y: {
+ type: YAxisType,
+ min: 0,
+ beginAtZero: true,
+ stacked,
+ grid: {
+ color: colors.chart.line,
+ },
+ border: {
+ color: colors.chart.line,
+ },
+ ticks: {
+ color: colors.chart.text,
+ callback: renderYLabel || renderNumberLabels,
+ },
+ },
+ },
+ };
+
+ const handleTooltip = ({ tooltip }: { tooltip: any }) => {
+ const { opacity } = tooltip;
+
+ setTooltip(opacity ?
: null);
+ };
+
+ return (
+
+ );
+}
+
+export default BarChart;
diff --git a/src/components/charts/BarChartTooltip.tsx b/src/components/charts/BarChartTooltip.tsx
new file mode 100644
index 00000000..b81d55fe
--- /dev/null
+++ b/src/components/charts/BarChartTooltip.tsx
@@ -0,0 +1,32 @@
+import { formatDate } from 'lib/date';
+import { Flexbox, StatusLight } from 'react-basics';
+import { formatLongNumber } from 'lib/format';
+import { useLocale } from 'components/hooks';
+
+const formats = {
+ millisecond: 'T',
+ second: 'pp',
+ minute: 'p',
+ hour: 'h:mm aaa - PP',
+ day: 'PPPP',
+ week: 'PPPP',
+ month: 'LLLL yyyy',
+ quarter: 'qqq',
+ year: 'yyyy',
+};
+
+export default function BarChartTooltip({ tooltip, unit }) {
+ const { locale } = useLocale();
+ const { labelColors, dataPoints } = tooltip;
+
+ return (
+
+ {formatDate(new Date(dataPoints[0].raw.x), formats[unit], locale)}
+
+
+ {formatLongNumber(dataPoints[0].raw.y)} {dataPoints[0].dataset.label}
+
+
+
+ );
+}
diff --git a/src/components/metrics/BarChart.module.css b/src/components/charts/Chart.module.css
similarity index 100%
rename from src/components/metrics/BarChart.module.css
rename to src/components/charts/Chart.module.css
diff --git a/src/components/charts/Chart.tsx b/src/components/charts/Chart.tsx
new file mode 100644
index 00000000..abaa6eb8
--- /dev/null
+++ b/src/components/charts/Chart.tsx
@@ -0,0 +1,141 @@
+import { useState, useRef, useEffect, ReactNode } from 'react';
+import { Loading } from 'react-basics';
+import classNames from 'classnames';
+import ChartJS, { LegendItem } from 'chart.js/auto';
+import HoverTooltip from 'components/common/HoverTooltip';
+import Legend from 'components/metrics/Legend';
+import { DEFAULT_ANIMATION_DURATION } from 'lib/constants';
+import styles from './Chart.module.css';
+
+export interface ChartProps {
+ type?: 'bar' | 'bubble' | 'doughnut' | 'pie' | 'line' | 'polarArea' | 'radar' | 'scatter';
+ data?: object;
+ isLoading?: boolean;
+ animationDuration?: number;
+ updateMode?: string;
+ onCreate?: (chart: any) => void;
+ onUpdate?: (chart: any) => void;
+ onTooltip?: (model: any) => void;
+ className?: string;
+ chartOptions?: { [key: string]: any };
+ tooltip?: ReactNode;
+}
+
+export function Chart({
+ type,
+ data,
+ isLoading = false,
+ animationDuration = DEFAULT_ANIMATION_DURATION,
+ tooltip,
+ updateMode,
+ onCreate,
+ onUpdate,
+ onTooltip,
+ className,
+ chartOptions,
+}: ChartProps) {
+ const canvas = useRef();
+ const chart = useRef(null);
+ const [legendItems, setLegendItems] = useState([]);
+
+ const options = {
+ responsive: true,
+ maintainAspectRatio: false,
+ animation: {
+ duration: animationDuration,
+ resize: {
+ duration: 0,
+ },
+ active: {
+ duration: 0,
+ },
+ },
+ plugins: {
+ legend: {
+ display: false,
+ },
+ tooltip: {
+ enabled: false,
+ external: onTooltip,
+ },
+ },
+ ...chartOptions,
+ };
+
+ const createChart = (data: any) => {
+ ChartJS.defaults.font.family = 'Inter';
+
+ chart.current = new ChartJS(canvas.current, {
+ type,
+ data,
+ options,
+ });
+
+ onCreate?.(chart.current);
+
+ setLegendItems(chart.current.legend.legendItems);
+ };
+
+ const updateChart = (data: any) => {
+ chart.current.data.datasets.forEach((dataset: { data: any }, index: string | number) => {
+ dataset.data = data?.datasets[index]?.data;
+ });
+
+ chart.current.options = options;
+
+ // Allow config changes before update
+ onUpdate?.(chart.current);
+
+ chart.current.update(updateMode);
+
+ setLegendItems(chart.current.legend.legendItems);
+ };
+
+ useEffect(() => {
+ if (data) {
+ if (!chart.current) {
+ createChart(data);
+ } else {
+ updateChart(data);
+ }
+ }
+ }, [data]);
+
+ const handleLegendClick = (item: LegendItem) => {
+ if (type === 'bar') {
+ const { datasetIndex } = item;
+ const meta = chart.current.getDatasetMeta(datasetIndex);
+
+ meta.hidden =
+ meta.hidden === null ? !chart.current.data.datasets[datasetIndex]?.hidden : null;
+ } else {
+ const { index } = item;
+ const meta = chart.current.getDatasetMeta(0);
+ const hidden = !!meta.data[index].hidden;
+
+ meta.data[index].hidden = !hidden;
+ chart.current.legend.legendItems[index].hidden = !hidden;
+ }
+
+ chart.current.update(updateMode);
+
+ setLegendItems(chart.current.legend.legendItems);
+ };
+
+ return (
+ <>
+
+ {isLoading && }
+
+
+
+ {tooltip && (
+
+ {tooltip}
+
+ )}
+ >
+ );
+}
+
+export default Chart;
diff --git a/src/components/charts/PieChart.tsx b/src/components/charts/PieChart.tsx
new file mode 100644
index 00000000..11ad125c
--- /dev/null
+++ b/src/components/charts/PieChart.tsx
@@ -0,0 +1,27 @@
+import { Chart, ChartProps } from 'components/charts/Chart';
+import { useState } from 'react';
+import { StatusLight } from 'react-basics';
+import { formatLongNumber } from 'lib/format';
+
+export interface PieChartProps extends ChartProps {
+ type?: 'doughnut' | 'pie';
+}
+
+export default function PieChart(props: PieChartProps) {
+ const [tooltip, setTooltip] = useState(null);
+ const { type } = props;
+
+ const handleTooltip = ({ tooltip }) => {
+ const { labelColors, dataPoints } = tooltip;
+
+ setTooltip(
+ tooltip.opacity ? (
+
+ {formatLongNumber(dataPoints?.[0]?.raw)} {dataPoints?.[0]?.label}
+
+ ) : null,
+ );
+ };
+
+ return
;
+}
diff --git a/src/components/common/Breadcrumb.tsx b/src/components/common/Breadcrumb.tsx
index fa7bde15..ebdce497 100644
--- a/src/components/common/Breadcrumb.tsx
+++ b/src/components/common/Breadcrumb.tsx
@@ -1,6 +1,7 @@
import Link from 'next/link';
import { Flexbox, Icon, Icons, Text } from 'react-basics';
import styles from './Breadcrumb.module.css';
+import { Fragment } from 'react';
export interface BreadcrumbProps {
data: {
@@ -14,7 +15,7 @@ export function Breadcrumb({ data }: BreadcrumbProps) {
{data.map((a, i) => {
return (
- <>
+
{a.url ? (
{a.label}
@@ -27,7 +28,7 @@ export function Breadcrumb({ data }: BreadcrumbProps) {
) : null}
- >
+
);
})}
diff --git a/src/components/metrics/BarChart.tsx b/src/components/metrics/BarChart.tsx
deleted file mode 100644
index 257b9334..00000000
--- a/src/components/metrics/BarChart.tsx
+++ /dev/null
@@ -1,192 +0,0 @@
-import { useState, useRef, useEffect, useCallback } from 'react';
-import { Loading } from 'react-basics';
-import classNames from 'classnames';
-import Chart from 'chart.js/auto';
-import HoverTooltip from 'components/common/HoverTooltip';
-import Legend from 'components/metrics/Legend';
-import { useLocale } from 'components/hooks';
-import { useTheme } from 'components/hooks';
-import { DEFAULT_ANIMATION_DURATION } from 'lib/constants';
-import { renderNumberLabels } from 'lib/charts';
-import styles from './BarChart.module.css';
-
-export interface BarChartProps {
- datasets?: any[];
- unit?: string;
- animationDuration?: number;
- stacked?: boolean;
- isLoading?: boolean;
- renderXLabel?: (label: string, index: number, values: any[]) => string;
- renderYLabel?: (label: string, index: number, values: any[]) => string;
- XAxisType?: string;
- YAxisType?: string;
- renderTooltipPopup?: (setTooltipPopup: (data: any) => void, model: any) => void;
- updateMode?: string;
- onCreate?: (chart: any) => void;
- onUpdate?: (chart: any) => void;
- className?: string;
-}
-
-export function BarChart({
- datasets = [],
- unit,
- animationDuration = DEFAULT_ANIMATION_DURATION,
- stacked = false,
- isLoading = false,
- renderXLabel,
- renderYLabel,
- XAxisType = 'time',
- YAxisType = 'linear',
- renderTooltipPopup,
- updateMode,
- onCreate,
- onUpdate,
- className,
-}: BarChartProps) {
- const canvas = useRef();
- const chart = useRef(null);
- const [tooltip, setTooltipPopup] = useState(null);
- const { locale } = useLocale();
- const { theme, colors } = useTheme();
- const [legendItems, setLegendItems] = useState([]);
-
- const getOptions = useCallback(() => {
- return {
- responsive: true,
- maintainAspectRatio: false,
- animation: {
- duration: animationDuration,
- resize: {
- duration: 0,
- },
- active: {
- duration: 0,
- },
- },
- plugins: {
- legend: {
- display: false,
- },
- tooltip: {
- enabled: false,
- external: renderTooltipPopup ? renderTooltipPopup.bind(null, setTooltipPopup) : undefined,
- },
- },
- scales: {
- x: {
- type: XAxisType,
- stacked: true,
- time: {
- unit,
- },
- grid: {
- display: false,
- },
- border: {
- color: colors.chart.line,
- },
- ticks: {
- color: colors.chart.text,
- autoSkip: false,
- maxRotation: 0,
- callback: renderXLabel,
- },
- },
- y: {
- type: YAxisType,
- min: 0,
- beginAtZero: true,
- stacked,
- grid: {
- color: colors.chart.line,
- },
- border: {
- color: colors.chart.line,
- },
- ticks: {
- color: colors.chart.text,
- callback: renderYLabel || renderNumberLabels,
- },
- },
- },
- };
- }, [
- animationDuration,
- renderTooltipPopup,
- renderXLabel,
- XAxisType,
- YAxisType,
- stacked,
- colors,
- unit,
- locale,
- ]);
-
- const createChart = (datasets: any[]) => {
- Chart.defaults.font.family = 'Inter';
-
- chart.current = new Chart(canvas.current, {
- type: 'bar',
- data: {
- datasets,
- },
- options: getOptions() as any,
- });
-
- onCreate?.(chart.current);
-
- setLegendItems(chart.current.legend.legendItems);
- };
-
- const updateChart = (datasets: any[]) => {
- setTooltipPopup(null);
-
- chart.current.data.datasets.forEach((dataset: { data: any }, index: string | number) => {
- dataset.data = datasets[index]?.data;
- });
-
- chart.current.options = getOptions();
-
- // Allow config changes before update
- onUpdate?.(chart.current);
-
- chart.current.update(updateMode);
-
- setLegendItems(chart.current.legend.legendItems);
- };
-
- useEffect(() => {
- if (datasets) {
- if (!chart.current) {
- createChart(datasets);
- } else {
- updateChart(datasets);
- }
- }
- }, [datasets, unit, theme, animationDuration, locale]);
-
- const handleLegendClick = (index: number) => {
- const meta = chart.current.getDatasetMeta(index);
-
- meta.hidden = meta.hidden === null ? !chart.current.data.datasets[index].hidden : null;
-
- chart.current.update();
- };
-
- return (
- <>
-
- {isLoading && }
-
-
-
- {tooltip && (
-
- {tooltip}
-
- )}
- >
- );
-}
-
-export default BarChart;
diff --git a/src/components/metrics/EventsChart.tsx b/src/components/metrics/EventsChart.tsx
index b1c581fc..87e65276 100644
--- a/src/components/metrics/EventsChart.tsx
+++ b/src/components/metrics/EventsChart.tsx
@@ -1,7 +1,7 @@
import { useMemo } from 'react';
import { Loading } from 'react-basics';
import { colord } from 'colord';
-import BarChart from './BarChart';
+import BarChart from 'components/charts/BarChart';
import { getDateArray } from 'lib/date';
import {
useLocale,
@@ -10,8 +10,8 @@ import {
useNavigation,
useWebsiteEvents,
} from 'components/hooks';
-import { EVENT_COLORS } from 'lib/constants';
-import { renderDateLabels, renderStatusTooltipPopup } from 'lib/charts';
+import { CHART_COLORS } from 'lib/constants';
+import { renderDateLabels } from 'lib/charts';
export interface EventsChartProps {
websiteId: string;
@@ -26,7 +26,6 @@ export function EventsChart({ websiteId, className, token }: EventsChartProps) {
const {
query: { url, event },
} = useNavigation();
-
const { data, isLoading } = useWebsiteEvents(websiteId, {
startAt: +startDate,
endAt: +endDate,
@@ -38,9 +37,8 @@ export function EventsChart({ websiteId, className, token }: EventsChartProps) {
offset,
});
- const datasets = useMemo(() => {
+ const chartData = useMemo(() => {
if (!data) return [];
- if (isLoading) return data;
const map = (data as any[]).reduce((obj, { x, t, y }) => {
if (!obj[x]) {
@@ -56,18 +54,20 @@ export function EventsChart({ websiteId, className, token }: EventsChartProps) {
map[key] = getDateArray(map[key], startDate, endDate, unit);
});
- return Object.keys(map).map((key, index) => {
- const color = colord(EVENT_COLORS[index % EVENT_COLORS.length]);
- return {
- label: key,
- data: map[key],
- lineTension: 0,
- backgroundColor: color.alpha(0.6).toRgbString(),
- borderColor: color.alpha(0.7).toRgbString(),
- borderWidth: 1,
- };
- });
- }, [data, isLoading, startDate, endDate, unit]);
+ return {
+ datasets: Object.keys(map).map((key, index) => {
+ const color = colord(CHART_COLORS[index % CHART_COLORS.length]);
+ return {
+ label: key,
+ data: map[key],
+ lineTension: 0,
+ backgroundColor: color.alpha(0.6).toRgbString(),
+ borderColor: color.alpha(0.7).toRgbString(),
+ borderWidth: 1,
+ };
+ }),
+ };
+ }, [data, startDate, endDate, unit]);
if (isLoading) {
return
;
@@ -76,11 +76,10 @@ export function EventsChart({ websiteId, className, token }: EventsChartProps) {
return (
);
diff --git a/src/components/metrics/Legend.tsx b/src/components/metrics/Legend.tsx
index c2bed639..5fbee827 100644
--- a/src/components/metrics/Legend.tsx
+++ b/src/components/metrics/Legend.tsx
@@ -1,9 +1,8 @@
-import { useEffect } from 'react';
import { StatusLight } from 'react-basics';
import { colord } from 'colord';
import classNames from 'classnames';
+import { LegendItem } from 'chart.js/auto';
import { useLocale } from 'components/hooks';
-import { useForceUpdate } from 'components/hooks';
import styles from './Legend.module.css';
export function Legend({
@@ -11,14 +10,9 @@ export function Legend({
onClick,
}: {
items: any[];
- onClick: (index: number) => void;
+ onClick: (index: LegendItem) => void;
}) {
const { locale } = useLocale();
- const forceUpdate = useForceUpdate();
-
- useEffect(() => {
- forceUpdate();
- }, [locale, forceUpdate]);
if (!items.find(({ text }) => text)) {
return null;
@@ -26,14 +20,15 @@ export function Legend({
return (
- {items.map(({ text, fillStyle, datasetIndex, hidden }) => {
+ {items.map(item => {
+ const { text, fillStyle, hidden } = item;
const color = colord(fillStyle);
return (
onClick(datasetIndex)}
+ onClick={() => onClick(item)}
>
{text}
diff --git a/src/components/metrics/PageviewsChart.tsx b/src/components/metrics/PageviewsChart.tsx
index 48e2b4f1..ea9720fd 100644
--- a/src/components/metrics/PageviewsChart.tsx
+++ b/src/components/metrics/PageviewsChart.tsx
@@ -1,7 +1,7 @@
import { useMemo } from 'react';
-import BarChart, { BarChartProps } from './BarChart';
+import BarChart, { BarChartProps } from 'components/charts/BarChart';
import { useLocale, useTheme, useMessages } from 'components/hooks';
-import { renderDateLabels, renderStatusTooltipPopup } from 'lib/charts';
+import { renderDateLabels } from 'lib/charts';
export interface PageviewsChartProps extends BarChartProps {
data: {
@@ -17,33 +17,36 @@ export function PageviewsChart({ data, unit, isLoading, ...props }: PageviewsCha
const { colors } = useTheme();
const { locale } = useLocale();
- const datasets = useMemo(() => {
- if (!data) return [];
+ const chartData = useMemo(() => {
+ if (!data) {
+ return {};
+ }
- return [
- {
- label: formatMessage(labels.visitors),
- data: data.sessions,
- borderWidth: 1,
- ...colors.chart.visitors,
- },
- {
- label: formatMessage(labels.views),
- data: data.pageviews,
- borderWidth: 1,
- ...colors.chart.views,
- },
- ];
- }, [data, colors, formatMessage, labels]);
+ return {
+ datasets: [
+ {
+ label: formatMessage(labels.visitors),
+ data: data.sessions,
+ borderWidth: 1,
+ ...colors.chart.visitors,
+ },
+ {
+ label: formatMessage(labels.views),
+ data: data.pageviews,
+ borderWidth: 1,
+ ...colors.chart.views,
+ },
+ ],
+ };
+ }, [data]);
return (
);
}
diff --git a/src/lib/charts.ts b/src/lib/charts.ts
new file mode 100644
index 00000000..8939b3c1
--- /dev/null
+++ b/src/lib/charts.ts
@@ -0,0 +1,27 @@
+import { formatDate } from 'lib/date';
+import { formatLongNumber } from 'lib/format';
+
+export function renderNumberLabels(label: string) {
+ return +label > 1000 ? formatLongNumber(+label) : label;
+}
+
+export function renderDateLabels(unit: string, locale: string) {
+ return (label: string, index: number, values: any[]) => {
+ const d = new Date(values[index].value);
+
+ switch (unit) {
+ case 'minute':
+ return formatDate(d, 'h:mm', locale);
+ case 'hour':
+ return formatDate(d, 'p', locale);
+ case 'day':
+ return formatDate(d, 'MMM d', locale);
+ case 'month':
+ return formatDate(d, 'MMM', locale);
+ case 'year':
+ return formatDate(d, 'YYY', locale);
+ default:
+ return label;
+ }
+ };
+}
diff --git a/src/lib/charts.tsx b/src/lib/charts.tsx
deleted file mode 100644
index c80bfe3f..00000000
--- a/src/lib/charts.tsx
+++ /dev/null
@@ -1,62 +0,0 @@
-import { StatusLight } from 'react-basics';
-import { formatDate } from 'lib/date';
-import { formatLongNumber } from 'lib/format';
-
-export function renderNumberLabels(label: string) {
- return +label > 1000 ? formatLongNumber(+label) : label;
-}
-
-export function renderDateLabels(unit: string, locale: string) {
- return (label: string, index: number, values: any[]) => {
- const d = new Date(values[index].value);
-
- switch (unit) {
- case 'minute':
- return formatDate(d, 'h:mm', locale);
- case 'hour':
- return formatDate(d, 'p', locale);
- case 'day':
- return formatDate(d, 'MMM d', locale);
- case 'month':
- return formatDate(d, 'MMM', locale);
- case 'year':
- return formatDate(d, 'YYY', locale);
- default:
- return label;
- }
- };
-}
-
-export function renderStatusTooltipPopup(unit: string, locale: string) {
- return (setTooltipPopup: (data: any) => void, model: any) => {
- const { opacity, labelColors, dataPoints } = model.tooltip;
-
- if (!dataPoints?.length || !opacity) {
- setTooltipPopup(null);
- return;
- }
-
- const formats = {
- millisecond: 'T',
- second: 'pp',
- minute: 'p',
- hour: 'h:mm aaa - PP',
- day: 'PPPP',
- week: 'PPPP',
- month: 'LLLL yyyy',
- quarter: 'qqq',
- year: 'yyyy',
- };
-
- setTooltipPopup(
- <>
- {formatDate(new Date(dataPoints[0].raw.x), formats[unit], locale)}
-
-
- {formatLongNumber(dataPoints[0].raw.y)} {dataPoints[0].dataset.label}
-
-
- >,
- );
- };
-}
diff --git a/src/lib/constants.ts b/src/lib/constants.ts
index 21a4aa5f..78e7a71e 100644
--- a/src/lib/constants.ts
+++ b/src/lib/constants.ts
@@ -194,7 +194,7 @@ export const THEME_COLORS = {
},
};
-export const EVENT_COLORS = [
+export const CHART_COLORS = [
'#2680eb',
'#9256d9',
'#44b556',