mirror of
https://github.com/kremalicious/umami.git
synced 2024-11-22 18:00:17 +01:00
Format long numbers. Updated README.
This commit is contained in:
parent
c6b9682296
commit
b1493dfef8
11
README.md
11
README.md
@ -1,7 +1,18 @@
|
|||||||
# umami
|
# umami
|
||||||
|
|
||||||
|
Umami is a simple, fast, website analytics alternative to Google Analytics.
|
||||||
|
|
||||||
|
## Getting started
|
||||||
|
|
||||||
|
A detailed getting started guide can be found at [https://umami.is/docs/](https://umami.is/docs/)
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
|
### Requirements
|
||||||
|
|
||||||
|
- A server with Node.js 10.13 or newer
|
||||||
|
- A database (MySQL or Postgresql)
|
||||||
|
|
||||||
### Get the source code
|
### Get the source code
|
||||||
|
|
||||||
```
|
```
|
||||||
|
@ -1,12 +1,9 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { useSpring, animated } from 'react-spring';
|
import { useSpring, animated } from 'react-spring';
|
||||||
|
import { formatNumber } from '../../lib/format';
|
||||||
import styles from './MetricCard.module.css';
|
import styles from './MetricCard.module.css';
|
||||||
|
|
||||||
function defaultFormat(n) {
|
const MetricCard = ({ value = 0, label, format = formatNumber }) => {
|
||||||
return Number(n).toFixed(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
const MetricCard = ({ value = 0, label, format = defaultFormat }) => {
|
|
||||||
const props = useSpring({ x: value, from: { x: 0 } });
|
const props = useSpring({ x: value, from: { x: 0 } });
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
width: 140px;
|
min-width: 140px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.value {
|
.value {
|
||||||
|
@ -2,13 +2,16 @@ import React, { useState, useEffect } from 'react';
|
|||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import MetricCard from './MetricCard';
|
import MetricCard from './MetricCard';
|
||||||
import { get } from 'lib/web';
|
import { get } from 'lib/web';
|
||||||
import { formatShortTime } from 'lib/format';
|
import { formatShortTime, formatNumber, formatLongNumber } from 'lib/format';
|
||||||
import styles from './MetricsBar.module.css';
|
import styles from './MetricsBar.module.css';
|
||||||
|
|
||||||
export default function MetricsBar({ websiteId, startDate, endDate, className }) {
|
export default function MetricsBar({ websiteId, startDate, endDate, className }) {
|
||||||
const [data, setData] = useState({});
|
const [data, setData] = useState({});
|
||||||
|
const [format, setFormat] = useState(true);
|
||||||
const { pageviews, uniques, bounces, totaltime } = data;
|
const { pageviews, uniques, bounces, totaltime } = data;
|
||||||
|
|
||||||
|
const formatFunc = format ? formatLongNumber : formatNumber;
|
||||||
|
|
||||||
async function loadData() {
|
async function loadData() {
|
||||||
setData(
|
setData(
|
||||||
await get(`/api/website/${websiteId}/metrics`, {
|
await get(`/api/website/${websiteId}/metrics`, {
|
||||||
@ -18,14 +21,18 @@ export default function MetricsBar({ websiteId, startDate, endDate, className })
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleSetFormat() {
|
||||||
|
setFormat(state => !state);
|
||||||
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadData();
|
loadData();
|
||||||
}, [websiteId, startDate, endDate]);
|
}, [websiteId, startDate, endDate]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={classNames(styles.container, className)}>
|
<div className={classNames(styles.bar, className)} onClick={handleSetFormat}>
|
||||||
<MetricCard label="Views" value={pageviews} />
|
<MetricCard label="Views" value={pageviews} format={formatFunc} />
|
||||||
<MetricCard label="Visitors" value={uniques} />
|
<MetricCard label="Visitors" value={uniques} format={formatFunc} />
|
||||||
<MetricCard
|
<MetricCard
|
||||||
label="Bounce rate"
|
label="Bounce rate"
|
||||||
value={uniques ? (bounces / uniques) * 100 : 0}
|
value={uniques ? (bounces / uniques) * 100 : 0}
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
.container {
|
.bar {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media only screen and (max-width: 1000px) {
|
@media only screen and (max-width: 992px) {
|
||||||
.container > div:last-child {
|
.container > div:last-child {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
@ -2,11 +2,11 @@ import React, { useState, useEffect, useMemo } from 'react';
|
|||||||
import { FixedSizeList } from 'react-window';
|
import { FixedSizeList } from 'react-window';
|
||||||
import { useSpring, animated, config } from 'react-spring';
|
import { useSpring, animated, config } from 'react-spring';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import CheckVisible from 'components/helpers/CheckVisible';
|
|
||||||
import Button from 'components/common/Button';
|
import Button from 'components/common/Button';
|
||||||
import Arrow from 'assets/arrow-right.svg';
|
import Arrow from 'assets/arrow-right.svg';
|
||||||
import { get } from 'lib/web';
|
import { get } from 'lib/web';
|
||||||
import { percentFilter } from 'lib/filters';
|
import { percentFilter } from 'lib/filters';
|
||||||
|
import { formatNumber, formatLongNumber } from 'lib/format';
|
||||||
import styles from './RankingsChart.module.css';
|
import styles from './RankingsChart.module.css';
|
||||||
|
|
||||||
export default function RankingsChart({
|
export default function RankingsChart({
|
||||||
@ -23,6 +23,8 @@ export default function RankingsChart({
|
|||||||
onExpand = () => {},
|
onExpand = () => {},
|
||||||
}) {
|
}) {
|
||||||
const [data, setData] = useState();
|
const [data, setData] = useState();
|
||||||
|
const [format, setFormat] = useState(true);
|
||||||
|
const formatFunc = format ? formatLongNumber : formatNumber;
|
||||||
|
|
||||||
const rankings = useMemo(() => {
|
const rankings = useMemo(() => {
|
||||||
if (data) {
|
if (data) {
|
||||||
@ -48,14 +50,8 @@ export default function RankingsChart({
|
|||||||
onDataLoad(updated);
|
onDataLoad(updated);
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
function handleSetFormat() {
|
||||||
if (websiteId) {
|
setFormat(state => !state);
|
||||||
loadData();
|
|
||||||
}
|
|
||||||
}, [websiteId, startDate, endDate, type]);
|
|
||||||
|
|
||||||
if (!data) {
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const Row = ({ index, style }) => {
|
const Row = ({ index, style }) => {
|
||||||
@ -67,16 +63,33 @@ export default function RankingsChart({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (websiteId) {
|
||||||
|
loadData();
|
||||||
|
}
|
||||||
|
}, [websiteId, startDate, endDate, type]);
|
||||||
|
|
||||||
|
if (!data) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={classNames(styles.container, className)}>
|
<div className={classNames(styles.container, className)}>
|
||||||
<div className={styles.header}>
|
<div className={styles.header} onClick={handleSetFormat}>
|
||||||
<div className={styles.title}>{title}</div>
|
<div className={styles.title}>{title}</div>
|
||||||
<div className={styles.heading}>{heading}</div>
|
<div className={styles.heading}>{heading}</div>
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.body}>
|
<div className={styles.body}>
|
||||||
{limit ? (
|
{limit ? (
|
||||||
rankings.map(({ x, y, z }) => (
|
rankings.map(({ x, y, z }) => (
|
||||||
<AnimatedRow key={x} label={x} value={y} percent={z} animate={limit} />
|
<AnimatedRow
|
||||||
|
key={x}
|
||||||
|
label={x}
|
||||||
|
value={y}
|
||||||
|
percent={z}
|
||||||
|
animate={limit}
|
||||||
|
format={formatFunc}
|
||||||
|
/>
|
||||||
))
|
))
|
||||||
) : (
|
) : (
|
||||||
<FixedSizeList height={600} itemCount={rankings.length} itemSize={30}>
|
<FixedSizeList height={600} itemCount={rankings.length} itemSize={30}>
|
||||||
@ -95,7 +108,7 @@ export default function RankingsChart({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const AnimatedRow = ({ label, value, percent, animate }) => {
|
const AnimatedRow = ({ label, value, percent, animate, format }) => {
|
||||||
const props = useSpring({
|
const props = useSpring({
|
||||||
width: percent,
|
width: percent,
|
||||||
y: value,
|
y: value,
|
||||||
@ -106,7 +119,7 @@ const AnimatedRow = ({ label, value, percent, animate }) => {
|
|||||||
return (
|
return (
|
||||||
<div className={styles.row}>
|
<div className={styles.row}>
|
||||||
<div className={styles.label}>{label}</div>
|
<div className={styles.label}>{label}</div>
|
||||||
<animated.div className={styles.value}>{props.y.interpolate(n => n.toFixed(0))}</animated.div>
|
<animated.div className={styles.value}>{props.y.interpolate(format)}</animated.div>
|
||||||
<div className={styles.percent}>
|
<div className={styles.percent}>
|
||||||
<animated.div
|
<animated.div
|
||||||
className={styles.bar}
|
className={styles.bar}
|
||||||
|
@ -10,6 +10,7 @@
|
|||||||
.header {
|
.header {
|
||||||
display: flex;
|
display: flex;
|
||||||
line-height: 40px;
|
line-height: 40px;
|
||||||
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.title {
|
.title {
|
||||||
|
@ -2,4 +2,5 @@
|
|||||||
padding: 0 30px;
|
padding: 0 30px;
|
||||||
background: var(--gray50);
|
background: var(--gray50);
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
@ -39,3 +39,23 @@ export function formatShortTime(val, formats = ['m', 's'], space = '') {
|
|||||||
|
|
||||||
return t;
|
return t;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function formatNumber(n) {
|
||||||
|
return Number(n).toFixed(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatLongNumber(n) {
|
||||||
|
if (n >= 1000000) {
|
||||||
|
return `${(n / 1000000).toFixed(1)}m`;
|
||||||
|
}
|
||||||
|
if (n >= 100000) {
|
||||||
|
return `${(n / 1000).toFixed(0)}k`;
|
||||||
|
}
|
||||||
|
if (n >= 10000) {
|
||||||
|
return `${(n / 1000).toFixed(1)}k`;
|
||||||
|
}
|
||||||
|
if (n >= 1000) {
|
||||||
|
return `${(n / 1000).toFixed(2)}k`;
|
||||||
|
}
|
||||||
|
return formatNumber(n);
|
||||||
|
}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "umami",
|
"name": "umami",
|
||||||
"version": "0.9.0",
|
"version": "0.10.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