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 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;' })
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div>
|
||||
<ChartDisplay :projects="projects ?? undefined" />
|
||||
<ChartDisplay :projects="projects ?? undefined" :personal="true" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="normal-page__content">
|
||||
<div class="universal-card">
|
||||
<h2>Analytics</h2>
|
||||
|
||||
|
||||
@@ -38,7 +38,22 @@ const hashProjectId = (projectId) => {
|
||||
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
|
||||
@@ -111,6 +126,7 @@ const emptyAnalytics = {
|
||||
colors: [],
|
||||
defaultColors: [],
|
||||
},
|
||||
projectIds: [],
|
||||
}
|
||||
|
||||
export const analyticsSetToCSVString = (analytics) => {
|
||||
@@ -150,7 +166,6 @@ export const processAnalytics = (category, projects, labelFn, sortFn, mapFn, cha
|
||||
const loadedProjectData = loadedProjectIds.map((id) => category[id])
|
||||
|
||||
// Convert each project's data into a list of [unix_ts_str, number] pairs
|
||||
// Sort, label&map
|
||||
const projectData = loadedProjectData
|
||||
.map((data) => Object.entries(data))
|
||||
.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)
|
||||
)
|
||||
|
||||
const projectIdsSortedBySum = chartData.map((p) => p.id)
|
||||
|
||||
return {
|
||||
// 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),
|
||||
@@ -210,6 +227,7 @@ export const processAnalytics = (category, projects, labelFn, sortFn, mapFn, cha
|
||||
return getDefaultColor(project.id)
|
||||
}),
|
||||
},
|
||||
projectIds: projectIdsSortedBySum,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -280,7 +298,12 @@ const useFetchAnalytics = (
|
||||
* @param {Ref<any[]>} projects
|
||||
* @param {undefined | () => any} onDataRefresh
|
||||
*/
|
||||
export const useFetchAllAnalytics = (onDataRefresh, projects) => {
|
||||
export const useFetchAllAnalytics = (
|
||||
onDataRefresh,
|
||||
projects,
|
||||
selectedProjects,
|
||||
personalRevenue = false
|
||||
) => {
|
||||
const timeResolution = ref(1440) // 1 day
|
||||
const timeRange = ref(43200) // 30 days
|
||||
|
||||
@@ -296,17 +319,27 @@ export const useFetchAllAnalytics = (onDataRefresh, projects) => {
|
||||
const error = ref(null)
|
||||
|
||||
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),
|
||||
views: processNumberAnalytics(viewData.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 normalQuery = new URLSearchParams(query)
|
||||
const revenueQuery = new URLSearchParams(query)
|
||||
revenueQuery.delete('projects')
|
||||
|
||||
if (personalRevenue) {
|
||||
revenueQuery.delete('project_ids')
|
||||
}
|
||||
|
||||
const qs = normalQuery.toString()
|
||||
const revenueQs = revenueQuery.toString()
|
||||
|
||||
@@ -324,7 +357,12 @@ export const useFetchAllAnalytics = (onDataRefresh, projects) => {
|
||||
|
||||
// collect project ids from projects.value into a 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 filtered = {}
|
||||
@@ -358,7 +396,7 @@ export const useFetchAllAnalytics = (onDataRefresh, projects) => {
|
||||
resolution_minutes: timeResolution.value,
|
||||
}
|
||||
|
||||
if (projects?.length) {
|
||||
if (projects.value?.length) {
|
||||
q.project_ids = JSON.stringify(projects.value.map((p) => p.id))
|
||||
}
|
||||
|
||||
@@ -385,10 +423,12 @@ export const useFetchAllAnalytics = (onDataRefresh, projects) => {
|
||||
}
|
||||
|
||||
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]) => {
|
||||
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
|
||||
validProjectIds,
|
||||
formattedData,
|
||||
totalData,
|
||||
loading,
|
||||
error,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user