mirror of
https://github.com/kremalicious/umami.git
synced 2024-12-18 15:23:38 +01:00
Refactor chart components.
This commit is contained in:
parent
37bc082efc
commit
7d659212b0
@ -4,6 +4,7 @@ import styles from './Dot.module.css';
|
|||||||
|
|
||||||
export default function Dot({ color, size, className }) {
|
export default function Dot({ color, size, className }) {
|
||||||
return (
|
return (
|
||||||
|
<div className={styles.wrapper}>
|
||||||
<div
|
<div
|
||||||
style={{ background: color }}
|
style={{ background: color }}
|
||||||
className={classNames(styles.dot, className, {
|
className={classNames(styles.dot, className, {
|
||||||
@ -11,5 +12,6 @@ export default function Dot({ color, size, className }) {
|
|||||||
[styles.large]: size === 'large',
|
[styles.large]: size === 'large',
|
||||||
})}
|
})}
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,9 +1,14 @@
|
|||||||
|
.wrapper {
|
||||||
|
background: var(--gray50);
|
||||||
|
margin-right: 10px;
|
||||||
|
border-radius: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
.dot {
|
.dot {
|
||||||
background: var(--green400);
|
background: var(--green400);
|
||||||
width: 10px;
|
width: 10px;
|
||||||
height: 10px;
|
height: 10px;
|
||||||
border-radius: 100%;
|
border-radius: 100%;
|
||||||
margin-right: 10px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.dot.small {
|
.dot.small {
|
||||||
|
@ -1,14 +1,14 @@
|
|||||||
import React, { useState, useRef, useEffect } from 'react';
|
import React, { useState, useRef, useEffect } from 'react';
|
||||||
import ReactTooltip from 'react-tooltip';
|
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import ChartJS from 'chart.js';
|
import ChartJS from 'chart.js';
|
||||||
import Dot from 'components/common/Dot';
|
import Legend from 'components/metrics/Legend';
|
||||||
import { formatLongNumber } from 'lib/format';
|
import { formatLongNumber } from 'lib/format';
|
||||||
import { dateFormat } from 'lib/lang';
|
import { dateFormat } from 'lib/lang';
|
||||||
import useLocale from 'hooks/useLocale';
|
import useLocale from 'hooks/useLocale';
|
||||||
import useTheme from 'hooks/useTheme';
|
import useTheme from 'hooks/useTheme';
|
||||||
import { DEFAUL_CHART_HEIGHT, DEFAULT_ANIMATION_DURATION, THEME_COLORS } from 'lib/constants';
|
import { DEFAUL_CHART_HEIGHT, DEFAULT_ANIMATION_DURATION, THEME_COLORS } from 'lib/constants';
|
||||||
import styles from './BarChart.module.css';
|
import styles from './BarChart.module.css';
|
||||||
|
import ChartTooltip from './ChartTooltip';
|
||||||
|
|
||||||
export default function BarChart({
|
export default function BarChart({
|
||||||
chartId,
|
chartId,
|
||||||
@ -25,7 +25,6 @@ export default function BarChart({
|
|||||||
}) {
|
}) {
|
||||||
const canvas = useRef();
|
const canvas = useRef();
|
||||||
const chart = useRef();
|
const chart = useRef();
|
||||||
const [legend, setLegend] = useState();
|
|
||||||
const [tooltip, setTooltip] = useState(null);
|
const [tooltip, setTooltip] = useState(null);
|
||||||
const [locale] = useLocale();
|
const [locale] = useLocale();
|
||||||
const [theme] = useTheme();
|
const [theme] = useTheme();
|
||||||
@ -143,7 +142,6 @@ export default function BarChart({
|
|||||||
callback: renderYLabel,
|
callback: renderYLabel,
|
||||||
beginAtZero: true,
|
beginAtZero: true,
|
||||||
fontColor: colors.text,
|
fontColor: colors.text,
|
||||||
precision: 0,
|
|
||||||
},
|
},
|
||||||
gridLines: {
|
gridLines: {
|
||||||
color: colors.line,
|
color: colors.line,
|
||||||
@ -164,8 +162,6 @@ export default function BarChart({
|
|||||||
},
|
},
|
||||||
options,
|
options,
|
||||||
});
|
});
|
||||||
|
|
||||||
chart.current.generateLegend();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateChart() {
|
function updateChart() {
|
||||||
@ -184,8 +180,6 @@ export default function BarChart({
|
|||||||
onUpdate(chart.current);
|
onUpdate(chart.current);
|
||||||
|
|
||||||
chart.current.update();
|
chart.current.update();
|
||||||
|
|
||||||
chart.current.generateLegend();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -196,7 +190,6 @@ export default function BarChart({
|
|||||||
setTooltip(null);
|
setTooltip(null);
|
||||||
updateChart();
|
updateChart();
|
||||||
}
|
}
|
||||||
setLegend({ ...chart.current.legend });
|
|
||||||
}
|
}
|
||||||
}, [datasets, unit, animationDuration, locale, theme]);
|
}, [datasets, unit, animationDuration, locale, theme]);
|
||||||
|
|
||||||
@ -210,35 +203,8 @@ export default function BarChart({
|
|||||||
>
|
>
|
||||||
<canvas ref={canvas} />
|
<canvas ref={canvas} />
|
||||||
</div>
|
</div>
|
||||||
<Legend items={legend?.legendItems} locale={locale} />
|
<Legend chart={chart.current} />
|
||||||
<ReactTooltip id={`${chartId}-tooltip`}>
|
<ChartTooltip chartId={chartId} tooltip={tooltip} />
|
||||||
{tooltip ? <Tooltip {...tooltip} /> : null}
|
|
||||||
</ReactTooltip>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const Legend = ({ items = [], locale }) => (
|
|
||||||
<div className={styles.legend}>
|
|
||||||
{items.map(({ text, fillStyle }) => (
|
|
||||||
<div key={text} className={styles.label}>
|
|
||||||
<Dot color={fillStyle} />
|
|
||||||
<span className={locale}>{text}</span>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
const Tooltip = ({ title, value, label, labelColor }) => (
|
|
||||||
<div className={styles.tooltip}>
|
|
||||||
<div className={styles.content}>
|
|
||||||
<div className={styles.title}>{title}</div>
|
|
||||||
<div className={styles.metric}>
|
|
||||||
<div className={styles.dot}>
|
|
||||||
<div className={styles.color} style={{ backgroundColor: labelColor }} />
|
|
||||||
</div>
|
|
||||||
{value} {label}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
@ -1,60 +1,3 @@
|
|||||||
.chart {
|
.chart {
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tooltip {
|
|
||||||
color: var(--msgColor);
|
|
||||||
pointer-events: none;
|
|
||||||
z-index: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.content {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.title {
|
|
||||||
font-size: var(--font-size-xsmall);
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.metric {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
font-size: var(--font-size-small);
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dot {
|
|
||||||
position: relative;
|
|
||||||
overflow: hidden;
|
|
||||||
border-radius: 100%;
|
|
||||||
margin-right: 8px;
|
|
||||||
background: var(--gray50);
|
|
||||||
}
|
|
||||||
|
|
||||||
.color {
|
|
||||||
width: 10px;
|
|
||||||
height: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.legend {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
margin-top: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.label {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
font-size: var(--font-size-xsmall);
|
|
||||||
}
|
|
||||||
|
|
||||||
.label + .label {
|
|
||||||
margin-left: 20px;
|
|
||||||
}
|
|
||||||
|
26
components/metrics/ChartTooltip.js
Normal file
26
components/metrics/ChartTooltip.js
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import Dot from 'components/common/Dot';
|
||||||
|
import styles from './ChartTooltip.module.css';
|
||||||
|
import ReactTooltip from 'react-tooltip';
|
||||||
|
|
||||||
|
export default function ChartTooltip({ chartId, tooltip }) {
|
||||||
|
if (!tooltip) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { title, value, label, labelColor } = tooltip;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ReactTooltip id={`${chartId}-tooltip`}>
|
||||||
|
<div className={styles.tooltip}>
|
||||||
|
<div className={styles.content}>
|
||||||
|
<div className={styles.title}>{title}</div>
|
||||||
|
<div className={styles.metric}>
|
||||||
|
<Dot color={labelColor} />
|
||||||
|
{value} {label}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ReactTooltip>
|
||||||
|
);
|
||||||
|
}
|
43
components/metrics/ChartTooltip.module.css
Normal file
43
components/metrics/ChartTooltip.module.css
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
.chart {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tooltip {
|
||||||
|
color: var(--msgColor);
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-size: var(--font-size-xsmall);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.metric {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
font-size: var(--font-size-small);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dot {
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
border-radius: 100%;
|
||||||
|
margin-right: 8px;
|
||||||
|
background: var(--gray50);
|
||||||
|
}
|
||||||
|
|
||||||
|
.color {
|
||||||
|
width: 10px;
|
||||||
|
height: 10px;
|
||||||
|
}
|
@ -16,7 +16,7 @@ export default function EventsChart({ websiteId, className, token }) {
|
|||||||
const { query } = usePageQuery();
|
const { query } = usePageQuery();
|
||||||
const shareToken = useShareToken();
|
const shareToken = useShareToken();
|
||||||
|
|
||||||
const { data } = useFetch(
|
const { data, loading } = useFetch(
|
||||||
`/api/website/${websiteId}/events`,
|
`/api/website/${websiteId}/events`,
|
||||||
{
|
{
|
||||||
params: {
|
params: {
|
||||||
@ -31,8 +31,10 @@ export default function EventsChart({ websiteId, className, token }) {
|
|||||||
},
|
},
|
||||||
[modified],
|
[modified],
|
||||||
);
|
);
|
||||||
|
|
||||||
const datasets = useMemo(() => {
|
const datasets = useMemo(() => {
|
||||||
if (!data) return [];
|
if (!data) return [];
|
||||||
|
if (loading) return data;
|
||||||
|
|
||||||
const map = data.reduce((obj, { x, t, y }) => {
|
const map = data.reduce((obj, { x, t, y }) => {
|
||||||
if (!obj[x]) {
|
if (!obj[x]) {
|
||||||
@ -59,7 +61,7 @@ export default function EventsChart({ websiteId, className, token }) {
|
|||||||
borderWidth: 1,
|
borderWidth: 1,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}, [data]);
|
}, [data, loading]);
|
||||||
|
|
||||||
function handleUpdate(chart) {
|
function handleUpdate(chart) {
|
||||||
chart.data.datasets = datasets;
|
chart.data.datasets = datasets;
|
||||||
@ -79,6 +81,7 @@ export default function EventsChart({ websiteId, className, token }) {
|
|||||||
unit={unit}
|
unit={unit}
|
||||||
records={getDateLength(startDate, endDate, unit)}
|
records={getDateLength(startDate, endDate, unit)}
|
||||||
onUpdate={handleUpdate}
|
onUpdate={handleUpdate}
|
||||||
|
loading={loading}
|
||||||
stacked
|
stacked
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
40
components/metrics/Legend.js
Normal file
40
components/metrics/Legend.js
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import Dot from 'components/common/Dot';
|
||||||
|
import useLocale from 'hooks/useLocale';
|
||||||
|
import styles from './Legend.module.css';
|
||||||
|
import useForceUpdate from '../../hooks/useForceUpdate';
|
||||||
|
|
||||||
|
export default function Legend({ chart }) {
|
||||||
|
const [locale] = useLocale();
|
||||||
|
const forceUpdate = useForceUpdate();
|
||||||
|
|
||||||
|
function handleClick(index) {
|
||||||
|
const meta = chart.getDatasetMeta(index);
|
||||||
|
|
||||||
|
meta.hidden = meta.hidden === null ? !chart.data.datasets[index].hidden : null;
|
||||||
|
|
||||||
|
chart.update();
|
||||||
|
|
||||||
|
forceUpdate();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!chart?.legend?.legendItems.find(({ text }) => text)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.legend}>
|
||||||
|
{chart.legend.legendItems.map(({ text, fillStyle, datasetIndex, hidden }) => (
|
||||||
|
<div
|
||||||
|
key={text}
|
||||||
|
className={classNames(styles.label, { [styles.hidden]: hidden })}
|
||||||
|
onClick={() => handleClick(datasetIndex)}
|
||||||
|
>
|
||||||
|
<Dot color={fillStyle} />
|
||||||
|
<span className={locale}>{text}</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
21
components/metrics/Legend.module.css
Normal file
21
components/metrics/Legend.module.css
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
.legend {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.label {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
font-size: var(--font-size-xsmall);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.label + .label {
|
||||||
|
margin-left: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hidden {
|
||||||
|
color: var(--gray400);
|
||||||
|
}
|
@ -31,7 +31,7 @@ export default function MetricsBar({ websiteId, className }) {
|
|||||||
},
|
},
|
||||||
headers: { [TOKEN_HEADER]: shareToken?.token },
|
headers: { [TOKEN_HEADER]: shareToken?.token },
|
||||||
},
|
},
|
||||||
[modified],
|
[url, modified],
|
||||||
);
|
);
|
||||||
|
|
||||||
const formatFunc = format ? formatLongNumber : formatNumber;
|
const formatFunc = format ? formatLongNumber : formatNumber;
|
||||||
|
@ -47,7 +47,7 @@ export default function WebsiteChart({
|
|||||||
onDataLoad,
|
onDataLoad,
|
||||||
headers: { [TOKEN_HEADER]: shareToken?.token },
|
headers: { [TOKEN_HEADER]: shareToken?.token },
|
||||||
},
|
},
|
||||||
[modified],
|
[url, modified],
|
||||||
);
|
);
|
||||||
|
|
||||||
const chartData = useMemo(() => {
|
const chartData = useMemo(() => {
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "umami",
|
"name": "umami",
|
||||||
"version": "0.96.0",
|
"version": "1.0.0",
|
||||||
"description": "A simple, fast, website analytics alternative to Google Analytics. ",
|
"description": "A simple, fast, website analytics alternative to Google Analytics. ",
|
||||||
"author": "Mike Cao <mike@mikecao.com>",
|
"author": "Mike Cao <mike@mikecao.com>",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
Loading…
Reference in New Issue
Block a user