Ensured info for selected visit in visits table gets highlighted in bar charts

This commit is contained in:
Alejandro Celaya
2020-04-04 20:16:20 +02:00
parent bd4255108d
commit f5cc1abe75
5 changed files with 56 additions and 15 deletions

View File

@@ -2,7 +2,7 @@ import { Card, CardHeader, CardBody, CardFooter } from 'reactstrap';
import { Doughnut, HorizontalBar } from 'react-chartjs-2'; import { Doughnut, HorizontalBar } from 'react-chartjs-2';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React from 'react'; import React from 'react';
import { keys, values } from 'ramda'; import { keys, values, zipObj } from 'ramda';
import './GraphCard.scss'; import './GraphCard.scss';
const propTypes = { const propTypes = {
@@ -11,9 +11,10 @@ const propTypes = {
isBarChart: PropTypes.bool, isBarChart: PropTypes.bool,
stats: PropTypes.object, stats: PropTypes.object,
max: PropTypes.number, max: PropTypes.number,
highlightedStats: PropTypes.object,
}; };
const generateGraphData = (title, isBarChart, labels, data) => ({ const generateGraphData = (title, isBarChart, labels, data, highlightedData) => ({
labels, labels,
datasets: [ datasets: [
{ {
@@ -31,23 +32,41 @@ const generateGraphData = (title, isBarChart, labels, data) => ({
borderColor: isBarChart ? 'rgba(70, 150, 229, 1)' : 'white', borderColor: isBarChart ? 'rgba(70, 150, 229, 1)' : 'white',
borderWidth: 2, borderWidth: 2,
}, },
], highlightedData && {
title,
label: 'Selected',
data: highlightedData,
backgroundColor: 'rgba(247, 127, 40, 0.4)',
borderColor: '#F77F28',
borderWidth: 2,
},
].filter(Boolean),
}); });
const dropLabelIfHidden = (label) => label.startsWith('hidden') ? '' : label; const dropLabelIfHidden = (label) => label.startsWith('hidden') ? '' : label;
const renderGraph = (title, isBarChart, stats, max) => { const renderGraph = (title, isBarChart, stats, max, highlightedStats) => {
const Component = isBarChart ? HorizontalBar : Doughnut; const Component = isBarChart ? HorizontalBar : Doughnut;
const labels = keys(stats).map(dropLabelIfHidden); const labels = keys(stats).map(dropLabelIfHidden);
const data = values(stats); const data = values(!highlightedStats ? stats : keys(highlightedStats).reduce((acc, highlightedKey) => {
if (acc[highlightedKey]) {
acc[highlightedKey] -= 1;
}
return acc;
}, stats));
const highlightedData = highlightedStats && values({ ...zipObj(labels, labels.map(() => 0)), ...highlightedStats });
const options = { const options = {
legend: isBarChart ? { display: false } : { position: 'right' }, legend: isBarChart ? { display: false } : { position: 'right' },
scales: isBarChart && { scales: isBarChart && {
xAxes: [ xAxes: [
{ {
ticks: { beginAtZero: true, max }, ticks: { beginAtZero: true, max },
stacked: true,
}, },
], ],
yAxes: [{ stacked: true }],
}, },
tooltips: { tooltips: {
intersect: !isBarChart, intersect: !isBarChart,
@@ -56,17 +75,17 @@ const renderGraph = (title, isBarChart, stats, max) => {
filter: ({ yLabel }) => !isBarChart || yLabel !== '', filter: ({ yLabel }) => !isBarChart || yLabel !== '',
}, },
}; };
const graphData = generateGraphData(title, isBarChart, labels, data); const graphData = generateGraphData(title, isBarChart, labels, data, highlightedData);
const height = isBarChart && labels.length > 20 ? labels.length * 8 : null; const height = isBarChart && labels.length > 20 ? labels.length * 8 : null;
// Provide a key based on the height, so that every time the dataset changes, a new graph is rendered // Provide a key based on the height, so that every time the dataset changes, a new graph is rendered
return <Component key={height} data={graphData} options={options} height={height} />; return <Component key={height} data={graphData} options={options} height={height} />;
}; };
const GraphCard = ({ title, footer, isBarChart, stats, max }) => ( const GraphCard = ({ title, footer, isBarChart, stats, max, highlightedStats }) => (
<Card className="mt-4"> <Card className="mt-4">
<CardHeader className="graph-card__header">{typeof title === 'function' ? title() : title}</CardHeader> <CardHeader className="graph-card__header">{typeof title === 'function' ? title() : title}</CardHeader>
<CardBody>{renderGraph(title, isBarChart, stats, max)}</CardBody> <CardBody>{renderGraph(title, isBarChart, stats, max, highlightedStats)}</CardBody>
{footer && <CardFooter className="graph-card__footer--sticky">{footer}</CardFooter>} {footer && <CardFooter className="graph-card__footer--sticky">{footer}</CardFooter>}
</Card> </Card>
); );

View File

@@ -15,6 +15,8 @@ import GraphCard from './GraphCard';
import { shortUrlDetailType } from './reducers/shortUrlDetail'; import { shortUrlDetailType } from './reducers/shortUrlDetail';
import VisitsTable from './VisitsTable'; import VisitsTable from './VisitsTable';
const highlightedVisitToStats = (highlightedVisit, prop) => highlightedVisit && { [highlightedVisit[prop]]: 1 };
const ShortUrlVisits = ( const ShortUrlVisits = (
{ processStatsFromVisits }, { processStatsFromVisits },
OpenMapModalBtn OpenMapModalBtn
@@ -40,6 +42,7 @@ const ShortUrlVisits = (
showTable: false, showTable: false,
tableIsSticky: false, tableIsSticky: false,
isMobileDevice: false, isMobileDevice: false,
highlightedVisit: undefined,
}; };
loadVisits = (loadDetail = false) => { loadVisits = (loadDetail = false) => {
@@ -114,9 +117,10 @@ const ShortUrlVisits = (
</div> </div>
<div className="col-xl-4"> <div className="col-xl-4">
<SortableBarGraph <SortableBarGraph
title="Referrers"
stats={referrers} stats={referrers}
withPagination={false} withPagination={false}
title="Referrers" highlightedStats={highlightedVisitToStats(this.state.highlightedVisit, 'referer')}
sortingItems={{ sortingItems={{
name: 'Referrer name', name: 'Referrer name',
amount: 'Visits amount', amount: 'Visits amount',
@@ -125,8 +129,9 @@ const ShortUrlVisits = (
</div> </div>
<div className="col-lg-6"> <div className="col-lg-6">
<SortableBarGraph <SortableBarGraph
stats={countries}
title="Countries" title="Countries"
stats={countries}
highlightedStats={highlightedVisitToStats(this.state.highlightedVisit, 'country')}
sortingItems={{ sortingItems={{
name: 'Country name', name: 'Country name',
amount: 'Visits amount', amount: 'Visits amount',
@@ -135,8 +140,9 @@ const ShortUrlVisits = (
</div> </div>
<div className="col-lg-6"> <div className="col-lg-6">
<SortableBarGraph <SortableBarGraph
stats={cities}
title="Cities" title="Cities"
stats={cities}
highlightedStats={highlightedVisitToStats(this.state.highlightedVisit, 'city')}
extraHeaderContent={(activeCities) => extraHeaderContent={(activeCities) =>
mapLocations.length > 0 && mapLocations.length > 0 &&
<OpenMapModalBtn modalTitle="Cities" locations={mapLocations} activeCities={activeCities} /> <OpenMapModalBtn modalTitle="Cities" locations={mapLocations} activeCities={activeCities} />
@@ -188,7 +194,11 @@ const ShortUrlVisits = (
onEntered={() => this.setState({ tableIsSticky: true })} onEntered={() => this.setState({ tableIsSticky: true })}
onExiting={() => this.setState({ tableIsSticky: false })} onExiting={() => this.setState({ tableIsSticky: false })}
> >
<VisitsTable visits={visits} isSticky={this.state.tableIsSticky} /> <VisitsTable
visits={visits}
isSticky={this.state.tableIsSticky}
onVisitSelected={(highlightedVisit) => this.setState({ highlightedVisit })}
/>
</Collapse> </Collapse>
)} )}

View File

@@ -15,6 +15,7 @@ const pickValueFromPair = ([ , value ]) => value;
export default class SortableBarGraph extends React.Component { export default class SortableBarGraph extends React.Component {
static propTypes = { static propTypes = {
stats: PropTypes.object.isRequired, stats: PropTypes.object.isRequired,
highlightedStats: PropTypes.object,
title: PropTypes.string.isRequired, title: PropTypes.string.isRequired,
sortingItems: PropTypes.object.isRequired, sortingItems: PropTypes.object.isRequired,
extraHeaderContent: PropTypes.func, extraHeaderContent: PropTypes.func,
@@ -73,7 +74,7 @@ export default class SortableBarGraph extends React.Component {
} }
render() { render() {
const { stats, sortingItems, title, extraHeaderContent, withPagination = true } = this.props; const { stats, sortingItems, title, extraHeaderContent, highlightedStats, withPagination = true } = this.props;
const { currentPageStats, pagination, max } = this.determineStats(stats, sortingItems); const { currentPageStats, pagination, max } = this.determineStats(stats, sortingItems);
const activeCities = keys(currentPageStats); const activeCities = keys(currentPageStats);
const computeTitle = () => ( const computeTitle = () => (
@@ -107,6 +108,15 @@ export default class SortableBarGraph extends React.Component {
</React.Fragment> </React.Fragment>
); );
return <GraphCard isBarChart title={computeTitle} stats={currentPageStats} footer={pagination} max={max} />; return (
<GraphCard
isBarChart
title={computeTitle}
stats={currentPageStats}
footer={pagination}
max={max}
highlightedStats={highlightedStats}
/>
);
} }
} }

View File

@@ -98,7 +98,7 @@ const VisitsTable = ({ visits, onVisitSelected, isSticky = false, matchMedia = w
'visits-table__sticky': isSticky, 'visits-table__sticky': isSticky,
})} })}
> >
<FontAwesomeIcon icon={checkIcon} /> <FontAwesomeIcon icon={checkIcon} className={classNames({ 'text-primary': selectedVisit !== undefined })} />
</th> </th>
<th className={headerCellsClass} onClick={orderByColumn('date')}> <th className={headerCellsClass} onClick={orderByColumn('date')}>
Date Date

View File

@@ -59,8 +59,10 @@ describe('<GraphCard />', () => {
xAxes: [ xAxes: [
{ {
ticks: { beginAtZero: true }, ticks: { beginAtZero: true },
stacked: true,
}, },
], ],
yAxes: [{ stacked: true }],
}); });
}); });
}); });