mirror of
https://github.com/shlinkio/shlink-web-client.git
synced 2025-01-11 18:57:31 +03:00
Added custom responsive legend to doughnut charts
This commit is contained in:
parent
770cc59448
commit
949e0da105
3 changed files with 94 additions and 21 deletions
|
@ -1,8 +1,10 @@
|
|||
import React from 'react';
|
||||
import React, { useRef } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Doughnut, HorizontalBar } from 'react-chartjs-2';
|
||||
import { keys, values } from 'ramda';
|
||||
import classNames from 'classnames';
|
||||
import { fillTheGaps } from '../../utils/helpers/visits';
|
||||
import './DefaultChart.scss';
|
||||
|
||||
const propTypes = {
|
||||
title: PropTypes.oneOfType([ PropTypes.string, PropTypes.func ]),
|
||||
|
@ -58,6 +60,39 @@ const determineHeight = (isBarChart, labels) => {
|
|||
return isBarChart && labels.length > 20 ? labels.length * 8 : null;
|
||||
};
|
||||
|
||||
/* eslint-disable react/prop-types */
|
||||
const renderPieChartLegend = ({ config }) => {
|
||||
const { labels, datasets } = config.data;
|
||||
const { defaultColor } = config.options;
|
||||
const [{ backgroundColor: colors }] = datasets;
|
||||
|
||||
return (
|
||||
<ul className="default-chart__pie-chart-legend">
|
||||
{labels.map((label, index) => (
|
||||
<li key={label} className="default-chart__pie-chart-legend-item d-flex">
|
||||
<div
|
||||
className="default-chart__pie-chart-legend-item-color"
|
||||
style={{ backgroundColor: colors[index] || defaultColor }}
|
||||
/>
|
||||
<small className="default-chart__pie-chart-legend-item-text flex-fill">{label}</small>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
);
|
||||
};
|
||||
/* eslint-enable react/prop-types */
|
||||
|
||||
const chartElementAtEvent = (onClick) => ([ chart ]) => {
|
||||
if (!onClick || !chart) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { _index, _chart: { data } } = chart;
|
||||
const { labels } = data;
|
||||
|
||||
onClick(labels[_index]);
|
||||
};
|
||||
|
||||
const DefaultChart = ({ title, isBarChart, stats, max, highlightedStats, highlightedLabel, onClick }) => {
|
||||
const hasHighlightedStats = highlightedStats && Object.keys(highlightedStats).length > 0;
|
||||
const Component = isBarChart ? HorizontalBar : Doughnut;
|
||||
|
@ -70,9 +105,11 @@ const DefaultChart = ({ title, isBarChart, stats, max, highlightedStats, highlig
|
|||
return acc;
|
||||
}, { ...stats }));
|
||||
const highlightedData = hasHighlightedStats && fillTheGaps(highlightedStats, labels);
|
||||
const chartRef = useRef();
|
||||
|
||||
const options = {
|
||||
legend: isBarChart ? { display: false } : { position: 'right' },
|
||||
legend: { display: false },
|
||||
legendCallback: !isBarChart && renderPieChartLegend,
|
||||
scales: isBarChart && {
|
||||
xAxes: [
|
||||
{
|
||||
|
@ -97,22 +134,23 @@ const DefaultChart = ({ title, isBarChart, stats, max, highlightedStats, highlig
|
|||
|
||||
// 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}
|
||||
getElementAtEvent={([ chart ]) => {
|
||||
if (!onClick || !chart) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { _index, _chart: { data } } = chart;
|
||||
const { labels } = data;
|
||||
|
||||
onClick(labels[_index]);
|
||||
}}
|
||||
/>
|
||||
<div className="row">
|
||||
<div className={classNames('col-sm-12', { 'col-md-7': !isBarChart })}>
|
||||
<Component
|
||||
ref={chartRef}
|
||||
key={height}
|
||||
data={graphData}
|
||||
options={options}
|
||||
height={height}
|
||||
getElementAtEvent={chartElementAtEvent(onClick)}
|
||||
/>
|
||||
</div>
|
||||
{!isBarChart && (
|
||||
<div className="col-sm-12 col-md-5">
|
||||
{chartRef.current && chartRef.current.chartInstance.generateLegend()}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
29
src/visits/helpers/DefaultChart.scss
Normal file
29
src/visits/helpers/DefaultChart.scss
Normal file
|
@ -0,0 +1,29 @@
|
|||
@import '../../utils/base';
|
||||
|
||||
.default-chart__pie-chart-legend {
|
||||
list-style-type: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
|
||||
@media (max-width: $smMax) {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
.default-chart__pie-chart-legend-item:not(:first-child) {
|
||||
margin-top: .3rem;
|
||||
}
|
||||
|
||||
.default-chart__pie-chart-legend-item-color {
|
||||
width: 20px;
|
||||
min-width: 20px;
|
||||
height: 20px;
|
||||
margin-right: 5px;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.default-chart__pie-chart-legend-item-text {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
|
@ -17,13 +17,14 @@ describe('<DefaultChart />', () => {
|
|||
wrapper = shallow(<DefaultChart title="The chart" stats={stats} />);
|
||||
const doughnut = wrapper.find(Doughnut);
|
||||
const horizontal = wrapper.find(HorizontalBar);
|
||||
const cols = wrapper.find('.col-sm-12');
|
||||
|
||||
expect(doughnut).toHaveLength(1);
|
||||
expect(horizontal).toHaveLength(0);
|
||||
|
||||
const { labels, datasets } = doughnut.prop('data');
|
||||
const [{ title, data, backgroundColor, borderColor }] = datasets;
|
||||
const { legend, scales } = doughnut.prop('options');
|
||||
const { legend, legendCallback, scales } = doughnut.prop('options');
|
||||
|
||||
expect(title).toEqual('The chart');
|
||||
expect(labels).toEqual(keys(stats));
|
||||
|
@ -43,24 +44,28 @@ describe('<DefaultChart />', () => {
|
|||
'#463730',
|
||||
]);
|
||||
expect(borderColor).toEqual('white');
|
||||
expect(legend).toEqual({ position: 'right' });
|
||||
expect(legend).toEqual({ display: false });
|
||||
expect(typeof legendCallback).toEqual('function');
|
||||
expect(scales).toBeUndefined();
|
||||
expect(cols).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('renders HorizontalBar when is not a bar chart', () => {
|
||||
wrapper = shallow(<DefaultChart isBarChart title="The chart" stats={stats} />);
|
||||
const doughnut = wrapper.find(Doughnut);
|
||||
const horizontal = wrapper.find(HorizontalBar);
|
||||
const cols = wrapper.find('.col-sm-12');
|
||||
|
||||
expect(doughnut).toHaveLength(0);
|
||||
expect(horizontal).toHaveLength(1);
|
||||
|
||||
const { datasets: [{ backgroundColor, borderColor }] } = horizontal.prop('data');
|
||||
const { legend, scales } = horizontal.prop('options');
|
||||
const { legend, legendCallback, scales } = horizontal.prop('options');
|
||||
|
||||
expect(backgroundColor).toEqual('rgba(70, 150, 229, 0.4)');
|
||||
expect(borderColor).toEqual('rgba(70, 150, 229, 1)');
|
||||
expect(legend).toEqual({ display: false });
|
||||
expect(legendCallback).toEqual(false);
|
||||
expect(scales).toEqual({
|
||||
xAxes: [
|
||||
{
|
||||
|
@ -70,6 +75,7 @@ describe('<DefaultChart />', () => {
|
|||
],
|
||||
yAxes: [{ stacked: true }],
|
||||
});
|
||||
expect(cols).toHaveLength(1);
|
||||
});
|
||||
|
||||
it.each([
|
Loading…
Reference in a new issue