src/visits/helpers/LineChartCard.js   A
last analyzed

Complexity

Total Complexity 4
Complexity/F 4

Size

Lines of Code 185
Function Count 1

Duplication

Duplicated Lines 0
Ratio 0 %

Test Coverage

Coverage 100%

Importance

Changes 0
Metric Value
wmc 4
eloc 158
mnd 3
bc 3
fnc 1
dl 0
loc 185
bpm 3
cpm 4
noi 0
c 0
b 0
f 0
ccs 49
cts 49
cp 1
rs 10

1 Function

Rating   Name   Duplication   Size   Complexity  
A LineChartCard.js ➔ weekly 0 5 1
1
import React, { useState, useMemo } from 'react';
2
import PropTypes from 'prop-types';
3
import {
4
  Card,
5
  CardHeader,
6
  CardBody,
7
  UncontrolledDropdown,
8
  DropdownToggle,
9
  DropdownMenu,
10
  DropdownItem,
11
} from 'reactstrap';
12
import { Line } from 'react-chartjs-2';
13
import { always, cond, reverse } from 'ramda';
14
import moment from 'moment';
15
import { VisitType } from '../types';
16
import { fillTheGaps } from '../../utils/helpers/visits';
17
import './LineChartCard.scss';
18
import { useToggle } from '../../utils/helpers/hooks';
19
import { rangeOf } from '../../utils/utils';
20
import Checkbox from '../../utils/Checkbox';
21
22 2
const propTypes = {
23
  title: PropTypes.string,
24
  highlightedLabel: PropTypes.string,
25
  visits: PropTypes.arrayOf(VisitType),
26
  highlightedVisits: PropTypes.arrayOf(VisitType),
27
};
28
29 2
const STEPS_MAP = {
30
  monthly: 'Month',
31
  weekly: 'Week',
32
  daily: 'Day',
33
  hourly: 'Hour',
34
};
35
36 2
const STEP_TO_DATE_UNIT_MAP = {
37
  hourly: 'hour',
38
  daily: 'day',
39
  weekly: 'week',
40
  monthly: 'month',
41
};
42
43 2
const STEP_TO_DATE_FORMAT = {
44 4
  hourly: (date) => moment(date).format('YYYY-MM-DD HH:00'),
45 1
  daily: (date) => moment(date).format('YYYY-MM-DD'),
46
  weekly(date) {
47 2
    const firstWeekDay = moment(date).isoWeekday(1).format('YYYY-MM-DD');
48 2
    const lastWeekDay = moment(date).isoWeekday(7).format('YYYY-MM-DD');
49
50 2
    return `${firstWeekDay} - ${lastWeekDay}`;
51
  },
52 8
  monthly: (date) => moment(date).format('YYYY-MM'),
53
};
54
55 2
const determineInitialStep = (oldestVisitDate) => {
56 10
  const now = moment();
57 10
  const oldestDate = moment(oldestVisitDate);
58 10
  const matcher = cond([
59 10
    [ () => now.diff(oldestDate, 'day') <= 2, always('hourly') ], // Less than 2 days
60 7
    [ () => now.diff(oldestDate, 'month') <= 1, always('daily') ], // Between 2 days and 1 month
61 6
    [ () => now.diff(oldestDate, 'month') <= 6, always('weekly') ], // Between 1 and 6 months
62
  ]);
63
64 10
  return matcher() || 'monthly';
65
};
66
67 25
const groupVisitsByStep = (step, visits) => visits.reduce((acc, visit) => {
68 11
  const key = STEP_TO_DATE_FORMAT[step](visit.date);
69
70 11
  acc[key] = acc[key] ? acc[key] + 1 : 1;
71
72 11
  return acc;
73
}, {});
74
75 2
const generateLabels = (step, visits) => {
76 1
  const unit = STEP_TO_DATE_UNIT_MAP[step];
77 1
  const formatter = STEP_TO_DATE_FORMAT[step];
78 1
  const newerDate = moment(visits[0].date);
79 1
  const oldestDate = moment(visits[visits.length - 1].date);
80 1
  const size = newerDate.diff(oldestDate, unit);
81
82 1
  return [
83
    formatter(oldestDate),
84 3
    ...rangeOf(size, () => formatter(oldestDate.add(1, unit))),
85
  ];
86
};
87
88 2
const generateLabelsAndGroupedVisits = (visits, groupedVisitsWithGaps, step, skipNoElements) => {
89 13
  if (skipNoElements) {
90 12
    return [ Object.keys(groupedVisitsWithGaps), groupedVisitsWithGaps ];
91
  }
92
93 1
  const labels = generateLabels(step, visits);
94
95 1
  return [ labels, fillTheGaps(groupedVisitsWithGaps, labels) ];
96
};
97
98 14
const generateDataset = (stats, label, color) => ({
99
  label,
100
  data: Object.values(stats),
101
  fill: false,
102
  lineTension: 0.2,
103
  borderColor: color,
104
  backgroundColor: color,
105
});
106
107 2
const LineChartCard = ({ title, visits, highlightedVisits, highlightedLabel = 'Selected' }) => {
108 13
  const [ step, setStep ] = useState(
109
    visits.length > 0 ? determineInitialStep(visits[visits.length - 1].date) : 'monthly'
110
  );
111 13
  const [ skipNoVisits, toggleSkipNoVisits ] = useToggle(true);
112
113 13
  const groupedVisitsWithGaps = useMemo(() => groupVisitsByStep(step, reverse(visits)), [ step, visits ]);
114 13
  const [ labels, groupedVisits ] = useMemo(
115 13
    () => generateLabelsAndGroupedVisits(visits, groupedVisitsWithGaps, step, skipNoVisits),
116
    [ visits, step, skipNoVisits ]
117
  );
118 13
  const groupedHighlighted = useMemo(
119 13
    () => fillTheGaps(groupVisitsByStep(step, reverse(highlightedVisits)), labels),
120
    [ highlightedVisits, step, labels ]
121
  );
122
123 13
  const data = {
124
    labels,
125
    datasets: [
126
      generateDataset(groupedVisits, 'Visits', '#4696e5'),
127
      highlightedVisits.length > 0 && generateDataset(groupedHighlighted, highlightedLabel, '#F77F28'),
128
    ].filter(Boolean),
129
  };
130 13
  const options = {
131
    maintainAspectRatio: false,
132
    legend: { display: false },
133
    scales: {
134
      yAxes: [
135
        {
136
          ticks: { beginAtZero: true, precision: 0 },
137
        },
138
      ],
139
      xAxes: [
140
        {
141
          scaleLabel: { display: true, labelString: STEPS_MAP[step] },
142
        },
143
      ],
144
    },
145
    tooltips: {
146
      intersect: false,
147
      axis: 'x',
148
    },
149
  };
150
151 13
  return (
152
    <Card>
153
      <CardHeader>
154
        {title}
155
        <div className="float-right">
156
          <UncontrolledDropdown>
157
            <DropdownToggle caret color="link" className="btn-sm p-0">
158
              Group by
159
            </DropdownToggle>
160
            <DropdownMenu right>
161
              {Object.entries(STEPS_MAP).map(([ value, menuText ]) => (
162 52
                <DropdownItem key={value} active={step === value} onClick={() => setStep(value)}>
163
                  {menuText}
164
                </DropdownItem>
165
              ))}
166
            </DropdownMenu>
167
          </UncontrolledDropdown>
168
        </div>
169
        <div className="float-right mr-2">
170
          <Checkbox checked={skipNoVisits} onChange={toggleSkipNoVisits}>
171
            <small>Skip dates with no visits</small>
172
          </Checkbox>
173
        </div>
174
      </CardHeader>
175
      <CardBody className="line-chart-card__body">
176
        <Line data={data} options={options} />
177
      </CardBody>
178
    </Card>
179
  );
180
};
181
182 2
LineChartCard.propTypes = propTypes;
183
184
export default LineChartCard;
185