@@ -5,6 +5,7 @@ import React, {
55 useRef ,
66 useCallback
77} from 'react'
8+ import LazyLoader from '../components/lazy-loader'
89import * as api from '../api'
910import * as url from '../util/url'
1011import { Tooltip } from '../util/tooltip'
@@ -544,6 +545,8 @@ function columnHeader(index, direction) {
544545export function FunnelExploration ( ) {
545546 const site = useSiteContext ( )
546547 const { dashboardState } = useDashboardStateContext ( )
548+ const [ inViewport , setInViewport ] = useState ( false )
549+
547550 const [ steps , setSteps ] = useState ( [ ] )
548551 const [ direction , setDirection ] = useState ( EXPLORATION_DIRECTIONS . FORWARD )
549552 const [ funnel , setFunnel ] = useState ( [ ] )
@@ -681,6 +684,8 @@ export function FunnelExploration() {
681684 // On subsequent renders (via user interaction) fetch next steps and,
682685 // if the journey changed, also refetch the funnel.
683686 useEffect ( ( ) => {
687+ if ( ! inViewport ) return
688+
684689 const journeyChanged =
685690 prevStepsRef . current !== steps ||
686691 prevDirectionRef . current !== direction ||
@@ -781,9 +786,12 @@ export function FunnelExploration() {
781786 return ( ) => {
782787 cancelled = true
783788 }
784- } , [ site , dashboardState , steps , direction , activeColumnFilter ] )
789+ } , [ site , dashboardState , steps , direction , activeColumnFilter , inViewport ] )
785790
786- const numColumns = Math . max ( steps . length + 1 , 3 )
791+ const initialLoading =
792+ ! inViewport || ( steps . length === 0 && activeColumnLoading )
793+ const numColumns = Math . max ( steps . length + 1 , initialLoading ? 1 : 3 )
794+ const gridColumns = Math . max ( numColumns , 3 )
787795 const activeColumnIndex = steps . length
788796 const containerRef = useRef ( null )
789797
@@ -818,109 +826,115 @@ export function FunnelExploration() {
818826 } , [ steps . length ] )
819827
820828 return (
821- < div className = "flex flex-col gap-4 pt-4" >
822- < div className = "flex flex-wrap items-center gap-x-3" >
823- < h4 className = "flex-1 text-base font-semibold dark:text-gray-100" >
824- { funnel . length >= 2
825- ? `${ funnel . length } -step user journey`
826- : 'Explore user journeys' }
827- </ h4 >
828- { overallConversionRate != null && (
829- < div className = "order-last sm:order-none w-full sm:w-auto flex items-center gap-2 text-xs text-gray-500 dark:text-gray-400" >
830- < span >
831- < span className = "font-medium sm:font-semibold text-gray-700 dark:text-gray-200" >
832- Conversion: { parseFloat ( overallConversionRate ) . toFixed ( 1 ) } %{ ' ' }
829+ < LazyLoader onVisible = { ( ) => setInViewport ( true ) } >
830+ < div className = "flex flex-col gap-4 pt-4" >
831+ < div className = "flex flex-wrap items-center gap-x-3" >
832+ < h4 className = "flex-1 text-base font-semibold dark:text-gray-100" >
833+ { funnel . length >= 2
834+ ? `${ funnel . length } -step user journey`
835+ : 'Explore user journeys' }
836+ </ h4 >
837+ { overallConversionRate != null && (
838+ < div className = "order-last sm:order-none w-full sm:w-auto flex items-center gap-2 text-xs text-gray-500 dark:text-gray-400" >
839+ < span >
840+ < span className = "font-medium sm:font-semibold text-gray-700 dark:text-gray-200" >
841+ Conversion: { parseFloat ( overallConversionRate ) . toFixed ( 1 ) } %{ ' ' }
842+ </ span >
843+ < span className = "text-gray-500 dark:text-gray-400" >
844+ ({ numberShortFormatter ( overallConversionVisitors ) } )
845+ </ span >
833846 </ span >
834- < span className = "text-gray-500 dark:text-gray-400 " >
835- ( { numberShortFormatter ( overallConversionVisitors ) } )
847+ < span className = "hidden sm:inline text-gray-300 dark:text-gray-600 select-none " >
848+ |
836849 </ span >
837- </ span >
838- < span className = "hidden sm:inline text-gray-300 dark:text-gray-600 select-none" >
839- |
840- </ span >
841- </ div >
842- ) }
843- < Tooltip
844- info = { < span className = "whitespace-nowrap" > Deselect all</ span > }
845- className = { steps . length === 0 ? 'invisible pointer-events-none' : '' }
846- >
847- < button
848- onClick = { handleReset }
849- className = { `${ popover . toggleButton . classNames . rounded } ${ popover . toggleButton . classNames . outline } justify-center !h-7 px-1.5` }
850+ </ div >
851+ ) }
852+ < Tooltip
853+ info = { < span className = "whitespace-nowrap" > Deselect all</ span > }
854+ className = {
855+ steps . length === 0 ? 'invisible pointer-events-none' : ''
856+ }
850857 >
851- < RefreshIcon className = "size-3.5" />
852- </ button >
853- </ Tooltip >
854- </ div >
858+ < button
859+ onClick = { handleReset }
860+ className = { `${ popover . toggleButton . classNames . rounded } ${ popover . toggleButton . classNames . outline } justify-center !h-7 px-1.5` }
861+ >
862+ < RefreshIcon className = "size-3.5" />
863+ </ button >
864+ </ Tooltip >
865+ </ div >
855866
856- < div
857- ref = { containerRef }
858- className = "relative grid gap-6 overflow-x-auto -mx-5 px-5 -mb-3 pb-3 [scrollbar-width:thin] [scrollbar-color:theme(colors.gray.300)_transparent] dark:[scrollbar-color:theme(colors.gray.600)_transparent]"
859- style = { {
860- gridTemplateColumns : `repeat(${ numColumns } , minmax(20rem, 1fr))`
861- } }
862- >
863- { Array . from ( { length : numColumns } , ( _ , i ) => {
864- const isActive = i === activeColumnIndex
865- const isReachable = steps . length >= i
866-
867- return (
868- < ExplorationColumn
869- key = { i }
870- colIndex = { i }
871- header = { columnHeader ( i , direction ) }
872- className = {
873- steps . length === 0 && i === 2
874- ? 'sm:hidden'
875- : steps . length === 0 && i === 1
876- ? 'sm:[grid-column:span_2]'
877- : undefined
878- }
879- active = { isReachable }
880- // Active column gets live results; previously-active (now
881- // selected) columns get the candidate list that was visible at
882- // the moment of selection so the user can switch options
883- // without losing context. Pre-selected columns (e.g. populated
884- // by interesting-funnel preload) have no frozen results and
885- // fall back to a single-item display sourced from funnel data.
886- results = {
887- isActive ? activeColumnResults : frozenColumnResults [ i ] || [ ]
888- }
889- loading = { isActive ? activeColumnLoading : false }
890- selected = { steps [ i ] || null }
891- selectedVisitors = {
892- provisionalFunnelEntries [ i ] ?. visitors ??
893- funnel [ i ] ?. visitors ??
894- null
895- }
896- selectedConversionRate = {
897- provisionalFunnelEntries [ i ] ?. conversion_rate ??
898- funnel [ i ] ?. conversion_rate ??
899- null
900- }
901- maxVisitors = { funnel [ 0 ] ?. visitors ?? null }
902- onSelect = { ( selected ) => handleSelect ( i , selected ) }
903- onFilterChange = { isActive ? setActiveColumnFilter : ( ) => { } }
904- filter = { isActive ? activeColumnFilter : '' }
905- direction = { direction }
906- onDirectionChange = { i === 0 ? handleDirectionSelect : undefined }
907- headerConversionRate = {
908- funnel [ i ] ?. conversion_rate != null
909- ? i === 0
910- ? '100%'
911- : `${ parseFloat ( funnel [ i ] . conversion_rate ) . toFixed ( 1 ) } %`
912- : null
913- }
914- />
915- )
916- } ) }
917- < PathConnectors
918- key = { connectorsKey }
919- containerRef = { containerRef }
920- steps = { steps }
921- />
867+ < div
868+ ref = { containerRef }
869+ className = "relative grid gap-6 overflow-x-auto -mx-5 px-5 -mb-3 pb-3 [scrollbar-width:thin] [scrollbar-color:theme(colors.gray.300)_transparent] dark:[scrollbar-color:theme(colors.gray.600)_transparent]"
870+ style = { {
871+ gridTemplateColumns : `repeat(${ gridColumns } , minmax(20rem, 1fr))`
872+ } }
873+ >
874+ { Array . from ( { length : numColumns } , ( _ , i ) => {
875+ const isActive = i === activeColumnIndex
876+ const isReachable = steps . length >= i
877+
878+ return (
879+ < ExplorationColumn
880+ key = { i }
881+ colIndex = { i }
882+ header = { columnHeader ( i , direction ) }
883+ className = {
884+ steps . length === 0 && i === 2
885+ ? 'sm:hidden'
886+ : steps . length === 0 && i === 1
887+ ? 'sm:[grid-column:span_2]'
888+ : undefined
889+ }
890+ active = { isReachable }
891+ // Active column gets live results; previously-active (now
892+ // selected) columns get the candidate list that was visible at
893+ // the moment of selection so the user can switch options
894+ // without losing context. Pre-selected columns (e.g. populated
895+ // by interesting-funnel preload) have no frozen results and
896+ // fall back to a single-item display sourced from funnel data.
897+ results = {
898+ isActive ? activeColumnResults : frozenColumnResults [ i ] || [ ]
899+ }
900+ loading = {
901+ isActive ? initialLoading || activeColumnLoading : false
902+ }
903+ selected = { steps [ i ] || null }
904+ selectedVisitors = {
905+ provisionalFunnelEntries [ i ] ?. visitors ??
906+ funnel [ i ] ?. visitors ??
907+ null
908+ }
909+ selectedConversionRate = {
910+ provisionalFunnelEntries [ i ] ?. conversion_rate ??
911+ funnel [ i ] ?. conversion_rate ??
912+ null
913+ }
914+ maxVisitors = { funnel [ 0 ] ?. visitors ?? null }
915+ onSelect = { ( selected ) => handleSelect ( i , selected ) }
916+ onFilterChange = { isActive ? setActiveColumnFilter : ( ) => { } }
917+ filter = { isActive ? activeColumnFilter : '' }
918+ direction = { direction }
919+ onDirectionChange = { i === 0 ? handleDirectionSelect : undefined }
920+ headerConversionRate = {
921+ funnel [ i ] ?. conversion_rate != null
922+ ? i === 0
923+ ? '100%'
924+ : `${ parseFloat ( funnel [ i ] . conversion_rate ) . toFixed ( 1 ) } %`
925+ : null
926+ }
927+ />
928+ )
929+ } ) }
930+ < PathConnectors
931+ key = { connectorsKey }
932+ containerRef = { containerRef }
933+ steps = { steps }
934+ />
935+ </ div >
922936 </ div >
923- </ div >
937+ </ LazyLoader >
924938 )
925939}
926940
0 commit comments