mirror of
https://github.com/umami-software/umami.git
synced 2026-02-08 22:57:12 +01:00
Refactored funnel report. Made BarChart more generic.
This commit is contained in:
parent
050cd2f5d9
commit
fb4dd75e18
24 changed files with 327 additions and 367 deletions
|
|
@ -1,102 +1,39 @@
|
|||
import { useState, useRef, useEffect, useMemo, useCallback } from 'react';
|
||||
import { StatusLight, Loading } from 'react-basics';
|
||||
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 { formatLongNumber } from 'lib/format';
|
||||
import { dateFormat } from 'lib/date';
|
||||
import useLocale from 'hooks/useLocale';
|
||||
import useTheme from 'hooks/useTheme';
|
||||
import { DEFAULT_ANIMATION_DURATION, THEME_COLORS } from 'lib/constants';
|
||||
import { DEFAULT_ANIMATION_DURATION } from 'lib/constants';
|
||||
import styles from './BarChart.module.css';
|
||||
|
||||
function defaultRenderYLabel(label) {
|
||||
return +label > 1000 ? formatLongNumber(label) : label;
|
||||
}
|
||||
|
||||
export function BarChart({
|
||||
datasets,
|
||||
unit,
|
||||
animationDuration = DEFAULT_ANIMATION_DURATION,
|
||||
stacked = false,
|
||||
loading = false,
|
||||
onCreate = () => {},
|
||||
onUpdate = () => {},
|
||||
renderXLabel,
|
||||
renderYLabel,
|
||||
XAxisType = 'time',
|
||||
YAxisType = 'linear',
|
||||
renderTooltip,
|
||||
onCreate,
|
||||
onUpdate,
|
||||
className,
|
||||
}) {
|
||||
const canvas = useRef();
|
||||
const chart = useRef(null);
|
||||
const [tooltip, setTooltip] = useState(null);
|
||||
const { locale } = useLocale();
|
||||
const [theme] = useTheme();
|
||||
|
||||
const colors = useMemo(
|
||||
() => ({
|
||||
text: THEME_COLORS[theme].gray700,
|
||||
line: THEME_COLORS[theme].gray200,
|
||||
}),
|
||||
[theme],
|
||||
);
|
||||
|
||||
const renderYLabel = label => {
|
||||
return +label > 1000 ? formatLongNumber(label) : label;
|
||||
};
|
||||
|
||||
const renderXLabel = useCallback(
|
||||
(label, index, values) => {
|
||||
const d = new Date(values[index].value);
|
||||
|
||||
switch (unit) {
|
||||
case 'minute':
|
||||
return dateFormat(d, 'h:mm', locale);
|
||||
case 'hour':
|
||||
return dateFormat(d, 'p', locale);
|
||||
case 'day':
|
||||
return dateFormat(d, 'MMM d', locale);
|
||||
case 'month':
|
||||
return dateFormat(d, 'MMM', locale);
|
||||
case 'year':
|
||||
return dateFormat(d, 'YYY', locale);
|
||||
default:
|
||||
return label;
|
||||
}
|
||||
},
|
||||
[locale, unit],
|
||||
);
|
||||
|
||||
const renderTooltip = useCallback(
|
||||
model => {
|
||||
const { opacity, labelColors, dataPoints } = model.tooltip;
|
||||
|
||||
if (!dataPoints?.length || !opacity) {
|
||||
setTooltip(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',
|
||||
};
|
||||
|
||||
setTooltip(
|
||||
<div className={styles.tooltip}>
|
||||
<div>{dateFormat(new Date(dataPoints[0].raw.x), formats[unit], locale)}</div>
|
||||
<div>
|
||||
<StatusLight color={labelColors?.[0]?.backgroundColor}>
|
||||
<div className={styles.value}>
|
||||
{formatLongNumber(dataPoints[0].raw.y)} {dataPoints[0].dataset.label}
|
||||
</div>
|
||||
</StatusLight>
|
||||
</div>
|
||||
</div>,
|
||||
);
|
||||
},
|
||||
[unit],
|
||||
);
|
||||
const { theme, colors } = useTheme();
|
||||
|
||||
const getOptions = useCallback(() => {
|
||||
return {
|
||||
|
|
@ -117,12 +54,12 @@ export function BarChart({
|
|||
},
|
||||
tooltip: {
|
||||
enabled: false,
|
||||
external: renderTooltip,
|
||||
external: renderTooltip ? renderTooltip.bind(null, setTooltip) : undefined,
|
||||
},
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
type: 'time',
|
||||
type: XAxisType,
|
||||
stacked: true,
|
||||
time: {
|
||||
unit,
|
||||
|
|
@ -131,34 +68,44 @@ export function BarChart({
|
|||
display: false,
|
||||
},
|
||||
border: {
|
||||
color: colors.line,
|
||||
color: colors.chart.line,
|
||||
},
|
||||
ticks: {
|
||||
color: colors.text,
|
||||
color: colors.chart.text,
|
||||
autoSkip: false,
|
||||
maxRotation: 0,
|
||||
callback: renderXLabel,
|
||||
},
|
||||
},
|
||||
y: {
|
||||
type: 'linear',
|
||||
type: YAxisType,
|
||||
min: 0,
|
||||
beginAtZero: true,
|
||||
stacked,
|
||||
grid: {
|
||||
color: colors.line,
|
||||
color: colors.chart.line,
|
||||
},
|
||||
border: {
|
||||
color: colors.line,
|
||||
color: colors.chart.line,
|
||||
},
|
||||
ticks: {
|
||||
color: colors.text,
|
||||
callback: renderYLabel,
|
||||
callback: renderYLabel || defaultRenderYLabel,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}, [animationDuration, renderTooltip, renderXLabel, stacked, colors, unit, locale]);
|
||||
}, [
|
||||
animationDuration,
|
||||
renderTooltip,
|
||||
renderXLabel,
|
||||
XAxisType,
|
||||
YAxisType,
|
||||
stacked,
|
||||
colors,
|
||||
unit,
|
||||
locale,
|
||||
]);
|
||||
|
||||
const createChart = () => {
|
||||
Chart.defaults.font.family = 'Inter';
|
||||
|
|
@ -173,7 +120,7 @@ export function BarChart({
|
|||
options,
|
||||
});
|
||||
|
||||
onCreate(chart.current);
|
||||
onCreate?.(chart.current);
|
||||
};
|
||||
|
||||
const updateChart = () => {
|
||||
|
|
@ -186,7 +133,7 @@ export function BarChart({
|
|||
|
||||
chart.current.options = getOptions();
|
||||
|
||||
onUpdate(chart.current);
|
||||
onUpdate?.(chart.current);
|
||||
|
||||
chart.current.update();
|
||||
};
|
||||
|
|
@ -208,7 +155,11 @@ export function BarChart({
|
|||
<canvas ref={canvas} />
|
||||
</div>
|
||||
<Legend chart={chart.current} />
|
||||
{tooltip && <HoverTooltip tooltip={tooltip} />}
|
||||
{tooltip && (
|
||||
<HoverTooltip>
|
||||
<div className={styles.tooltip}>{tooltip}</div>
|
||||
</HoverTooltip>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,8 +5,8 @@ import { useSpring, animated, config } from 'react-spring';
|
|||
import classNames from 'classnames';
|
||||
import NoData from 'components/common/NoData';
|
||||
import { formatNumber, formatLongNumber } from 'lib/format';
|
||||
import useMessages from 'hooks/useMessages';
|
||||
import styles from './DataTable.module.css';
|
||||
import useMessages from '../../hooks/useMessages';
|
||||
|
||||
export function DataTable({
|
||||
data = [],
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
.table {
|
||||
position: relative;
|
||||
height: 100%;
|
||||
display: grid;
|
||||
grid-template-rows: fit-content(100%) auto;
|
||||
overflow: hidden;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.body {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
.bar {
|
||||
display: flex;
|
||||
cursor: pointer;
|
||||
min-height: 80px;
|
||||
min-height: 110px;
|
||||
gap: 20px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,33 +1,73 @@
|
|||
import { useMemo } from 'react';
|
||||
import { colord } from 'colord';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import { StatusLight } from 'react-basics';
|
||||
import BarChart from './BarChart';
|
||||
import { THEME_COLORS } from 'lib/constants';
|
||||
import useTheme from 'hooks/useTheme';
|
||||
import useMessages from 'hooks/useMessages';
|
||||
import useLocale from 'hooks/useLocale';
|
||||
import { dateFormat } from 'lib/date';
|
||||
import { formatLongNumber } from 'lib/format';
|
||||
|
||||
export function PageviewsChart({ websiteId, data, unit, records, className, loading, ...props }) {
|
||||
export function PageviewsChart({ websiteId, data, unit, className, loading, ...props }) {
|
||||
const { formatMessage, labels } = useMessages();
|
||||
const [theme] = useTheme();
|
||||
const { colors } = useTheme();
|
||||
const { locale } = useLocale();
|
||||
|
||||
const colors = useMemo(() => {
|
||||
const primaryColor = colord(THEME_COLORS[theme].primary);
|
||||
return {
|
||||
views: {
|
||||
hoverBackgroundColor: primaryColor.alpha(0.7).toRgbString(),
|
||||
backgroundColor: primaryColor.alpha(0.4).toRgbString(),
|
||||
borderColor: primaryColor.alpha(0.7).toRgbString(),
|
||||
hoverBorderColor: primaryColor.toRgbString(),
|
||||
},
|
||||
visitors: {
|
||||
hoverBackgroundColor: primaryColor.alpha(0.9).toRgbString(),
|
||||
backgroundColor: primaryColor.alpha(0.6).toRgbString(),
|
||||
borderColor: primaryColor.alpha(0.9).toRgbString(),
|
||||
hoverBorderColor: primaryColor.toRgbString(),
|
||||
},
|
||||
};
|
||||
}, [theme]);
|
||||
const renderXLabel = useCallback(
|
||||
(label, index, values) => {
|
||||
const d = new Date(values[index].value);
|
||||
|
||||
switch (unit) {
|
||||
case 'minute':
|
||||
return dateFormat(d, 'h:mm', locale);
|
||||
case 'hour':
|
||||
return dateFormat(d, 'p', locale);
|
||||
case 'day':
|
||||
return dateFormat(d, 'MMM d', locale);
|
||||
case 'month':
|
||||
return dateFormat(d, 'MMM', locale);
|
||||
case 'year':
|
||||
return dateFormat(d, 'YYY', locale);
|
||||
default:
|
||||
return label;
|
||||
}
|
||||
},
|
||||
[locale, unit],
|
||||
);
|
||||
|
||||
const renderTooltip = useCallback(
|
||||
(setTooltip, model) => {
|
||||
const { opacity, labelColors, dataPoints } = model.tooltip;
|
||||
|
||||
if (!dataPoints?.length || !opacity) {
|
||||
setTooltip(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',
|
||||
};
|
||||
|
||||
setTooltip(
|
||||
<>
|
||||
<div>{dateFormat(new Date(dataPoints[0].raw.x), formats[unit], locale)}</div>
|
||||
<div>
|
||||
<StatusLight color={labelColors?.[0]?.backgroundColor}>
|
||||
{formatLongNumber(dataPoints[0].raw.y)} {dataPoints[0].dataset.label}
|
||||
</StatusLight>
|
||||
</div>
|
||||
</>,
|
||||
);
|
||||
},
|
||||
[unit],
|
||||
);
|
||||
|
||||
const datasets = useMemo(() => {
|
||||
if (!data) return [];
|
||||
|
|
@ -37,13 +77,13 @@ export function PageviewsChart({ websiteId, data, unit, records, className, load
|
|||
label: formatMessage(labels.uniqueVisitors),
|
||||
data: data.sessions,
|
||||
borderWidth: 1,
|
||||
...colors.visitors,
|
||||
...colors.chart.visitors,
|
||||
},
|
||||
{
|
||||
label: formatMessage(labels.pageViews),
|
||||
data: data.pageviews,
|
||||
borderWidth: 1,
|
||||
...colors.views,
|
||||
...colors.chart.views,
|
||||
},
|
||||
];
|
||||
}, [data, locale, colors]);
|
||||
|
|
@ -55,8 +95,9 @@ export function PageviewsChart({ websiteId, data, unit, records, className, load
|
|||
className={className}
|
||||
datasets={datasets}
|
||||
unit={unit}
|
||||
records={records}
|
||||
loading={loading}
|
||||
renderXLabel={renderXLabel}
|
||||
renderTooltip={renderTooltip}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,9 @@
|
|||
.header {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.title {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue