Skip to content

Commit

Permalink
[results] show currently highlighted transition on the ToolsExperienc…
Browse files Browse the repository at this point in the history
…eTransitionsChart
  • Loading branch information
plouc committed Feb 9, 2022
1 parent 60d2969 commit 37e14c3
Show file tree
Hide file tree
Showing 11 changed files with 252 additions and 110 deletions.

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import React, { useMemo, useState } from 'react'
import React, { useCallback, useMemo, useState } from 'react'
import styled from 'styled-components'
import { ToolExperienceId } from 'core/bucket_keys'
import { useLegends } from 'core/helpers/useBucketKeys'
import Block from 'core/blocks/block/BlockVariant'
import {
ToolsExperienceTransitionsBlockData,
ApiToolExperienceTransitions,
} from './types'
import { Grid } from './Grid'
import { ToolsExperienceTransitionsChart } from './ToolsExperienceTransitionsChart'
import { useLegends } from 'core/helpers/useBucketKeys'

export const ToolsExperienceTransitionsBlock = ({
block,
Expand All @@ -16,14 +16,36 @@ export const ToolsExperienceTransitionsBlock = ({
block: ToolsExperienceTransitionsBlockData
data: ApiToolExperienceTransitions[]
}) => {
console.log(data)
const filteredData = useMemo(() =>
data.filter(toolData => toolData.experienceTransitions.nodes.length > 0),
[data]
)

const [currentExperience, setCurrentExperience] = useState<ToolExperienceId>('interested')
const [currentTransition, setCurrentTransition] = useState<[ToolExperienceId, ToolExperienceId] | null>(null)
const [currentExperience, _setCurrentExperience] = useState<ToolExperienceId>('interested')
const [currentTransition, _setCurrentTransition] = useState<[ToolExperienceId, ToolExperienceId] | null>([
'interested',
'would_use'
])

// avoid creating a new transition array if the values don't change
const setCurrentTransition = useCallback((transition: [ToolExperienceId, ToolExperienceId] | null) => {
_setCurrentTransition((previous) => {
if (transition === null) return null
if (previous !== null && previous[0] === transition[0] && previous[1] === transition[1]) {
return previous
}

return transition
})
}, [_setCurrentTransition])

// reset the current transition when a new source experience is selected
const setCurrentExperience = useCallback((experience: ToolExperienceId) => {
if (experience === currentExperience) return

_setCurrentExperience(experience)
_setCurrentTransition(null)
}, [currentExperience, _setCurrentTransition])

const keys = data[0].experienceTransitions.keys
const legends = useLegends(block, keys, 'tools')
Expand Down Expand Up @@ -55,4 +77,12 @@ export const ToolsExperienceTransitionsBlock = ({
</Grid>
</Block>
)
}
}

const Grid = styled.div`
display: grid;
width: 100%;
grid-template-columns: repeat(auto-fit, minmax(min(240px, 100%), 1fr));
column-gap: 24px;
row-gap: 16px;
`
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,13 @@ import React, { useMemo } from 'react'
import { ToolExperienceId } from 'core/bucket_keys'
import { SankeyNodeDatum, SankeyLinkDatum, SankeyYear } from '../types'
import { staticProps } from './config'
import { useChartContext } from './state'
import { YearsLegend } from './YearsLegend'
import { LinksBackground } from './LinksBackground'
import { LinkPercentages } from './LinkPercentages'
import { Nodes } from './Nodes'
import { ExperienceLinks } from './ExperienceLinks'
import { useChartContext } from './state'
import { TransitionLegend } from './TransitionLegend'

/**
* Used to entirely replace the default nivo Sankey component,
Expand All @@ -23,12 +24,10 @@ export const CustomSankey = ({ nodes, links }: {
const {
currentExperience,
setCurrentExperience,
currentTransition,
} = useChartContext()

const {
years,
linksByExperience,
} = useMemo(() => {
const { years, linksByExperience } = useMemo(() => {
const _years: SankeyYear[] = []

const _nodesByExperience: Partial<Record<ToolExperienceId, SankeyNodeDatum[]>> = {}
Expand Down Expand Up @@ -66,24 +65,9 @@ export const CustomSankey = ({ nodes, links }: {

_years.sort((a, b) => a.year - b.year)

const _startNodes: SankeyNodeDatum[] = []
const _endNodes: SankeyNodeDatum[] = []
const firstYear = _years[0]
const lastYear = _years[_years.length - 1]
nodes.forEach(node => {
if (node.year === firstYear.year) {
_startNodes.push(node)
}
if (node.year === lastYear.year) {
_endNodes.push(node)
}
})

return {
years: _years,
nodesByChoice: _nodesByExperience,
startNodes: _startNodes,
endNodes: _endNodes,
linksByExperience: Object.values(_linksByExperience),
}
}, [nodes, links])
Expand All @@ -93,6 +77,13 @@ export const CustomSankey = ({ nodes, links }: {
[linksByExperience, currentExperience]
)

let currentTransitionLink: SankeyLinkDatum | undefined = undefined
if (currentTransition !== null) {
currentTransitionLink = currentLinks!.links.find(link => {
return link.source.choice === currentTransition[0] && link.target.choice === currentTransition[1]
})
}

return (
<>
<YearsLegend years={years} />
Expand All @@ -113,6 +104,7 @@ export const CustomSankey = ({ nodes, links }: {
/>
))}
<LinkPercentages links={currentLinks!.links} />
<TransitionLegend link={currentTransitionLink} />
</>
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ export const NonMemoizedExperienceLinks = ({
<LinkWithGradient
key={`${link.source.id}.${link.target.id}`}
link={link}
isActive={isActive}
/>
))}
</animated.g>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,36 @@
import React, { memo } from 'react'
import React, { memo, useCallback } from 'react'
import { SankeyLinkDatum } from '../types'
import { getLinkPath } from './getLinkPath'
import { useChartContext } from './state'
import styled from 'styled-components'

const NonMemoizedLinkWithGradient = ({ link }: {
const NonMemoizedLinkWithGradient = ({ link, isActive }: {
link: SankeyLinkDatum
isActive: boolean
}) => {
const linkId = `${link.source.id}.${link.target.id}`
const gradientId = `${linkId}Gradient`

const { currentTransition, setCurrentTransition } = useChartContext()

const sourceExperience = link.source.choice
const targetExperience = link.target.choice

const handleClick = useCallback(() => {
setCurrentTransition([sourceExperience, targetExperience])
}, [
setCurrentTransition,
sourceExperience,
targetExperience,
])

let opacity = 1
// change the opacity of the inactive links in case
// users selected a specific transition.
if (currentTransition !== null && (link.source.choice !== currentTransition[0] || link.target.choice !== currentTransition[1])) {
opacity = .3
}

return (
<>
<defs>
Expand All @@ -16,12 +39,22 @@ const NonMemoizedLinkWithGradient = ({ link }: {
<stop offset="100%" stopColor={link.target.color} />
</linearGradient>
</defs>
<path
fill={`url(#${gradientId})`}
<Path
$isActive={isActive}
onClick={handleClick}
d={getLinkPath(link, 1)}
fill={`url(#${gradientId})`}
opacity={opacity}
/>
</>
)
}

const Path = styled.path<{
$isActive: boolean
}>`
pointer-events: ${({ $isActive }) => $isActive ? 'auto' : 'none'};
cursor: ${({ $isActive }) => $isActive ? 'pointer' : 'auto'};
`

export const LinkWithGradient = memo(NonMemoizedLinkWithGradient)
Original file line number Diff line number Diff line change
@@ -1,24 +1,29 @@
import React from 'react'
import styled from 'styled-components'
import { staticProps } from './config'
import { Entity } from 'core/types'

export const ChartContainer = styled.div`
display: flex;
align-items: stretch;
height: 180px;
`
export const ToolLegend = ({ tool }: {
tool: Entity
}) => {
return (
<Container>
<Label>
{tool.name}
</Label>
</Container>
)
}

export const ToolLegendContainer = styled.div`
const Container = styled.div`
flex-grow: 0;
flex-shrink: 0;
width: 20px;
margin-top: ${staticProps.margin.top}px;
margin-bottom: ${staticProps.margin.bottom}px;
display: flex;
align-items: center;
justify-content: center;
`

export const ToolLegend = styled.div`
export const Label = styled.div`
height: 20px;
display: flex;
align-items: center;
Expand All @@ -27,4 +32,4 @@ export const ToolLegend = styled.div`
transform: rotate(-90deg);
font-size: ${({ theme }) => theme.typography.size.smaller};
font-weight: ${({ theme }) => theme.typography.weight.bold};
`
`
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import React, { useCallback, useMemo } from 'react'
import { useTheme } from 'styled-components'
import styled, { useTheme } from 'styled-components'
import { ResponsiveSankey } from '@nivo/sankey'
import { ToolExperienceId } from 'core/bucket_keys'
import { ApiToolExperienceTransitions } from '../types'
import { staticProps } from './config'
import { ChartContainer, ToolLegendContainer, ToolLegend } from './ChartContainer'
import { ChartContextProvider } from './state'
import { ToolLegend } from './ToolLegend'

export const ToolsExperienceTransitionsChart = ({
data,
Expand All @@ -22,7 +22,7 @@ export const ToolsExperienceTransitionsChart = ({
}) => {
const theme = useTheme()

const chartData = {
const chartData = useMemo(() => ({
nodes: data.experienceTransitions.nodes,
links: data.experienceTransitions.transitions.map(transition => {
return {
Expand All @@ -32,7 +32,7 @@ export const ToolsExperienceTransitionsChart = ({
percentage: transition.percentage,
}
}),
}
}), [data])

const context = useMemo(() => ({
toolId: data.id,
Expand All @@ -54,27 +54,36 @@ export const ToolsExperienceTransitionsChart = ({

return (
<ChartContextProvider value={context}>
<ChartContainer>
<ToolLegendContainer>
<ToolLegend>
{data.entity.name}
</ToolLegend>
</ToolLegendContainer>
<ResponsiveSankey
margin={staticProps.margin}
data={chartData}
sort="input"
align="justify"
colors={getColor}
nodeThickness={18}
nodeInnerPadding={1}
nodeSpacing={2}
linkContract={1}
animate={false}
// @ts-ignore
layers={staticProps.layers}
/>
</ChartContainer>
<Container>
<ToolLegend tool={data.entity} />
<ChartContainer>
<ResponsiveSankey
margin={staticProps.margin}
data={chartData}
sort="input"
align="justify"
colors={getColor}
nodeThickness={18}
nodeInnerPadding={1}
nodeSpacing={2}
linkContract={1}
animate={false}
// @ts-ignore
layers={staticProps.layers}
/>
</ChartContainer>
</Container>
</ChartContextProvider>
)
}
}

const Container = styled.div`
display: flex;
overflow: hidden;
align-items: center;
`

const ChartContainer = styled.div`
width: calc(100% - 20px);
height: ${staticProps.margin.top + staticProps.chartHeight + staticProps.margin.bottom}px;
`
Loading

0 comments on commit 37e14c3

Please sign in to comment.