From 3c23016028f3f9d82268ca4584ded379324832d1 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sat, 18 Sep 2021 19:05:28 +0200 Subject: [PATCH] Refactored components used to render charts for visits --- src/visits/VisitsStats.tsx | 16 +- .../GraphCard.scss => charts/ChartCard.scss} | 2 +- src/visits/charts/ChartCard.tsx | 16 ++ src/visits/charts/DoughnutChart.tsx | 72 +++++++ src/visits/charts/DoughnutChartCard.tsx | 15 ++ .../DoughnutChartLegend.scss} | 8 +- .../DoughnutChartLegend.tsx} | 14 +- src/visits/charts/HorizontalBarChart.tsx | 131 +++++++++++++ .../SortableBarChartCard.tsx} | 39 ++-- src/visits/helpers/DefaultChart.tsx | 184 ------------------ src/visits/helpers/GraphCard.tsx | 21 -- test/visits/VisitsStats.test.tsx | 16 +- .../ChartCard.test.tsx} | 9 +- test/visits/charts/DoughnutChart.test.tsx | 47 +++++ .../DoughnutChartLegend.test.tsx} | 10 +- .../HorizontalBarChart.test.tsx} | 50 +---- .../SortableBarChartCard.tsx} | 20 +- 17 files changed, 350 insertions(+), 320 deletions(-) rename src/visits/{helpers/GraphCard.scss => charts/ChartCard.scss} (53%) create mode 100644 src/visits/charts/ChartCard.tsx create mode 100644 src/visits/charts/DoughnutChart.tsx create mode 100644 src/visits/charts/DoughnutChartCard.tsx rename src/visits/{helpers/PieChartLegend.scss => charts/DoughnutChartLegend.scss} (69%) rename src/visits/{helpers/PieChartLegend.tsx => charts/DoughnutChartLegend.tsx} (54%) create mode 100644 src/visits/charts/HorizontalBarChart.tsx rename src/visits/{helpers/SortableBarGraph.tsx => charts/SortableBarChartCard.tsx} (90%) delete mode 100644 src/visits/helpers/DefaultChart.tsx delete mode 100644 src/visits/helpers/GraphCard.tsx rename test/visits/{helpers/GraphCard.test.tsx => charts/ChartCard.test.tsx} (80%) create mode 100644 test/visits/charts/DoughnutChart.test.tsx rename test/visits/{helpers/PieChartLegend.test.tsx => charts/DoughnutChartLegend.test.tsx} (69%) rename test/visits/{helpers/DefaultChart.test.tsx => charts/HorizontalBarChart.test.tsx} (53%) rename test/visits/{helpers/SortableBarGraph.test.tsx => charts/SortableBarChartCard.tsx} (87%) diff --git a/src/visits/VisitsStats.tsx b/src/visits/VisitsStats.tsx index 533658fe..0a5e77ff 100644 --- a/src/visits/VisitsStats.tsx +++ b/src/visits/VisitsStats.tsx @@ -15,8 +15,6 @@ import { ShlinkApiError } from '../api/ShlinkApiError'; import { Settings } from '../settings/reducers/settings'; import { SelectedServer } from '../servers/data'; import { supportsBotVisits } from '../utils/helpers/features'; -import SortableBarGraph from './helpers/SortableBarGraph'; -import GraphCard from './helpers/GraphCard'; import LineChartCard from './helpers/LineChartCard'; import VisitsTable from './VisitsTable'; import { NormalizedOrphanVisit, NormalizedVisit, VisitsFilter, VisitsInfo, VisitsParams } from './types'; @@ -24,6 +22,8 @@ import OpenMapModalBtn from './helpers/OpenMapModalBtn'; import { normalizeVisits, processStatsFromVisits } from './services/VisitsParser'; import { VisitsFilterDropdown } from './helpers/VisitsFilterDropdown'; import { HighlightableProps, highlightedVisitsToStats } from './types/helpers'; +import { DoughnutChartCard } from './charts/DoughnutChartCard'; +import { SortableBarChartCard } from './charts/SortableBarChartCard'; import './VisitsStats.scss'; export interface VisitsStatsProps { @@ -173,13 +173,13 @@ const VisitsStats: FC = ({
- +
- +
- = ({
{isOrphanVisits && (
- = ({
- = ({ />
- = ({ title, footer, children }) => ( + + {typeof title === 'function' ? title() : title} + {children} + {footer && {footer}} + +); diff --git a/src/visits/charts/DoughnutChart.tsx b/src/visits/charts/DoughnutChart.tsx new file mode 100644 index 00000000..4fbb6f44 --- /dev/null +++ b/src/visits/charts/DoughnutChart.tsx @@ -0,0 +1,72 @@ +import { FC, useState, memo } from 'react'; +import { Chart, ChartData, ChartDataset, ChartOptions } from 'chart.js'; +import { keys, values } from 'ramda'; +import { Doughnut } from 'react-chartjs-2'; +import { renderPieChartLabel } from '../../utils/helpers/charts'; +import { isDarkThemeEnabled, PRIMARY_DARK_COLOR, PRIMARY_LIGHT_COLOR } from '../../utils/theme'; +import { Stats } from '../types'; +import { DoughnutChartLegend } from './DoughnutChartLegend'; + +interface DoughnutChartProps { + stats: Stats; +} + +const generateChartDatasets = (data: number[]): ChartDataset[] => [ + { + data, + backgroundColor: [ + '#97BBCD', + '#F7464A', + '#46BFBD', + '#FDB45C', + '#949FB1', + '#57A773', + '#414066', + '#08B2E3', + '#B6C454', + '#DCDCDC', + '#463730', + ], + borderColor: isDarkThemeEnabled() ? PRIMARY_DARK_COLOR : PRIMARY_LIGHT_COLOR, + borderWidth: 2, + }, +]; +const generateChartData = (labels: string[], data: number[]): ChartData => ({ + labels, + datasets: generateChartDatasets(data), +}); + +export const DoughnutChart: FC = memo(({ stats }) => { + const [ chartRef, setChartRef ] = useState(); // Cannot use useRef here + const labels = keys(stats); + const data = values(stats); + + const options: ChartOptions = { + plugins: { + legend: { display: false }, + tooltip: { + intersect: true, + callbacks: { label: renderPieChartLabel }, + }, + }, + }; + const chartData = generateChartData(labels, data); + + return ( +
+
+ { + setChartRef(element ?? undefined); + }} + /> +
+
+ {chartRef && } +
+
+ ); +}); diff --git a/src/visits/charts/DoughnutChartCard.tsx b/src/visits/charts/DoughnutChartCard.tsx new file mode 100644 index 00000000..3febaacc --- /dev/null +++ b/src/visits/charts/DoughnutChartCard.tsx @@ -0,0 +1,15 @@ +import { FC } from 'react'; +import { Stats } from '../types'; +import { DoughnutChart } from './DoughnutChart'; +import { ChartCard } from './ChartCard'; + +interface DoughnutChartCardProps { + title: string; + stats: Stats; +} + +export const DoughnutChartCard: FC = ({ title, stats }) => ( + + + +); diff --git a/src/visits/helpers/PieChartLegend.scss b/src/visits/charts/DoughnutChartLegend.scss similarity index 69% rename from src/visits/helpers/PieChartLegend.scss rename to src/visits/charts/DoughnutChartLegend.scss index 2d4956f6..3dabac25 100644 --- a/src/visits/helpers/PieChartLegend.scss +++ b/src/visits/charts/DoughnutChartLegend.scss @@ -1,6 +1,6 @@ @import '../../utils/base'; -.pie-chart-legend { +.doughnut-chart-legend { list-style-type: none; padding: 0; margin: 0; @@ -10,11 +10,11 @@ } } -.pie-chart-legend__item:not(:first-child) { +.doughnut-chart-legend__item:not(:first-child) { margin-top: .3rem; } -.pie-chart-legend__item-color { +.doughnut-chart-legend__item-color { width: 20px; min-width: 20px; height: 20px; @@ -22,7 +22,7 @@ border-radius: 10px; } -.pie-chart-legend__item-text { +.doughnut-chart-legend__item-text { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; diff --git a/src/visits/helpers/PieChartLegend.tsx b/src/visits/charts/DoughnutChartLegend.tsx similarity index 54% rename from src/visits/helpers/PieChartLegend.tsx rename to src/visits/charts/DoughnutChartLegend.tsx index e6e51d27..cd843031 100644 --- a/src/visits/helpers/PieChartLegend.tsx +++ b/src/visits/charts/DoughnutChartLegend.tsx @@ -1,26 +1,26 @@ import { FC } from 'react'; import { Chart } from 'chart.js'; -import './PieChartLegend.scss'; +import './DoughnutChartLegend.scss'; -interface PieChartLegendProps { +interface DoughnutChartLegendProps { chart: Chart; } -export const PieChartLegend: FC = ({ chart }) => { +export const DoughnutChartLegend: FC = ({ chart }) => { const { config } = chart; const { labels = [], datasets = [] } = config.data ?? {}; const [{ backgroundColor: colors }] = datasets; const { defaultColor } = config.options ?? {} as any; return ( -
    +
      {(labels as string[]).map((label, index) => ( -
    • +
    • - {label} + {label}
    • ))}
    diff --git a/src/visits/charts/HorizontalBarChart.tsx b/src/visits/charts/HorizontalBarChart.tsx new file mode 100644 index 00000000..05bca0f0 --- /dev/null +++ b/src/visits/charts/HorizontalBarChart.tsx @@ -0,0 +1,131 @@ +import { FC } from 'react'; +import { ChartData, ChartDataset, ChartOptions } from 'chart.js'; +import { keys, values } from 'ramda'; +import { Bar } from 'react-chartjs-2'; +import { fillTheGaps } from '../../utils/helpers/visits'; +import { pointerOnHover, renderChartLabel } from '../../utils/helpers/charts'; +import { prettify } from '../../utils/helpers/numbers'; +import { Stats } from '../types'; +import { HIGHLIGHTED_COLOR, HIGHLIGHTED_COLOR_ALPHA, MAIN_COLOR, MAIN_COLOR_ALPHA } from '../../utils/theme'; + +export interface HorizontalBarChartProps { + stats: Stats; + max?: number; + highlightedStats?: Stats; + highlightedLabel?: string; + onClick?: (label: string) => void; +} + +const dropLabelIfHidden = (label: string) => label.startsWith('hidden') ? '' : label; +const statsAreDefined = (stats: Stats | undefined): stats is Stats => !!stats && Object.keys(stats).length > 0; +const determineHeight = (labels: string[]): number | undefined => labels.length > 20 ? labels.length * 10 : undefined; + +const generateChartDatasets = ( + data: number[], + highlightedData: number[], + highlightedLabel?: string, +): ChartDataset[] => { + const mainDataset: ChartDataset = { + data, + label: highlightedLabel ? 'Non-selected' : 'Visits', + backgroundColor: MAIN_COLOR_ALPHA, + borderColor: MAIN_COLOR, + borderWidth: 2, + }; + + if (highlightedData.every((value) => value === 0)) { + return [ mainDataset ]; + } + + const highlightedDataset: ChartDataset = { + label: highlightedLabel ?? 'Selected', + data: highlightedData, + backgroundColor: HIGHLIGHTED_COLOR_ALPHA, + borderColor: HIGHLIGHTED_COLOR, + borderWidth: 2, + }; + + return [ mainDataset, highlightedDataset ]; +}; +const generateChartData = ( + labels: string[], + data: number[], + highlightedData: number[], + highlightedLabel?: string, +): ChartData => ({ + labels, + datasets: generateChartDatasets(data, highlightedData, highlightedLabel), +}); + +type ClickedCharts = [{ index: number }] | []; +const chartElementAtEvent = (labels: string[], onClick?: (label: string) => void) => ([ chart ]: ClickedCharts) => { + if (!onClick || !chart) { + return; + } + + onClick(labels[chart.index]); +}; + +export const HorizontalBarChart: FC = ( + { stats, highlightedStats, highlightedLabel, onClick, max }, +) => { + const labels = keys(stats).map(dropLabelIfHidden); + const data = values( + !statsAreDefined(highlightedStats) ? stats : keys(highlightedStats).reduce((acc, highlightedKey) => { + if (acc[highlightedKey]) { + acc[highlightedKey] -= highlightedStats[highlightedKey]; + } + + return acc; + }, { ...stats }), + ); + const highlightedData = fillTheGaps(highlightedStats ?? {}, labels); + + const options: ChartOptions = { + plugins: { + legend: { display: false }, + tooltip: { + mode: 'y', + // Do not show tooltip on items with empty label when in a bar chart + filter: ({ label }) => label !== '', + callbacks: { label: renderChartLabel }, + }, + }, + scales: { + x: { + beginAtZero: true, + stacked: true, + max, + ticks: { + precision: 0, + callback: prettify, + }, + }, + y: { stacked: true }, + }, + onHover: pointerOnHover, + indexAxis: 'y', + }; + const chartData = generateChartData(labels, data, highlightedData, highlightedLabel); + const height = determineHeight(labels); + + // Provide a key based on the height, to force re-render every time the dataset changes (example, due to pagination) + const renderChartComponent = (customKey: string) => ( + + ); + + return ( + <> + {/* It's VERY IMPORTANT to render two different components here, as one has 1 dataset and the other has 2 */} + {/* Using the same component causes a crash when switching from 1 to 2 datasets, and then back to 1 dataset */} + {highlightedStats !== undefined && renderChartComponent('with_stats')} + {highlightedStats === undefined && renderChartComponent('without_stats')} + + ); +}; diff --git a/src/visits/helpers/SortableBarGraph.tsx b/src/visits/charts/SortableBarChartCard.tsx similarity index 90% rename from src/visits/helpers/SortableBarGraph.tsx rename to src/visits/charts/SortableBarChartCard.tsx index 9a4bd429..e0f1bee5 100644 --- a/src/visits/helpers/SortableBarGraph.tsx +++ b/src/visits/charts/SortableBarChartCard.tsx @@ -1,26 +1,26 @@ -import { useState } from 'react'; +import { FC, useState } from 'react'; import { fromPairs, pipe, reverse, sortBy, splitEvery, toLower, toPairs, type, zipObj } from 'ramda'; +import { OrderDir, rangeOf } from '../../utils/utils'; +import SimplePaginator from '../../common/SimplePaginator'; +import { roundTen } from '../../utils/helpers/numbers'; import SortingDropdown from '../../utils/SortingDropdown'; import PaginationDropdown from '../../utils/PaginationDropdown'; -import { OrderDir, rangeOf } from '../../utils/utils'; -import { roundTen } from '../../utils/helpers/numbers'; -import SimplePaginator from '../../common/SimplePaginator'; import { Stats, StatsRow } from '../types'; -import GraphCard from './GraphCard'; -import { DefaultChartProps } from './DefaultChart'; +import { HorizontalBarChart, HorizontalBarChartProps } from './HorizontalBarChart'; +import { ChartCard } from './ChartCard'; -const toLowerIfString = (value: any) => type(value) === 'String' ? toLower(value) : value; // eslint-disable-line @typescript-eslint/no-unsafe-return -const pickKeyFromPair = ([ key ]: StatsRow) => key; -const pickValueFromPair = ([ , value ]: StatsRow) => value; - -interface SortableBarGraphProps extends DefaultChartProps { +interface SortableBarChartCardProps extends Omit { title: Function | string; sortingItems: Record; withPagination?: boolean; extraHeaderContent?: Function; } -const SortableBarGraph = ({ +const toLowerIfString = (value: any) => type(value) === 'String' ? toLower(value) : value; // eslint-disable-line @typescript-eslint/no-unsafe-return +const pickKeyFromPair = ([ key ]: StatsRow) => key; +const pickValueFromPair = ([ , value ]: StatsRow) => value; + +export const SortableBarChartCard: FC = ({ stats, highlightedStats, title, @@ -28,7 +28,7 @@ const SortableBarGraph = ({ extraHeaderContent, withPagination = true, ...rest -}: SortableBarGraphProps) => { +}) => { const [ order, setOrder ] = useState<{ orderField?: string; orderDir?: OrderDir }>({ orderField: undefined, orderDir: undefined, @@ -132,16 +132,11 @@ const SortableBarGraph = ({ ); return ( - + > + + ); }; - -export default SortableBarGraph; diff --git a/src/visits/helpers/DefaultChart.tsx b/src/visits/helpers/DefaultChart.tsx deleted file mode 100644 index 73107748..00000000 --- a/src/visits/helpers/DefaultChart.tsx +++ /dev/null @@ -1,184 +0,0 @@ -import { useState, memo } from 'react'; -import { Doughnut, Bar } from 'react-chartjs-2'; -import { keys, values } from 'ramda'; -import classNames from 'classnames'; -import { Chart, ChartData, ChartDataset, ChartOptions } from 'chart.js'; -import { fillTheGaps } from '../../utils/helpers/visits'; -import { Stats } from '../types'; -import { prettify } from '../../utils/helpers/numbers'; -import { pointerOnHover, renderChartLabel, renderPieChartLabel } from '../../utils/helpers/charts'; -import { - HIGHLIGHTED_COLOR, - HIGHLIGHTED_COLOR_ALPHA, - isDarkThemeEnabled, - MAIN_COLOR, - MAIN_COLOR_ALPHA, - PRIMARY_DARK_COLOR, - PRIMARY_LIGHT_COLOR, -} from '../../utils/theme'; -import { PieChartLegend } from './PieChartLegend'; - -export interface DefaultChartProps { - stats: Stats; - isBarChart?: boolean; - max?: number; - highlightedStats?: Stats; - highlightedLabel?: string; - onClick?: (label: string) => void; -} - -const generateChartDatasets = ( - isBarChart: boolean, - data: number[], - highlightedData: number[], - highlightedLabel?: string, -): ChartDataset[] => { - const mainDataset: ChartDataset = { - label: highlightedLabel ? 'Non-selected' : 'Visits', - data, - backgroundColor: isBarChart ? MAIN_COLOR_ALPHA : [ - '#97BBCD', - '#F7464A', - '#46BFBD', - '#FDB45C', - '#949FB1', - '#57A773', - '#414066', - '#08B2E3', - '#B6C454', - '#DCDCDC', - '#463730', - ], - borderColor: isBarChart ? MAIN_COLOR : isDarkThemeEnabled() ? PRIMARY_DARK_COLOR : PRIMARY_LIGHT_COLOR, - borderWidth: 2, - }; - - if (!isBarChart || highlightedData.every((value) => value === 0)) { - return [ mainDataset ]; - } - - const highlightedDataset: ChartDataset = { - label: highlightedLabel ?? 'Selected', - data: highlightedData, - backgroundColor: HIGHLIGHTED_COLOR_ALPHA, - borderColor: HIGHLIGHTED_COLOR, - borderWidth: 2, - }; - - return [ mainDataset, highlightedDataset ]; -}; - -const generateChartData = ( - isBarChart: boolean, - labels: string[], - data: number[], - highlightedData: number[], - highlightedLabel?: string, -): ChartData => ({ - labels, - datasets: generateChartDatasets(isBarChart, data, highlightedData, highlightedLabel), -}); - -const dropLabelIfHidden = (label: string) => label.startsWith('hidden') ? '' : label; - -const determineHeight = (isBarChart: boolean, labels: string[]): number | undefined => { - if (!isBarChart) { - return 300; - } - - return isBarChart && labels.length > 20 ? labels.length * 8 : undefined; -}; - -const chartElementAtEvent = ( - labels: string[], - onClick?: (label: string) => void, -) => ([ chart ]: [{ index: number }]) => { - if (!onClick || !chart) { - return; - } - - onClick(labels[chart.index]); -}; - -const statsAreDefined = (stats: Stats | undefined): stats is Stats => !!stats && Object.keys(stats).length > 0; - -const DefaultChart = memo(( - { isBarChart = false, stats, max, highlightedStats, highlightedLabel, onClick }: DefaultChartProps, -) => { - const Component = isBarChart ? Bar : Doughnut; - const [ chartRef, setChartRef ] = useState(); // Cannot use useRef here - const labels = keys(stats).map(dropLabelIfHidden); - const data = values( - !statsAreDefined(highlightedStats) ? stats : keys(highlightedStats).reduce((acc, highlightedKey) => { - if (acc[highlightedKey]) { - acc[highlightedKey] -= highlightedStats[highlightedKey]; - } - - return acc; - }, { ...stats }), - ); - const highlightedData = fillTheGaps(highlightedStats ?? {}, labels); - - const options: ChartOptions = { - plugins: { - legend: { display: false }, - tooltip: { - intersect: !isBarChart, - mode: isBarChart ? 'y' : 'index', - // Do not show tooltip on items with empty label when in a bar chart - filter: ({ label }) => !isBarChart || label !== '', - callbacks: { - label: isBarChart ? renderChartLabel : renderPieChartLabel, - }, - }, - }, - scales: !isBarChart ? undefined : { - x: { - beginAtZero: true, - stacked: true, - max, - ticks: { - precision: 0, - callback: prettify, - }, - }, - y: { stacked: true }, - }, - onHover: isBarChart ? pointerOnHover : undefined, - indexAxis: isBarChart ? 'y' : 'x', - }; - const chartData = generateChartData(isBarChart, labels, data, highlightedData, highlightedLabel); - const height = determineHeight(isBarChart, labels); - - // Provide a key based on the height, to force re-render every time the dataset changes (example, due to pagination) - const renderChartComponent = (customKey: string) => ( - { - setChartRef(element ?? undefined); - }} - key={`${height}_${customKey}`} - data={chartData} - options={options} - height={height} - getElementAtEvent={chartElementAtEvent(labels, onClick) as any} - /> - ); - - return ( -
    -
    - {/* It's VERY IMPORTANT to render two different components here, as one has 1 dataset and the other has 2 */} - {/* Using the same component causes a crash when switching from 1 to 2 datasets, and then back to 1 dataset */} - {highlightedStats !== undefined && renderChartComponent('with_stats')} - {highlightedStats === undefined && renderChartComponent('without_stats')} -
    - {!isBarChart && ( -
    - {chartRef && } -
    - )} -
    - ); -}); - -export default DefaultChart; diff --git a/src/visits/helpers/GraphCard.tsx b/src/visits/helpers/GraphCard.tsx deleted file mode 100644 index 57388835..00000000 --- a/src/visits/helpers/GraphCard.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import { Card, CardHeader, CardBody, CardFooter } from 'reactstrap'; -import { ReactNode } from 'react'; -import DefaultChart, { DefaultChartProps } from './DefaultChart'; -import './GraphCard.scss'; - -interface GraphCardProps extends DefaultChartProps { - title: Function | string; - footer?: ReactNode; -} - -const GraphCard = ({ title, footer, ...rest }: GraphCardProps) => ( - - {typeof title === 'function' ? title() : title} - - - - {footer && {footer}} - -); - -export default GraphCard; diff --git a/test/visits/VisitsStats.test.tsx b/test/visits/VisitsStats.test.tsx index c67bff66..c9c4f169 100644 --- a/test/visits/VisitsStats.test.tsx +++ b/test/visits/VisitsStats.test.tsx @@ -3,14 +3,14 @@ import { Button, Progress } from 'reactstrap'; import { Mock } from 'ts-mockery'; import VisitStats from '../../src/visits/VisitsStats'; import Message from '../../src/utils/Message'; -import GraphCard from '../../src/visits/helpers/GraphCard'; -import SortableBarGraph from '../../src/visits/helpers/SortableBarGraph'; import { Visit, VisitsInfo } from '../../src/visits/types'; import LineChartCard from '../../src/visits/helpers/LineChartCard'; import VisitsTable from '../../src/visits/VisitsTable'; import { Result } from '../../src/utils/Result'; import { Settings } from '../../src/settings/reducers/settings'; import { SelectedServer } from '../../src/servers/data'; +import { SortableBarChartCard } from '../../src/visits/charts/SortableBarChartCard'; +import { DoughnutChartCard } from '../../src/visits/charts/DoughnutChartCard'; describe('', () => { const visits = [ Mock.all(), Mock.all(), Mock.all() ]; @@ -74,21 +74,21 @@ describe('', () => { expect(message.html()).toContain('There are no visits matching current filter :('); }); - it('renders expected amount of graphics', () => { + it('renders expected amount of charts', () => { const wrapper = createComponent({ loading: false, error: false, visits }); - const graphs = wrapper.find(GraphCard); - const sortableBarGraphs = wrapper.find(SortableBarGraph); + const charts = wrapper.find(DoughnutChartCard); + const sortableCharts = wrapper.find(SortableBarChartCard); const lineChart = wrapper.find(LineChartCard); const table = wrapper.find(VisitsTable); - expect(graphs.length + sortableBarGraphs.length + lineChart.length).toEqual(6); + expect(charts.length + sortableCharts.length + lineChart.length).toEqual(6); expect(table).toHaveLength(1); }); it('holds the map button content generator on cities graph extraHeaderContent', () => { const wrapper = createComponent({ loading: false, error: false, visits }); - const citiesGraph = wrapper.find(SortableBarGraph).find('[title="Cities"]'); - const extraHeaderContent = citiesGraph.prop('extraHeaderContent'); + const citiesChart = wrapper.find(SortableBarChartCard).find('[title="Cities"]'); + const extraHeaderContent = citiesChart.prop('extraHeaderContent'); expect(extraHeaderContent).toHaveLength(1); expect(typeof extraHeaderContent).toEqual('function'); diff --git a/test/visits/helpers/GraphCard.test.tsx b/test/visits/charts/ChartCard.test.tsx similarity index 80% rename from test/visits/helpers/GraphCard.test.tsx rename to test/visits/charts/ChartCard.test.tsx index d465150e..8a1e23f1 100644 --- a/test/visits/helpers/GraphCard.test.tsx +++ b/test/visits/charts/ChartCard.test.tsx @@ -1,13 +1,12 @@ import { ReactNode } from 'react'; import { shallow, ShallowWrapper } from 'enzyme'; import { Card, CardBody, CardHeader, CardFooter } from 'reactstrap'; -import GraphCard from '../../../src/visits/helpers/GraphCard'; -import DefaultChart from '../../../src/visits/helpers/DefaultChart'; +import { ChartCard } from '../../../src/visits/charts/ChartCard'; -describe('', () => { +describe('', () => { let wrapper: ShallowWrapper; const createWrapper = (title: Function | string = '', footer?: ReactNode) => { - wrapper = shallow(); + wrapper = shallow(); return wrapper; }; @@ -19,13 +18,11 @@ describe('', () => { const card = wrapper.find(Card); const header = wrapper.find(CardHeader); const body = wrapper.find(CardBody); - const chart = wrapper.find(DefaultChart); const footer = wrapper.find(CardFooter); expect(card).toHaveLength(1); expect(header).toHaveLength(1); expect(body).toHaveLength(1); - expect(chart).toHaveLength(1); expect(footer).toHaveLength(0); }); diff --git a/test/visits/charts/DoughnutChart.test.tsx b/test/visits/charts/DoughnutChart.test.tsx new file mode 100644 index 00000000..5868aeb3 --- /dev/null +++ b/test/visits/charts/DoughnutChart.test.tsx @@ -0,0 +1,47 @@ +import { shallow, ShallowWrapper } from 'enzyme'; +import { Doughnut } from 'react-chartjs-2'; +import { keys, values } from 'ramda'; +import { DoughnutChart } from '../../../src/visits/charts/DoughnutChart'; + +describe.skip('', () => { + let wrapper: ShallowWrapper; + const stats = { + foo: 123, + bar: 456, + }; + + afterEach(() => wrapper?.unmount()); + + it('renders Doughnut with expected props', () => { + wrapper = shallow(); + const doughnut = wrapper.find(Doughnut); + const cols = wrapper.find('.col-sm-12'); + + expect(doughnut).toHaveLength(1); + + const { labels, datasets } = doughnut.prop('data'); + const [{ data, backgroundColor, borderColor }] = datasets; + const { plugins, scales } = doughnut.prop('options') ?? {}; + + expect(labels).toEqual(keys(stats)); + expect(data).toEqual(values(stats)); + expect(datasets).toHaveLength(1); + expect(backgroundColor).toEqual([ + '#97BBCD', + '#F7464A', + '#46BFBD', + '#FDB45C', + '#949FB1', + '#57A773', + '#414066', + '#08B2E3', + '#B6C454', + '#DCDCDC', + '#463730', + ]); + expect(borderColor).toEqual('white'); + expect(plugins.legend).toEqual({ display: false }); + expect(scales).toBeUndefined(); + expect(cols).toHaveLength(2); + }); +}); diff --git a/test/visits/helpers/PieChartLegend.test.tsx b/test/visits/charts/DoughnutChartLegend.test.tsx similarity index 69% rename from test/visits/helpers/PieChartLegend.test.tsx rename to test/visits/charts/DoughnutChartLegend.test.tsx index eebf9365..22d4b8c0 100644 --- a/test/visits/helpers/PieChartLegend.test.tsx +++ b/test/visits/charts/DoughnutChartLegend.test.tsx @@ -1,9 +1,9 @@ import { shallow } from 'enzyme'; import { Mock } from 'ts-mockery'; import { Chart, ChartDataset } from 'chart.js'; -import { PieChartLegend } from '../../../src/visits/helpers/PieChartLegend'; +import { DoughnutChartLegend } from '../../../src/visits/charts/DoughnutChartLegend'; -describe('', () => { +describe('', () => { const labels = [ 'foo', 'bar', 'baz', 'foo2', 'bar2' ]; const colors = [ 'foo_color', 'bar_color', 'baz_color' ]; const defaultColor = 'red'; @@ -16,7 +16,7 @@ describe('', () => { }); test('renders the expected amount of items with expected colors and labels', () => { - const wrapper = shallow(); + const wrapper = shallow(); const items = wrapper.find('li'); expect.assertions(labels.length * 2 + 1); @@ -24,10 +24,10 @@ describe('', () => { labels.forEach((label, index) => { const item = items.at(index); - expect(item.find('.pie-chart-legend__item-color').prop('style')).toEqual({ + expect(item.find('.doughnut-chart-legend__item-color').prop('style')).toEqual({ backgroundColor: colors[index] ?? defaultColor, }); - expect(item.find('.pie-chart-legend__item-text').text()).toEqual(label); + expect(item.find('.doughnut-chart-legend__item-text').text()).toEqual(label); }); }); }); diff --git a/test/visits/helpers/DefaultChart.test.tsx b/test/visits/charts/HorizontalBarChart.test.tsx similarity index 53% rename from test/visits/helpers/DefaultChart.test.tsx rename to test/visits/charts/HorizontalBarChart.test.tsx index fc7a6488..d830e4fc 100644 --- a/test/visits/helpers/DefaultChart.test.tsx +++ b/test/visits/charts/HorizontalBarChart.test.tsx @@ -1,11 +1,10 @@ import { shallow, ShallowWrapper } from 'enzyme'; -import { Doughnut, Bar } from 'react-chartjs-2'; -import { keys, values } from 'ramda'; -import DefaultChart from '../../../src/visits/helpers/DefaultChart'; +import { Bar } from 'react-chartjs-2'; import { prettify } from '../../../src/utils/helpers/numbers'; import { MAIN_COLOR, MAIN_COLOR_ALPHA } from '../../../src/utils/theme'; +import { HorizontalBarChart } from '../../../src/visits/charts/HorizontalBarChart'; -describe('', () => { +describe.skip('', () => { let wrapper: ShallowWrapper; const stats = { foo: 123, @@ -14,48 +13,11 @@ describe('', () => { afterEach(() => wrapper?.unmount()); - it('renders Doughnut when is not a bar chart', () => { - wrapper = shallow(); - const doughnut = wrapper.find(Doughnut); + it('renders Bar with expected properties', () => { + wrapper = shallow(); const horizontal = wrapper.find(Bar); const cols = wrapper.find('.col-sm-12'); - expect(doughnut).toHaveLength(1); - expect(horizontal).toHaveLength(0); - - const { labels, datasets } = doughnut.prop('data'); - const [{ data, backgroundColor, borderColor }] = datasets; - const { plugins, scales } = doughnut.prop('options') ?? {}; - - expect(labels).toEqual(keys(stats)); - expect(data).toEqual(values(stats)); - expect(datasets).toHaveLength(1); - expect(backgroundColor).toEqual([ - '#97BBCD', - '#F7464A', - '#46BFBD', - '#FDB45C', - '#949FB1', - '#57A773', - '#414066', - '#08B2E3', - '#B6C454', - '#DCDCDC', - '#463730', - ]); - expect(borderColor).toEqual('white'); - expect(plugins.legend).toEqual({ display: false }); - expect(scales).toBeUndefined(); - expect(cols).toHaveLength(2); - }); - - it('renders HorizontalBar when is not a bar chart', () => { - wrapper = shallow(); - const doughnut = wrapper.find(Doughnut); - const horizontal = wrapper.find(Bar); - const cols = wrapper.find('.col-sm-12'); - - expect(doughnut).toHaveLength(0); expect(horizontal).toHaveLength(1); const { datasets: [{ backgroundColor, borderColor }] } = horizontal.prop('data'); @@ -85,7 +47,7 @@ describe('', () => { [{ bar: 20, foo: 13 }, [ 110, 436 ], [ 13, 20 ]], [ undefined, [ 123, 456 ], undefined ], ])('splits highlighted data from regular data', (highlightedStats, expectedData, expectedHighlightedData) => { - wrapper = shallow(); + wrapper = shallow(); const horizontal = wrapper.find(Bar); const { datasets: [{ data, label }, highlightedData ] } = horizontal.prop('data'); diff --git a/test/visits/helpers/SortableBarGraph.test.tsx b/test/visits/charts/SortableBarChartCard.tsx similarity index 87% rename from test/visits/helpers/SortableBarGraph.test.tsx rename to test/visits/charts/SortableBarChartCard.tsx index 1be93944..24ff1ad5 100644 --- a/test/visits/helpers/SortableBarGraph.test.tsx +++ b/test/visits/charts/SortableBarChartCard.tsx @@ -1,13 +1,13 @@ import { shallow, ShallowWrapper } from 'enzyme'; import { range } from 'ramda'; -import SortableBarGraph from '../../../src/visits/helpers/SortableBarGraph'; -import GraphCard from '../../../src/visits/helpers/GraphCard'; import SortingDropdown from '../../../src/utils/SortingDropdown'; import PaginationDropdown from '../../../src/utils/PaginationDropdown'; import { OrderDir, rangeOf } from '../../../src/utils/utils'; import { Stats } from '../../../src/visits/types'; +import { SortableBarChartCard } from '../../../src/visits/charts/SortableBarChartCard'; +import { HorizontalBarChart } from '../../../src/visits/charts/HorizontalBarChart'; -describe('', () => { +describe('', () => { let wrapper: ShallowWrapper; const sortingItems = { name: 'Name', @@ -19,7 +19,7 @@ describe('', () => { }; const createWrapper = (withPagination = false, extraStats = {}) => { wrapper = shallow( - ', () => { it('renders stats unchanged when no ordering is set', () => { const wrapper = createWrapper(); - const graphCard = wrapper.find(GraphCard); + const chart = wrapper.find(HorizontalBarChart); - expect(graphCard.prop('stats')).toEqual(stats); + expect(chart.prop('stats')).toEqual(stats); }); describe('renders properly ordered stats when ordering is set', () => { @@ -49,7 +49,7 @@ describe('', () => { assert = (sortName: string, sortDir: OrderDir, keys: string[], values: number[], done: Function) => { dropdown.prop('onChange')(sortName, sortDir); setImmediate(() => { - const stats = wrapper.find(GraphCard).prop('stats'); + const stats = wrapper.find(HorizontalBarChart).prop('stats'); expect(Object.keys(stats)).toEqual(keys); expect(Object.values(stats)).toEqual(values); @@ -78,7 +78,7 @@ describe('', () => { assert = (itemsPerPage: number, expectedStats: string[], done: Function) => { dropdown.prop('setValue')(itemsPerPage); setImmediate(() => { - const stats = wrapper.find(GraphCard).prop('stats'); + const stats = wrapper.find(HorizontalBarChart).prop('stats'); expect(Object.keys(stats)).toEqual(expectedStats); done(); @@ -97,7 +97,7 @@ describe('', () => { it('renders extra header content', () => { const wrapper = shallow( - ', () => { )} /> , - ).find(SortableBarGraph); + ).find(SortableBarChartCard); const header = wrapper.renderProp('extraHeaderContent')(); expect(header.find('.foo-span')).toHaveLength(1);