mirror of
https://github.com/shlinkio/shlink-web-client.git
synced 2025-01-09 09:47:28 +03:00
Created first version of the time-based visits chart
This commit is contained in:
parent
c670d86955
commit
61867366e7
5 changed files with 98 additions and 12 deletions
|
@ -1,4 +1,5 @@
|
|||
import bowser from 'bowser';
|
||||
import { zipObj } from 'ramda';
|
||||
import { hasValue } from '../utils';
|
||||
|
||||
const DEFAULT = 'Others';
|
||||
|
@ -35,3 +36,5 @@ export const extractDomain = (url) => {
|
|||
|
||||
return domain.split(':')[0];
|
||||
};
|
||||
|
||||
export const fillTheGaps = (stats, labels) => Object.values({ ...zipObj(labels, labels.map(() => 0)), ...stats });
|
||||
|
|
|
@ -2,7 +2,8 @@ import { Card, CardHeader, CardBody, CardFooter } from 'reactstrap';
|
|||
import { Doughnut, HorizontalBar } from 'react-chartjs-2';
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import { keys, values, zipObj } from 'ramda';
|
||||
import { keys, values } from 'ramda';
|
||||
import { fillTheGaps } from '../utils/helpers/visits';
|
||||
import './GraphCard.scss';
|
||||
|
||||
const propTypes = {
|
||||
|
@ -20,7 +21,7 @@ const generateGraphData = (title, isBarChart, labels, data, highlightedData) =>
|
|||
datasets: [
|
||||
{
|
||||
title,
|
||||
label: highlightedData && 'Non-selected',
|
||||
label: highlightedData ? 'Non-selected' : 'Visits',
|
||||
data,
|
||||
backgroundColor: isBarChart ? 'rgba(70, 150, 229, 0.4)' : [
|
||||
'#97BBCD',
|
||||
|
@ -70,9 +71,7 @@ const renderGraph = (title, isBarChart, stats, max, highlightedStats, onClick) =
|
|||
|
||||
return acc;
|
||||
}, { ...stats }));
|
||||
const highlightedData = hasHighlightedStats && values(
|
||||
{ ...zipObj(labels, labels.map(() => 0)), ...highlightedStats }
|
||||
);
|
||||
const highlightedData = hasHighlightedStats && fillTheGaps(highlightedStats, labels);
|
||||
|
||||
const options = {
|
||||
legend: isBarChart ? { display: false } : { position: 'right' },
|
||||
|
@ -120,7 +119,7 @@ const renderGraph = (title, isBarChart, stats, max, highlightedStats, onClick) =
|
|||
};
|
||||
|
||||
const GraphCard = ({ title, footer, isBarChart, stats, max, highlightedStats, onClick }) => (
|
||||
<Card className="mt-4">
|
||||
<Card>
|
||||
<CardHeader className="graph-card__header">{typeof title === 'function' ? title() : title}</CardHeader>
|
||||
<CardBody>{renderGraph(title, isBarChart, stats, max, highlightedStats, onClick)}</CardBody>
|
||||
{footer && <CardFooter className="graph-card__footer--sticky">{footer}</CardFooter>}
|
||||
|
|
|
@ -11,6 +11,7 @@ import { formatDate } from '../utils/helpers/date';
|
|||
import { useToggle } from '../utils/helpers/hooks';
|
||||
import SortableBarGraph from './SortableBarGraph';
|
||||
import GraphCard from './GraphCard';
|
||||
import LineChartCard from './helpers/LineChartCard';
|
||||
import VisitsTable from './VisitsTable';
|
||||
import { VisitsInfoType } from './types';
|
||||
|
||||
|
@ -109,13 +110,16 @@ const VisitsStats = ({ processStatsFromVisits, normalizeVisits }, OpenMapModalBt
|
|||
|
||||
return (
|
||||
<div className="row">
|
||||
<div className="col-xl-4 col-lg-6">
|
||||
<div className="col-12 mt-4">
|
||||
<LineChartCard title="Visits during time" visits={visits} highlightedVisits={highlightedVisits} />
|
||||
</div>
|
||||
<div className="col-xl-4 col-lg-6 mt-4">
|
||||
<GraphCard title="Operating systems" stats={os} />
|
||||
</div>
|
||||
<div className="col-xl-4 col-lg-6">
|
||||
<div className="col-xl-4 col-lg-6 mt-4">
|
||||
<GraphCard title="Browsers" stats={browsers} />
|
||||
</div>
|
||||
<div className="col-xl-4">
|
||||
<div className="col-xl-4 mt-4">
|
||||
<SortableBarGraph
|
||||
title="Referrers"
|
||||
stats={referrers}
|
||||
|
@ -128,7 +132,7 @@ const VisitsStats = ({ processStatsFromVisits, normalizeVisits }, OpenMapModalBt
|
|||
onClick={highlightVisitsForProp('referer')}
|
||||
/>
|
||||
</div>
|
||||
<div className="col-lg-6">
|
||||
<div className="col-lg-6 mt-4">
|
||||
<SortableBarGraph
|
||||
title="Countries"
|
||||
stats={countries}
|
||||
|
@ -140,7 +144,7 @@ const VisitsStats = ({ processStatsFromVisits, normalizeVisits }, OpenMapModalBt
|
|||
onClick={highlightVisitsForProp('country')}
|
||||
/>
|
||||
</div>
|
||||
<div className="col-lg-6">
|
||||
<div className="col-lg-6 mt-4">
|
||||
<SortableBarGraph
|
||||
title="Cities"
|
||||
stats={cities}
|
||||
|
|
79
src/visits/helpers/LineChartCard.js
Normal file
79
src/visits/helpers/LineChartCard.js
Normal file
|
@ -0,0 +1,79 @@
|
|||
import React, { useState, useMemo } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Card, CardHeader, CardBody } from 'reactstrap';
|
||||
import { Line } from 'react-chartjs-2';
|
||||
import { reverse } from 'ramda';
|
||||
import moment from 'moment';
|
||||
import { VisitType } from '../types';
|
||||
import { fillTheGaps } from '../../utils/helpers/visits';
|
||||
|
||||
const propTypes = {
|
||||
title: PropTypes.string,
|
||||
visits: PropTypes.arrayOf(VisitType),
|
||||
highlightedVisits: PropTypes.arrayOf(VisitType),
|
||||
};
|
||||
|
||||
const STEP_TO_DATE_FORMAT_MAP = {
|
||||
hourly: 'YYYY-MM-DD HH:00',
|
||||
daily: 'YYYY-MM-DD',
|
||||
weekly: '',
|
||||
monthly: 'YYYY-MM',
|
||||
};
|
||||
|
||||
const groupVisitsByStep = (step, visits) => visits.reduce((acc, visit) => {
|
||||
const key = moment(visit.date).format(STEP_TO_DATE_FORMAT_MAP[step]);
|
||||
|
||||
acc[key] = acc[key] ? acc[key] + 1 : 1;
|
||||
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
const generateDataset = (stats, label, color) => ({
|
||||
label,
|
||||
data: Object.values(stats),
|
||||
fill: false,
|
||||
lineTension: 0.2,
|
||||
borderColor: color,
|
||||
backgroundColor: color,
|
||||
});
|
||||
|
||||
const LineChartCard = ({ title, visits, highlightedVisits }) => {
|
||||
const [ step ] = useState('monthly'); // hourly, daily, weekly, monthly
|
||||
const groupedVisits = useMemo(() => groupVisitsByStep(step, reverse(visits)), [ visits, step ]);
|
||||
const labels = useMemo(() => Object.keys(groupedVisits), [ groupedVisits ]);
|
||||
const groupedHighlighted = useMemo(
|
||||
() => fillTheGaps(groupVisitsByStep(step, reverse(highlightedVisits)), labels),
|
||||
[ highlightedVisits, step, labels ]
|
||||
);
|
||||
|
||||
const data = {
|
||||
labels,
|
||||
datasets: [
|
||||
generateDataset(groupedVisits, 'Visits', '#4696e5'),
|
||||
highlightedVisits.length > 0 && generateDataset(groupedHighlighted, 'Selected', '#F77F28'),
|
||||
].filter(Boolean),
|
||||
};
|
||||
const options = {
|
||||
legend: { display: false },
|
||||
scales: {
|
||||
yAxes: [
|
||||
{
|
||||
ticks: { beginAtZero: true, precision: 0 },
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>{title}</CardHeader>
|
||||
<CardBody>
|
||||
<Line data={data} options={options} height={80} />
|
||||
</CardBody>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
LineChartCard.propTypes = propTypes;
|
||||
|
||||
export default LineChartCard;
|
|
@ -82,8 +82,9 @@ describe('<GraphCard />', () => {
|
|||
wrapper = shallow(<GraphCard isBarChart title="The chart" stats={stats} highlightedStats={highlightedStats} />);
|
||||
const horizontal = wrapper.find(HorizontalBar);
|
||||
|
||||
const { datasets: [{ data }, highlightedData ] } = horizontal.prop('data');
|
||||
const { datasets: [{ data, label }, highlightedData ] } = horizontal.prop('data');
|
||||
|
||||
expect(label).toEqual(highlightedStats ? 'Non-selected' : 'Visits');
|
||||
expect(data).toEqual(expectedData);
|
||||
expectedHighlightedData && expect(highlightedData.data).toEqual(expectedHighlightedData);
|
||||
!expectedHighlightedData && expect(highlightedData).toBeUndefined();
|
||||
|
|
Loading…
Reference in a new issue