Skip to content

Commit

Permalink
tooltip styled
Browse files Browse the repository at this point in the history
  • Loading branch information
ncomerci committed Sep 15, 2023
1 parent e257e3b commit 59a8ef4
Show file tree
Hide file tree
Showing 5 changed files with 107 additions and 44 deletions.
41 changes: 37 additions & 4 deletions src/components/Charts/ProposalVPChart.css
Original file line number Diff line number Diff line change
@@ -1,16 +1,49 @@
#ProposalVPChartTooltip {
background: rgba(0, 0, 0, 0.7);
border-radius: 3px;
color: white;
--padding-size: 9px;
display: flex;
flex-direction: row;
align-items: center;
background: rgba(22, 20, 26, 0.9);
border-radius: 5px;
color: var(--white-900);
opacity: 1;
pointer-events: none;
position: absolute;
transform: translate(-50%, 0);
transition: all 0.1s ease;
width: max-content;
padding: 4px;
padding: var(--padding-size);
z-index: 100;
}

#ProposalVPChartTooltip .avatar {
--size: 40px;
min-width: var(--size) !important;
max-width: var(--size) !important;
min-height: var(--size) !important;
max-height: var(--size) !important;
border-radius: 100%;
background-color: #7a7a7a;
vertical-align: middle;
margin: 0;
}

#ProposalVPChartTooltip .container {
margin: 0;
margin-left: var(--padding-size);
}

#ProposalVPChartTooltip .title {
font-size: 13px;
font-style: normal;
line-height: 18px;
max-width: 160px;
}

#ProposalVPChartTooltip .details {
font-size: 10px;
font-style: normal;
font-weight: 400;
line-height: 18px;
opacity: 0.6;
}
45 changes: 24 additions & 21 deletions src/components/Charts/ProposalVPChart.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
import React, { useEffect, useMemo, useRef, useState } from 'react'
import { Chart } from 'react-chartjs-2'

import { ChartData, Chart as ChartJS } from 'chart.js'
import { ChartData, Chart as ChartJS, ScriptableTooltipContext } from 'chart.js'
import 'chart.js/auto'
import 'chartjs-adapter-dayjs-4/dist/chartjs-adapter-dayjs-4.esm'
import annotationPlugin from 'chartjs-plugin-annotation'

import { Vote } from '../../entities/Votes/types'
import useAbbreviatedFormatter from '../../hooks/useAbbreviatedFormatter'
import useFormatMessage from '../../hooks/useFormatMessage'
import useProfiles from '../../hooks/useProfiles'
import { Avatar } from '../../utils/Catalyst/types'
import Section from '../Proposal/View/Section'

import './ProposalVPChart.css'
Expand All @@ -27,6 +29,7 @@ ChartJS.register(annotationPlugin)
interface Props {
requiredToPass?: number | null
voteMap: Record<string, Vote>
isLoadingVotes?: boolean
}

const COMMON_DATASET_OPTIONS = {
Expand All @@ -36,19 +39,33 @@ const COMMON_DATASET_OPTIONS = {
pointHoverBorderWidth: 8,
}

function ProposalVPChart({ requiredToPass, voteMap }: Props) {
function ProposalVPChart({ requiredToPass, voteMap, isLoadingVotes }: Props) {
const t = useFormatMessage()
const YAxisFormat = useAbbreviatedFormatter()
const chartRef = useRef<ChartJS>(null)
const votes = useMemo(() => getSortedVotes(voteMap), [voteMap])
const { yesVotes, noVotes, abstainVotes } = useMemo(() => getSegregatedVotes(votes), [votes])

const { profiles, isLoadingProfiles } = useProfiles(votes.map((vote) => vote.address))
const profileByAddress = useMemo(
() =>
profiles.reduce((acc, { profile }) => {
acc.set(profile.ethAddress.toLowerCase(), profile)
return acc
}, new Map<string, Avatar>()),
[profiles]
)
const { yesVotes, noVotes, abstainVotes } = useMemo(
() => getSegregatedVotes(votes, profileByAddress),
[profileByAddress, votes]
)
const segregatedVotesMap = {
Yes: yesVotes,
No: noVotes,
Abstain: abstainVotes,
}

const tooltipTitle = (choice: string, vp: number) =>
t('page.proposal_view.votes_chart.tooltip_title', { choice, vp: vp.toLocaleString('en-US') })

const [chartData, setChartData] = useState<ChartData<'line'>>({ datasets: [] })
useEffect(() => {
setChartData({
Expand Down Expand Up @@ -93,24 +110,10 @@ function ProposalVPChart({ requiredToPass, voteMap }: Props) {
},
},
tooltip: {
// displayColors: false,
// callbacks: {
// label: (context: TooltipItem<'line'>) => {
// const vp = context.formattedValue
// const choice = context.dataset.label
// return t('page.proposal_view.votes_chart.tooltip_label', { choice, vp })
// },
// afterLabel: (context: TooltipItem<'line'>) => {
// const idx = context.dataIndex
// const choice = context.dataset.label as keyof typeof segregatedVotesMap
// const vote = segregatedVotesMap[choice][idx]
// const formattedVP = vote.vp.toLocaleString('en-US')
// return t('page.proposal_view.votes_chart.tooltip_voter', { address: vote.address, vp: formattedVP })
// },
// },
enabled: false,
position: 'nearest' as const,
external: externalTooltipHandler,
external: (context: ScriptableTooltipContext<'line'>) =>
externalTooltipHandler({ context, datasetMap: segregatedVotesMap, title: tooltipTitle }),
},
annotation: {
annotations: {
Expand Down Expand Up @@ -165,7 +168,7 @@ function ProposalVPChart({ requiredToPass, voteMap }: Props) {
}

return (
<Section title={t('page.proposal_view.votes_chart.title')} isNew>
<Section title={t('page.proposal_view.votes_chart.title')} isNew isLoading={isLoadingVotes || isLoadingProfiles}>
<Chart className="ProposalVPChart" ref={chartRef} options={options} data={chartData} type="line" />
</Section>
)
Expand Down
60 changes: 44 additions & 16 deletions src/components/Charts/ProposalVPChart.utils.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
import type { Chart, ScriptableTooltipContext } from 'chart.js'

import { Vote } from '../../entities/Votes/types'
import { DEFAULT_AVATAR_IMAGE } from '../../utils/Catalyst'
import { Avatar } from '../../utils/Catalyst/types'
import Time from '../../utils/date/Time'

type VoteWithAddress = Vote & { address: string }
type VoteWithProfile = VoteWithAddress & { profile?: Avatar }

const TOOLTIP_ID = 'ProposalVPChartTooltip'

Expand All @@ -12,18 +16,21 @@ export function getSortedVotes(votesMap: Record<string, Vote>) {
.sort((a, b) => a.timestamp - b.timestamp)
}

export function getSegregatedVotes(votes: VoteWithAddress[]) {
const yesVotes: VoteWithAddress[] = []
const noVotes: VoteWithAddress[] = []
const abstainVotes: VoteWithAddress[] = []
export function getSegregatedVotes(votes: VoteWithAddress[], profileMap: Map<string, Avatar>) {
const yesVotes: VoteWithProfile[] = []
const noVotes: VoteWithProfile[] = []
const abstainVotes: VoteWithProfile[] = []

for (const vote of votes) {
if (vote.choice === 1) {
yesVotes.push(vote)
} else if (vote.choice === 2) {
noVotes.push(vote)
} else if (vote.choice === 3) {
abstainVotes.push(vote)
const profile = profileMap.get(vote.address.toLowerCase())
const voteWithProfile = { ...vote, profile }

if (voteWithProfile.choice === 1) {
yesVotes.push(voteWithProfile)
} else if (voteWithProfile.choice === 2) {
noVotes.push(voteWithProfile)
} else if (voteWithProfile.choice === 3) {
abstainVotes.push(voteWithProfile)
}
}

Expand Down Expand Up @@ -70,31 +77,51 @@ function getOrCreateTooltip(chart: Chart<'line'>) {
return customTooltip
}

export function externalTooltipHandler(context: ScriptableTooltipContext<'line'>) {
type TooltipHandlerProps = {
context: ScriptableTooltipContext<'line'>
datasetMap: Record<string, VoteWithProfile[]>
title: (choice: string, vp: number) => string
}
export function externalTooltipHandler({ context, datasetMap, title }: TooltipHandlerProps) {
// Tooltip Element
const { chart, tooltip } = context
const tooltipEl = getOrCreateTooltip(chart)
const dataPoint = tooltip.dataPoints?.[0].raw as { x: number; y: number } | undefined
const dataIdx: number | undefined = tooltip.dataPoints?.[0].dataIndex
const datasetLabel = tooltip.dataPoints?.[0].dataset.label
const datasetLabel = tooltip.dataPoints?.[0].dataset.label || ''

const vote = datasetMap[datasetLabel]?.[dataIdx]

const username = vote?.profile?.name || vote?.address.slice(0, 7)
const userVP = vote?.vp || 0
const formattedTimestamp = Time(dataPoint?.x || 0).format('DD/MM/YY, HH:mm z')

// Hide if no tooltip
if (tooltip.opacity === 0) {
tooltipEl.style.opacity = '0'
return
}

//Set Avatar
const avatar = document.createElement('img')
avatar.className = 'avatar'
avatar.src = vote?.profile?.avatar?.snapshots?.face256 || DEFAULT_AVATAR_IMAGE

// Set Text
const textContainer = document.createElement('div')
textContainer.className = 'container'

const titleElement = document.createElement('div')
const title = document.createTextNode(`voted "${datasetLabel}"`)
titleElement.appendChild(title)
const userElement = document.createElement('strong')
const userText = document.createTextNode(username)
userElement.appendChild(userText)
titleElement.appendChild(userElement)
const titleText = document.createTextNode(title(datasetLabel, userVP))
titleElement.appendChild(titleText)
titleElement.className = 'title'

const detailsElement = document.createElement('div')
const details = document.createTextNode(`Acc. ${dataPoint?.y.toLocaleString('en-US')} VP`)
const details = document.createTextNode(`${formattedTimestamp}. Acc. VP: ${dataPoint?.y.toLocaleString('en-US')}`)
detailsElement.appendChild(details)
detailsElement.className = 'details'

Expand All @@ -107,11 +134,12 @@ export function externalTooltipHandler(context: ScriptableTooltipContext<'line'>
}

// Add new children
tooltipEl.appendChild(avatar)
tooltipEl.appendChild(textContainer)

const { offsetLeft: positionX, offsetTop: positionY, width } = chart.canvas

const maxX = width * 0.9
const maxX = width * 0.8

// Display, position, and set styles for font
tooltipEl.style.opacity = '1'
Expand Down
3 changes: 1 addition & 2 deletions src/intl/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -870,8 +870,7 @@
"votes_chart": {
"title": "Voting Timeline",
"pass_threshold": "Acceptance threshold",
"tooltip_label": "{choice}: {vp} VP",
"tooltip_voter": "\nVoter: {address}\nVP used: {vp}"
"tooltip_title": ", voted \"{choice}\" with {vp} VP"
}
},
"proposal_bidding_tendering": {
Expand Down
2 changes: 1 addition & 1 deletion src/utils/Catalyst/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { isSameAddress } from '../../entities/Snapshot/utils'
import { Avatar, ProfileResponse } from './types'

const CATALYST_URL = 'https://peer.decentraland.org'
const DEFAULT_AVATAR_IMAGE = 'https://decentraland.org/images/male.png'
export const DEFAULT_AVATAR_IMAGE = 'https://decentraland.org/images/male.png'

export async function getProfile(address: string): Promise<Avatar | null> {
if (!isEthereumAddress(address)) {
Expand Down

0 comments on commit 59a8ef4

Please sign in to comment.