mirror of
https://github.com/kremalicious/umami.git
synced 2024-11-22 01:46:58 +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 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
|
||||
|
||||
### Requirements
|
||||
|
||||
- A server with Node.js 10.13 or newer
|
||||
- A database (MySQL or Postgresql)
|
||||
|
||||
### Get the source code
|
||||
|
||||
```
|
||||
|
@ -1,12 +1,9 @@
|
||||
import React from 'react';
|
||||
import { useSpring, animated } from 'react-spring';
|
||||
import { formatNumber } from '../../lib/format';
|
||||
import styles from './MetricCard.module.css';
|
||||
|
||||
function defaultFormat(n) {
|
||||
return Number(n).toFixed(0);
|
||||
}
|
||||
|
||||
const MetricCard = ({ value = 0, label, format = defaultFormat }) => {
|
||||
const MetricCard = ({ value = 0, label, format = formatNumber }) => {
|
||||
const props = useSpring({ x: value, from: { x: 0 } });
|
||||
|
||||
return (
|
||||
|
@ -2,7 +2,7 @@
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
width: 140px;
|
||||
min-width: 140px;
|
||||
}
|
||||
|
||||
.value {
|
||||
|
@ -2,13 +2,16 @@ import React, { useState, useEffect } from 'react';
|
||||
import classNames from 'classnames';
|
||||
import MetricCard from './MetricCard';
|
||||
import { get } from 'lib/web';
|
||||
import { formatShortTime } from 'lib/format';
|
||||
import { formatShortTime, formatNumber, formatLongNumber } from 'lib/format';
|
||||
import styles from './MetricsBar.module.css';
|
||||
|
||||
export default function MetricsBar({ websiteId, startDate, endDate, className }) {
|
||||
const [data, setData] = useState({});
|
||||
const [format, setFormat] = useState(true);
|
||||
const { pageviews, uniques, bounces, totaltime } = data;
|
||||
|
||||
const formatFunc = format ? formatLongNumber : formatNumber;
|
||||
|
||||
async function loadData() {
|
||||
setData(
|
||||
await get(`/api/website/${websiteId}/metrics`, {
|
||||
@ -18,14 +21,18 @@ export default function MetricsBar({ websiteId, startDate, endDate, className })
|
||||
);
|
||||
}
|
||||
|
||||
function handleSetFormat() {
|
||||
setFormat(state => !state);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
loadData();
|
||||
}, [websiteId, startDate, endDate]);
|
||||
|
||||
return (
|
||||
<div className={classNames(styles.container, className)}>
|
||||
<MetricCard label="Views" value={pageviews} />
|
||||
<MetricCard label="Visitors" value={uniques} />
|
||||
<div className={classNames(styles.bar, className)} onClick={handleSetFormat}>
|
||||
<MetricCard label="Views" value={pageviews} format={formatFunc} />
|
||||
<MetricCard label="Visitors" value={uniques} format={formatFunc} />
|
||||
<MetricCard
|
||||
label="Bounce rate"
|
||||
value={uniques ? (bounces / uniques) * 100 : 0}
|
||||
|
@ -1,8 +1,9 @@
|
||||
.container {
|
||||
.bar {
|
||||
display: flex;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 1000px) {
|
||||
@media only screen and (max-width: 992px) {
|
||||
.container > div:last-child {
|
||||
display: none;
|
||||
}
|
||||
|
@ -2,11 +2,11 @@ import React, { useState, useEffect, useMemo } from 'react';
|
||||
import { FixedSizeList } from 'react-window';
|
||||
import { useSpring, animated, config } from 'react-spring';
|
||||
import classNames from 'classnames';
|
||||
import CheckVisible from 'components/helpers/CheckVisible';
|
||||
import Button from 'components/common/Button';
|
||||
import Arrow from 'assets/arrow-right.svg';
|
||||
import { get } from 'lib/web';
|
||||
import { percentFilter } from 'lib/filters';
|
||||
import { formatNumber, formatLongNumber } from 'lib/format';
|
||||
import styles from './RankingsChart.module.css';
|
||||
|
||||
export default function RankingsChart({
|
||||
@ -23,6 +23,8 @@ export default function RankingsChart({
|
||||
onExpand = () => {},
|
||||
}) {
|
||||
const [data, setData] = useState();
|
||||
const [format, setFormat] = useState(true);
|
||||
const formatFunc = format ? formatLongNumber : formatNumber;
|
||||
|
||||
const rankings = useMemo(() => {
|
||||
if (data) {
|
||||
@ -48,14 +50,8 @@ export default function RankingsChart({
|
||||
onDataLoad(updated);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (websiteId) {
|
||||
loadData();
|
||||
}
|
||||
}, [websiteId, startDate, endDate, type]);
|
||||
|
||||
if (!data) {
|
||||
return null;
|
||||
function handleSetFormat() {
|
||||
setFormat(state => !state);
|
||||
}
|
||||
|
||||
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 (
|
||||
<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.heading}>{heading}</div>
|
||||
</div>
|
||||
<div className={styles.body}>
|
||||
{limit ? (
|
||||
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}>
|
||||
@ -95,7 +108,7 @@ export default function RankingsChart({
|
||||
);
|
||||
}
|
||||
|
||||
const AnimatedRow = ({ label, value, percent, animate }) => {
|
||||
const AnimatedRow = ({ label, value, percent, animate, format }) => {
|
||||
const props = useSpring({
|
||||
width: percent,
|
||||
y: value,
|
||||
@ -106,7 +119,7 @@ const AnimatedRow = ({ label, value, percent, animate }) => {
|
||||
return (
|
||||
<div className={styles.row}>
|
||||
<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}>
|
||||
<animated.div
|
||||
className={styles.bar}
|
||||
|
@ -10,6 +10,7 @@
|
||||
.header {
|
||||
display: flex;
|
||||
line-height: 40px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.title {
|
||||
|
@ -2,4 +2,5 @@
|
||||
padding: 0 30px;
|
||||
background: var(--gray50);
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
@ -39,3 +39,23 @@ export function formatShortTime(val, formats = ['m', 's'], space = '') {
|
||||
|
||||
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",
|
||||
"version": "0.9.0",
|
||||
"version": "0.10.0",
|
||||
"description": "A simple, fast, website analytics alternative to Google Analytics. ",
|
||||
"author": "Mike Cao <mike@mikecao.com>",
|
||||
"license": "MIT",
|
||||
|
Loading…
Reference in New Issue
Block a user