From 5feb08571d36bdfba8985af918d3f5fbfdbd7c7a Mon Sep 17 00:00:00 2001 From: Eric Duong Date: Thu, 25 Feb 2021 11:42:40 -0500 Subject: [PATCH] Revert "Revert "935 trend legend (#3434)" (#3487)" This reverts commit 5784f90df604e2c45b446845bbba50eb3f793196. --- frontend/src/lib/components/PHCheckbox.tsx | 42 ++++ frontend/src/lib/constants.ts | 52 ----- frontend/src/lib/constants.tsx | 180 ++++++++++++++++++ frontend/src/lib/utils.test.js | 4 +- frontend/src/lib/utils.tsx | 2 - .../insights/ActionFilter/ActionFilterRow.js | 127 +----------- frontend/src/scenes/insights/Insights.js | 18 +- frontend/src/scenes/insights/LineGraph.js | 75 ++++---- frontend/src/scenes/insights/TrendLegend.tsx | 88 +++++++++ frontend/src/scenes/trends/trendsLogic.ts | 44 ++++- .../src/scenes/trends/viz/ActionsLineGraph.js | 11 +- 11 files changed, 419 insertions(+), 224 deletions(-) create mode 100644 frontend/src/lib/components/PHCheckbox.tsx delete mode 100644 frontend/src/lib/constants.ts create mode 100644 frontend/src/lib/constants.tsx create mode 100644 frontend/src/scenes/insights/TrendLegend.tsx diff --git a/frontend/src/lib/components/PHCheckbox.tsx b/frontend/src/lib/components/PHCheckbox.tsx new file mode 100644 index 0000000000000..bb1fe77d846bc --- /dev/null +++ b/frontend/src/lib/components/PHCheckbox.tsx @@ -0,0 +1,42 @@ +import React from 'react' + +interface Props { + checked: boolean + onChange: () => void + color: string + disabled?: boolean +} + +export const PHCheckbox = ({ checked, color = 'blue', disabled = false, ...props }: Props): JSX.Element => ( +
+
{}} + > + + + +
+
+) diff --git a/frontend/src/lib/constants.ts b/frontend/src/lib/constants.ts deleted file mode 100644 index 0d6f578dbc975..0000000000000 --- a/frontend/src/lib/constants.ts +++ /dev/null @@ -1,52 +0,0 @@ -export const ACTIONS_LINE_GRAPH_LINEAR = 'ActionsLineGraph' -export const ACTIONS_LINE_GRAPH_CUMULATIVE = 'ActionsLineGraphCumulative' -export const ACTIONS_TABLE = 'ActionsTable' -export const ACTIONS_PIE_CHART = 'ActionsPie' -export const ACTIONS_BAR_CHART = 'ActionsBar' -export const ACTIONS_BAR_CHART_VALUE = 'ActionsBarValue' -export const PATHS_VIZ = 'PathsViz' -export const FUNNEL_VIZ = 'FunnelViz' - -export const VOLUME = 'Volume' -export const STICKINESS = 'Stickiness' -export const LIFECYCLE = 'Lifecycle' - -export enum OrganizationMembershipLevel { - Member = 1, - Admin = 8, - Owner = 15, -} - -export const organizationMembershipLevelToName = new Map([ - [OrganizationMembershipLevel.Member, 'member'], - [OrganizationMembershipLevel.Admin, 'administrator'], - [OrganizationMembershipLevel.Owner, 'owner'], -]) - -export enum AnnotationScope { - DashboardItem = 'dashboard_item', - Project = 'project', - Organization = 'organization', -} - -export const annotationScopeToName = new Map([ - [AnnotationScope.DashboardItem, 'dashboard item'], - [AnnotationScope.Project, 'project'], - [AnnotationScope.Organization, 'organization'], -]) - -export const PERSON_DISTINCT_ID_MAX_SIZE = 3 - -export const PAGEVIEW = '$pageview' -export const AUTOCAPTURE = '$autocapture' -export const SCREEN = '$screen' -export const CUSTOM_EVENT = 'custom_event' - -export const ACTION_TYPE = 'action_type' -export const EVENT_TYPE = 'event_type' - -export enum ShownAsValue { - VOLUME = 'Volume', - STICKINESS = 'Stickiness', - LIFECYCLE = 'Lifecycle', -} diff --git a/frontend/src/lib/constants.tsx b/frontend/src/lib/constants.tsx new file mode 100644 index 0000000000000..f483dea6d9a0e --- /dev/null +++ b/frontend/src/lib/constants.tsx @@ -0,0 +1,180 @@ +import React from 'react' + +export const ACTIONS_LINE_GRAPH_LINEAR = 'ActionsLineGraph' +export const ACTIONS_LINE_GRAPH_CUMULATIVE = 'ActionsLineGraphCumulative' +export const ACTIONS_TABLE = 'ActionsTable' +export const ACTIONS_PIE_CHART = 'ActionsPie' +export const ACTIONS_BAR_CHART = 'ActionsBar' +export const ACTIONS_BAR_CHART_VALUE = 'ActionsBarValue' +export const PATHS_VIZ = 'PathsViz' +export const FUNNEL_VIZ = 'FunnelViz' + +export const VOLUME = 'Volume' +export const STICKINESS = 'Stickiness' +export const LIFECYCLE = 'Lifecycle' + +export enum OrganizationMembershipLevel { + Member = 1, + Admin = 8, + Owner = 15, +} + +export const organizationMembershipLevelToName = new Map([ + [OrganizationMembershipLevel.Member, 'member'], + [OrganizationMembershipLevel.Admin, 'administrator'], + [OrganizationMembershipLevel.Owner, 'owner'], +]) + +export enum AnnotationScope { + DashboardItem = 'dashboard_item', + Project = 'project', + Organization = 'organization', +} + +export const annotationScopeToName = new Map([ + [AnnotationScope.DashboardItem, 'dashboard item'], + [AnnotationScope.Project, 'project'], + [AnnotationScope.Organization, 'organization'], +]) + +export const PERSON_DISTINCT_ID_MAX_SIZE = 3 + +export const PAGEVIEW = '$pageview' +export const AUTOCAPTURE = '$autocapture' +export const SCREEN = '$screen' +export const CUSTOM_EVENT = 'custom_event' + +export const ACTION_TYPE = 'action_type' +export const EVENT_TYPE = 'event_type' + +export enum ShownAsValue { + VOLUME = 'Volume', + STICKINESS = 'Stickiness', + LIFECYCLE = 'Lifecycle', +} + +export const PROPERTY_MATH_TYPE = 'property' +export const EVENT_MATH_TYPE = 'event' + +export const MATHS: Record = { + total: { + name: 'Total volume', + description: ( + <> + Total event volume. +
+ If a user performs an event 3 times in a given day/week/month, it counts as 3. + + ), + onProperty: false, + type: EVENT_MATH_TYPE, + }, + dau: { + name: 'Active users', + description: ( + <> + Users active in the time interval. +
+ If a user performs an event 3 times in a given day/week/month, it counts only as 1. + + ), + onProperty: false, + type: EVENT_MATH_TYPE, + }, + sum: { + name: 'Sum', + description: ( + <> + Event property sum. +
+ For example 3 events captured with property amount equal to 10, 12 and 20, result in 42. + + ), + onProperty: true, + type: PROPERTY_MATH_TYPE, + }, + avg: { + name: 'Average', + description: ( + <> + Event property average. +
+ For example 3 events captured with property amount equal to 10, 12 and 20, result in 14. + + ), + onProperty: true, + type: PROPERTY_MATH_TYPE, + }, + min: { + name: 'Minimum', + description: ( + <> + Event property minimum. +
+ For example 3 events captured with property amount equal to 10, 12 and 20, result in 10. + + ), + onProperty: true, + type: PROPERTY_MATH_TYPE, + }, + max: { + name: 'Maximum', + description: ( + <> + Event property maximum. +
+ For example 3 events captured with property amount equal to 10, 12 and 20, result in 20. + + ), + onProperty: true, + type: PROPERTY_MATH_TYPE, + }, + median: { + name: 'Median', + description: ( + <> + Event property median (50th percentile). +
+ For example 100 events captured with property amount equal to 101..200, result in 150. + + ), + onProperty: true, + type: PROPERTY_MATH_TYPE, + }, + p90: { + name: '90th percentile', + description: ( + <> + Event property 90th percentile. +
+ For example 100 events captured with property amount equal to 101..200, result in 190. + + ), + onProperty: true, + type: 'property', + }, + p95: { + name: '95th percentile', + description: ( + <> + Event property 95th percentile. +
+ For example 100 events captured with property amount equal to 101..200, result in 195. + + ), + onProperty: true, + type: PROPERTY_MATH_TYPE, + }, + p99: { + name: '99th percentile', + description: ( + <> + Event property 90th percentile. +
+ For example 100 events captured with property amount equal to 101..200, result in 199. + + ), + onProperty: true, + type: PROPERTY_MATH_TYPE, + }, +} diff --git a/frontend/src/lib/utils.test.js b/frontend/src/lib/utils.test.js index 4fa3ef1758c83..da4ef11ceced4 100644 --- a/frontend/src/lib/utils.test.js +++ b/frontend/src/lib/utils.test.js @@ -37,7 +37,7 @@ describe('formatLabel()', () => { given('action', () => ({})) it('formats the label', () => { - expect(given.subject).toEqual('some_event (Total) ') + expect(given.subject).toEqual('some_event') }) describe('DAU queries', () => { @@ -60,7 +60,7 @@ describe('formatLabel()', () => { given('action', () => ({ properties: [{ value: 'hello' }, { operator: 'gt', value: 5 }] })) it('is formatted', () => { - expect(given.subject).toEqual('some_event (Total) (= hello, > 5)') + expect(given.subject).toEqual('some_event (= hello, > 5)') }) }) }) diff --git a/frontend/src/lib/utils.tsx b/frontend/src/lib/utils.tsx index b801559b06c4e..b1b4966bf89e6 100644 --- a/frontend/src/lib/utils.tsx +++ b/frontend/src/lib/utils.tsx @@ -241,8 +241,6 @@ export function formatLabel( label += ` (Active Users) ` } else if (['sum', 'avg', 'min', 'max', 'median', 'p90', 'p95', 'p99'].includes(action.math)) { label += ` (${action.math} of ${action.math_property}) ` - } else { - label += ' (Total) ' } if (action?.properties?.length) { label += ` (${action.properties diff --git a/frontend/src/scenes/insights/ActionFilter/ActionFilterRow.js b/frontend/src/scenes/insights/ActionFilter/ActionFilterRow.js index ab85b63611bbb..449436d667be9 100644 --- a/frontend/src/scenes/insights/ActionFilter/ActionFilterRow.js +++ b/frontend/src/scenes/insights/ActionFilter/ActionFilterRow.js @@ -4,137 +4,12 @@ import { Button, Tooltip, Col, Row, Select } from 'antd' import { EntityTypes } from '../../trends/trendsLogic' import { ActionFilterDropdown } from './ActionFilterDropdown' import { PropertyFilters } from 'lib/components/PropertyFilters/PropertyFilters' +import { PROPERTY_MATH_TYPE, EVENT_MATH_TYPE, MATHS } from 'lib/constants' import { userLogic } from 'scenes/userLogic' import { DownOutlined, DeleteOutlined } from '@ant-design/icons' import { SelectGradientOverflow } from 'lib/components/SelectGradientOverflow' import './ActionFilterRow.scss' -const PROPERTY_MATH_TYPE = 'property' -const EVENT_MATH_TYPE = 'event' - -const MATHS = { - total: { - name: 'Total volume', - description: ( - <> - Total event volume. -
- If a user performs an event 3 times in a given day/week/month, it counts as 3. - - ), - onProperty: false, - type: EVENT_MATH_TYPE, - }, - dau: { - name: 'Active users', - description: ( - <> - Users active in the time interval. -
- If a user performs an event 3 times in a given day/week/month, it counts only as 1. - - ), - onProperty: false, - type: EVENT_MATH_TYPE, - }, - sum: { - name: 'Sum', - description: ( - <> - Event property sum. -
- For example 3 events captured with property amount equal to 10, 12 and 20, result in 42. - - ), - onProperty: true, - type: PROPERTY_MATH_TYPE, - }, - avg: { - name: 'Average', - description: ( - <> - Event property average. -
- For example 3 events captured with property amount equal to 10, 12 and 20, result in 14. - - ), - onProperty: true, - type: PROPERTY_MATH_TYPE, - }, - min: { - name: 'Minimum', - description: ( - <> - Event property minimum. -
- For example 3 events captured with property amount equal to 10, 12 and 20, result in 10. - - ), - onProperty: true, - type: PROPERTY_MATH_TYPE, - }, - max: { - name: 'Maximum', - description: ( - <> - Event property maximum. -
- For example 3 events captured with property amount equal to 10, 12 and 20, result in 20. - - ), - onProperty: true, - type: PROPERTY_MATH_TYPE, - }, - median: { - name: 'Median', - description: ( - <> - Event property median (50th percentile). -
- For example 100 events captured with property amount equal to 101..200, result in 150. - - ), - onProperty: true, - type: PROPERTY_MATH_TYPE, - }, - p90: { - name: '90th percentile', - description: ( - <> - Event property 90th percentile. -
- For example 100 events captured with property amount equal to 101..200, result in 190. - - ), - onProperty: true, - type: 'property', - }, - p95: { - name: '95th percentile', - description: ( - <> - Event property 95th percentile. -
- For example 100 events captured with property amount equal to 101..200, result in 195. - - ), - onProperty: true, - type: PROPERTY_MATH_TYPE, - }, - p99: { - name: '99th percentile', - description: ( - <> - Event property 90th percentile. -
- For example 100 events captured with property amount equal to 101..200, result in 199. - - ), - onProperty: true, - type: PROPERTY_MATH_TYPE, - }, -} - const EVENT_MATH_ENTRIES = Object.entries(MATHS).filter(([, item]) => item.type == EVENT_MATH_TYPE) const PROPERTY_MATH_ENTRIES = Object.entries(MATHS).filter(([, item]) => item.type == PROPERTY_MATH_TYPE) diff --git a/frontend/src/scenes/insights/Insights.js b/frontend/src/scenes/insights/Insights.js index 2aae1141109a4..dff69ffaf4514 100644 --- a/frontend/src/scenes/insights/Insights.js +++ b/frontend/src/scenes/insights/Insights.js @@ -1,5 +1,5 @@ import React, { useState } from 'react' -import { useActions, useMountedLogic, useValues } from 'kea' +import { useActions, useMountedLogic, useValues, BindLogic } from 'kea' import { Loading } from 'lib/utils' import { SaveToDashboard } from 'lib/components/SaveToDashboard/SaveToDashboard' @@ -13,6 +13,7 @@ import { ChartFilter } from 'lib/components/ChartFilter' import { Tabs, Row, Col, Card, Button } from 'antd' import { ACTIONS_LINE_GRAPH_LINEAR, + ACTIONS_LINE_GRAPH_CUMULATIVE, ACTIONS_TABLE, ACTIONS_PIE_CHART, ACTIONS_BAR_CHART_VALUE, @@ -41,7 +42,9 @@ import './Insights.scss' import { ErrorMessage, TimeOut } from './EmptyStates' import { featureFlagLogic } from 'lib/logic/featureFlagLogic' import { People } from 'scenes/funnels/People' +import { TrendLegend } from './TrendLegend' import { TrendInsight } from 'scenes/trends/Trends' +import { trendsLogic } from 'scenes/trends/trendsLogic' const { TabPane } = Tabs @@ -306,6 +309,19 @@ function _Insights() { )} + {(!allFilters.display || + allFilters.display === ACTIONS_LINE_GRAPH_LINEAR || + allFilters.display === ACTIONS_LINE_GRAPH_CUMULATIVE) && + (activeView === ViewType.TRENDS || activeView === ViewType.SESSIONS) && ( + + + + + + )} )} diff --git a/frontend/src/scenes/insights/LineGraph.js b/frontend/src/scenes/insights/LineGraph.js index 533d889fdfa68..9abc812a10c78 100644 --- a/frontend/src/scenes/insights/LineGraph.js +++ b/frontend/src/scenes/insights/LineGraph.js @@ -22,6 +22,7 @@ const noop = () => {} export function LineGraph({ datasets, + visibilityMap = null, labels, color, type, @@ -62,7 +63,7 @@ export function LineGraph({ useEffect(() => { buildChart() - }, [datasets, color]) + }, [datasets, color, visibilityMap]) // annotation related effects @@ -136,42 +137,46 @@ export function LineGraph({ myLineChart.current.destroy() } // if chart is line graph, make duplicate lines and overlay to show dotted lines - datasets = - type === 'line' - ? [ - ...datasets.map((dataset, index) => { - let datasetCopy = Object.assign({}, dataset) - let data = [...dataset.data] - let _labels = [...dataset.labels] - let days = [...dataset.days] - data.pop() - _labels.pop() - days.pop() - datasetCopy.data = data - datasetCopy.labels = _labels - datasetCopy.days = days - return processDataset(datasetCopy, index) - }), - ...datasets.map((dataset, index) => { - let datasetCopy = Object.assign({}, dataset) - let datasetLength = datasetCopy.data.length - datasetCopy.dotted = true + const isLineGraph = type === 'line' + datasets = isLineGraph + ? [ + ...datasets.map((dataset, index) => { + let datasetCopy = Object.assign({}, dataset) + let data = [...dataset.data] + let _labels = [...dataset.labels] + let days = [...dataset.days] + data.pop() + _labels.pop() + days.pop() + datasetCopy.data = data + datasetCopy.labels = _labels + datasetCopy.days = days + return processDataset(datasetCopy, index) + }), + ...datasets.map((dataset, index) => { + let datasetCopy = Object.assign({}, dataset) + let datasetLength = datasetCopy.data.length + datasetCopy.dotted = true - // if last date is still active show dotted line - if (isInProgress) { - datasetCopy.borderDash = [10, 10] - } + // if last date is still active show dotted line + if (isInProgress) { + datasetCopy.borderDash = [10, 10] + } - datasetCopy.data = - datasetCopy.data.length > 2 - ? datasetCopy.data.map((datum, idx) => - idx === datasetLength - 1 || idx === datasetLength - 2 ? datum : null - ) - : datasetCopy.data - return processDataset(datasetCopy, index) - }), - ] - : datasets.map((dataset, index) => processDataset(dataset, index)) + datasetCopy.data = + datasetCopy.data.length > 2 + ? datasetCopy.data.map((datum, idx) => + idx === datasetLength - 1 || idx === datasetLength - 2 ? datum : null + ) + : datasetCopy.data + return processDataset(datasetCopy, index) + }), + ] + : datasets.map((dataset, index) => processDataset(dataset, index)) + + if (isLineGraph) { + datasets = datasets.filter((data) => visibilityMap[data.id]) + } let options = { responsive: true, diff --git a/frontend/src/scenes/insights/TrendLegend.tsx b/frontend/src/scenes/insights/TrendLegend.tsx new file mode 100644 index 0000000000000..02ed4b009ce92 --- /dev/null +++ b/frontend/src/scenes/insights/TrendLegend.tsx @@ -0,0 +1,88 @@ +import React from 'react' +import { Table } from 'antd' +import { useActions, useValues } from 'kea' +import { IndexedTrendResult, trendsLogic } from 'scenes/trends/trendsLogic' +import { PHCheckbox } from 'lib/components/PHCheckbox' +import { getChartColors } from 'lib/colors' +import { MATHS } from 'lib/constants' + +function formatLabel(item: IndexedTrendResult): string { + const name = item.action?.name || item.label + const math = item.action?.math + const mathLabel = math ? MATHS[math].name : '' + const propNum = item.action?.properties.length + const propLabel = propNum ? propNum + (propNum === 1 ? ' property' : ' properties') : '' + return name + (mathLabel ? ' — ' + mathLabel : '') + (propLabel ? ' — ' + propLabel : '') +} + +export function TrendLegend(): JSX.Element { + const { indexedResults, visibilityMap, filters } = useValues(trendsLogic) + const { toggleVisibility } = useActions(trendsLogic) + const isSingleEntity = indexedResults.length === 1 + + const columns = [ + { + title: '', + render: function RenderCheckbox({}, item: IndexedTrendResult, index: number) { + // legend will always be on insight page where the background is white + return ( + toggleVisibility(item.id)} + disabled={isSingleEntity} + /> + ) + }, + fixed: 'left', + width: 60, + }, + { + title: 'Label', + render: function RenderLabel({}, item: IndexedTrendResult) { + return ( + !isSingleEntity && toggleVisibility(item.id)} + > + {formatLabel(item)} + + ) + }, + fixed: 'left', + width: 150, + }, + ...(filters.breakdown + ? [ + { + title: 'Breakdown Value', + render: function RenderBreakdownValue({}, item: IndexedTrendResult) { + return item.breakdown_value === 'nan' ? 'Other' : item.breakdown_value + }, + fixed: 'left', + width: 150, + }, + ] + : []), + ...(indexedResults && indexedResults.length > 0 + ? indexedResults[0].data.map(({}, index: number) => ({ + title: indexedResults[0].labels[index], + render: function RenderPeriod({}, item: IndexedTrendResult) { + return item.data[index] + }, + })) + : []), + ] + + return ( + 0 ? { x: indexedResults[0].data.length * 160 } : {}} + /> + ) +} diff --git a/frontend/src/scenes/trends/trendsLogic.ts b/frontend/src/scenes/trends/trendsLogic.ts index 631480770465c..bd6d041cdeffc 100644 --- a/frontend/src/scenes/trends/trendsLogic.ts +++ b/frontend/src/scenes/trends/trendsLogic.ts @@ -24,7 +24,7 @@ import { trendsLogicType } from './trendsLogicType' import { toast, ToastId } from 'react-toastify' import { dashboardItemsModel } from '~/models/dashboardItemsModel' -interface ActionFilter { +export interface ActionFilter { id: number | string math?: string math_property?: string @@ -34,6 +34,20 @@ interface ActionFilter { type: EntityType } +export interface TrendResult { + action: ActionFilter + count: number + data: number[] + days: string[] + label: string + labels: string[] + breakdown_value?: string +} + +export interface IndexedTrendResult extends TrendResult { + id: number +} + interface TrendPeople { people: PersonType[] breakdown_value?: string @@ -189,6 +203,9 @@ export const trendsLogic = kea ({ results }), + toggleVisibility: (index: number) => ({ index }), + setVisibilityById: (entry: Record) => ({ entry }), }), reducers: ({ props }) => ({ @@ -227,6 +244,25 @@ export const trendsLogic = kea isShowing, }, ], + indexedResults: [ + [], + { + setIndexedResults: ({}, { results }) => results, + }, + ], + visibilityMap: [ + {} as Record, + { + setVisibilityById: (state: Record, { entry }: { entry: Record }) => ({ + ...state, + ...entry, + }), + toggleVisibility: (state: Record, { index }: { index: number }) => ({ + ...state, + [`${index}`]: !state[index], + }), + }, + ], }), selectors: ({ selectors }) => ({ @@ -370,6 +406,12 @@ export const trendsLogic = kea { + actions.setVisibilityById({ [`${index}`]: true }) + return { ...element, id: index } + }) + actions.setIndexedResults(indexedResults) }, [dashboardItemsModel.actionTypes.refreshAllDashboardItems]: (filters: Record) => { if (props.dashboardItemId) { diff --git a/frontend/src/scenes/trends/viz/ActionsLineGraph.js b/frontend/src/scenes/trends/viz/ActionsLineGraph.js index abc5c2c2a943f..0f856260616df 100644 --- a/frontend/src/scenes/trends/viz/ActionsLineGraph.js +++ b/frontend/src/scenes/trends/viz/ActionsLineGraph.js @@ -21,21 +21,22 @@ export function ActionsLineGraph({ filters: filtersParam, cachedResults, }) - const { filters, results, resultsLoading } = useValues(logic) + const { filters, indexedResults, resultsLoading, visibilityMap } = useValues(logic) const { loadPeople } = useActions(logic) const { people_action, people_day, ...otherFilters } = filters // eslint-disable-line const [{ fromItem }] = useState(router.values.hashParams) - return results && !resultsLoading ? ( - results.reduce((total, item) => total + item.count, 0) !== 0 ? ( + return indexedResults && !resultsLoading ? ( + indexedResults.reduce((total, item) => total + item.count, 0) !== 0 ? (