fix: heatmap colors for dark mode [CW-3241] (#9278)

* feat: add new heatmap colors

* fix: loader

* fix: move new styles to tailwind

* feat: update tw classes

* refactor: update styles

* feat: add useI18n composable

* feat: use composition api

* fix: empty div

* chore: don't import defineProps

Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>

---------

Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>
This commit is contained in:
Shivam Mishra
2024-04-25 10:23:15 +05:30
committed by GitHub
parent 8c813097db
commit 9086650fe2
2 changed files with 171 additions and 274 deletions

View File

@@ -0,0 +1,32 @@
import { computed, getCurrentInstance } from 'vue';
import Vue from 'vue';
import VueI18n from 'vue-i18n';
let i18nInstance = VueI18n;
export function useI18n() {
if (!i18nInstance) throw new Error('vue-i18n not initialized');
const i18n = i18nInstance;
const instance = getCurrentInstance();
const vm = instance?.proxy || instance || new Vue({});
const locale = computed({
get() {
return i18n.locale;
},
set(v) {
i18n.locale = v;
},
});
return {
locale,
t: vm.$t.bind(vm),
tc: vm.$tc.bind(vm),
d: vm.$d.bind(vm),
te: vm.$te.bind(vm),
n: vm.$n.bind(vm),
};
}

View File

@@ -1,70 +1,16 @@
<template> <script setup>
<div class="heatmap-container"> import { computed } from 'vue';
<template v-if="isLoading">
<div class="heatmap-labels">
<div
v-for="ii in 7"
:key="ii"
class="loading-cell heatmap-axis-label"
/>
</div>
<div class="heatmap-grid">
<div v-for="ii in 7" :key="ii" class="heatmap-grid-row">
<div v-for="jj in 24" :key="jj" class="heatmap-tile loading-cell">
<div class="heatmap-tile__label loading-cell" />
</div>
</div>
</div>
<div class="heatmap-timeline" />
<div class="heatmap-markers">
<div v-for="ii in 24" :key="ii">{{ ii - 1 }} {{ ii }}</div>
</div>
</template>
<template v-else>
<div class="heatmap-labels">
<div
v-for="dateKey in processedData.keys()"
:key="dateKey"
class="heatmap-axis-label"
>
{{ getDayOfTheWeek(new Date(dateKey)) }}
<time>{{ formatDate(dateKey) }}</time>
</div>
</div>
<div class="heatmap-grid">
<div
v-for="dateKey in processedData.keys()"
:key="dateKey"
class="heatmap-grid-row"
>
<div
v-for="data in processedData.get(dateKey)"
:key="data.timestamp"
v-tooltip.top="getCountTooltip(data.value)"
class="heatmap-tile"
:class="getHeatmapLevelClass(data.value)"
>
<div class="heatmap-tile__label" />
</div>
</div>
</div>
<div class="heatmap-timeline" />
<div class="heatmap-markers">
<div v-for="ii in 24" :key="ii">{{ ii - 1 }} {{ ii }}</div>
</div>
</template>
</div>
</template>
<script>
import { getQuantileIntervals } from '@chatwoot/utils';
import format from 'date-fns/format'; import format from 'date-fns/format';
import getDay from 'date-fns/getDay'; import getDay from 'date-fns/getDay';
import { groupHeatmapByDay } from 'helpers/ReportsDataHelper'; import { getQuantileIntervals } from '@chatwoot/utils';
export default { import { groupHeatmapByDay } from 'helpers/ReportsDataHelper';
name: 'Heatmap', import { useI18n } from 'dashboard/composables/useI18n';
props: {
const { t } = useI18n();
const props = defineProps({
heatData: { heatData: {
type: Array, type: Array,
default: () => [], default: () => [],
@@ -73,236 +19,155 @@ export default {
type: Boolean, type: Boolean,
default: false, default: false,
}, },
}, });
computed: {
processedData() { const processedData = computed(() => {
return groupHeatmapByDay(this.heatData); return groupHeatmapByDay(props.heatData);
}, });
quantileRange() {
const flattendedData = this.heatData.map(data => data.value); const quantileRange = computed(() => {
return getQuantileIntervals( const flattendedData = props.heatData.map(data => data.value);
flattendedData, return getQuantileIntervals(flattendedData, [0.2, 0.4, 0.6, 0.8, 0.9, 0.99]);
[0.2, 0.4, 0.6, 0.8, 0.9, 0.99] });
);
}, function getCountTooltip(value) {
},
methods: {
getCountTooltip(value) {
if (!value) { if (!value) {
return this.$t( return t('OVERVIEW_REPORTS.CONVERSATION_HEATMAP.NO_CONVERSATIONS');
'OVERVIEW_REPORTS.CONVERSATION_HEATMAP.NO_CONVERSATIONS'
);
} }
if (value === 1) { if (value === 1) {
return this.$t('OVERVIEW_REPORTS.CONVERSATION_HEATMAP.CONVERSATION', { return t('OVERVIEW_REPORTS.CONVERSATION_HEATMAP.CONVERSATION', {
count: value, count: value,
}); });
} }
return this.$t('OVERVIEW_REPORTS.CONVERSATION_HEATMAP.CONVERSATIONS', { return t('OVERVIEW_REPORTS.CONVERSATION_HEATMAP.CONVERSATIONS', {
count: value, count: value,
}); });
}, }
formatDate(dateString) {
function formatDate(dateString) {
return format(new Date(dateString), 'MMM d, yyyy'); return format(new Date(dateString), 'MMM d, yyyy');
}, }
getDayOfTheWeek(date) {
function getDayOfTheWeek(date) {
const dayIndex = getDay(date); const dayIndex = getDay(date);
const days = [ const days = [
this.$t('DAYS_OF_WEEK.SUNDAY'), t('DAYS_OF_WEEK.SUNDAY'),
this.$t('DAYS_OF_WEEK.MONDAY'), t('DAYS_OF_WEEK.MONDAY'),
this.$t('DAYS_OF_WEEK.TUESDAY'), t('DAYS_OF_WEEK.TUESDAY'),
this.$t('DAYS_OF_WEEK.WEDNESDAY'), t('DAYS_OF_WEEK.WEDNESDAY'),
this.$t('DAYS_OF_WEEK.THURSDAY'), t('DAYS_OF_WEEK.THURSDAY'),
this.$t('DAYS_OF_WEEK.FRIDAY'), t('DAYS_OF_WEEK.FRIDAY'),
this.$t('DAYS_OF_WEEK.SATURDAY'), t('DAYS_OF_WEEK.SATURDAY'),
]; ];
return days[dayIndex]; return days[dayIndex];
}, }
getHeatmapLevelClass(value) { function getHeatmapLevelClass(value) {
if (!value) return ''; if (!value)
return 'outline-slate-100 dark:outline-slate-700 dark:bg-slate-700/40 bg-slate-50/50';
const level = [...this.quantileRange, Infinity].findIndex( let level = [...quantileRange.value, Infinity].findIndex(
range => value <= range && value > 0 range => value <= range && value > 0
); );
if (level > 6) { if (level > 6) level = 5;
return 'l6';
if (level === 0) {
return 'outline-slate-100 dark:outline-slate-700 dark:bg-slate-700/40 bg-slate-50/50';
} }
return `l${level}`; const classes = [
}, 'bg-woot-50 dark:bg-woot-800/40 dark:outline-woot-800/80',
}, 'bg-woot-100 dark:bg-woot-800/30 dark:outline-woot-800/80',
}; 'bg-woot-200 dark:bg-woot-500/40 dark:outline-woot-700/80',
'bg-woot-300 dark:bg-woot-500/60 dark:outline-woot-600/80',
'bg-woot-600 dark:bg-woot-500/80 dark:outline-woot-500/80',
'bg-woot-800 dark:bg-woot-500 dark:outline-woot-400/80',
];
return classes[level - 1];
}
</script> </script>
<style scoped lang="scss"> <template>
$heatmap-colors: ( <div
level-1: var(--w-50), class="grid relative w-full gap-x-4 gap-y-2.5 overflow-y-scroll md:overflow-visible grid-cols-[80px_1fr] min-h-72"
level-2: var(--w-100), >
level-3: var(--w-300), <template v-if="isLoading">
level-4: var(--w-500), <div class="grid gap-[5px] flex-shrink-0">
level-5: var(--w-700), <div
level-6: var(--w-900), v-for="ii in 7"
); :key="ii"
class="w-full rounded-sm bg-slate-100 dark:bg-slate-900 animate-loader-pulse h-8 min-w-[70px]"
$heatmap-hover-border-color: ( />
level-1: var(--w-25), </div>
level-2: var(--w-50), <div class="grid gap-[5px] w-full min-w-[700px]">
level-3: var(--w-100), <div
level-4: var(--w-300), v-for="ii in 7"
level-5: var(--w-500), :key="ii"
level-6: var(--w-700), class="grid gap-[5px] grid-cols-[repeat(24,_1fr)]"
); >
<div
$tile-height: 1.875rem; v-for="jj in 24"
$tile-gap: var(--space-smaller); :key="jj"
$container-gap-row: var(--space-one); class="w-full h-8 rounded-sm bg-slate-100 dark:bg-slate-900 animate-loader-pulse"
$container-gap-column: var(--space-two); />
$marker-height: var(--space-two); </div>
</div>
@mixin heatmap-level($level) { <div />
$color: map-get($heatmap-colors, 'level-#{$level}'); <div
background-color: $color; class="grid grid-cols-[repeat(24,_1fr)] gap-[5px] w-full text-[8px] font-semibold h-5 text-slate-800 dark:text-slate-200"
&:hover { >
border: 1px solid map-get($heatmap-hover-border-color, 'level-#{$level}'); <div
} v-for="ii in 24"
} :key="ii"
class="flex items-center justify-center"
@media screen and (max-width: 768px) { >
.heatmap-container { {{ ii - 1 }} {{ ii }}
overflow-y: auto; </div>
} </div>
} </template>
<template v-else>
.loading-cell { <div class="grid gap-[5px] flex-shrink-0">
background-color: var(--color-background-light); <div
border: 0px; v-for="dateKey in processedData.keys()"
:key="dateKey"
animation: loading-pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite; class="h-8 min-w-[70px] text-slate-800 dark:text-slate-200 text-[10px] font-semibold flex flex-col items-end justify-center"
} >
{{ getDayOfTheWeek(new Date(dateKey)) }}
@keyframes loading-pulse { <time class="font-normal text-slate-700 dark:text-slate-200">
0%, {{ formatDate(dateKey) }}
100% { </time>
opacity: 1; </div>
} </div>
50% { <div class="grid gap-[5px] w-full min-w-[700px]">
opacity: 0; <div
} v-for="dateKey in processedData.keys()"
} :key="dateKey"
class="grid gap-[5px] grid-cols-[repeat(24,_1fr)]"
.heatmap-container { >
display: grid; <div
position: relative; v-for="data in processedData.get(dateKey)"
width: 100%; :key="data.timestamp"
gap: $container-gap-row $container-gap-column; v-tooltip.top="getCountTooltip(data.value)"
grid-template-columns: 80px 1fr; class="h-8 rounded-sm shadow-inner dark:outline dark:outline-1 shadow-black"
min-height: calc( :class="getHeatmapLevelClass(data.value)"
7 * #{$tile-height} + 6 * #{$tile-gap} + #{$container-gap-row} + #{$marker-height} />
); </div>
} </div>
<div />
.heatmap-labels { <div
display: grid; class="grid grid-cols-[repeat(24,_1fr)] gap-[5px] w-full text-[8px] font-semibold h-5 text-slate-800 dark:text-slate-200"
grid-template-rows: 1fr; >
gap: $tile-gap; <div
flex-shrink: 0; v-for="ii in 24"
:key="ii"
.heatmap-axis-label { class="flex items-center justify-center"
height: $tile-height; >
min-width: 70px; {{ ii - 1 }} {{ ii }}
font-size: var(--font-size-micro); </div>
font-weight: var(--font-weight-bold); </div>
display: flex; </template>
flex-direction: column; </div>
align-items: flex-end; </template>
justify-content: center;
@apply text-slate-800 dark:text-slate-200;
time {
font-size: var(--font-size-micro);
font-weight: var(--font-weight-normal);
@apply text-slate-700 dark:text-slate-200;
}
}
}
.heatmap-grid {
display: grid;
grid-template-rows: 1fr;
gap: $tile-gap;
min-width: 700px;
width: 100%;
.heatmap-grid-row {
display: grid;
gap: $tile-gap;
grid-template-columns: repeat(24, 1fr);
}
.heatmap-tile {
width: auto;
height: $tile-height;
border-radius: var(--border-radius-normal);
&:hover {
box-shadow: var(--shadow-large);
transform: translateY(-2px);
transition:
transform 0.2s ease-in-out,
box-shadow 0.2s ease-in-out;
}
&:not(.l1):not(.l2):not(.l3):not(.l4):not(.l5):not(.l6) {
background-color: var(--color-background-light);
border: 1px solid var(--color-border-light);
&:hover {
transform: translateY(0);
box-shadow: none;
border: 1px solid var(--color-border-light);
}
}
&.l1 {
@include heatmap-level(1);
}
&.l2 {
@include heatmap-level(2);
}
&.l3 {
@include heatmap-level(3);
}
&.l4 {
@include heatmap-level(4);
}
&.l5 {
@include heatmap-level(5);
}
&.l6 {
@include heatmap-level(6);
}
}
}
.heatmap-markers {
display: grid;
grid-template-columns: repeat(24, 1fr);
gap: $tile-gap;
width: 100%;
font-size: var(--font-size-nano);
font-weight: var(--font-weight-bold);
height: $marker-height;
color: var(--color-body);
@apply text-slate-800 dark:text-slate-200;
div {
display: flex;
align-items: center;
justify-content: center;
}
}
</style>