mirror of
https://github.com/shlinkio/shlink-web-client.git
synced 2026-03-16 20:43:48 +00:00
Added custom responsive legend to doughnut charts
This commit is contained in:
@@ -1,8 +1,10 @@
|
|||||||
import React from 'react';
|
import React, { useRef } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { Doughnut, HorizontalBar } from 'react-chartjs-2';
|
import { Doughnut, HorizontalBar } from 'react-chartjs-2';
|
||||||
import { keys, values } from 'ramda';
|
import { keys, values } from 'ramda';
|
||||||
|
import classNames from 'classnames';
|
||||||
import { fillTheGaps } from '../../utils/helpers/visits';
|
import { fillTheGaps } from '../../utils/helpers/visits';
|
||||||
|
import './DefaultChart.scss';
|
||||||
|
|
||||||
const propTypes = {
|
const propTypes = {
|
||||||
title: PropTypes.oneOfType([ PropTypes.string, PropTypes.func ]),
|
title: PropTypes.oneOfType([ PropTypes.string, PropTypes.func ]),
|
||||||
@@ -58,6 +60,39 @@ const determineHeight = (isBarChart, labels) => {
|
|||||||
return isBarChart && labels.length > 20 ? labels.length * 8 : null;
|
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 DefaultChart = ({ title, isBarChart, stats, max, highlightedStats, highlightedLabel, onClick }) => {
|
||||||
const hasHighlightedStats = highlightedStats && Object.keys(highlightedStats).length > 0;
|
const hasHighlightedStats = highlightedStats && Object.keys(highlightedStats).length > 0;
|
||||||
const Component = isBarChart ? HorizontalBar : Doughnut;
|
const Component = isBarChart ? HorizontalBar : Doughnut;
|
||||||
@@ -70,9 +105,11 @@ const DefaultChart = ({ title, isBarChart, stats, max, highlightedStats, highlig
|
|||||||
return acc;
|
return acc;
|
||||||
}, { ...stats }));
|
}, { ...stats }));
|
||||||
const highlightedData = hasHighlightedStats && fillTheGaps(highlightedStats, labels);
|
const highlightedData = hasHighlightedStats && fillTheGaps(highlightedStats, labels);
|
||||||
|
const chartRef = useRef();
|
||||||
|
|
||||||
const options = {
|
const options = {
|
||||||
legend: isBarChart ? { display: false } : { position: 'right' },
|
legend: { display: false },
|
||||||
|
legendCallback: !isBarChart && renderPieChartLegend,
|
||||||
scales: isBarChart && {
|
scales: isBarChart && {
|
||||||
xAxes: [
|
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
|
// Provide a key based on the height, so that every time the dataset changes, a new graph is rendered
|
||||||
return (
|
return (
|
||||||
<Component
|
<div className="row">
|
||||||
key={height}
|
<div className={classNames('col-sm-12', { 'col-md-7': !isBarChart })}>
|
||||||
data={graphData}
|
<Component
|
||||||
options={options}
|
ref={chartRef}
|
||||||
height={height}
|
key={height}
|
||||||
getElementAtEvent={([ chart ]) => {
|
data={graphData}
|
||||||
if (!onClick || !chart) {
|
options={options}
|
||||||
return;
|
height={height}
|
||||||
}
|
getElementAtEvent={chartElementAtEvent(onClick)}
|
||||||
|
/>
|
||||||
const { _index, _chart: { data } } = chart;
|
</div>
|
||||||
const { labels } = data;
|
{!isBarChart && (
|
||||||
|
<div className="col-sm-12 col-md-5">
|
||||||
onClick(labels[_index]);
|
{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} />);
|
wrapper = shallow(<DefaultChart title="The chart" stats={stats} />);
|
||||||
const doughnut = wrapper.find(Doughnut);
|
const doughnut = wrapper.find(Doughnut);
|
||||||
const horizontal = wrapper.find(HorizontalBar);
|
const horizontal = wrapper.find(HorizontalBar);
|
||||||
|
const cols = wrapper.find('.col-sm-12');
|
||||||
|
|
||||||
expect(doughnut).toHaveLength(1);
|
expect(doughnut).toHaveLength(1);
|
||||||
expect(horizontal).toHaveLength(0);
|
expect(horizontal).toHaveLength(0);
|
||||||
|
|
||||||
const { labels, datasets } = doughnut.prop('data');
|
const { labels, datasets } = doughnut.prop('data');
|
||||||
const [{ title, data, backgroundColor, borderColor }] = datasets;
|
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(title).toEqual('The chart');
|
||||||
expect(labels).toEqual(keys(stats));
|
expect(labels).toEqual(keys(stats));
|
||||||
@@ -43,24 +44,28 @@ describe('<DefaultChart />', () => {
|
|||||||
'#463730',
|
'#463730',
|
||||||
]);
|
]);
|
||||||
expect(borderColor).toEqual('white');
|
expect(borderColor).toEqual('white');
|
||||||
expect(legend).toEqual({ position: 'right' });
|
expect(legend).toEqual({ display: false });
|
||||||
|
expect(typeof legendCallback).toEqual('function');
|
||||||
expect(scales).toBeUndefined();
|
expect(scales).toBeUndefined();
|
||||||
|
expect(cols).toHaveLength(2);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders HorizontalBar when is not a bar chart', () => {
|
it('renders HorizontalBar when is not a bar chart', () => {
|
||||||
wrapper = shallow(<DefaultChart isBarChart title="The chart" stats={stats} />);
|
wrapper = shallow(<DefaultChart isBarChart title="The chart" stats={stats} />);
|
||||||
const doughnut = wrapper.find(Doughnut);
|
const doughnut = wrapper.find(Doughnut);
|
||||||
const horizontal = wrapper.find(HorizontalBar);
|
const horizontal = wrapper.find(HorizontalBar);
|
||||||
|
const cols = wrapper.find('.col-sm-12');
|
||||||
|
|
||||||
expect(doughnut).toHaveLength(0);
|
expect(doughnut).toHaveLength(0);
|
||||||
expect(horizontal).toHaveLength(1);
|
expect(horizontal).toHaveLength(1);
|
||||||
|
|
||||||
const { datasets: [{ backgroundColor, borderColor }] } = horizontal.prop('data');
|
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(backgroundColor).toEqual('rgba(70, 150, 229, 0.4)');
|
||||||
expect(borderColor).toEqual('rgba(70, 150, 229, 1)');
|
expect(borderColor).toEqual('rgba(70, 150, 229, 1)');
|
||||||
expect(legend).toEqual({ display: false });
|
expect(legend).toEqual({ display: false });
|
||||||
|
expect(legendCallback).toEqual(false);
|
||||||
expect(scales).toEqual({
|
expect(scales).toEqual({
|
||||||
xAxes: [
|
xAxes: [
|
||||||
{
|
{
|
||||||
@@ -70,6 +75,7 @@ describe('<DefaultChart />', () => {
|
|||||||
],
|
],
|
||||||
yAxes: [{ stacked: true }],
|
yAxes: [{ stacked: true }],
|
||||||
});
|
});
|
||||||
|
expect(cols).toHaveLength(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it.each([
|
it.each([
|
||||||
Reference in New Issue
Block a user