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:
1
assets/icons/palette.svg
Normal file
1
assets/icons/palette.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-palette" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M12 21a9 9 0 0 1 0 -18c4.97 0 9 3.582 9 8c0 1.06 -.474 2.078 -1.318 2.828c-.844 .75 -1.989 1.172 -3.182 1.172h-2.5a2 2 0 0 0 -1 3.75a1.3 1.3 0 0 1 -1 2.25" /><path d="M8.5 10.5m-1 0a1 1 0 1 0 2 0a1 1 0 1 0 -2 0" /><path d="M12.5 7.5m-1 0a1 1 0 1 0 2 0a1 1 0 1 0 -2 0" /><path d="M16.5 10.5m-1 0a1 1 0 1 0 2 0a1 1 0 1 0 -2 0" /></svg>
|
||||||
|
After Width: | Height: | Size: 619 B |
@@ -3,11 +3,6 @@ import dayjs from 'dayjs'
|
|||||||
import { formatNumber, formatMoney } from 'omorphia'
|
import { formatNumber, formatMoney } from 'omorphia'
|
||||||
import VueApexCharts from 'vue3-apexcharts'
|
import VueApexCharts from 'vue3-apexcharts'
|
||||||
|
|
||||||
// let VueApexCharts
|
|
||||||
// if (process.client) {
|
|
||||||
// VueApexCharts = defineAsyncComponent(() => import('vue3-apexcharts'))
|
|
||||||
// }
|
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
name: {
|
name: {
|
||||||
type: String,
|
type: String,
|
||||||
@@ -158,120 +153,124 @@ function generateTooltip({ series, seriesIndex, dataPointIndex, w }, props) {
|
|||||||
return tooltip
|
return tooltip
|
||||||
}
|
}
|
||||||
|
|
||||||
const chartOptions = {
|
const chartOptions = computed(() => {
|
||||||
chart: {
|
return {
|
||||||
id: props.name,
|
chart: {
|
||||||
fontFamily:
|
id: props.name,
|
||||||
'Inter, -apple-system, BlinkMacSystemFont, Segoe UI, Oxygen, Ubuntu, Roboto, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif',
|
fontFamily:
|
||||||
foreColor: 'var(--color-base)',
|
'Inter, -apple-system, BlinkMacSystemFont, Segoe UI, Oxygen, Ubuntu, Roboto, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif',
|
||||||
selection: {
|
foreColor: 'var(--color-base)',
|
||||||
enabled: true,
|
selection: {
|
||||||
fill: {
|
enabled: true,
|
||||||
color: 'var(--color-brand)',
|
fill: {
|
||||||
|
color: 'var(--color-brand)',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
toolbar: {
|
||||||
|
show: false,
|
||||||
|
},
|
||||||
|
stacked: props.stacked,
|
||||||
|
stackType: props.percentStacked ? '100%' : 'normal',
|
||||||
|
zoom: {
|
||||||
|
autoScaleYaxis: true,
|
||||||
|
},
|
||||||
|
animations: {
|
||||||
|
enabled: props.disableAnimations,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
toolbar: {
|
xaxis: {
|
||||||
show: false,
|
type: props.xAxisType,
|
||||||
},
|
categories: props.labels,
|
||||||
stacked: props.stacked,
|
labels: {
|
||||||
stackType: props.percentStacked ? '100%' : 'normal',
|
style: {
|
||||||
zoom: {
|
borderRadius: 'var(--radius-sm)',
|
||||||
autoScaleYaxis: true,
|
},
|
||||||
},
|
},
|
||||||
animations: {
|
axisTicks: {
|
||||||
enabled: props.disableAnimations,
|
show: false,
|
||||||
},
|
},
|
||||||
},
|
tooltip: {
|
||||||
xaxis: {
|
enabled: false,
|
||||||
type: props.xAxisType,
|
|
||||||
categories: props.labels,
|
|
||||||
labels: {
|
|
||||||
style: {
|
|
||||||
borderRadius: 'var(--radius-sm)',
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
axisTicks: {
|
yaxis: {
|
||||||
show: false,
|
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: {
|
tooltip: {
|
||||||
enabled: false,
|
custom: (d) => generateTooltip(d, props),
|
||||||
},
|
},
|
||||||
},
|
fill:
|
||||||
yaxis: {
|
props.type === 'area'
|
||||||
tooltip: {
|
? {
|
||||||
enabled: false,
|
colors: props.colors,
|
||||||
},
|
type: 'gradient',
|
||||||
},
|
opacity: 1,
|
||||||
colors: props.colors,
|
gradient: {
|
||||||
dataLabels: {
|
shade: 'light',
|
||||||
enabled: false,
|
type: 'vertical',
|
||||||
background: {
|
shadeIntensity: 0,
|
||||||
enabled: true,
|
gradientToColors: props.colors,
|
||||||
borderRadius: 20,
|
inverseColors: true,
|
||||||
},
|
opacityFrom: 0.5,
|
||||||
},
|
opacityTo: 0,
|
||||||
grid: {
|
stops: [0, 100],
|
||||||
borderColor: 'var(--color-button-bg)',
|
colorStops: [],
|
||||||
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: [],
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
const chart = ref(null)
|
const chart = ref(null)
|
||||||
|
|
||||||
@@ -287,6 +286,7 @@ const flipLegend = (legend, newVal) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const resetChart = () => {
|
const resetChart = () => {
|
||||||
|
if (!chart.value) return
|
||||||
chart.value.updateSeries([...props.data])
|
chart.value.updateSeries([...props.data])
|
||||||
chart.value.updateOptions({
|
chart.value.updateOptions({
|
||||||
xaxis: {
|
xaxis: {
|
||||||
@@ -299,31 +299,14 @@ const resetChart = () => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const updateColors = (colors) => {
|
|
||||||
chart.value.updateOptions({
|
|
||||||
colors,
|
|
||||||
})
|
|
||||||
chart.value.resetSeries()
|
|
||||||
}
|
|
||||||
|
|
||||||
defineExpose({
|
defineExpose({
|
||||||
resetChart,
|
resetChart,
|
||||||
updateColors,
|
|
||||||
flipLegend,
|
flipLegend,
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<VueApexCharts
|
<VueApexCharts ref="chart" :type="type" :options="chartOptions" :series="data" class="chart" />
|
||||||
ref="chart"
|
|
||||||
:type="type"
|
|
||||||
:options="{
|
|
||||||
...chartOptions,
|
|
||||||
fill: type === 'area' ? fillOptions : {},
|
|
||||||
}"
|
|
||||||
:series="data"
|
|
||||||
class="chart"
|
|
||||||
/>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
|
|||||||
@@ -25,7 +25,7 @@
|
|||||||
? 'chart-button-base__selected button-base__selected'
|
? 'chart-button-base__selected button-base__selected'
|
||||||
: ''
|
: ''
|
||||||
}`"
|
}`"
|
||||||
:onclick="() => setSelectedChart('downloads')"
|
:onclick="() => (selectedChart = 'downloads')"
|
||||||
role="button"
|
role="button"
|
||||||
/>
|
/>
|
||||||
</client-only>
|
</client-only>
|
||||||
@@ -42,7 +42,7 @@
|
|||||||
:class="`clickable chart-button-base button-base ${
|
:class="`clickable chart-button-base button-base ${
|
||||||
selectedChart === 'views' ? 'chart-button-base__selected button-base__selected' : ''
|
selectedChart === 'views' ? 'chart-button-base__selected button-base__selected' : ''
|
||||||
}`"
|
}`"
|
||||||
:onclick="() => setSelectedChart('views')"
|
:onclick="() => (selectedChart = 'views')"
|
||||||
role="button"
|
role="button"
|
||||||
/>
|
/>
|
||||||
</client-only>
|
</client-only>
|
||||||
@@ -59,7 +59,7 @@
|
|||||||
:class="`clickable chart-button-base button-base ${
|
:class="`clickable chart-button-base button-base ${
|
||||||
selectedChart === 'revenue' ? 'chart-button-base__selected button-base__selected' : ''
|
selectedChart === 'revenue' ? 'chart-button-base__selected button-base__selected' : ''
|
||||||
}`"
|
}`"
|
||||||
:onclick="() => setSelectedChart('revenue')"
|
:onclick="() => (selectedChart = 'revenue')"
|
||||||
role="button"
|
role="button"
|
||||||
/>
|
/>
|
||||||
</client-only>
|
</client-only>
|
||||||
@@ -74,7 +74,7 @@
|
|||||||
</h2>
|
</h2>
|
||||||
<div class="chart-controls__buttons">
|
<div class="chart-controls__buttons">
|
||||||
<Button v-tooltip="'Toggle project colors'" icon-only @click="onToggleColors">
|
<Button v-tooltip="'Toggle project colors'" icon-only @click="onToggleColors">
|
||||||
<EyeIcon />
|
<PaletteIcon />
|
||||||
</Button>
|
</Button>
|
||||||
<Button v-tooltip="'Download this data as CSV'" icon-only @click="onDownloadSetAsCSV">
|
<Button v-tooltip="'Download this data as CSV'" icon-only @click="onDownloadSetAsCSV">
|
||||||
<DownloadIcon />
|
<DownloadIcon />
|
||||||
@@ -102,7 +102,11 @@
|
|||||||
:data="analytics.formattedData.value.downloads.chart.data"
|
:data="analytics.formattedData.value.downloads.chart.data"
|
||||||
:labels="analytics.formattedData.value.downloads.chart.labels"
|
: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>"
|
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
|
<Chart
|
||||||
v-if="analytics.formattedData.value.views && selectedChart === 'views'"
|
v-if="analytics.formattedData.value.views && selectedChart === 'views'"
|
||||||
@@ -113,7 +117,11 @@
|
|||||||
:data="analytics.formattedData.value.views.chart.data"
|
:data="analytics.formattedData.value.views.chart.data"
|
||||||
:labels="analytics.formattedData.value.views.chart.labels"
|
: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>"
|
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
|
<Chart
|
||||||
v-if="analytics.formattedData.value.revenue && selectedChart === 'revenue'"
|
v-if="analytics.formattedData.value.revenue && selectedChart === 'revenue'"
|
||||||
@@ -125,22 +133,26 @@
|
|||||||
:labels="analytics.formattedData.value.revenue.chart.labels"
|
:labels="analytics.formattedData.value.revenue.chart.labels"
|
||||||
is-money
|
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>"
|
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>
|
</client-only>
|
||||||
</div>
|
</div>
|
||||||
<div class="legend">
|
<div class="legend">
|
||||||
<div class="legend__items">
|
<div class="legend__items">
|
||||||
<template v-for="project in props.projects" :key="project.id">
|
<template v-for="project in selectedDataSetProjects" :key="project">
|
||||||
<button
|
<button
|
||||||
v-if="analytics.validProjectIds.value.includes(project.id)"
|
|
||||||
v-tooltip="project.title"
|
v-tooltip="project.title"
|
||||||
:class="`legend__item button-base btn-transparent ${
|
:class="`legend__item button-base btn-transparent ${
|
||||||
!projectIsOnDisplay(project.id) ? 'btn-dimmed' : ''
|
!projectIsOnDisplay(project.id) ? 'btn-dimmed' : ''
|
||||||
}`"
|
}`"
|
||||||
@click="
|
@click="
|
||||||
() =>
|
() =>
|
||||||
projectIsOnDisplay(project.id)
|
projectIsOnDisplay(project.id) &&
|
||||||
|
analytics.validProjectIds.value.includes(project.id)
|
||||||
? removeProjectFromDisplay(project.id)
|
? removeProjectFromDisplay(project.id)
|
||||||
: addProjectToDisplay(project.id)
|
: addProjectToDisplay(project.id)
|
||||||
"
|
"
|
||||||
@@ -286,7 +298,6 @@ import {
|
|||||||
formatNumber,
|
formatNumber,
|
||||||
DropdownSelect,
|
DropdownSelect,
|
||||||
formatCategoryHeader,
|
formatCategoryHeader,
|
||||||
EyeIcon,
|
|
||||||
} from 'omorphia'
|
} from 'omorphia'
|
||||||
import dayjs from 'dayjs'
|
import dayjs from 'dayjs'
|
||||||
import { defineProps, ref, computed } from 'vue'
|
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 { UiChartsCompactChart as CompactChart, UiChartsChart as Chart } from '#components'
|
||||||
|
|
||||||
|
import PaletteIcon from '~/assets/icons/palette.svg'
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const theme = useTheme()
|
const theme = useTheme()
|
||||||
|
|
||||||
@@ -306,14 +319,18 @@ const props = withDefaults(
|
|||||||
*/
|
*/
|
||||||
resoloutions?: Record<string, number>
|
resoloutions?: Record<string, number>
|
||||||
ranges?: Record<number, [string, number] | string>
|
ranges?: Record<number, [string, number] | string>
|
||||||
|
personal?: boolean
|
||||||
}>(),
|
}>(),
|
||||||
{
|
{
|
||||||
projects: undefined,
|
projects: undefined,
|
||||||
resoloutions: () => defaultResoloutions,
|
resoloutions: () => defaultResoloutions,
|
||||||
ranges: () => defaultRanges,
|
ranges: () => defaultRanges,
|
||||||
|
personal: false,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const projects = ref(props.projects || [])
|
||||||
|
|
||||||
const selectableRanges = Object.entries(props.ranges).map(([duration, extra]) => ({
|
const selectableRanges = Object.entries(props.ranges).map(([duration, extra]) => ({
|
||||||
label: typeof extra === 'string' ? extra : extra[0],
|
label: typeof extra === 'string' ? extra : extra[0],
|
||||||
value: Number(duration),
|
value: Number(duration),
|
||||||
@@ -321,22 +338,24 @@ const selectableRanges = Object.entries(props.ranges).map(([duration, extra]) =>
|
|||||||
}))
|
}))
|
||||||
|
|
||||||
// const selectedChart = ref('downloads')
|
// const selectedChart = ref('downloads')
|
||||||
const selectedChart = computed(() => {
|
const selectedChart = computed({
|
||||||
const id = (router.currentRoute.value.query?.chart as string | undefined) || 'downloads'
|
get: () => {
|
||||||
// if the id is anything but the 3 charts we have or undefined, throw an error
|
const id = (router.currentRoute.value.query?.chart as string | undefined) || 'downloads'
|
||||||
if (!['downloads', 'views', 'revenue'].includes(id)) {
|
// if the id is anything but the 3 charts we have or undefined, throw an error
|
||||||
throw new Error(`Unknown chart ${id}`)
|
if (!['downloads', 'views', 'revenue'].includes(id)) {
|
||||||
}
|
throw new Error(`Unknown chart ${id}`)
|
||||||
return 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
|
// Chart refs
|
||||||
const downloadsChart = ref()
|
const downloadsChart = ref()
|
||||||
@@ -360,25 +379,7 @@ const addProjectToDisplay = (id: string) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const projectIsOnDisplay = (id: string) => {
|
const projectIsOnDisplay = (id: string) => {
|
||||||
return selectedDisplayProjects.value.some((p) => p.id === id)
|
return selectedDisplayProjects.value?.some((p) => p.id === id) ?? false
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const resetCharts = () => {
|
const resetCharts = () => {
|
||||||
@@ -389,16 +390,31 @@ const resetCharts = () => {
|
|||||||
tinyDownloadChart.value?.resetChart()
|
tinyDownloadChart.value?.resetChart()
|
||||||
tinyViewChart.value?.resetChart()
|
tinyViewChart.value?.resetChart()
|
||||||
tinyRevenueChart.value?.resetChart()
|
tinyRevenueChart.value?.resetChart()
|
||||||
|
|
||||||
setChartColors(isUsingProjectColors)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const isUsingProjectColors = ref(true)
|
const isUsingProjectColors = computed({
|
||||||
watch(() => isUsingProjectColors, setChartColors, {
|
get: () => {
|
||||||
deep: true,
|
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
|
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 downloadSelectedSetAsCSV = () => {
|
||||||
const selectedChartName = selectedChart.value
|
const selectedChartName = selectedChart.value
|
||||||
|
|
||||||
let downloadsDataSet
|
const csv = analyticsSetToCSVString(selectedDataSet.value)
|
||||||
|
|
||||||
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 blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' })
|
const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' })
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<ChartDisplay :projects="projects ?? undefined" />
|
<ChartDisplay :projects="projects ?? undefined" :personal="true" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div class="normal-page__content">
|
||||||
<div class="universal-card">
|
<div class="universal-card">
|
||||||
<h2>Analytics</h2>
|
<h2>Analytics</h2>
|
||||||
|
|
||||||
|
|||||||
@@ -38,7 +38,22 @@ const hashProjectId = (projectId) => {
|
|||||||
return projectId.split('').reduce((acc, char) => acc + char.charCodeAt(0), 0) % 30
|
return projectId.split('').reduce((acc, char) => acc + char.charCodeAt(0), 0) % 30
|
||||||
}
|
}
|
||||||
|
|
||||||
export const defaultColors = ['#ff496e', '#ffa347', '#1bd96a', '#4f9cff', '#c78aff']
|
export const defaultColors = [
|
||||||
|
'#ff496e', // Original: Bright pink
|
||||||
|
'#ffa347', // Original: Bright orange
|
||||||
|
'#1bd96a', // Original: Bright green
|
||||||
|
'#4f9cff', // Original: Bright blue
|
||||||
|
'#c78aff', // Original: Bright purple
|
||||||
|
'#ffeb3b', // Added: Bright yellow
|
||||||
|
'#00bcd4', // Added: Bright cyan
|
||||||
|
'#ff5722', // Added: Bright red-orange
|
||||||
|
'#9c27b0', // Added: Bright deep purple
|
||||||
|
'#3f51b5', // Added: Bright indigo
|
||||||
|
'#009688', // Added: Bright teal
|
||||||
|
'#cddc39', // Added: Bright lime
|
||||||
|
'#795548', // Added: Bright brown
|
||||||
|
'#607d8b', // Added: Bright blue-grey
|
||||||
|
]
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {string | number} value
|
* @param {string | number} value
|
||||||
@@ -111,6 +126,7 @@ const emptyAnalytics = {
|
|||||||
colors: [],
|
colors: [],
|
||||||
defaultColors: [],
|
defaultColors: [],
|
||||||
},
|
},
|
||||||
|
projectIds: [],
|
||||||
}
|
}
|
||||||
|
|
||||||
export const analyticsSetToCSVString = (analytics) => {
|
export const analyticsSetToCSVString = (analytics) => {
|
||||||
@@ -150,7 +166,6 @@ export const processAnalytics = (category, projects, labelFn, sortFn, mapFn, cha
|
|||||||
const loadedProjectData = loadedProjectIds.map((id) => category[id])
|
const loadedProjectData = loadedProjectIds.map((id) => category[id])
|
||||||
|
|
||||||
// Convert each project's data into a list of [unix_ts_str, number] pairs
|
// Convert each project's data into a list of [unix_ts_str, number] pairs
|
||||||
// Sort, label&map
|
|
||||||
const projectData = loadedProjectData
|
const projectData = loadedProjectData
|
||||||
.map((data) => Object.entries(data))
|
.map((data) => Object.entries(data))
|
||||||
.map((data) => data.sort(sortFn))
|
.map((data) => data.sort(sortFn))
|
||||||
@@ -183,6 +198,8 @@ export const processAnalytics = (category, projects, labelFn, sortFn, mapFn, cha
|
|||||||
b.data.reduce((acc, cur) => acc + cur, 0) - a.data.reduce((acc, cur) => acc + cur, 0)
|
b.data.reduce((acc, cur) => acc + cur, 0) - a.data.reduce((acc, cur) => acc + cur, 0)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const projectIdsSortedBySum = chartData.map((p) => p.id)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
// The total count of all the values across all projects
|
// The total count of all the values across all projects
|
||||||
sum: projectData.reduce((acc, cur) => acc + cur.reduce((a, c) => a + c[1], 0), 0),
|
sum: projectData.reduce((acc, cur) => acc + cur.reduce((a, c) => a + c[1], 0), 0),
|
||||||
@@ -210,6 +227,7 @@ export const processAnalytics = (category, projects, labelFn, sortFn, mapFn, cha
|
|||||||
return getDefaultColor(project.id)
|
return getDefaultColor(project.id)
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
|
projectIds: projectIdsSortedBySum,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -280,7 +298,12 @@ const useFetchAnalytics = (
|
|||||||
* @param {Ref<any[]>} projects
|
* @param {Ref<any[]>} projects
|
||||||
* @param {undefined | () => any} onDataRefresh
|
* @param {undefined | () => any} onDataRefresh
|
||||||
*/
|
*/
|
||||||
export const useFetchAllAnalytics = (onDataRefresh, projects) => {
|
export const useFetchAllAnalytics = (
|
||||||
|
onDataRefresh,
|
||||||
|
projects,
|
||||||
|
selectedProjects,
|
||||||
|
personalRevenue = false
|
||||||
|
) => {
|
||||||
const timeResolution = ref(1440) // 1 day
|
const timeResolution = ref(1440) // 1 day
|
||||||
const timeRange = ref(43200) // 30 days
|
const timeRange = ref(43200) // 30 days
|
||||||
|
|
||||||
@@ -296,17 +319,27 @@ export const useFetchAllAnalytics = (onDataRefresh, projects) => {
|
|||||||
const error = ref(null)
|
const error = ref(null)
|
||||||
|
|
||||||
const formattedData = computed(() => ({
|
const formattedData = computed(() => ({
|
||||||
|
downloads: processNumberAnalytics(downloadData.value, selectedProjects.value),
|
||||||
|
views: processNumberAnalytics(viewData.value, selectedProjects.value),
|
||||||
|
revenue: processRevAnalytics(revenueData.value, selectedProjects.value),
|
||||||
|
downloadsByCountry: processCountryAnalytics(downloadsByCountry.value, selectedProjects.value),
|
||||||
|
viewsByCountry: processCountryAnalytics(viewsByCountry.value, selectedProjects.value),
|
||||||
|
}))
|
||||||
|
|
||||||
|
const totalData = computed(() => ({
|
||||||
downloads: processNumberAnalytics(downloadData.value, projects.value),
|
downloads: processNumberAnalytics(downloadData.value, projects.value),
|
||||||
views: processNumberAnalytics(viewData.value, projects.value),
|
views: processNumberAnalytics(viewData.value, projects.value),
|
||||||
revenue: processRevAnalytics(revenueData.value, projects.value),
|
revenue: processRevAnalytics(revenueData.value, projects.value),
|
||||||
downloadsByCountry: processCountryAnalytics(downloadsByCountry.value, projects.value),
|
|
||||||
viewsByCountry: processCountryAnalytics(viewsByCountry.value, projects.value),
|
|
||||||
}))
|
}))
|
||||||
|
|
||||||
const fetchData = async (query) => {
|
const fetchData = async (query) => {
|
||||||
const normalQuery = new URLSearchParams(query)
|
const normalQuery = new URLSearchParams(query)
|
||||||
const revenueQuery = new URLSearchParams(query)
|
const revenueQuery = new URLSearchParams(query)
|
||||||
revenueQuery.delete('projects')
|
|
||||||
|
if (personalRevenue) {
|
||||||
|
revenueQuery.delete('project_ids')
|
||||||
|
}
|
||||||
|
|
||||||
const qs = normalQuery.toString()
|
const qs = normalQuery.toString()
|
||||||
const revenueQs = revenueQuery.toString()
|
const revenueQs = revenueQuery.toString()
|
||||||
|
|
||||||
@@ -324,7 +357,12 @@ export const useFetchAllAnalytics = (onDataRefresh, projects) => {
|
|||||||
|
|
||||||
// collect project ids from projects.value into a set
|
// collect project ids from projects.value into a set
|
||||||
const projectIds = new Set()
|
const projectIds = new Set()
|
||||||
projects.value.forEach((p) => projectIds.add(p.id))
|
if (projects.value) {
|
||||||
|
projects.value.forEach((p) => projectIds.add(p.id))
|
||||||
|
} else {
|
||||||
|
// if projects.value is not set, we assume that we want all project ids
|
||||||
|
Object.keys(responses[0] || {}).forEach((id) => projectIds.add(id))
|
||||||
|
}
|
||||||
|
|
||||||
const filterProjectIds = (data) => {
|
const filterProjectIds = (data) => {
|
||||||
const filtered = {}
|
const filtered = {}
|
||||||
@@ -358,7 +396,7 @@ export const useFetchAllAnalytics = (onDataRefresh, projects) => {
|
|||||||
resolution_minutes: timeResolution.value,
|
resolution_minutes: timeResolution.value,
|
||||||
}
|
}
|
||||||
|
|
||||||
if (projects?.length) {
|
if (projects.value?.length) {
|
||||||
q.project_ids = JSON.stringify(projects.value.map((p) => p.id))
|
q.project_ids = JSON.stringify(projects.value.map((p) => p.id))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -385,10 +423,12 @@ export const useFetchAllAnalytics = (onDataRefresh, projects) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (revenueData.value) {
|
if (revenueData.value) {
|
||||||
// revenue will always have all project ids, but the ids may have an empty object as value.
|
// revenue will always have all project ids, but the ids may have an empty object or a ton of keys below a cent (0.00...) as values. We want to filter those out
|
||||||
Object.entries(revenueData.value).forEach(([id, data]) => {
|
Object.entries(revenueData.value).forEach(([id, data]) => {
|
||||||
if (Object.keys(data).length) {
|
if (Object.keys(data).length) {
|
||||||
ids.add(id)
|
if (Object.values(data).some((v) => v >= 0.01)) {
|
||||||
|
ids.add(id)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -414,6 +454,7 @@ export const useFetchAllAnalytics = (onDataRefresh, projects) => {
|
|||||||
// Computed state
|
// Computed state
|
||||||
validProjectIds,
|
validProjectIds,
|
||||||
formattedData,
|
formattedData,
|
||||||
|
totalData,
|
||||||
loading,
|
loading,
|
||||||
error,
|
error,
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user