Funnel Chart

Shows progressive reduction of data through stages (conversion pipeline).

Loading demo…

import

 

{

 

FunnelChart

 

}

 

from

 

'@platform-blocks/charts'

;

 

import

 

{

SALES_FUNNEL

}

 

from

 

'./data'

;

 

const

compact

=

 

(

value

:

 

number

)

 

=>

 

{

  

const

abs

=

 

Math

.

abs

(

value

)

;

  

if

 

(

abs

>=

1_000_000

)

 

return

 

`${(value / 1_000_000).toFixed(1).replace(/\.0$/, '')}M`

;

  

if

 

(

abs

>=

1_000

)

 

return

 

`${(value / 1_000).toFixed(1).replace(/\.0$/, '')}K`

;

  

return

 

`${value}`

;

}

;

 

export

 

function

 

Demo

(

)

 

{

  

return

 

(

    

<FunnelChart

      

title

=

"Product acquisition funnel"

      

maxWidth

=

{

420

}

      

height

=

{

420

}

      

series

=

{

SALES_FUNNEL

}

      

layout

=

{

{

        shape

:

 

'trapezoid'

,

        gap

:

 

8

,

        showConversion

:

 

false

,

        align

:

 

'center'

,

        connectors

:

 

{

show

:

 

false

 

}

,

      

}

}

      

valueFormatter

=

{

(

value

)

 

=>

 

compact

(

value

)

}

      

legend

=

{

{

show

:

 

false

 

}

}

      

tooltip

=

{

{

        show

:

 

true

,

        formatter

:

 

(

step

)

 

=>

 

`${step.label}: ${step.value.toLocaleString()}`

,

      

}

}

    

/>

  

)

;

}

Loading demo…

import

 

{

 

FunnelChart

 

}

 

from

 

'@platform-blocks/charts'

;

 

import

 

{

PIPELINE_QUALITY

,

 

PipelineMeta

 

}

 

from

 

'./data'

;

 

const

compact

=

 

(

value

:

 

number

)

 

=>

 

{

  

const

abs

=

 

Math

.

abs

(

value

)

;

  

if

 

(

abs

>=

1_000_000

)

 

return

 

`${(value / 1_000_000).toFixed(1).replace(/\.0$/, '')}M`

;

  

if

 

(

abs

>=

1_000

)

 

return

 

`${(value / 1_000).toFixed(1).replace(/\.0$/, '')}K`

;

  

return

 

`${value}`

;

}

;

 

export

 

function

 

Demo

(

)

 

{

  

return

 

(

    

<FunnelChart

      

title

=

"Data pipeline quality checks"

      

subtitle

=

"From ingestion to certified datasets"

      

maxWidth

=

{

520

}

      

height

=

{

440

}

      

series

=

{

PIPELINE_QUALITY

}

      

layout

=

{

{

        shape

:

 

'trapezoid'

,

        gap

:

 

8

,

        align

:

 

'center'

,

        showConversion

:

 

false

,

        connectors

:

 

{

show

:

 

false

 

}

,

      

}

}

      

valueFormatter

=

{

(

value

)

 

=>

 

compact

(

value

)

}

      

legend

=

{

{

show

:

 

false

 

}

}

      

tooltip

=

{

{

        show

:

 

true

,

        formatter

:

 

(

step

)

 

=>

 

{

          

const

idx

=

PIPELINE_QUALITY

.

steps

.

findIndex

(

(

candidate

)

 

=>

candidate

.

label

===

step

.

label

)

;

          

const

previous

=

idx

>

 

0

 

?

PIPELINE_QUALITY

.

steps

[

idx

-

 

1

]

 

:

 

undefined

;

          

const

dropValue

=

previous

?

previous

.

value

-

step

.

value

:

 

0

;

          

const

dropRate

=

previous

&&

previous

.

value

>

 

0

 

?

 

(

dropValue

/

previous

.

value

)

 

*

 

100

 

:

 

0

;

          

const

meta

=

step

.

meta as

PipelineMeta

 

|

 

undefined

;

          

return

 

[

            step

.

label

,

            

`${step.value.toLocaleString()} rows`

,

            previous

?

 

`Filtered: ${dropValue.toLocaleString()} (${dropRate.toFixed(1)}%)`

 

:

 

'Ingestion baseline'

,

            meta

?

.

note

,

          

]

            

.

filter

(

Boolean

)

            

.

join

(

'\n'

)

;

        

}

,

      

}

}

    

/>

  

)

;

}

Loading demo…

import

 

{

 

FunnelChart

 

}

 

from

 

'@platform-blocks/charts'

;

 

import

 

{

CHECKOUT_FUNNEL

,

 

CheckoutMeta

 

}

 

from

 

'./data'

;

 

const

compact

=

 

(

value

:

 

number

)

 

=>

 

{

  

const

abs

=

 

Math

.

abs

(

value

)

;

  

if

 

(

abs

>=

1_000_000

)

 

return

 

`${(value / 1_000_000).toFixed(1).replace(/\.0$/, '')}M`

;

  

if

 

(

abs

>=

1_000

)

 

return

 

`${(value / 1_000).toFixed(1).replace(/\.0$/, '')}K`

;

  

return

 

`${value}`

;

}

;

 

const

formatPaymentSplit

=

 

(

split

:

 

CheckoutMeta

[

'paymentSplit'

]

)

 

=>

 

{

  

if

 

(

!

split

)

 

return

 

undefined

;

  

return

 

`Payment mix: ${Math.round(split.stripe * 100)}% Stripe • ${Math.round(split.paypal * 100)}% PayPal • ${Math.round(split.bnpl * 100)}% BNPL`

;

}

;

 

export

 

function

 

Demo

(

)

 

{

  

return

 

(

    

<FunnelChart

      

title

=

"Ecommerce checkout conversion"

      

subtitle

=

"Drop-off by stage"

      

maxWidth

=

{

520

}

      

height

=

{

440

}

      

series

=

{

CHECKOUT_FUNNEL

}

      

layout

=

{

{

        shape

:

 

'trapezoid'

,

        gap

:

 

8

,

        align

:

 

'center'

,

        showConversion

:

 

false

,

        connectors

:

 

{

show

:

 

false

 

}

,

      

}

}

      

valueFormatter

=

{

(

value

)

 

=>

 

compact

(

value

)

}

      

legend

=

{

{

show

:

 

false

 

}

}

      

tooltip

=

{

{

        show

:

 

true

,

        formatter

:

 

(

step

)

 

=>

 

{

          

const

idx

=

CHECKOUT_FUNNEL

.

steps

.

findIndex

(

(

candidate

)

 

=>

candidate

.

label

===

step

.

label

)

;

          

const

previous

=

idx

>

 

0

 

?

CHECKOUT_FUNNEL

.

steps

[

idx

-

 

1

]

 

:

 

undefined

;

          

const

dropValue

=

previous

?

previous

.

value

-

step

.

value

:

 

0

;

          

const

dropRate

=

previous

&&

previous

.

value

>

 

0

 

?

 

(

dropValue

/

previous

.

value

)

 

*

 

100

 

:

 

0

;

          

const

meta

=

step

.

meta as

CheckoutMeta

 

|

 

undefined

;

          

const

paymentSplit

=

 

formatPaymentSplit

(

meta

?

.

paymentSplit

)

;

          

return

 

[

            step

.

label

,

            

`${step.value.toLocaleString()} sessions`

,

            previous

?

 

`Drop: ${dropValue.toLocaleString()} (${dropRate.toFixed(1)}%)`

 

:

 

'Entry point'

,

            meta

?

.

insight

,

            paymentSplit

,

          

]

            

.

filter

(

Boolean

)

            

.

join

(

'\n'

)

;

        

}

,

      

}

}

    

/>

  

)

;

}

Loading demo…

import

 

{

 

FunnelChart

 

}

 

from

 

'@platform-blocks/charts'

;

 

import

 

{

HIRING_SERIES

,

 

HiringMeta

,

STEP_LOOKUP

}

 

from

 

'./data'

;

 

const

compact

=

 

(

value

:

 

number

)

 

=>

 

{

  

const

abs

=

 

Math

.

abs

(

value

)

;

  

if

 

(

abs

>=

1_000_000

)

 

return

 

`${(value / 1_000_000).toFixed(1).replace(/\.0$/, '')}M`

;

  

if

 

(

abs

>=

1_000

)

 

return

 

`${(value / 1_000).toFixed(1).replace(/\.0$/, '')}K`

;

  

return

 

`${value}`

;

}

;

 

const

findSeriesContext

=

 

(

step

:

unknown

)

 

=>

STEP_LOOKUP

.

get

(

step as

any

)

 

??

 

null

;

 

export

 

function

 

Demo

(

)

 

{

  

return

 

(

    

<FunnelChart

      

title

=

"Hiring funnel — Staff engineer"

      

subtitle

=

"External candidates vs. internal transfers"

      

maxWidth

=

{

620

}

      

height

=

{

480

}

      

series

=

{

HIRING_SERIES

}

      

layout

=

{

{

        shape

:

 

'bar'

,

        gap

:

 

10

,

        align

:

 

'center'

,

        showConversion

:

 

false

,

        seriesMode

:

 

'grouped'

,

        connectors

:

 

{

show

:

 

false

 

}

,

      

}

}

      

valueFormatter

=

{

(

value

)

 

=>

 

compact

(

value

)

}

      

tooltip

=

{

{

        show

:

 

true

,

        formatter

:

 

(

step

)

 

=>

 

{

          

const

lookup

=

 

findSeriesContext

(

step as

any

)

 

??

 

undefined

;

          

if

 

(

!

lookup

)

 

{

            

return

 

`${step.label}: ${step.value.toLocaleString()} candidates`

;

          

}

          

const

series

=

lookup

.

series

;

          

const

stepIndex

=

lookup

.

stepIndex

;

          

const

previous

=

stepIndex

>

 

0

 

?

series

.

steps

[

stepIndex

-

 

1

]

 

:

 

undefined

;

          

const

dropValue

=

previous

?

previous

.

value

-

step

.

value

:

 

0

;

          

const

dropRate

=

previous

&&

previous

.

value

>

 

0

 

?

 

(

dropValue

/

previous

.

value

)

 

*

 

100

 

:

 

0

;

          

const

meta

=

step

.

meta as

HiringMeta

 

|

 

undefined

;

          

return

 

[

            

`${step.label} • ${series.name}`

,

            

`${step.value.toLocaleString()} candidates`

,

            previous

?

 

`Drop: ${dropValue.toLocaleString()} (${dropRate.toFixed(1)}%)`

 

:

 

'Pipeline intake'

,

            meta

?

.

medianDays

!=

 

null

 

?

 

`Median time in stage: ${meta.medianDays} days`

 

:

 

undefined

,

            meta

?

.

topDeclineReason

?

 

`Top decline reason: ${meta.topDeclineReason}`

 

:

 

undefined

,

          

]

            

.

filter

(

Boolean

)

            

.

join

(

'\n'

)

;

        

}

,

      

}

}

    

/>

  

)

;

}

Loading demo…

import

 

{

 

FunnelChart

 

}

 

from

 

'@platform-blocks/charts'

;

 

import

 

{

INCIDENT_RESPONSE

,

 

IncidentMeta

 

}

 

from

 

'./data'

;

 

const

compact

=

 

(

value

:

 

number

)

 

=>

 

{

  

const

abs

=

 

Math

.

abs

(

value

)

;

  

if

 

(

abs

>=

1_000_000

)

 

return

 

`${(value / 1_000_000).toFixed(1).replace(/\.0$/, '')}M`

;

  

if

 

(

abs

>=

1_000

)

 

return

 

`${(value / 1_000).toFixed(1).replace(/\.0$/, '')}K`

;

  

return

 

`${value}`

;

}

;

 

export

 

function

 

Demo

(

)

 

{

  

return

 

(

    

<FunnelChart

      

title

=

"Incident response workflow"

      

subtitle

=

"Volume flowing through each stage"

      

maxWidth

=

{

520

}

      

height

=

{

460

}

      

series

=

{

INCIDENT_RESPONSE

}

      

layout

=

{

{

        shape

:

 

'trapezoid'

,

        gap

:

 

8

,

        align

:

 

'center'

,

        showConversion

:

 

false

,

        connectors

:

 

{

show

:

 

false

 

}

,

      

}

}

      

valueFormatter

=

{

(

value

)

 

=>

 

compact

(

value

)

}

      

legend

=

{

{

show

:

 

false

 

}

}

      

tooltip

=

{

{

        show

:

 

true

,

        formatter

:

 

(

step

)

 

=>

 

{

          

const

idx

=

INCIDENT_RESPONSE

.

steps

.

findIndex

(

(

candidate

)

 

=>

candidate

.

label

===

step

.

label

)

;

          

const

previous

=

idx

>

 

0

 

?

INCIDENT_RESPONSE

.

steps

[

idx

-

 

1

]

 

:

 

undefined

;

          

const

dropValue

=

previous

?

previous

.

value

-

step

.

value

:

 

0

;

          

const

dropRate

=

previous

&&

previous

.

value

>

 

0

 

?

 

(

dropValue

/

previous

.

value

)

 

*

 

100

 

:

 

0

;

          

const

meta

=

step

.

meta as

IncidentMeta

 

|

 

undefined

;

          

return

 

[

            

`${step.label}`

,

            

`${step.value.toLocaleString()} incidents`

,

            meta

?

.

medianDuration

,

            previous

?

 

`Drop since prior: ${dropValue.toLocaleString()} (${dropRate.toFixed(1)}%)`

 

:

 

'Start of workflow'

,

            meta

?

.

automationWin

?

 

`Automation impact: ${meta.automationWin}`

 

:

 

undefined

,

          

]

            

.

filter

(

Boolean

)

            

.

join

(

'\n'

)

;

        

}

,

      

}

}

    

/>

  

)

;

}

Loading demo…

import

 

{

 

FunnelChart

 

}

 

from

 

'@platform-blocks/charts'

;

 

import

 

{

TRIAL_CONVERSION

,

 

TrialMeta

 

}

 

from

 

'./data'

;

 

const

compact

=

 

(

value

:

 

number

)

 

=>

 

{

  

const

abs

=

 

Math

.

abs

(

value

)

;

  

if

 

(

abs

>=

1_000_000

)

 

return

 

`${(value / 1_000_000).toFixed(1).replace(/\.0$/, '')}M`

;

  

if

 

(

abs

>=

1_000

)

 

return

 

`${(value / 1_000).toFixed(1).replace(/\.0$/, '')}K`

;

  

return

 

`${value}`

;

}

;

 

export

 

function

 

Demo

(

)

 

{

  

return

 

(

    

<FunnelChart

      

title

=

"SaaS trial-to-paid conversion"

      

subtitle

=

"Retention from sign-up to paid"

      

maxWidth

=

{

520

}

      

height

=

{

440

}

      

series

=

{

TRIAL_CONVERSION

}

      

layout

=

{

{

        shape

:

 

'trapezoid'

,

        gap

:

 

8

,

        align

:

 

'center'

,

        showConversion

:

 

false

,

        connectors

:

 

{

show

:

 

false

 

}

,

      

}

}

      

valueFormatter

=

{

(

value

)

 

=>

 

compact

(

value

)

}

      

legend

=

{

{

show

:

 

false

 

}

}

      

tooltip

=

{

{

        show

:

 

true

,

        formatter

:

 

(

step

)

 

=>

 

{

          

const

index

=

TRIAL_CONVERSION

.

steps

.

findIndex

(

(

candidate

)

 

=>

candidate

.

label

===

step

.

label

)

;

          

const

previous

=

index

>

 

0

 

?

TRIAL_CONVERSION

.

steps

[

index

-

 

1

]

 

:

 

undefined

;

          

const

dropValue

=

previous

?

previous

.

value

-

step

.

value

:

 

0

;

          

const

dropRate

=

previous

&&

previous

.

value

>

 

0

 

?

 

(

dropValue

/

previous

.

value

)

 

*

 

100

 

:

 

0

;

          

const

reason

=

 

(

step

.

meta as

TrialMeta

 

|

 

undefined

)

?

.

dropReason

;

          

return

 

[

            

`${step.label}`

,

            

`${step.value.toLocaleString()} accounts`

,

            previous

?

 

`Drop: ${dropValue.toLocaleString()} (${dropRate.toFixed(1)}%)`

 

:

 

'Starting cohort'

,

            reason

?

 

`Top reason: ${reason}`

 

:

 

undefined

,

          

]

            

.

filter

(

Boolean

)

            

.

join

(

'\n'

)

;

        

}

,

      

}

}

    

/>

  

)

;

}

Scan to open this page on your phonereact-ui-library.com/components/FunnelChart

</>

react-ui-library

100+ accessible, themeable components that work seamlessly across iOS, Android, and Web.

A Platform Blocks product.