From de509e7cccfbe988a627f498ea3a400a893e563e Mon Sep 17 00:00:00 2001 From: Brian Cao Date: Thu, 18 May 2023 11:17:35 -0700 Subject: [PATCH] checkpoint --- components/layout/NavBar.js | 1 + components/layout/ReportsLayout.js | 23 +++ components/layout/ReportsLayout.module.css | 23 +++ components/layout/SettingsLayout.js | 1 - components/pages/reports/FunnelChart.js | 42 ---- .../pages/reports/FunnelChart.module.css | 7 - components/pages/reports/FunnelDetails.js | 0 components/pages/reports/ReportDropdown.js | 0 components/pages/reports/ReportForm.js | 28 +++ .../pages/reports/ReportForm.module.css | 23 +++ .../pages/reports/funnel/FunnelChart.js | 186 ++++++++++++++++++ .../reports/funnel/FunnelChart.module.css | 23 +++ .../pages/reports/{ => funnel}/FunnelForm.js | 8 +- .../{ => funnel}/FunnelForm.module.css | 5 - .../pages/reports/{ => funnel}/FunnelPage.js | 20 +- .../{ => funnel}/FunnelPage.module.css | 0 .../pages/reports/{ => funnel}/FunnelTable.js | 3 +- package.json | 1 - pages/_app.js | 1 - pages/{settings => }/reports/funnel.js | 9 +- pages/{settings => }/reports/index.js | 2 +- .../analytics/pageview/getPageviewFunnel.ts | 17 +- styles/funnelChart.css | 148 -------------- 23 files changed, 335 insertions(+), 236 deletions(-) create mode 100644 components/layout/ReportsLayout.js create mode 100644 components/layout/ReportsLayout.module.css delete mode 100644 components/pages/reports/FunnelChart.js delete mode 100644 components/pages/reports/FunnelChart.module.css delete mode 100644 components/pages/reports/FunnelDetails.js delete mode 100644 components/pages/reports/ReportDropdown.js create mode 100644 components/pages/reports/ReportForm.module.css create mode 100644 components/pages/reports/funnel/FunnelChart.js create mode 100644 components/pages/reports/funnel/FunnelChart.module.css rename components/pages/reports/{ => funnel}/FunnelForm.js (96%) rename components/pages/reports/{ => funnel}/FunnelForm.module.css (83%) rename components/pages/reports/{ => funnel}/FunnelPage.js (67%) rename components/pages/reports/{ => funnel}/FunnelPage.module.css (100%) rename components/pages/reports/{ => funnel}/FunnelTable.js (73%) rename pages/{settings => }/reports/funnel.js (54%) rename pages/{settings => }/reports/index.js (90%) delete mode 100644 styles/funnelChart.css diff --git a/components/layout/NavBar.js b/components/layout/NavBar.js index 5a6c877e..ba06d172 100644 --- a/components/layout/NavBar.js +++ b/components/layout/NavBar.js @@ -19,6 +19,7 @@ export function NavBar() { const links = [ { label: formatMessage(labels.dashboard), url: '/dashboard' }, { label: formatMessage(labels.realtime), url: '/realtime' }, + { label: formatMessage(labels.reports), url: '/reports/funnel' }, !cloudMode && { label: formatMessage(labels.settings), url: '/settings' }, ].filter(n => n); diff --git a/components/layout/ReportsLayout.js b/components/layout/ReportsLayout.js new file mode 100644 index 00000000..fd63a67e --- /dev/null +++ b/components/layout/ReportsLayout.js @@ -0,0 +1,23 @@ +import { Column, Row } from 'react-basics'; +import styles from './ReportsLayout.module.css'; + +export function SettingsLayout({ children, filter, header }) { + return ( + <> + {header} + + {filter && ( + +

Filters

+ {filter} +
+ )} + + {children} + +
+ + ); +} + +export default SettingsLayout; diff --git a/components/layout/ReportsLayout.module.css b/components/layout/ReportsLayout.module.css new file mode 100644 index 00000000..6922665f --- /dev/null +++ b/components/layout/ReportsLayout.module.css @@ -0,0 +1,23 @@ +.filter { + margin-top: 30px; + min-width: 200px; + max-width: 100vw; + padding: 10px; + background: var(--base50); + border-radius: 5px; + border: 1px solid var(--border-color); +} + +.filter h2 { + padding-bottom: 20px; +} + +.content { + min-height: 50vh; +} + +@media only screen and (max-width: 768px) { + .menu { + display: none; + } +} diff --git a/components/layout/SettingsLayout.js b/components/layout/SettingsLayout.js index d58154ca..c79f0909 100644 --- a/components/layout/SettingsLayout.js +++ b/components/layout/SettingsLayout.js @@ -15,7 +15,6 @@ export function SettingsLayout({ children }) { const items = [ { key: 'websites', label: formatMessage(labels.websites), url: '/settings/websites' }, { key: 'teams', label: formatMessage(labels.teams), url: '/settings/teams' }, - { key: 'reports', label: 'Reports', url: '/settings/reports/funnel' }, user.isAdmin && { key: 'users', label: formatMessage(labels.users), url: '/settings/users' }, { key: 'profile', label: formatMessage(labels.profile), url: '/settings/profile' }, ].filter(n => n); diff --git a/components/pages/reports/FunnelChart.js b/components/pages/reports/FunnelChart.js deleted file mode 100644 index ec03acab..00000000 --- a/components/pages/reports/FunnelChart.js +++ /dev/null @@ -1,42 +0,0 @@ -import FunnelGraph from 'funnel-graph-js/dist/js/funnel-graph'; -import { useEffect, useRef } from 'react'; -import EmptyPlaceholder from 'components/common/EmptyPlaceholder'; -import useMessages from 'hooks/useMessages'; -import styles from './FunnelChart.module.css'; -import classNames from 'classnames'; - -export default function FunnelChart({ data }) { - const { formatMessage, labels, messages } = useMessages(); - const funnel = useRef(null); - - useEffect(() => { - if (data && data.length > 0) { - funnel.current.innerHTML = ''; - - const chartData = { - labels: data.map(a => a.url), - colors: ['#147af3', '#e0f2ff'], - values: data.map(a => a.count), - }; - - const graph = new FunnelGraph({ - container: '.funnel', - gradientDirection: 'horizontal', - data: chartData, - displayPercent: true, - direction: 'Vertical', - width: 1000, - height: 350, - }); - - graph.draw(); - } - }, [data]); - - return ( - <> - {data?.length > 0 &&
} - {data?.length === 0 && } - - ); -} diff --git a/components/pages/reports/FunnelChart.module.css b/components/pages/reports/FunnelChart.module.css deleted file mode 100644 index 1d7eb37e..00000000 --- a/components/pages/reports/FunnelChart.module.css +++ /dev/null @@ -1,7 +0,0 @@ -.funnel div { - color: var(--font-color100) !important; -} - -.funnel svg { - max-width: 100%; -} diff --git a/components/pages/reports/FunnelDetails.js b/components/pages/reports/FunnelDetails.js deleted file mode 100644 index e69de29b..00000000 diff --git a/components/pages/reports/ReportDropdown.js b/components/pages/reports/ReportDropdown.js deleted file mode 100644 index e69de29b..00000000 diff --git a/components/pages/reports/ReportForm.js b/components/pages/reports/ReportForm.js index e69de29b..b32a8531 100644 --- a/components/pages/reports/ReportForm.js +++ b/components/pages/reports/ReportForm.js @@ -0,0 +1,28 @@ +import useMessages from 'hooks/useMessages'; +import { Form, FormButtons, FormInput, FormRow, SubmitButton, TextField } from 'react-basics'; + +export function FunnelForm({ onSearch }) { + const { formatMessage, labels } = useMessages(); + + const handleSubmit = () => {}; + + return ( + <> +
+ + + + + + + + + Save + + +
+ + ); +} + +export default FunnelForm; diff --git a/components/pages/reports/ReportForm.module.css b/components/pages/reports/ReportForm.module.css new file mode 100644 index 00000000..2a07d552 --- /dev/null +++ b/components/pages/reports/ReportForm.module.css @@ -0,0 +1,23 @@ +.filter { + min-width: 200px; +} + +.hiddenInput { + max-height: 100px; +} + +.hiddenInput { + visibility: hidden; + min-height: 0px; + max-height: 0px; +} + +.hidden { + visibility: hidden; + min-height: 0px; + max-height: 0px; +} + +.urlFormRow label { + min-width: 80px; +} diff --git a/components/pages/reports/funnel/FunnelChart.js b/components/pages/reports/funnel/FunnelChart.js new file mode 100644 index 00000000..5a0b5699 --- /dev/null +++ b/components/pages/reports/funnel/FunnelChart.js @@ -0,0 +1,186 @@ +import Chart from 'chart.js/auto'; +import classNames from 'classnames'; +import { colord } from 'colord'; +import HoverTooltip from 'components/common/HoverTooltip'; +import Legend from 'components/metrics/Legend'; +import useLocale from 'hooks/useLocale'; +import useMessages from 'hooks/useMessages'; +import useTheme from 'hooks/useTheme'; +import { DEFAULT_ANIMATION_DURATION, THEME_COLORS } from 'lib/constants'; +import { dateFormat } from 'lib/date'; +import { formatLongNumber } from 'lib/format'; +import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import { Loading, StatusLight } from 'react-basics'; +import styles from './FunnelChart.module.css'; + +export function FunnelChart({ + data, + animationDuration = DEFAULT_ANIMATION_DURATION, + stacked = false, + loading = false, + onCreate = () => {}, + onUpdate = () => {}, + className, +}) { + const { formatMessage, labels } = useMessages(); + const canvas = useRef(); + const chart = useRef(null); + const [tooltip, setTooltip] = useState(null); + const { locale } = useLocale(); + const [theme] = useTheme(); + + const datasets = useMemo(() => { + const primaryColor = colord(THEME_COLORS[theme].primary); + return [ + { + label: formatMessage(labels.uniqueVisitors), + data: data, + borderWidth: 1, + hoverBackgroundColor: primaryColor.alpha(0.9).toRgbString(), + backgroundColor: primaryColor.alpha(0.6).toRgbString(), + borderColor: primaryColor.alpha(0.9).toRgbString(), + hoverBorderColor: primaryColor.toRgbString(), + }, + ]; + }, [data]); + + const colors = useMemo( + () => ({ + text: THEME_COLORS[theme].gray700, + line: THEME_COLORS[theme].gray200, + }), + [theme], + ); + + const renderYLabel = label => { + return +label > 1000 ? formatLongNumber(label) : label; + }; + + const renderTooltip = useCallback(model => { + const { opacity, labelColors, dataPoints } = model.tooltip; + + if (!dataPoints?.length || !opacity) { + setTooltip(null); + return; + } + + setTooltip( +
+
+ +
+
{dataPoints[0].raw.x}
+
{formatLongNumber(dataPoints[0].raw.y)}
+
+
+
+
, + ); + }, []); + + const getOptions = useCallback(() => { + return { + responsive: true, + maintainAspectRatio: false, + animation: { + duration: animationDuration, + resize: { + duration: 0, + }, + active: { + duration: 0, + }, + }, + plugins: { + legend: { + display: false, + }, + tooltip: { + enabled: false, + external: renderTooltip, + }, + }, + scales: { + x: { + grid: { + display: false, + }, + border: { + color: colors.line, + }, + ticks: { + color: colors.text, + autoSkip: false, + maxRotation: 0, + }, + }, + y: { + type: 'linear', + min: 0, + beginAtZero: true, + stacked, + grid: { + color: colors.line, + }, + border: { + color: colors.line, + }, + ticks: { + color: colors.text, + callback: renderYLabel, + }, + }, + }, + }; + }, [animationDuration, renderTooltip, stacked, colors, locale]); + + const createChart = () => { + Chart.defaults.font.family = 'Inter'; + + const options = getOptions(); + + chart.current = new Chart(canvas.current, { + type: 'bar', + data: { datasets }, + options, + }); + + onCreate(chart.current); + }; + + const updateChart = () => { + setTooltip(null); + + chart.current.data.datasets[0].data = datasets[0].data; + chart.current.data.datasets[0].label = datasets[0].label; + + chart.current.options = getOptions(); + + onUpdate(chart.current); + + chart.current.update(); + }; + + useEffect(() => { + if (datasets) { + if (!chart.current) { + createChart(); + } else { + updateChart(); + } + } + }, [datasets, theme, animationDuration, locale]); + + return ( + <> +
+ {loading && } + +
+ + {tooltip && } + + ); +} + +export default FunnelChart; diff --git a/components/pages/reports/funnel/FunnelChart.module.css b/components/pages/reports/funnel/FunnelChart.module.css new file mode 100644 index 00000000..f071a29e --- /dev/null +++ b/components/pages/reports/funnel/FunnelChart.module.css @@ -0,0 +1,23 @@ +.chart { + position: relative; + height: 400px; + overflow: hidden; +} + +.tooltip { + display: flex; + flex-direction: column; + gap: 10px; +} + +.tooltip .value { + display: flex; + flex-direction: column; + text-transform: lowercase; +} + +@media only screen and (max-width: 992px) { + .chart { + /*height: 200px;*/ + } +} diff --git a/components/pages/reports/FunnelForm.js b/components/pages/reports/funnel/FunnelForm.js similarity index 96% rename from components/pages/reports/FunnelForm.js rename to components/pages/reports/funnel/FunnelForm.js index 5f9758f0..30edcc56 100644 --- a/components/pages/reports/FunnelForm.js +++ b/components/pages/reports/funnel/FunnelForm.js @@ -37,7 +37,11 @@ export function FunnelForm({ onSearch }) { const handleAddUrl = () => setUrls([...urls, '']); - const handleRemoveUrl = i => setUrls(urls.splice(i, 1)); + const handleRemoveUrl = i => { + const nextUrls = [...urls]; + nextUrls.splice(i, 1); + setUrls(nextUrls); + }; const handleWindowChange = value => setWindow(value.target.value); @@ -103,7 +107,7 @@ export function FunnelForm({ onSearch }) { - Search + Query diff --git a/components/pages/reports/FunnelForm.module.css b/components/pages/reports/funnel/FunnelForm.module.css similarity index 83% rename from components/pages/reports/FunnelForm.module.css rename to components/pages/reports/funnel/FunnelForm.module.css index 2706a99a..2a07d552 100644 --- a/components/pages/reports/FunnelForm.module.css +++ b/components/pages/reports/funnel/FunnelForm.module.css @@ -18,11 +18,6 @@ max-height: 0px; } -.urlFormRow { - flex-direction: row; - gap: 0em; -} - .urlFormRow label { min-width: 80px; } diff --git a/components/pages/reports/FunnelPage.js b/components/pages/reports/funnel/FunnelPage.js similarity index 67% rename from components/pages/reports/FunnelPage.js rename to components/pages/reports/funnel/FunnelPage.js index b69319b1..3bfbb9e3 100644 --- a/components/pages/reports/FunnelPage.js +++ b/components/pages/reports/funnel/FunnelPage.js @@ -1,17 +1,19 @@ import { useMutation } from '@tanstack/react-query'; import Page from 'components/layout/Page'; import PageHeader from 'components/layout/PageHeader'; +import ReportsLayout from 'components/layout/ReportsLayout'; import useApi from 'hooks/useApi'; import { useState } from 'react'; import FunnelChart from './FunnelChart'; import FunnelTable from './FunnelTable'; import FunnelForm from './FunnelForm'; + import styles from './FunnelPage.module.css'; export default function FunnelPage() { const { post } = useApi(); const { mutate, error, isLoading } = useMutation(data => post('/reports/funnel', data)); - const [data, setData] = useState(); + const [data, setData] = useState([{}]); const [formData, setFormData] = useState(); function handleOnSearch(data) { @@ -25,14 +27,12 @@ export default function FunnelPage() { } return ( - - - - -
-

Filters

- -
-
+ } header={'test'}> + + + + + + ); } diff --git a/components/pages/reports/FunnelPage.module.css b/components/pages/reports/funnel/FunnelPage.module.css similarity index 100% rename from components/pages/reports/FunnelPage.module.css rename to components/pages/reports/funnel/FunnelPage.module.css diff --git a/components/pages/reports/FunnelTable.js b/components/pages/reports/funnel/FunnelTable.js similarity index 73% rename from components/pages/reports/FunnelTable.js rename to components/pages/reports/funnel/FunnelTable.js index 036e20c3..2bbabc81 100644 --- a/components/pages/reports/FunnelTable.js +++ b/components/pages/reports/funnel/FunnelTable.js @@ -1,13 +1,12 @@ import DataTable from 'components/metrics/DataTable'; import useMessages from 'hooks/useMessages'; -import { useState } from 'react'; export function DevicesTable({ ...props }) { const { formatMessage, labels } = useMessages(); const { data } = props; const tableData = - data?.map(a => ({ x: a.url, y: a.count, z: (a.count / data[0].count) * 100 })) || []; + data?.map(a => ({ x: a.x, y: a.y, z: Math.floor(a.y / data[0].y) * 100 })) || []; return ; } diff --git a/package.json b/package.json index 7f365db1..0db07f49 100644 --- a/package.json +++ b/package.json @@ -80,7 +80,6 @@ "dotenv": "^10.0.0", "formik": "^2.2.9", "fs-extra": "^10.0.1", - "funnel-graph-js": "^1.3.7", "immer": "^9.0.12", "ipaddr.js": "^2.0.1", "is-ci": "^3.0.1", diff --git a/pages/_app.js b/pages/_app.js index bc55355b..22458215 100644 --- a/pages/_app.js +++ b/pages/_app.js @@ -9,7 +9,6 @@ import useConfig from 'hooks/useConfig'; import '@fontsource/inter/400.css'; import '@fontsource/inter/700.css'; import 'react-basics/dist/styles.css'; -import 'styles/funnelChart.css'; import 'styles/variables.css'; import 'styles/locale.css'; import 'styles/index.css'; diff --git a/pages/settings/reports/funnel.js b/pages/reports/funnel.js similarity index 54% rename from pages/settings/reports/funnel.js rename to pages/reports/funnel.js index d8d7a5b8..3ba11306 100644 --- a/pages/settings/reports/funnel.js +++ b/pages/reports/funnel.js @@ -1,16 +1,13 @@ import AppLayout from 'components/layout/AppLayout'; -import SettingsLayout from 'components/layout/SettingsLayout'; -import FunnelPage from 'components/pages/reports/FunnelPage'; +import FunnelPage from 'components/pages/reports/funnel/FunnelPage'; import useMessages from 'hooks/useMessages'; -export default function DetailsPage() { +export default function Funnel() { const { formatMessage, labels } = useMessages(); return ( - - - + ); } diff --git a/pages/settings/reports/index.js b/pages/reports/index.js similarity index 90% rename from pages/settings/reports/index.js rename to pages/reports/index.js index ce0a3726..cdd6ae38 100644 --- a/pages/settings/reports/index.js +++ b/pages/reports/index.js @@ -1,6 +1,6 @@ import { useRouter } from 'next/router'; import AppLayout from 'components/layout/AppLayout'; -import FunnelPage from 'components/pages/reports/FunnelPage'; +import FunnelPage from 'components/pages/reports/funnel/FunnelPage'; import useMessages from 'hooks/useMessages'; import SettingsLayout from 'components/layout/SettingsLayout'; diff --git a/queries/analytics/pageview/getPageviewFunnel.ts b/queries/analytics/pageview/getPageviewFunnel.ts index 591310cf..ef62e526 100644 --- a/queries/analytics/pageview/getPageviewFunnel.ts +++ b/queries/analytics/pageview/getPageviewFunnel.ts @@ -29,9 +29,8 @@ async function relationalQuery( }, ): Promise< { - level: number; - url: string; - count: any; + x: string; + y: number; }[] > { const { windowMinutes, startDate, endDate, urls } = criteria; @@ -58,7 +57,7 @@ async function relationalQuery( `, params, ).then((a: { [key: string]: number }) => { - return urls.map((b, i) => ({ level: i + 1, url: b, count: a[`level${i + 1}`] || 0 })); + return urls.map((b, i) => ({ x: b, y: a[`level${i + 1}`] || 0 })); }); } @@ -72,9 +71,8 @@ async function clickhouseQuery( }, ): Promise< { - level: number; - url: string; - count: number; + x: string; + y: number; }[] > { const { windowMinutes, startDate, endDate, urls } = criteria; @@ -110,9 +108,8 @@ async function clickhouseQuery( params, ).then(results => { return urls.map((a, i) => ({ - level: i + 1, - url: a, - count: results[i + 1]?.count || 0, + x: a, + y: results[i + 1]?.count || 0, })); }); } diff --git a/styles/funnelChart.css b/styles/funnelChart.css deleted file mode 100644 index c72d42e7..00000000 --- a/styles/funnelChart.css +++ /dev/null @@ -1,148 +0,0 @@ -.svg-funnel-js { - display: inline-block; - position: relative; -} -.svg-funnel-js svg { - display: block; -} -.svg-funnel-js .svg-funnel-js__labels { - position: absolute; - display: flex; - width: 100%; - height: 100%; - top: 0; - left: 0; -} -.svg-funnel-js.svg-funnel-js--vertical .svg-funnel-js__labels { - flex-direction: column; -} - -.svg-funnel-js body { - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; -} - -.svg-funnel-js { - font-family: 'Open Sans', sans-serif; -} -.svg-funnel-js .svg-funnel-js__container { - width: 100%; - height: 100%; -} -.svg-funnel-js .svg-funnel-js__labels { - width: 100%; - box-sizing: border-box; -} -.svg-funnel-js .svg-funnel-js__labels .svg-funnel-js__label { - flex: 1 1 0; - position: relative; -} -.svg-funnel-js .svg-funnel-js__labels .svg-funnel-js__label .label__value { - font-size: 24px; - color: #fff; - line-height: 18px; - margin-bottom: 6px; -} -.svg-funnel-js .svg-funnel-js__labels .svg-funnel-js__label .label__title { - font-size: 12px; - font-weight: bold; - color: #21ffa2; -} -.svg-funnel-js .svg-funnel-js__labels .svg-funnel-js__label .label__percentage { - font-size: 16px; - font-weight: bold; - color: #9896dc; -} -.svg-funnel-js .svg-funnel-js__labels .svg-funnel-js__label .label__segment-percentages { - position: absolute; - top: 50%; - transform: translateY(-50%); - width: 100%; - left: 0; - padding: 8px 24px; - box-sizing: border-box; - background-color: rgba(8, 7, 48, 0.8); - margin-top: 24px; - opacity: 0; - transition: opacity 0.1s ease; - cursor: default; -} -.svg-funnel-js .svg-funnel-js__labels .svg-funnel-js__label .label__segment-percentages ul { - margin: 0; - padding: 0; - list-style-type: none; -} -.svg-funnel-js .svg-funnel-js__labels .svg-funnel-js__label .label__segment-percentages ul li { - font-size: 13px; - line-height: 16px; - color: #fff; - margin: 18px 0; -} -.svg-funnel-js - .svg-funnel-js__labels - .svg-funnel-js__label - .label__segment-percentages - ul - li - .percentage__list-label { - font-weight: bold; - color: #05df9d; -} -.svg-funnel-js .svg-funnel-js__labels .svg-funnel-js__label:hover .label__segment-percentages { - opacity: 1; -} -.svg-funnel-js:not(.svg-funnel-js--vertical) { - padding-top: 64px; - padding-bottom: 16px; -} -.svg-funnel-js:not(.svg-funnel-js--vertical) .svg-funnel-js__label { - padding-left: 24px; -} -.svg-funnel-js:not(.svg-funnel-js--vertical) .svg-funnel-js__label:not(:first-child) { - border-left: 1px solid #9896dc; -} -.svg-funnel-js.svg-funnel-js--vertical { - padding-left: 120px; - padding-right: 16px; -} -.svg-funnel-js.svg-funnel-js--vertical .svg-funnel-js__label { - padding-top: 24px; -} -.svg-funnel-js.svg-funnel-js--vertical .svg-funnel-js__label:not(:first-child) { - border-top: 1px solid #9896dc; -} -.svg-funnel-js.svg-funnel-js--vertical .svg-funnel-js__label .label__segment-percentages { - margin-top: 0; - margin-left: 106px; - width: calc(100% - 106px); -} -.svg-funnel-js.svg-funnel-js--vertical - .svg-funnel-js__label - .label__segment-percentages - .segment-percentage__list { - display: flex; - justify-content: space-around; -} -.svg-funnel-js .svg-funnel-js__subLabels { - display: flex; - justify-content: center; - margin-top: 24px; - position: absolute; - width: 100%; - left: 0; -} -.svg-funnel-js .svg-funnel-js__subLabels .svg-funnel-js__subLabel { - display: flex; - font-size: 12px; - color: #fff; - line-height: 16px; -} -.svg-funnel-js .svg-funnel-js__subLabels .svg-funnel-js__subLabel:not(:first-child) { - margin-left: 16px; -} -.svg-funnel-js .svg-funnel-js__subLabels .svg-funnel-js__subLabel .svg-funnel-js__subLabel--color { - width: 12px; - height: 12px; - border-radius: 50%; - margin: 2px 8px 2px 0; -}