From 749c757cbd1b8418b7e0ee6df104a0c844526018 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sun, 20 Sep 2020 10:43:21 +0200 Subject: [PATCH 1/5] Removed unneeded condition on travis deploy step, as the same condition is on the parent job --- .travis.yml | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/.travis.yml b/.travis.yml index e4c12ff4..18690565 100644 --- a/.travis.yml +++ b/.travis.yml @@ -43,12 +43,10 @@ jobs: - name: 'Publish release' if: tag IS present - before_deploy: npm run build ${TRAVIS_TAG#?} # Before deploying, build dist file for current travis tag + before_deploy: npm run build ${TRAVIS_TAG#?} deploy: - - provider: releases - api_key: - secure: jBvPwC7EAbViaNR83rwMSt5XQDK0Iu9rgvEMa7GoyShbHcvUCCPd73Tu9quNpKi6NKsDY3INHgtch3vgonjGNGDGJ+yDyIBzXcvsAX2x3UcHpRbgY12uiINVmQxBI1+OVQB016Nm+cKC/i5Z36K4EmDbYfo+MrKndngM6AjcQFTwI8EwniIMaQgg4gNes//K8NhP5u0c3gwG+Q6jEGnq6uH3kcRgh6/epIZYpQyxjqWqKwF77sgcYj+X2Nf6XxtB5neuCi301UKLoLx8G0skh/Lm6KAIO4s9iIhIFa3UpoF21Ka0TxLpd2JxalLryCnFGlWWE6lxC9Htmc0TeRowJQlGdJXCskJ37xT9MljKY0fwNMu06VS/FUgykuCv+jP3zQu51pKu7Ew7+WeNPjautoOTu54VkdGyHcf2ThBNEyJQuiEwAQe4u7yAxY6R5ovEdvHBSIg4w1E5/Mxy5SMTCUlIAv6H7QQ1X9Z/zJm9HH5KeKz5tsHvQ/RIdSpgHXq/tC8o4Yup/LCFucXfrgvy/8pJoO1UpOlmvm62974NFfo0EG5YWwv6brUqz3QXpMjb8sWqgjltYMYJX3J7WZ34rIc+zt4NAmfhqgczaOC4pUGCiJ8jX3rMWIaQRn1AJ+5V337jL9fNDpTHny4phQjHrMJ1e0HZuNp0Xb5Q8wgqDPM= - file: "./dist/shlink-web-client_${TRAVIS_TAG#?}_dist.zip" - skip_cleanup: true - on: - tags: true + provider: releases + api_key: + secure: jBvPwC7EAbViaNR83rwMSt5XQDK0Iu9rgvEMa7GoyShbHcvUCCPd73Tu9quNpKi6NKsDY3INHgtch3vgonjGNGDGJ+yDyIBzXcvsAX2x3UcHpRbgY12uiINVmQxBI1+OVQB016Nm+cKC/i5Z36K4EmDbYfo+MrKndngM6AjcQFTwI8EwniIMaQgg4gNes//K8NhP5u0c3gwG+Q6jEGnq6uH3kcRgh6/epIZYpQyxjqWqKwF77sgcYj+X2Nf6XxtB5neuCi301UKLoLx8G0skh/Lm6KAIO4s9iIhIFa3UpoF21Ka0TxLpd2JxalLryCnFGlWWE6lxC9Htmc0TeRowJQlGdJXCskJ37xT9MljKY0fwNMu06VS/FUgykuCv+jP3zQu51pKu7Ew7+WeNPjautoOTu54VkdGyHcf2ThBNEyJQuiEwAQe4u7yAxY6R5ovEdvHBSIg4w1E5/Mxy5SMTCUlIAv6H7QQ1X9Z/zJm9HH5KeKz5tsHvQ/RIdSpgHXq/tC8o4Yup/LCFucXfrgvy/8pJoO1UpOlmvm62974NFfo0EG5YWwv6brUqz3QXpMjb8sWqgjltYMYJX3J7WZ34rIc+zt4NAmfhqgczaOC4pUGCiJ8jX3rMWIaQRn1AJ+5V337jL9fNDpTHny4phQjHrMJ1e0HZuNp0Xb5Q8wgqDPM= + file: "./dist/shlink-web-client_${TRAVIS_TAG#?}_dist.zip" + skip_cleanup: true From 78d97a64aaf6fb7b158c3b4f7b5b86983bce689f Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sun, 20 Sep 2020 11:43:24 +0200 Subject: [PATCH 2/5] Added visits highlightning capabilities to line chart --- src/utils/helpers/charts.ts | 5 +++ src/visits/VisitsStats.tsx | 1 + src/visits/helpers/DefaultChart.tsx | 9 ++-- src/visits/helpers/LineChartCard.tsx | 52 ++++++++++++++++++++-- test/visits/helpers/LineChartCard.test.tsx | 4 +- 5 files changed, 59 insertions(+), 12 deletions(-) create mode 100644 src/utils/helpers/charts.ts diff --git a/src/utils/helpers/charts.ts b/src/utils/helpers/charts.ts new file mode 100644 index 00000000..807519a9 --- /dev/null +++ b/src/utils/helpers/charts.ts @@ -0,0 +1,5 @@ +import { ChangeEvent, FC } from 'react'; + +export const pointerOnHover = ({ target }: ChangeEvent, chartElement: FC[]) => { + target.style.cursor = chartElement[0] ? 'pointer' : 'default'; +}; diff --git a/src/visits/VisitsStats.tsx b/src/visits/VisitsStats.tsx index b31cd5fc..bd48fec8 100644 --- a/src/visits/VisitsStats.tsx +++ b/src/visits/VisitsStats.tsx @@ -128,6 +128,7 @@ const VisitsStats: FC = ( visits={normalizedVisits} highlightedVisits={highlightedVisits} highlightedLabel={highlightedLabel} + setSelectedVisits={setSelectedVisits} />
diff --git a/src/visits/helpers/DefaultChart.tsx b/src/visits/helpers/DefaultChart.tsx index 01a38fb2..fcba3a5c 100644 --- a/src/visits/helpers/DefaultChart.tsx +++ b/src/visits/helpers/DefaultChart.tsx @@ -1,4 +1,4 @@ -import React, { ChangeEvent, useRef } from 'react'; +import React, { useRef } from 'react'; import { Doughnut, HorizontalBar } from 'react-chartjs-2'; import { keys, values } from 'ramda'; import classNames from 'classnames'; @@ -7,6 +7,7 @@ import { fillTheGaps, renderDoughnutChartLabel, renderNonDoughnutChartLabel } fr import { Stats } from '../types'; import { prettify } from '../../utils/helpers/numbers'; import './DefaultChart.scss'; +import { pointerOnHover } from '../../utils/helpers/charts'; export interface DefaultChartProps { title: Function | string; @@ -145,11 +146,7 @@ const DefaultChart = ( label: isBarChart ? renderNonDoughnutChartLabel('xLabel') : renderDoughnutChartLabel, }, }, - onHover: !isBarChart ? undefined : ((e: ChangeEvent, chartElement: HorizontalBar[] | Doughnut[]) => { - const { target } = e; - - target.style.cursor = chartElement[0] ? 'pointer' : 'default'; - }) as any, // TODO Types seem to be incorrectly defined + onHover: !isBarChart ? undefined : (pointerOnHover) as any, // TODO Types seem to be incorrectly defined in @types/chart.js }; const graphData = generateGraphData(title, isBarChart, labels, data, highlightedData, highlightedLabel); const height = determineHeight(isBarChart, labels); diff --git a/src/visits/helpers/LineChartCard.tsx b/src/visits/helpers/LineChartCard.tsx index bcac1b41..bfc6adcb 100644 --- a/src/visits/helpers/LineChartCard.tsx +++ b/src/visits/helpers/LineChartCard.tsx @@ -11,7 +11,7 @@ import { import { Line } from 'react-chartjs-2'; import { always, cond, reverse } from 'ramda'; import moment from 'moment'; -import { ChartData, ChartDataSets, ChartOptions } from 'chart.js'; +import Chart, { ChartData, ChartDataSets, ChartOptions } from 'chart.js'; import { NormalizedVisit, Stats } from '../types'; import { fillTheGaps, renderNonDoughnutChartLabel } from '../../utils/helpers/visits'; import { useToggle } from '../../utils/helpers/hooks'; @@ -19,12 +19,14 @@ import { rangeOf } from '../../utils/utils'; import ToggleSwitch from '../../utils/ToggleSwitch'; import { prettify } from '../../utils/helpers/numbers'; import './LineChartCard.scss'; +import { pointerOnHover } from '../../utils/helpers/charts'; interface LineChartCardProps { title: string; highlightedLabel?: string; visits: NormalizedVisit[]; highlightedVisits: NormalizedVisit[]; + setSelectedVisits?: (visits: NormalizedVisit[]) => void; } type Step = 'monthly' | 'weekly' | 'daily' | 'hourly'; @@ -71,13 +73,25 @@ const groupVisitsByStep = (step: Step, visits: NormalizedVisit[]): Stats => visi (acc, visit) => { const key = STEP_TO_DATE_FORMAT[step](visit.date); - acc[key] = acc[key] ? acc[key] + 1 : 1; + acc[key] = (acc[key] || 0) + 1; return acc; }, {}, ); +const visitsToDatasetGroups = (step: Step, visits: NormalizedVisit[]) => visits.reduce( + (acc, visit) => { + const key = STEP_TO_DATE_FORMAT[step](visit.date); + + acc[key] = acc[key] ?? []; + acc[key].push(visit); + + return acc; + }, + {} as Record, +); + const generateLabels = (step: Step, visits: NormalizedVisit[]): string[] => { const unit = STEP_TO_DATE_UNIT_MAP[step]; const formatter = STEP_TO_DATE_FORMAT[step]; @@ -115,12 +129,37 @@ const generateDataset = (data: number[], label: string, color: string): ChartDat backgroundColor: color, }); -const LineChartCard = ({ title, visits, highlightedVisits, highlightedLabel = 'Selected' }: LineChartCardProps) => { +let selectedLabel: string | null = null; + +const chartElementAtEvent = ( + datasetsByPoint: Record, + setSelectedVisits?: (visits: NormalizedVisit[]) => void, +) => ([ chart ]: [{ _index: number; _chart: Chart }]) => { + if (!setSelectedVisits || !chart) { + return; + } + + const { _index: index, _chart: { data } } = chart; + const { labels } = data as { labels: string[] }; + + if (selectedLabel === labels[index]) { + setSelectedVisits([]); + selectedLabel = null; + } else { + setSelectedVisits(labels[index] && datasetsByPoint[labels[index]] || []); + selectedLabel = labels[index] ?? null; + } +}; + +const LineChartCard = ( + { title, visits, highlightedVisits, highlightedLabel = 'Selected', setSelectedVisits }: LineChartCardProps, +) => { const [ step, setStep ] = useState( visits.length > 0 ? determineInitialStep(visits[visits.length - 1].date) : 'monthly', ); const [ skipNoVisits, toggleSkipNoVisits ] = useToggle(true); + const datasetsByPoint = useMemo(() => visitsToDatasetGroups(step, visits), [ step, visits ]); const groupedVisitsWithGaps = useMemo(() => groupVisitsByStep(step, reverse(visits)), [ step, visits ]); const [ labels, groupedVisits ] = useMemo( () => generateLabelsAndGroupedVisits(visits, groupedVisitsWithGaps, step, skipNoVisits), @@ -166,6 +205,7 @@ const LineChartCard = ({ title, visits, highlightedVisits, highlightedLabel = 'S label: renderNonDoughnutChartLabel('yLabel'), }, }, + onHover: (pointerOnHover) as any, // TODO Types seem to be incorrectly defined in @types/chart.js }; return ( @@ -193,7 +233,11 @@ const LineChartCard = ({ title, visits, highlightedVisits, highlightedLabel = 'S
- + ); diff --git a/test/visits/helpers/LineChartCard.test.tsx b/test/visits/helpers/LineChartCard.test.tsx index 8aaf2a48..27cfc33a 100644 --- a/test/visits/helpers/LineChartCard.test.tsx +++ b/test/visits/helpers/LineChartCard.test.tsx @@ -53,7 +53,7 @@ describe('', () => { const wrapper = createWrapper(); const chart = wrapper.find(Line); - expect(chart.prop('options')).toEqual({ + expect(chart.prop('options')).toEqual(expect.objectContaining({ maintainAspectRatio: false, legend: { display: false }, scales: { @@ -72,7 +72,7 @@ describe('', () => { intersect: false, axis: 'x', }), - }); + })); }); it.each([ From 421cc5b7180a1aecc1103048571ac5cf50bdef2e Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sun, 20 Sep 2020 11:46:07 +0200 Subject: [PATCH 3/5] Put together all chart-related helper functions --- src/utils/helpers/charts.ts | 25 +++++++++++++++++++++++++ src/utils/helpers/visits.ts | 25 ------------------------- src/visits/helpers/DefaultChart.tsx | 4 ++-- src/visits/helpers/LineChartCard.tsx | 4 ++-- 4 files changed, 29 insertions(+), 29 deletions(-) diff --git a/src/utils/helpers/charts.ts b/src/utils/helpers/charts.ts index 807519a9..fcc6f1fb 100644 --- a/src/utils/helpers/charts.ts +++ b/src/utils/helpers/charts.ts @@ -1,5 +1,30 @@ import { ChangeEvent, FC } from 'react'; +import { ChartData, ChartTooltipItem } from 'chart.js'; +import { prettify } from './numbers'; export const pointerOnHover = ({ target }: ChangeEvent, chartElement: FC[]) => { target.style.cursor = chartElement[0] ? 'pointer' : 'default'; }; + +export const renderNonDoughnutChartLabel = (labelToPick: 'yLabel' | 'xLabel') => ( + item: ChartTooltipItem, + { datasets }: ChartData, +) => { + const { datasetIndex } = item; + const value = item[labelToPick]; + const datasetLabel = datasetIndex !== undefined && datasets?.[datasetIndex]?.label || ''; + + return `${datasetLabel}: ${prettify(Number(value))}`; +}; + +export const renderDoughnutChartLabel = ( + { datasetIndex, index }: ChartTooltipItem, + { labels, datasets }: ChartData, +) => { + const datasetLabel = index !== undefined && labels?.[index] || ''; + const value = datasetIndex !== undefined && index !== undefined + && datasets?.[datasetIndex]?.data?.[index] + || ''; + + return `${datasetLabel}: ${prettify(Number(value))}`; +}; diff --git a/src/utils/helpers/visits.ts b/src/utils/helpers/visits.ts index 89dc1d57..3b081814 100644 --- a/src/utils/helpers/visits.ts +++ b/src/utils/helpers/visits.ts @@ -1,9 +1,7 @@ import bowser from 'bowser'; import { zipObj } from 'ramda'; -import { ChartData, ChartTooltipItem } from 'chart.js'; import { Empty, hasValue } from '../utils'; import { Stats, UserAgent } from '../../visits/types'; -import { prettify } from './numbers'; const DEFAULT = 'Others'; const BROWSERS_WHITELIST = [ @@ -40,26 +38,3 @@ export const extractDomain = (url: string | Empty): string => { export const fillTheGaps = (stats: Stats, labels: string[]): number[] => Object.values({ ...zipObj(labels, labels.map(() => 0)), ...stats }); - -export const renderDoughnutChartLabel = ( - { datasetIndex, index }: ChartTooltipItem, - { labels, datasets }: ChartData, -) => { - const datasetLabel = index !== undefined && labels?.[index] || ''; - const value = datasetIndex !== undefined && index !== undefined - && datasets?.[datasetIndex]?.data?.[index] - || ''; - - return `${datasetLabel}: ${prettify(Number(value))}`; -}; - -export const renderNonDoughnutChartLabel = (labelToPick: 'yLabel' | 'xLabel') => ( - item: ChartTooltipItem, - { datasets }: ChartData, -) => { - const { datasetIndex } = item; - const value = item[labelToPick]; - const datasetLabel = datasetIndex !== undefined && datasets?.[datasetIndex]?.label || ''; - - return `${datasetLabel}: ${prettify(Number(value))}`; -}; diff --git a/src/visits/helpers/DefaultChart.tsx b/src/visits/helpers/DefaultChart.tsx index fcba3a5c..ebd92456 100644 --- a/src/visits/helpers/DefaultChart.tsx +++ b/src/visits/helpers/DefaultChart.tsx @@ -3,11 +3,11 @@ import { Doughnut, HorizontalBar } from 'react-chartjs-2'; import { keys, values } from 'ramda'; import classNames from 'classnames'; import Chart, { ChartData, ChartDataSets, ChartOptions } from 'chart.js'; -import { fillTheGaps, renderDoughnutChartLabel, renderNonDoughnutChartLabel } from '../../utils/helpers/visits'; +import { fillTheGaps} from '../../utils/helpers/visits'; import { Stats } from '../types'; import { prettify } from '../../utils/helpers/numbers'; +import { pointerOnHover, renderDoughnutChartLabel, renderNonDoughnutChartLabel } from '../../utils/helpers/charts'; import './DefaultChart.scss'; -import { pointerOnHover } from '../../utils/helpers/charts'; export interface DefaultChartProps { title: Function | string; diff --git a/src/visits/helpers/LineChartCard.tsx b/src/visits/helpers/LineChartCard.tsx index bfc6adcb..c21ba53b 100644 --- a/src/visits/helpers/LineChartCard.tsx +++ b/src/visits/helpers/LineChartCard.tsx @@ -13,13 +13,13 @@ import { always, cond, reverse } from 'ramda'; import moment from 'moment'; import Chart, { ChartData, ChartDataSets, ChartOptions } from 'chart.js'; import { NormalizedVisit, Stats } from '../types'; -import { fillTheGaps, renderNonDoughnutChartLabel } from '../../utils/helpers/visits'; +import { fillTheGaps} from '../../utils/helpers/visits'; import { useToggle } from '../../utils/helpers/hooks'; import { rangeOf } from '../../utils/utils'; import ToggleSwitch from '../../utils/ToggleSwitch'; import { prettify } from '../../utils/helpers/numbers'; +import { pointerOnHover, renderNonDoughnutChartLabel } from '../../utils/helpers/charts'; import './LineChartCard.scss'; -import { pointerOnHover } from '../../utils/helpers/charts'; interface LineChartCardProps { title: string; From bf7455ad6e5ad733c2eea792737e7e789bf1eaef Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sun, 20 Sep 2020 11:49:19 +0200 Subject: [PATCH 4/5] Updated changelog --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 018e7837..51235362 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), By default updates are immediately applied if real-time updates are enabled, to keep the behavior as it was. +* [#277](https://github.com/shlinkio/shlink-web-client/issues/277) Added highlighting capabilities to the visits line chart. + #### Changed * [#150](https://github.com/shlinkio/shlink-web-client/issues/150) The list of short URLs is now ordered by the creation date, showing newest results first. From 335cceeb82ab3c75cc9ed928db3fbd601b18a008 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sun, 20 Sep 2020 11:58:40 +0200 Subject: [PATCH 5/5] Fixed coding styles --- src/visits/helpers/DefaultChart.tsx | 2 +- src/visits/helpers/LineChartCard.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/visits/helpers/DefaultChart.tsx b/src/visits/helpers/DefaultChart.tsx index ebd92456..48889119 100644 --- a/src/visits/helpers/DefaultChart.tsx +++ b/src/visits/helpers/DefaultChart.tsx @@ -3,7 +3,7 @@ import { Doughnut, HorizontalBar } from 'react-chartjs-2'; import { keys, values } from 'ramda'; import classNames from 'classnames'; import Chart, { ChartData, ChartDataSets, ChartOptions } from 'chart.js'; -import { fillTheGaps} from '../../utils/helpers/visits'; +import { fillTheGaps } from '../../utils/helpers/visits'; import { Stats } from '../types'; import { prettify } from '../../utils/helpers/numbers'; import { pointerOnHover, renderDoughnutChartLabel, renderNonDoughnutChartLabel } from '../../utils/helpers/charts'; diff --git a/src/visits/helpers/LineChartCard.tsx b/src/visits/helpers/LineChartCard.tsx index c21ba53b..a9cdc190 100644 --- a/src/visits/helpers/LineChartCard.tsx +++ b/src/visits/helpers/LineChartCard.tsx @@ -13,7 +13,7 @@ import { always, cond, reverse } from 'ramda'; import moment from 'moment'; import Chart, { ChartData, ChartDataSets, ChartOptions } from 'chart.js'; import { NormalizedVisit, Stats } from '../types'; -import { fillTheGaps} from '../../utils/helpers/visits'; +import { fillTheGaps } from '../../utils/helpers/visits'; import { useToggle } from '../../utils/helpers/hooks'; import { rangeOf } from '../../utils/utils'; import ToggleSwitch from '../../utils/ToggleSwitch';