You've already forked AstralRinth
forked from didirus/AstralRinth
Fix graph state & data handling (#1578)
* Rip out external color state * Fix styling errors * Allow charts to display personal/entity perspectives on routes * Refactor analytics data processing and selection * Include custom color icon
This commit is contained in:
@@ -3,11 +3,6 @@ import dayjs from 'dayjs'
|
||||
import { formatNumber, formatMoney } from 'omorphia'
|
||||
import VueApexCharts from 'vue3-apexcharts'
|
||||
|
||||
// let VueApexCharts
|
||||
// if (process.client) {
|
||||
// VueApexCharts = defineAsyncComponent(() => import('vue3-apexcharts'))
|
||||
// }
|
||||
|
||||
const props = defineProps({
|
||||
name: {
|
||||
type: String,
|
||||
@@ -158,120 +153,124 @@ function generateTooltip({ series, seriesIndex, dataPointIndex, w }, props) {
|
||||
return tooltip
|
||||
}
|
||||
|
||||
const chartOptions = {
|
||||
chart: {
|
||||
id: props.name,
|
||||
fontFamily:
|
||||
'Inter, -apple-system, BlinkMacSystemFont, Segoe UI, Oxygen, Ubuntu, Roboto, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif',
|
||||
foreColor: 'var(--color-base)',
|
||||
selection: {
|
||||
enabled: true,
|
||||
fill: {
|
||||
color: 'var(--color-brand)',
|
||||
const chartOptions = computed(() => {
|
||||
return {
|
||||
chart: {
|
||||
id: props.name,
|
||||
fontFamily:
|
||||
'Inter, -apple-system, BlinkMacSystemFont, Segoe UI, Oxygen, Ubuntu, Roboto, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif',
|
||||
foreColor: 'var(--color-base)',
|
||||
selection: {
|
||||
enabled: true,
|
||||
fill: {
|
||||
color: 'var(--color-brand)',
|
||||
},
|
||||
},
|
||||
toolbar: {
|
||||
show: false,
|
||||
},
|
||||
stacked: props.stacked,
|
||||
stackType: props.percentStacked ? '100%' : 'normal',
|
||||
zoom: {
|
||||
autoScaleYaxis: true,
|
||||
},
|
||||
animations: {
|
||||
enabled: props.disableAnimations,
|
||||
},
|
||||
},
|
||||
toolbar: {
|
||||
show: false,
|
||||
},
|
||||
stacked: props.stacked,
|
||||
stackType: props.percentStacked ? '100%' : 'normal',
|
||||
zoom: {
|
||||
autoScaleYaxis: true,
|
||||
},
|
||||
animations: {
|
||||
enabled: props.disableAnimations,
|
||||
},
|
||||
},
|
||||
xaxis: {
|
||||
type: props.xAxisType,
|
||||
categories: props.labels,
|
||||
labels: {
|
||||
style: {
|
||||
borderRadius: 'var(--radius-sm)',
|
||||
xaxis: {
|
||||
type: props.xAxisType,
|
||||
categories: props.labels,
|
||||
labels: {
|
||||
style: {
|
||||
borderRadius: 'var(--radius-sm)',
|
||||
},
|
||||
},
|
||||
axisTicks: {
|
||||
show: false,
|
||||
},
|
||||
tooltip: {
|
||||
enabled: false,
|
||||
},
|
||||
},
|
||||
axisTicks: {
|
||||
show: false,
|
||||
yaxis: {
|
||||
tooltip: {
|
||||
enabled: false,
|
||||
},
|
||||
},
|
||||
colors: props.colors,
|
||||
dataLabels: {
|
||||
enabled: false,
|
||||
background: {
|
||||
enabled: true,
|
||||
borderRadius: 20,
|
||||
},
|
||||
},
|
||||
grid: {
|
||||
borderColor: 'var(--color-button-bg)',
|
||||
tickColor: 'var(--color-button-bg)',
|
||||
},
|
||||
legend: {
|
||||
show: !props.hideLegend,
|
||||
position: props.legendPosition,
|
||||
showForZeroSeries: false,
|
||||
showForSingleSeries: false,
|
||||
showForNullSeries: false,
|
||||
fontSize: 'var(--font-size-nm)',
|
||||
fontFamily:
|
||||
'Inter, -apple-system, BlinkMacSystemFont, Segoe UI, Oxygen, Ubuntu, Roboto, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif',
|
||||
onItemClick: {
|
||||
toggleDataSeries: true,
|
||||
},
|
||||
},
|
||||
markers: {
|
||||
size: 0,
|
||||
strokeColor: 'var(--color-contrast)',
|
||||
strokeWidth: 3,
|
||||
strokeOpacity: 1,
|
||||
fillOpacity: 1,
|
||||
hover: {
|
||||
size: 6,
|
||||
},
|
||||
},
|
||||
plotOptions: {
|
||||
bar: {
|
||||
horizontal: props.horizontalBar,
|
||||
columnWidth: '80%',
|
||||
endingShape: 'rounded',
|
||||
borderRadius: 5,
|
||||
borderRadiusApplication: 'end',
|
||||
borderRadiusWhenStacked: 'last',
|
||||
},
|
||||
},
|
||||
stroke: {
|
||||
curve: 'smooth',
|
||||
width: 2,
|
||||
},
|
||||
tooltip: {
|
||||
enabled: false,
|
||||
custom: (d) => generateTooltip(d, props),
|
||||
},
|
||||
},
|
||||
yaxis: {
|
||||
tooltip: {
|
||||
enabled: false,
|
||||
},
|
||||
},
|
||||
colors: props.colors,
|
||||
dataLabels: {
|
||||
enabled: false,
|
||||
background: {
|
||||
enabled: true,
|
||||
borderRadius: 20,
|
||||
},
|
||||
},
|
||||
grid: {
|
||||
borderColor: 'var(--color-button-bg)',
|
||||
tickColor: 'var(--color-button-bg)',
|
||||
},
|
||||
legend: {
|
||||
show: !props.hideLegend,
|
||||
position: props.legendPosition,
|
||||
showForZeroSeries: false,
|
||||
showForSingleSeries: false,
|
||||
showForNullSeries: false,
|
||||
fontSize: 'var(--font-size-nm)',
|
||||
fontFamily:
|
||||
'Inter, -apple-system, BlinkMacSystemFont, Segoe UI, Oxygen, Ubuntu, Roboto, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif',
|
||||
onItemClick: {
|
||||
toggleDataSeries: true,
|
||||
},
|
||||
},
|
||||
markers: {
|
||||
size: 0,
|
||||
strokeColor: 'var(--color-contrast)',
|
||||
strokeWidth: 3,
|
||||
strokeOpacity: 1,
|
||||
fillOpacity: 1,
|
||||
hover: {
|
||||
size: 6,
|
||||
},
|
||||
},
|
||||
plotOptions: {
|
||||
bar: {
|
||||
horizontal: props.horizontalBar,
|
||||
columnWidth: '80%',
|
||||
endingShape: 'rounded',
|
||||
borderRadius: 5,
|
||||
borderRadiusApplication: 'end',
|
||||
borderRadiusWhenStacked: 'last',
|
||||
},
|
||||
},
|
||||
stroke: {
|
||||
curve: 'smooth',
|
||||
width: 2,
|
||||
},
|
||||
tooltip: {
|
||||
custom: (d) => generateTooltip(d, props),
|
||||
},
|
||||
}
|
||||
|
||||
const fillOptions = {
|
||||
colors: props.colors,
|
||||
type: 'gradient',
|
||||
opacity: 1,
|
||||
gradient: {
|
||||
shade: 'light',
|
||||
type: 'vertical',
|
||||
shadeIntensity: 0,
|
||||
gradientToColors: props.colors,
|
||||
inverseColors: true,
|
||||
opacityFrom: 0.5,
|
||||
opacityTo: 0,
|
||||
stops: [0, 100],
|
||||
colorStops: [],
|
||||
},
|
||||
}
|
||||
fill:
|
||||
props.type === 'area'
|
||||
? {
|
||||
colors: props.colors,
|
||||
type: 'gradient',
|
||||
opacity: 1,
|
||||
gradient: {
|
||||
shade: 'light',
|
||||
type: 'vertical',
|
||||
shadeIntensity: 0,
|
||||
gradientToColors: props.colors,
|
||||
inverseColors: true,
|
||||
opacityFrom: 0.5,
|
||||
opacityTo: 0,
|
||||
stops: [0, 100],
|
||||
colorStops: [],
|
||||
},
|
||||
}
|
||||
: {},
|
||||
}
|
||||
})
|
||||
|
||||
const chart = ref(null)
|
||||
|
||||
@@ -287,6 +286,7 @@ const flipLegend = (legend, newVal) => {
|
||||
}
|
||||
|
||||
const resetChart = () => {
|
||||
if (!chart.value) return
|
||||
chart.value.updateSeries([...props.data])
|
||||
chart.value.updateOptions({
|
||||
xaxis: {
|
||||
@@ -299,31 +299,14 @@ const resetChart = () => {
|
||||
})
|
||||
}
|
||||
|
||||
const updateColors = (colors) => {
|
||||
chart.value.updateOptions({
|
||||
colors,
|
||||
})
|
||||
chart.value.resetSeries()
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
resetChart,
|
||||
updateColors,
|
||||
flipLegend,
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VueApexCharts
|
||||
ref="chart"
|
||||
:type="type"
|
||||
:options="{
|
||||
...chartOptions,
|
||||
fill: type === 'area' ? fillOptions : {},
|
||||
}"
|
||||
:series="data"
|
||||
class="chart"
|
||||
/>
|
||||
<VueApexCharts ref="chart" :type="type" :options="chartOptions" :series="data" class="chart" />
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
? 'chart-button-base__selected button-base__selected'
|
||||
: ''
|
||||
}`"
|
||||
:onclick="() => setSelectedChart('downloads')"
|
||||
:onclick="() => (selectedChart = 'downloads')"
|
||||
role="button"
|
||||
/>
|
||||
</client-only>
|
||||
@@ -42,7 +42,7 @@
|
||||
:class="`clickable chart-button-base button-base ${
|
||||
selectedChart === 'views' ? 'chart-button-base__selected button-base__selected' : ''
|
||||
}`"
|
||||
:onclick="() => setSelectedChart('views')"
|
||||
:onclick="() => (selectedChart = 'views')"
|
||||
role="button"
|
||||
/>
|
||||
</client-only>
|
||||
@@ -59,7 +59,7 @@
|
||||
:class="`clickable chart-button-base button-base ${
|
||||
selectedChart === 'revenue' ? 'chart-button-base__selected button-base__selected' : ''
|
||||
}`"
|
||||
:onclick="() => setSelectedChart('revenue')"
|
||||
:onclick="() => (selectedChart = 'revenue')"
|
||||
role="button"
|
||||
/>
|
||||
</client-only>
|
||||
@@ -74,7 +74,7 @@
|
||||
</h2>
|
||||
<div class="chart-controls__buttons">
|
||||
<Button v-tooltip="'Toggle project colors'" icon-only @click="onToggleColors">
|
||||
<EyeIcon />
|
||||
<PaletteIcon />
|
||||
</Button>
|
||||
<Button v-tooltip="'Download this data as CSV'" icon-only @click="onDownloadSetAsCSV">
|
||||
<DownloadIcon />
|
||||
@@ -102,7 +102,11 @@
|
||||
:data="analytics.formattedData.value.downloads.chart.data"
|
||||
:labels="analytics.formattedData.value.downloads.chart.labels"
|
||||
suffix="<svg xmlns='http://www.w3.org/2000/svg' class='h-6 w-6' fill='none' viewBox='0 0 24 24' stroke='currentColor' stroke-width='2'><path stroke-linecap='round' stroke-linejoin='round' d='M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4' /></svg>"
|
||||
:colors="analytics.formattedData.value.downloads.chart.colors"
|
||||
:colors="
|
||||
isUsingProjectColors
|
||||
? analytics.formattedData.value.downloads.chart.colors
|
||||
: analytics.formattedData.value.downloads.chart.defaultColors
|
||||
"
|
||||
/>
|
||||
<Chart
|
||||
v-if="analytics.formattedData.value.views && selectedChart === 'views'"
|
||||
@@ -113,7 +117,11 @@
|
||||
:data="analytics.formattedData.value.views.chart.data"
|
||||
:labels="analytics.formattedData.value.views.chart.labels"
|
||||
suffix="<svg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='currentColor' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'><path d='M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z'/><circle cx='12' cy='12' r='3'/></svg>"
|
||||
:colors="analytics.formattedData.value.views.chart.colors"
|
||||
:colors="
|
||||
isUsingProjectColors
|
||||
? analytics.formattedData.value.views.chart.colors
|
||||
: analytics.formattedData.value.views.chart.defaultColors
|
||||
"
|
||||
/>
|
||||
<Chart
|
||||
v-if="analytics.formattedData.value.revenue && selectedChart === 'revenue'"
|
||||
@@ -125,22 +133,26 @@
|
||||
:labels="analytics.formattedData.value.revenue.chart.labels"
|
||||
is-money
|
||||
suffix="<svg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='currentColor' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'><line x1='12' y1='2' x2='12' y2='22'></line><path d='M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6'></path></svg>"
|
||||
:colors="analytics.formattedData.value.revenue.chart.colors"
|
||||
:colors="
|
||||
isUsingProjectColors
|
||||
? analytics.formattedData.value.revenue.chart.colors
|
||||
: analytics.formattedData.value.revenue.chart.defaultColors
|
||||
"
|
||||
/>
|
||||
</client-only>
|
||||
</div>
|
||||
<div class="legend">
|
||||
<div class="legend__items">
|
||||
<template v-for="project in props.projects" :key="project.id">
|
||||
<template v-for="project in selectedDataSetProjects" :key="project">
|
||||
<button
|
||||
v-if="analytics.validProjectIds.value.includes(project.id)"
|
||||
v-tooltip="project.title"
|
||||
:class="`legend__item button-base btn-transparent ${
|
||||
!projectIsOnDisplay(project.id) ? 'btn-dimmed' : ''
|
||||
}`"
|
||||
@click="
|
||||
() =>
|
||||
projectIsOnDisplay(project.id)
|
||||
projectIsOnDisplay(project.id) &&
|
||||
analytics.validProjectIds.value.includes(project.id)
|
||||
? removeProjectFromDisplay(project.id)
|
||||
: addProjectToDisplay(project.id)
|
||||
"
|
||||
@@ -286,7 +298,6 @@ import {
|
||||
formatNumber,
|
||||
DropdownSelect,
|
||||
formatCategoryHeader,
|
||||
EyeIcon,
|
||||
} from 'omorphia'
|
||||
import dayjs from 'dayjs'
|
||||
import { defineProps, ref, computed } from 'vue'
|
||||
@@ -295,6 +306,8 @@ import { analyticsSetToCSVString, intToRgba } from '~/utils/analytics.js'
|
||||
|
||||
import { UiChartsCompactChart as CompactChart, UiChartsChart as Chart } from '#components'
|
||||
|
||||
import PaletteIcon from '~/assets/icons/palette.svg'
|
||||
|
||||
const router = useRouter()
|
||||
const theme = useTheme()
|
||||
|
||||
@@ -306,14 +319,18 @@ const props = withDefaults(
|
||||
*/
|
||||
resoloutions?: Record<string, number>
|
||||
ranges?: Record<number, [string, number] | string>
|
||||
personal?: boolean
|
||||
}>(),
|
||||
{
|
||||
projects: undefined,
|
||||
resoloutions: () => defaultResoloutions,
|
||||
ranges: () => defaultRanges,
|
||||
personal: false,
|
||||
}
|
||||
)
|
||||
|
||||
const projects = ref(props.projects || [])
|
||||
|
||||
const selectableRanges = Object.entries(props.ranges).map(([duration, extra]) => ({
|
||||
label: typeof extra === 'string' ? extra : extra[0],
|
||||
value: Number(duration),
|
||||
@@ -321,22 +338,24 @@ const selectableRanges = Object.entries(props.ranges).map(([duration, extra]) =>
|
||||
}))
|
||||
|
||||
// const selectedChart = ref('downloads')
|
||||
const selectedChart = computed(() => {
|
||||
const id = (router.currentRoute.value.query?.chart as string | undefined) || 'downloads'
|
||||
// if the id is anything but the 3 charts we have or undefined, throw an error
|
||||
if (!['downloads', 'views', 'revenue'].includes(id)) {
|
||||
throw new Error(`Unknown chart ${id}`)
|
||||
}
|
||||
return id
|
||||
const selectedChart = computed({
|
||||
get: () => {
|
||||
const id = (router.currentRoute.value.query?.chart as string | undefined) || 'downloads'
|
||||
// if the id is anything but the 3 charts we have or undefined, throw an error
|
||||
if (!['downloads', 'views', 'revenue'].includes(id)) {
|
||||
throw new Error(`Unknown chart ${id}`)
|
||||
}
|
||||
return id
|
||||
},
|
||||
set: (chart) => {
|
||||
router.push({
|
||||
query: {
|
||||
...router.currentRoute.value.query,
|
||||
chart,
|
||||
},
|
||||
})
|
||||
},
|
||||
})
|
||||
const setSelectedChart = (chart: string) => {
|
||||
router.push({
|
||||
query: {
|
||||
...router.currentRoute.value.query,
|
||||
chart,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// Chart refs
|
||||
const downloadsChart = ref()
|
||||
@@ -360,25 +379,7 @@ const addProjectToDisplay = (id: string) => {
|
||||
}
|
||||
|
||||
const projectIsOnDisplay = (id: string) => {
|
||||
return selectedDisplayProjects.value.some((p) => p.id === id)
|
||||
}
|
||||
|
||||
const setChartColors = (updatedVal: Ref<boolean>) => {
|
||||
downloadsChart.value?.updateColors(
|
||||
updatedVal.value
|
||||
? analytics.formattedData.value.downloads.chart.colors
|
||||
: analytics.formattedData.value.downloads.chart.defaultColors
|
||||
)
|
||||
viewsChart.value?.updateColors(
|
||||
updatedVal.value
|
||||
? analytics.formattedData.value.views.chart.colors
|
||||
: analytics.formattedData.value.views.chart.defaultColors
|
||||
)
|
||||
revenueChart.value?.updateColors(
|
||||
updatedVal.value
|
||||
? analytics.formattedData.value.revenue.chart.colors
|
||||
: analytics.formattedData.value.revenue.chart.defaultColors
|
||||
)
|
||||
return selectedDisplayProjects.value?.some((p) => p.id === id) ?? false
|
||||
}
|
||||
|
||||
const resetCharts = () => {
|
||||
@@ -389,16 +390,31 @@ const resetCharts = () => {
|
||||
tinyDownloadChart.value?.resetChart()
|
||||
tinyViewChart.value?.resetChart()
|
||||
tinyRevenueChart.value?.resetChart()
|
||||
|
||||
setChartColors(isUsingProjectColors)
|
||||
}
|
||||
|
||||
const isUsingProjectColors = ref(true)
|
||||
watch(() => isUsingProjectColors, setChartColors, {
|
||||
deep: true,
|
||||
const isUsingProjectColors = computed({
|
||||
get: () => {
|
||||
return (
|
||||
router.currentRoute.value.query?.colors === 'true' ||
|
||||
router.currentRoute.value.query?.colors === undefined
|
||||
)
|
||||
},
|
||||
set: (newValue) => {
|
||||
router.push({
|
||||
query: {
|
||||
...router.currentRoute.value.query,
|
||||
colors: newValue ? 'true' : 'false',
|
||||
},
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
const analytics = useFetchAllAnalytics(resetCharts, selectedDisplayProjects)
|
||||
const analytics = useFetchAllAnalytics(
|
||||
resetCharts,
|
||||
projects,
|
||||
selectedDisplayProjects,
|
||||
props.personal
|
||||
)
|
||||
|
||||
const { startDate, endDate, timeRange, timeResolution } = analytics
|
||||
|
||||
@@ -422,26 +438,28 @@ const selectedRange = computed({
|
||||
},
|
||||
})
|
||||
|
||||
const selectedDataSet = computed(() => {
|
||||
switch (selectedChart.value) {
|
||||
case 'downloads':
|
||||
return analytics.totalData.value.downloads
|
||||
case 'views':
|
||||
return analytics.totalData.value.views
|
||||
case 'revenue':
|
||||
return analytics.totalData.value.revenue
|
||||
default:
|
||||
throw new Error(`Unknown chart ${selectedChart.value}`)
|
||||
}
|
||||
})
|
||||
const selectedDataSetProjects = computed(() => {
|
||||
return selectedDataSet.value.projectIds
|
||||
.map((id) => props.projects?.find((p) => p?.id === id))
|
||||
.filter(Boolean)
|
||||
})
|
||||
|
||||
const downloadSelectedSetAsCSV = () => {
|
||||
const selectedChartName = selectedChart.value
|
||||
|
||||
let downloadsDataSet
|
||||
|
||||
switch (selectedChartName) {
|
||||
case 'downloads':
|
||||
downloadsDataSet = analytics.formattedData.value.downloads
|
||||
break
|
||||
case 'views':
|
||||
downloadsDataSet = analytics.formattedData.value.views
|
||||
break
|
||||
case 'revenue':
|
||||
downloadsDataSet = analytics.formattedData.value.revenue
|
||||
break
|
||||
default:
|
||||
throw new Error(`Unknown chart ${selectedChartName}`)
|
||||
}
|
||||
|
||||
const csv = analyticsSetToCSVString(downloadsDataSet)
|
||||
const csv = analyticsSetToCSVString(selectedDataSet.value)
|
||||
|
||||
const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' })
|
||||
|
||||
|
||||
Reference in New Issue
Block a user