mirror of
https://github.com/shlinkio/shlink-web-client.git
synced 2026-03-14 11:33:51 +00:00
Ensured info for selected visit in visits table gets highlighted in bar charts
This commit is contained in:
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -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}
|
||||||
|
/>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -59,8 +59,10 @@ describe('<GraphCard />', () => {
|
|||||||
xAxes: [
|
xAxes: [
|
||||||
{
|
{
|
||||||
ticks: { beginAtZero: true },
|
ticks: { beginAtZero: true },
|
||||||
|
stacked: true,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
yAxes: [{ stacked: true }],
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user