mirror of
https://github.com/shlinkio/shlink-web-client.git
synced 2024-12-23 17:40:23 +03:00
Merge pull request #272 from acelaya-forks/feature/line-chart-improvements
Some improvements on LineChartCard
This commit is contained in:
commit
daf9e7cf64
3 changed files with 86 additions and 12 deletions
|
@ -14,7 +14,10 @@ import { reverse } from 'ramda';
|
||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
import { VisitType } from '../types';
|
import { VisitType } from '../types';
|
||||||
import { fillTheGaps } from '../../utils/helpers/visits';
|
import { fillTheGaps } from '../../utils/helpers/visits';
|
||||||
import './LineCHartCard.scss';
|
import './LineChartCard.scss';
|
||||||
|
import { useToggle } from '../../utils/helpers/hooks';
|
||||||
|
import { rangeOf } from '../../utils/utils';
|
||||||
|
import Checkbox from '../../utils/Checkbox';
|
||||||
|
|
||||||
const propTypes = {
|
const propTypes = {
|
||||||
title: PropTypes.string,
|
title: PropTypes.string,
|
||||||
|
@ -22,12 +25,19 @@ const propTypes = {
|
||||||
highlightedVisits: PropTypes.arrayOf(VisitType),
|
highlightedVisits: PropTypes.arrayOf(VisitType),
|
||||||
};
|
};
|
||||||
|
|
||||||
const steps = [
|
const STEPS_MAP = {
|
||||||
{ value: 'monthly', menuText: 'Month' },
|
monthly: 'Month',
|
||||||
{ value: 'weekly', menuText: 'Week' },
|
weekly: 'Week',
|
||||||
{ value: 'daily', menuText: 'Day' },
|
daily: 'Day',
|
||||||
{ value: 'hourly', menuText: 'Hour' },
|
hourly: 'Hour',
|
||||||
];
|
};
|
||||||
|
|
||||||
|
const STEP_TO_DATE_UNIT_MAP = {
|
||||||
|
hourly: 'hour',
|
||||||
|
daily: 'day',
|
||||||
|
weekly: 'week',
|
||||||
|
monthly: 'month',
|
||||||
|
};
|
||||||
|
|
||||||
const STEP_TO_DATE_FORMAT = {
|
const STEP_TO_DATE_FORMAT = {
|
||||||
hourly: (date) => moment(date).format('YYYY-MM-DD HH:00'),
|
hourly: (date) => moment(date).format('YYYY-MM-DD HH:00'),
|
||||||
|
@ -49,6 +59,31 @@ const groupVisitsByStep = (step, visits) => visits.reduce((acc, visit) => {
|
||||||
return acc;
|
return acc;
|
||||||
}, {});
|
}, {});
|
||||||
|
|
||||||
|
const generateLabels = (step, visits) => {
|
||||||
|
const unit = STEP_TO_DATE_UNIT_MAP[step];
|
||||||
|
const formatter = STEP_TO_DATE_FORMAT[step];
|
||||||
|
const newerDate = moment(visits[0].date);
|
||||||
|
const oldestDate = moment(visits[visits.length - 1].date);
|
||||||
|
const size = newerDate.diff(oldestDate, unit);
|
||||||
|
|
||||||
|
return [
|
||||||
|
formatter(oldestDate),
|
||||||
|
...rangeOf(size, () => formatter(oldestDate.add(1, unit))),
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
const generateLabelsAndGroupedVisits = (visits, step, skipNoElements) => {
|
||||||
|
const groupedVisits = groupVisitsByStep(step, reverse(visits));
|
||||||
|
|
||||||
|
if (skipNoElements) {
|
||||||
|
return [ Object.keys(groupedVisits), groupedVisits ];
|
||||||
|
}
|
||||||
|
|
||||||
|
const labels = generateLabels(step, visits);
|
||||||
|
|
||||||
|
return [ labels, fillTheGaps(groupedVisits, labels) ];
|
||||||
|
};
|
||||||
|
|
||||||
const generateDataset = (stats, label, color) => ({
|
const generateDataset = (stats, label, color) => ({
|
||||||
label,
|
label,
|
||||||
data: Object.values(stats),
|
data: Object.values(stats),
|
||||||
|
@ -59,9 +94,13 @@ const generateDataset = (stats, label, color) => ({
|
||||||
});
|
});
|
||||||
|
|
||||||
const LineChartCard = ({ title, visits, highlightedVisits }) => {
|
const LineChartCard = ({ title, visits, highlightedVisits }) => {
|
||||||
const [ step, setStep ] = useState(steps[0].value);
|
const [ step, setStep ] = useState('monthly');
|
||||||
const groupedVisits = useMemo(() => groupVisitsByStep(step, reverse(visits)), [ visits, step ]);
|
const [ skipNoVisits, toggleSkipNoVisits ] = useToggle(true);
|
||||||
const labels = useMemo(() => Object.keys(groupedVisits), [ groupedVisits ]);
|
|
||||||
|
const [ labels, groupedVisits ] = useMemo(
|
||||||
|
() => generateLabelsAndGroupedVisits(visits, step, skipNoVisits),
|
||||||
|
[ visits, step, skipNoVisits ]
|
||||||
|
);
|
||||||
const groupedHighlighted = useMemo(
|
const groupedHighlighted = useMemo(
|
||||||
() => fillTheGaps(groupVisitsByStep(step, reverse(highlightedVisits)), labels),
|
() => fillTheGaps(groupVisitsByStep(step, reverse(highlightedVisits)), labels),
|
||||||
[ highlightedVisits, step, labels ]
|
[ highlightedVisits, step, labels ]
|
||||||
|
@ -83,6 +122,15 @@ const LineChartCard = ({ title, visits, highlightedVisits }) => {
|
||||||
ticks: { beginAtZero: true, precision: 0 },
|
ticks: { beginAtZero: true, precision: 0 },
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
xAxes: [
|
||||||
|
{
|
||||||
|
scaleLabel: { display: true, labelString: STEPS_MAP[step] },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
tooltips: {
|
||||||
|
intersect: false,
|
||||||
|
axis: 'x',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -96,7 +144,7 @@ const LineChartCard = ({ title, visits, highlightedVisits }) => {
|
||||||
Group by
|
Group by
|
||||||
</DropdownToggle>
|
</DropdownToggle>
|
||||||
<DropdownMenu right>
|
<DropdownMenu right>
|
||||||
{steps.map(({ menuText, value }) => (
|
{Object.entries(STEPS_MAP).map(([ value, menuText ]) => (
|
||||||
<DropdownItem key={value} active={step === value} onClick={() => setStep(value)}>
|
<DropdownItem key={value} active={step === value} onClick={() => setStep(value)}>
|
||||||
{menuText}
|
{menuText}
|
||||||
</DropdownItem>
|
</DropdownItem>
|
||||||
|
@ -104,6 +152,11 @@ const LineChartCard = ({ title, visits, highlightedVisits }) => {
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
</UncontrolledDropdown>
|
</UncontrolledDropdown>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="float-right mr-2">
|
||||||
|
<Checkbox checked={skipNoVisits} onChange={toggleSkipNoVisits}>
|
||||||
|
<small>Skip dates with no visits</small>
|
||||||
|
</Checkbox>
|
||||||
|
</div>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardBody className="line-chart-card__body">
|
<CardBody className="line-chart-card__body">
|
||||||
<Line data={data} options={options} />
|
<Line data={data} options={options} />
|
||||||
|
|
|
@ -3,8 +3,9 @@ import { shallow } from 'enzyme';
|
||||||
import { CardHeader, DropdownItem } from 'reactstrap';
|
import { CardHeader, DropdownItem } from 'reactstrap';
|
||||||
import { Line } from 'react-chartjs-2';
|
import { Line } from 'react-chartjs-2';
|
||||||
import LineChartCard from '../../../src/visits/helpers/LineChartCard';
|
import LineChartCard from '../../../src/visits/helpers/LineChartCard';
|
||||||
|
import Checkbox from '../../../src/utils/Checkbox';
|
||||||
|
|
||||||
describe('<LineCHartCard />', () => {
|
describe('<LineChartCard />', () => {
|
||||||
let wrapper;
|
let wrapper;
|
||||||
const createWrapper = (visits = [], highlightedVisits = []) => {
|
const createWrapper = (visits = [], highlightedVisits = []) => {
|
||||||
wrapper = shallow(<LineChartCard title="Cool title" visits={visits} highlightedVisits={highlightedVisits} />);
|
wrapper = shallow(<LineChartCard title="Cool title" visits={visits} highlightedVisits={highlightedVisits} />);
|
||||||
|
@ -49,6 +50,15 @@ describe('<LineCHartCard />', () => {
|
||||||
ticks: { beginAtZero: true, precision: 0 },
|
ticks: { beginAtZero: true, precision: 0 },
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
xAxes: [
|
||||||
|
{
|
||||||
|
scaleLabel: { display: true, labelString: 'Month' },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
tooltips: {
|
||||||
|
intersect: false,
|
||||||
|
axis: 'x',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -63,4 +73,15 @@ describe('<LineCHartCard />', () => {
|
||||||
|
|
||||||
expect(datasets).toHaveLength(expectedLines);
|
expect(datasets).toHaveLength(expectedLines);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('includes stats for visits with no dates if selected', () => {
|
||||||
|
const wrapper = createWrapper([
|
||||||
|
{ date: '2016-04-01' },
|
||||||
|
{ date: '2016-01-01' },
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect(wrapper.find(Line).prop('data').labels).toHaveLength(2);
|
||||||
|
wrapper.find(Checkbox).simulate('change');
|
||||||
|
expect(wrapper.find(Line).prop('data').labels).toHaveLength(4);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
Loading…
Reference in a new issue