You've already forked AstralRinth
forked from didirus/AstralRinth
Improved graphs (#120)
* Complete chart overhaul * Update package.json * Update pnpm-lock.yaml * run lint * whoops * Update pnpm-lock.yaml * Update analytics.md * Try again? * Update Chart.vue * Added Compact/Spark charts * Added number formatting and cleanup * Touch ups * improve default colors * removed unnecessary tooltip
This commit is contained in:
1
lib/assets/icons/zoom-in.svg
Normal file
1
lib/assets/icons/zoom-in.svg
Normal file
@@ -0,0 +1 @@
|
||||
<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" class="lucide lucide-zoom-in"><circle cx="11" cy="11" r="8"/><line x1="21" x2="16.65" y1="21" y2="16.65"/><line x1="11" x2="11" y1="8" y2="14"/><line x1="8" x2="14" y1="11" y2="11"/></svg>
|
||||
|
After Width: | Height: | Size: 369 B |
1
lib/assets/icons/zoom-out.svg
Normal file
1
lib/assets/icons/zoom-out.svg
Normal file
@@ -0,0 +1 @@
|
||||
<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" class="lucide lucide-zoom-out"><circle cx="11" cy="11" r="8"/><line x1="21" x2="16.65" y1="21" y2="16.65"/><line x1="8" x2="14" y1="11" y2="11"/></svg>
|
||||
|
After Width: | Height: | Size: 332 B |
@@ -165,6 +165,13 @@ svg {
|
||||
width: 1em;
|
||||
}
|
||||
|
||||
.chart {
|
||||
svg {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.button-animation {
|
||||
transition: opacity 0.5s ease-in-out, filter 0.2s ease-in-out, transform 0.05s ease-in-out,
|
||||
outline 0.2s ease-in-out;
|
||||
|
||||
@@ -1,115 +0,0 @@
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import { Bar } from 'vue-chartjs'
|
||||
import {
|
||||
Chart as ChartJS,
|
||||
Title,
|
||||
Tooltip,
|
||||
Legend,
|
||||
BarElement,
|
||||
CategoryScale,
|
||||
LinearScale,
|
||||
} from 'chart.js'
|
||||
import dayjs from 'dayjs'
|
||||
|
||||
ChartJS.register(Title, Tooltip, Legend, BarElement, CategoryScale, LinearScale)
|
||||
|
||||
const props = defineProps({
|
||||
data: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
formatLabels: {
|
||||
type: Function,
|
||||
default: (label) => dayjs(label).format('MMM D'),
|
||||
},
|
||||
})
|
||||
|
||||
const decimalToRgba = (decimalColor, alpha = 0.75) => {
|
||||
const red = (decimalColor >> 16) & 255
|
||||
const green = (decimalColor >> 8) & 255
|
||||
const blue = decimalColor & 255
|
||||
|
||||
return `rgba(${red}, ${green}, ${blue}, ${alpha})`
|
||||
}
|
||||
|
||||
const chartData = ref({
|
||||
labels: props.data.labels.map((date) => props.formatLabels(date)),
|
||||
datasets: props.data.data.map((project) => ({
|
||||
label: project.title,
|
||||
borderColor: decimalToRgba(project.color, 1),
|
||||
borderWidth: 2,
|
||||
borderSkipped: 'bottom',
|
||||
backgroundColor: decimalToRgba(project.color, 0.5),
|
||||
data: project.data,
|
||||
})),
|
||||
})
|
||||
|
||||
const chartOptions = ref({
|
||||
responsive: true,
|
||||
scales: {
|
||||
x: {
|
||||
stacked: true,
|
||||
grid: {
|
||||
color: getComputedStyle(document.documentElement).getPropertyValue('--color-button-bg'),
|
||||
},
|
||||
ticks: {
|
||||
color: getComputedStyle(document.documentElement).getPropertyValue('--color-base'),
|
||||
},
|
||||
},
|
||||
y: {
|
||||
stacked: true,
|
||||
grid: {
|
||||
color: getComputedStyle(document.documentElement).getPropertyValue('--color-button-bg'),
|
||||
},
|
||||
ticks: {
|
||||
color: getComputedStyle(document.documentElement).getPropertyValue('--color-base'),
|
||||
},
|
||||
},
|
||||
},
|
||||
interaction: {
|
||||
mode: 'index',
|
||||
},
|
||||
plugins: {
|
||||
legend: {
|
||||
position: 'right',
|
||||
align: 'start',
|
||||
labels: {
|
||||
color: getComputedStyle(document.documentElement).getPropertyValue('--color-base'),
|
||||
font: {
|
||||
size: 12,
|
||||
family: 'Inter',
|
||||
},
|
||||
},
|
||||
},
|
||||
tooltip: {
|
||||
position: 'nearest',
|
||||
backgroundColor: getComputedStyle(document.documentElement).getPropertyValue(
|
||||
'--color-raised-bg'
|
||||
),
|
||||
borderColor: getComputedStyle(document.documentElement).getPropertyValue('--color-button-bg'),
|
||||
borderWidth: 1,
|
||||
titleColor: getComputedStyle(document.documentElement).getPropertyValue('--color-contrast'),
|
||||
titleFont: {
|
||||
size: 16,
|
||||
family: 'Inter',
|
||||
},
|
||||
bodyColor: getComputedStyle(document.documentElement).getPropertyValue('--color-base'),
|
||||
bodyFont: {
|
||||
size: 12,
|
||||
family: 'Inter',
|
||||
},
|
||||
boxPadding: 8,
|
||||
intersect: false,
|
||||
padding: 12,
|
||||
displayColors: false,
|
||||
},
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Bar id="my-chart-id" :options="chartOptions" :data="chartData" />
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
417
lib/components/chart/Chart.vue
Normal file
417
lib/components/chart/Chart.vue
Normal file
@@ -0,0 +1,417 @@
|
||||
<script setup>
|
||||
import dayjs from 'dayjs'
|
||||
import { Button, DownloadIcon, UpdatedIcon, Checkbox, formatNumber } from '@'
|
||||
import { defineAsyncComponent, ref } from 'vue'
|
||||
|
||||
const VueApexCharts = defineAsyncComponent(() => import('vue3-apexcharts'))
|
||||
|
||||
const props = defineProps({
|
||||
name: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
labels: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
data: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
formatLabels: {
|
||||
type: Function,
|
||||
default: (label) => dayjs(label).format('MMM D'),
|
||||
},
|
||||
colors: {
|
||||
type: Array,
|
||||
default: () => [
|
||||
'var(--color-brand)',
|
||||
'var(--color-blue)',
|
||||
'var(--color-purple)',
|
||||
'var(--color-red)',
|
||||
'var(--color-orange)',
|
||||
],
|
||||
},
|
||||
prefix: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
suffix: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
hideToolbar: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
hideLegend: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
stacked: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
default: 'bar',
|
||||
},
|
||||
hideTotal: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
})
|
||||
|
||||
const chartOptions = ref({
|
||||
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,
|
||||
},
|
||||
xaxis: {
|
||||
type: 'datetime',
|
||||
categories: props.labels,
|
||||
labels: {
|
||||
style: {
|
||||
borderRadius: 'var(--radius-sm)',
|
||||
},
|
||||
},
|
||||
axisTicks: {
|
||||
show: false,
|
||||
},
|
||||
tooltip: {
|
||||
enabled: 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: false,
|
||||
},
|
||||
markers: {
|
||||
size: 0,
|
||||
strokeColor: 'var(--color-contrast)',
|
||||
strokeWidth: 3,
|
||||
strokeOpacity: 1,
|
||||
fillOpacity: 1,
|
||||
hover: {
|
||||
size: 6,
|
||||
},
|
||||
},
|
||||
plotOptions: {
|
||||
bar: {
|
||||
columnWidth: '80%',
|
||||
endingShape: 'rounded',
|
||||
borderRadius: 5,
|
||||
borderRadiusApplication: 'end',
|
||||
borderRadiusWhenStacked: 'last',
|
||||
},
|
||||
},
|
||||
tooltip: {
|
||||
custom: function ({ series, seriesIndex, dataPointIndex, w }) {
|
||||
console.log(seriesIndex, w)
|
||||
return (
|
||||
'<div class="bar-tooltip">' +
|
||||
'<div class="seperated-entry title">' +
|
||||
'<div class="label">' +
|
||||
props.formatLabels(w.globals.lastXAxis.categories[dataPointIndex]) +
|
||||
'</div>' +
|
||||
(!props.hideTotal
|
||||
? `<div class="value">
|
||||
${props.prefix}
|
||||
${formatNumber(series.reduce((a, b) => a + b[dataPointIndex], 0).toString(), false)}
|
||||
${props.suffix}
|
||||
</div>`
|
||||
: ``) +
|
||||
'</div><hr class="card-divider" />' +
|
||||
series
|
||||
.map((value, index) =>
|
||||
value[dataPointIndex] > 0
|
||||
? `<div class="list-entry">
|
||||
<span class="circle" style="background-color: ${w.globals.colors[index]}"> </span>
|
||||
<div class="label">
|
||||
${w.globals.seriesNames[index]}
|
||||
</div>
|
||||
<div class="value">
|
||||
${props.prefix}
|
||||
${formatNumber(value[dataPointIndex], false)}
|
||||
${props.suffix}
|
||||
</div>
|
||||
</div>`
|
||||
: ''
|
||||
)
|
||||
.reverse()
|
||||
.reduce((a, b) => a + b) +
|
||||
'</div>'
|
||||
)
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const chart = ref(null)
|
||||
|
||||
const legendValues = ref(
|
||||
[...props.data].map((project, index) => {
|
||||
return { name: project.name, visible: true, color: props.colors[index] }
|
||||
})
|
||||
)
|
||||
|
||||
const flipLegend = (legend, newVal) => {
|
||||
legend.visible = newVal
|
||||
chart.value.toggleSeries(legend.name)
|
||||
}
|
||||
|
||||
const downloadCSV = () => {
|
||||
const csvContent =
|
||||
'data:text/csv;charset=utf-8,' +
|
||||
props.labels.join(',') +
|
||||
'\n' +
|
||||
props.data.map((project) => project.data.join(',')).reduce((a, b) => a + '\n' + b)
|
||||
|
||||
const encodedUri = encodeURI(csvContent)
|
||||
const link = document.createElement('a')
|
||||
link.setAttribute('href', encodedUri)
|
||||
link.setAttribute('download', `${props.name}.csv`)
|
||||
document.body.appendChild(link) // Required for FF
|
||||
|
||||
link.click()
|
||||
}
|
||||
|
||||
const resetChart = () => {
|
||||
chart.value.resetSeries()
|
||||
legendValues.value.forEach((legend) => {
|
||||
legend.visible = true
|
||||
})
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
resetChart,
|
||||
downloadCSV,
|
||||
flipLegend,
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="bar-chart">
|
||||
<div class="title-bar">
|
||||
<slot />
|
||||
<div v-if="!hideToolbar" class="toolbar">
|
||||
<Button v-tooltip="'Download data as CSV'" icon-only @click="downloadCSV">
|
||||
<DownloadIcon />
|
||||
</Button>
|
||||
<Button v-tooltip="'Reset chart'" icon-only @click="resetChart">
|
||||
<UpdatedIcon />
|
||||
</Button>
|
||||
<slot name="toolbar" />
|
||||
</div>
|
||||
</div>
|
||||
<VueApexCharts ref="chart" :type="type" :options="chartOptions" :series="data" class="chart" />
|
||||
<div v-if="!hideLegend" class="legend">
|
||||
<Checkbox
|
||||
v-for="legend in legendValues"
|
||||
:key="legend.name"
|
||||
class="legend-checkbox"
|
||||
:style="`--color: ${legend.color};`"
|
||||
:model-value="legend.visible"
|
||||
@update:model-value="(newVal) => flipLegend(legend, newVal)"
|
||||
>
|
||||
{{ legend.name }}
|
||||
</Checkbox>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.chart {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
svg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.bar-chart {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.title-bar {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: var(--gap-xs);
|
||||
}
|
||||
|
||||
.toolbar {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: var(--gap-xs);
|
||||
z-index: 1;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
:deep(.apexcharts-menu),
|
||||
:deep(.apexcharts-tooltip),
|
||||
:deep(.apexcharts-yaxistooltip) {
|
||||
background: var(--color-raised-bg) !important;
|
||||
border-radius: var(--radius-sm) !important;
|
||||
border: 1px solid var(--color-button-bg) !important;
|
||||
box-shadow: var(--shadow-floating) !important;
|
||||
font-size: var(--font-size-nm) !important;
|
||||
}
|
||||
|
||||
:deep(.apexcharts-grid-borders) {
|
||||
line {
|
||||
stroke: var(--color-button-bg) !important;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.apexcharts-yaxistooltip),
|
||||
:deep(.apexcharts-xaxistooltip) {
|
||||
background: var(--color-raised-bg) !important;
|
||||
border-radius: var(--radius-sm) !important;
|
||||
border: 1px solid var(--color-button-bg) !important;
|
||||
font-size: var(--font-size-nm) !important;
|
||||
color: var(--color-base) !important;
|
||||
|
||||
.apexcharts-xaxistooltip-text {
|
||||
font-size: var(--font-size-nm) !important;
|
||||
color: var(--color-base) !important;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.apexcharts-yaxistooltip-left:after) {
|
||||
border-left-color: var(--color-raised-bg) !important;
|
||||
}
|
||||
|
||||
:deep(.apexcharts-yaxistooltip-left:before) {
|
||||
border-left-color: var(--color-button-bg) !important;
|
||||
}
|
||||
|
||||
:deep(.apexcharts-xaxistooltip-bottom:after) {
|
||||
border-bottom-color: var(--color-raised-bg) !important;
|
||||
}
|
||||
|
||||
:deep(.apexcharts-xaxistooltip-bottom:before) {
|
||||
border-bottom-color: var(--color-button-bg) !important;
|
||||
}
|
||||
|
||||
:deep(.apexcharts-menu-item) {
|
||||
border-radius: var(--radius-sm) !important;
|
||||
padding: var(--gap-xs) var(--gap-sm) !important;
|
||||
|
||||
&:hover {
|
||||
transition: all 0.3s !important;
|
||||
color: var(--color-accent-contrast) !important;
|
||||
background: var(--color-brand) !important;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.apexcharts-tooltip) {
|
||||
.bar-tooltip {
|
||||
min-width: 10rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--gap-xs);
|
||||
padding: var(--gap-sm);
|
||||
|
||||
.card-divider {
|
||||
margin: var(--gap-xs) 0;
|
||||
}
|
||||
|
||||
.seperated-entry {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-weight: bolder;
|
||||
}
|
||||
|
||||
.label {
|
||||
color: var(--color-contrast);
|
||||
}
|
||||
|
||||
.value {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: var(--gap-xs);
|
||||
color: var(--color-base);
|
||||
}
|
||||
|
||||
.list-entry {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
font-size: var(--font-size-sm);
|
||||
|
||||
.value {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.label {
|
||||
margin-right: var(--gap-xl);
|
||||
}
|
||||
}
|
||||
|
||||
.circle {
|
||||
width: 0.5rem;
|
||||
height: 0.5rem;
|
||||
border-radius: 50%;
|
||||
display: inline-block;
|
||||
margin-right: var(--gap-sm);
|
||||
}
|
||||
|
||||
svg {
|
||||
height: 1em;
|
||||
width: 1em;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.legend {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: var(--gap-md);
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.legend-checkbox :deep(.checkbox.checked) {
|
||||
background-color: var(--color);
|
||||
}
|
||||
</style>
|
||||
278
lib/components/chart/CompactChart.vue
Normal file
278
lib/components/chart/CompactChart.vue
Normal file
@@ -0,0 +1,278 @@
|
||||
<script setup>
|
||||
import { Card, formatNumber } from '@'
|
||||
import { defineAsyncComponent, ref } from 'vue'
|
||||
import dayjs from 'dayjs'
|
||||
const VueApexCharts = defineAsyncComponent(() => import('vue3-apexcharts'))
|
||||
|
||||
const props = defineProps({
|
||||
value: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
data: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
labels: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
prefix: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
suffix: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
})
|
||||
|
||||
//no grid lines, no toolbar, no legend, no data labels
|
||||
const chartOptions = ref({
|
||||
chart: {
|
||||
id: props.title,
|
||||
fontFamily:
|
||||
'Inter, -apple-system, BlinkMacSystemFont, Segoe UI, Oxygen, Ubuntu, Roboto, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif',
|
||||
foreColor: 'var(--color-base)',
|
||||
toolbar: {
|
||||
show: false,
|
||||
},
|
||||
zoom: {
|
||||
enabled: false,
|
||||
},
|
||||
sparkline: {
|
||||
enabled: true,
|
||||
},
|
||||
},
|
||||
stroke: {
|
||||
curve: 'straight',
|
||||
},
|
||||
fill: {
|
||||
colors: ['var(--color-brand)'],
|
||||
type: 'gradient',
|
||||
opacity: 1,
|
||||
gradient: {
|
||||
shade: 'light',
|
||||
type: 'vertical',
|
||||
shadeIntensity: 0,
|
||||
gradientToColors: ['var(--color-brand)'],
|
||||
inverseColors: true,
|
||||
opacityFrom: 0.5,
|
||||
opacityTo: 0,
|
||||
stops: [0, 100],
|
||||
colorStops: [],
|
||||
},
|
||||
},
|
||||
grid: {
|
||||
show: false,
|
||||
},
|
||||
legend: {
|
||||
show: false,
|
||||
},
|
||||
colors: ['var(--color-brand)'],
|
||||
dataLabels: {
|
||||
enabled: false,
|
||||
},
|
||||
xaxis: {
|
||||
type: 'datetime',
|
||||
categories: props.labels,
|
||||
labels: {
|
||||
show: false,
|
||||
},
|
||||
axisTicks: {
|
||||
show: false,
|
||||
},
|
||||
tooltip: {
|
||||
enabled: false,
|
||||
},
|
||||
},
|
||||
yaxis: {
|
||||
labels: {
|
||||
show: false,
|
||||
},
|
||||
axisBorder: {
|
||||
show: false,
|
||||
},
|
||||
axisTicks: {
|
||||
show: false,
|
||||
},
|
||||
tooltip: {
|
||||
enabled: false,
|
||||
},
|
||||
},
|
||||
tooltip: {
|
||||
custom: function ({ series, seriesIndex, dataPointIndex, w }) {
|
||||
console.log(seriesIndex, w)
|
||||
return (
|
||||
'<div class="bar-tooltip">' +
|
||||
series
|
||||
.map((value) =>
|
||||
value[dataPointIndex] > 0
|
||||
? `<div class="list-entry">
|
||||
<div class="label">
|
||||
<span class="circle" style="background-color: ${w.globals.colors[0]}"> </span>
|
||||
${dayjs(w.globals.lastXAxis.categories[dataPointIndex]).format('MMM D')}
|
||||
</div>
|
||||
<div class="divider">
|
||||
|
|
||||
</div>
|
||||
<div class="value">
|
||||
${props.prefix}
|
||||
${formatNumber(value[dataPointIndex], false)}
|
||||
${props.suffix}
|
||||
</div>
|
||||
</div>`
|
||||
: ''
|
||||
)
|
||||
.reverse()
|
||||
.reduce((a, b) => a + b) +
|
||||
'</div>'
|
||||
)
|
||||
},
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Card class="compact-chart">
|
||||
<h1 class="value">
|
||||
{{ value }}
|
||||
</h1>
|
||||
<div class="subtitle">
|
||||
{{ title }}
|
||||
</div>
|
||||
<VueApexCharts
|
||||
ref="chart"
|
||||
type="area"
|
||||
height="120"
|
||||
:options="chartOptions"
|
||||
:series="data"
|
||||
class="chart"
|
||||
/>
|
||||
</Card>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.compact-chart {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--gap-xs);
|
||||
border: 1px solid var(--color-button-bg);
|
||||
border-radius: var(--radius-md);
|
||||
background-color: var(--color-raised-bg);
|
||||
box-shadow: var(--shadow-floating);
|
||||
color: var(--color-base);
|
||||
font-size: var(--font-size-nm);
|
||||
width: 100%;
|
||||
padding-bottom: 0;
|
||||
|
||||
.value {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.chart {
|
||||
width: calc(100% + 3rem);
|
||||
margin: 0 -1.5rem 0.25rem -1.5rem;
|
||||
}
|
||||
|
||||
svg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
:deep(.apexcharts-menu),
|
||||
:deep(.apexcharts-tooltip),
|
||||
:deep(.apexcharts-yaxistooltip) {
|
||||
background: var(--color-raised-bg) !important;
|
||||
border-radius: var(--radius-sm) !important;
|
||||
border: 1px solid var(--color-button-bg) !important;
|
||||
box-shadow: var(--shadow-floating) !important;
|
||||
font-size: var(--font-size-nm) !important;
|
||||
}
|
||||
|
||||
:deep(.apexcharts-graphical) {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
:deep(.apexcharts-tooltip) {
|
||||
.bar-tooltip {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--gap-xs);
|
||||
padding: var(--gap-sm);
|
||||
|
||||
.card-divider {
|
||||
margin: var(--gap-xs) 0;
|
||||
}
|
||||
|
||||
.label {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.value {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: var(--gap-xs);
|
||||
color: var(--color-base);
|
||||
}
|
||||
|
||||
.list-entry {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: var(--gap-md);
|
||||
}
|
||||
|
||||
.circle {
|
||||
width: 0.5rem;
|
||||
height: 0.5rem;
|
||||
border-radius: 50%;
|
||||
display: inline-block;
|
||||
margin-right: var(--gap-sm);
|
||||
}
|
||||
|
||||
svg {
|
||||
height: 1em;
|
||||
width: 1em;
|
||||
}
|
||||
|
||||
.divider {
|
||||
font-size: var(--font-size-lg);
|
||||
font-weight: 400;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.legend {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: var(--gap-md);
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
:deep(.apexcharts-grid-borders) {
|
||||
line {
|
||||
stroke: var(--color-button-bg) !important;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.apexcharts-xaxis) {
|
||||
line {
|
||||
stroke: none;
|
||||
}
|
||||
}
|
||||
|
||||
.legend-checkbox :deep(.checkbox.checked) {
|
||||
background-color: var(--color);
|
||||
}
|
||||
</style>
|
||||
@@ -1,127 +0,0 @@
|
||||
<template>
|
||||
<Line :options="chartOptions" :data="chartData" />
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import { Line } from 'vue-chartjs'
|
||||
import {
|
||||
Chart as ChartJS,
|
||||
Title,
|
||||
Tooltip,
|
||||
PointElement,
|
||||
LineElement,
|
||||
CategoryScale,
|
||||
LinearScale,
|
||||
Filler,
|
||||
} from 'chart.js'
|
||||
import dayjs from 'dayjs'
|
||||
|
||||
ChartJS.register(Title, Tooltip, PointElement, LineElement, CategoryScale, LinearScale, Filler)
|
||||
|
||||
const props = defineProps({
|
||||
data: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
formatLabels: {
|
||||
type: Function,
|
||||
default: (label) => dayjs(label).format('MMM D'),
|
||||
},
|
||||
})
|
||||
|
||||
const decimalToRgba = (decimalColor, alpha = 0.75) => {
|
||||
const red = (decimalColor >> 16) & 255
|
||||
const green = (decimalColor >> 8) & 255
|
||||
const blue = decimalColor & 255
|
||||
|
||||
return `rgba(${red}, ${green}, ${blue}, ${alpha})`
|
||||
}
|
||||
|
||||
const chartData = ref({
|
||||
labels: props.data.labels.map((date) => props.formatLabels(date)),
|
||||
datasets: props.data.data.map((project) => ({
|
||||
label: project.title,
|
||||
backgroundColor: decimalToRgba(project.color, 0.5),
|
||||
borderColor: decimalToRgba(project.color),
|
||||
data: project.data,
|
||||
})),
|
||||
})
|
||||
|
||||
const chartOptions = ref({
|
||||
responsive: true,
|
||||
scales: {
|
||||
x: {
|
||||
grid: {
|
||||
color: getComputedStyle(document.documentElement).getPropertyValue('--color-button-bg'),
|
||||
},
|
||||
ticks: {
|
||||
color: getComputedStyle(document.documentElement).getPropertyValue('--color-base'),
|
||||
},
|
||||
},
|
||||
y: {
|
||||
grid: {
|
||||
color: getComputedStyle(document.documentElement).getPropertyValue('--color-button-bg'),
|
||||
},
|
||||
ticks: {
|
||||
color: getComputedStyle(document.documentElement).getPropertyValue('--color-base'),
|
||||
},
|
||||
},
|
||||
},
|
||||
interaction: {
|
||||
mode: 'index',
|
||||
intersect: false,
|
||||
axis: 'xy',
|
||||
},
|
||||
plugins: {
|
||||
legend: {
|
||||
position: 'right',
|
||||
align: 'start',
|
||||
labels: {
|
||||
color: getComputedStyle(document.documentElement).getPropertyValue('--color-base'),
|
||||
font: {
|
||||
size: 12,
|
||||
family: 'Inter',
|
||||
},
|
||||
},
|
||||
},
|
||||
tooltip: {
|
||||
position: 'nearest',
|
||||
backgroundColor: getComputedStyle(document.documentElement).getPropertyValue(
|
||||
'--color-raised-bg'
|
||||
),
|
||||
borderColor: getComputedStyle(document.documentElement).getPropertyValue('--color-button-bg'),
|
||||
borderWidth: 1,
|
||||
titleColor: getComputedStyle(document.documentElement).getPropertyValue('--color-contrast'),
|
||||
titleFont: {
|
||||
size: 14,
|
||||
family: 'Inter',
|
||||
},
|
||||
bodyColor: getComputedStyle(document.documentElement).getPropertyValue('--color-base'),
|
||||
bodyFont: {
|
||||
size: 12,
|
||||
family: 'Inter',
|
||||
},
|
||||
boxPadding: 8,
|
||||
intersect: false,
|
||||
padding: 12,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
/*
|
||||
The data for the graph should look like this
|
||||
|
||||
downloads, views, likes = {
|
||||
dates: [ '2021-01-01', '2021-01-02', '2021-01-03' ], // Last 2 weeks
|
||||
data: [
|
||||
{
|
||||
title: projectName,
|
||||
color: projectColor,
|
||||
data: [ ... ],
|
||||
},
|
||||
...
|
||||
]
|
||||
}
|
||||
*/
|
||||
</script>
|
||||
@@ -1,93 +0,0 @@
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import { Pie } from 'vue-chartjs'
|
||||
import {
|
||||
Chart as ChartJS,
|
||||
Title,
|
||||
Tooltip,
|
||||
PieController,
|
||||
ArcElement,
|
||||
Legend,
|
||||
CategoryScale,
|
||||
LinearScale,
|
||||
} from 'chart.js'
|
||||
|
||||
ChartJS.register(Title, Tooltip, PieController, ArcElement, Legend, CategoryScale, LinearScale)
|
||||
|
||||
const props = defineProps({
|
||||
data: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
})
|
||||
|
||||
const decimalToRgba = (decimalColor, alpha = 1) => {
|
||||
const red = (decimalColor >> 16) & 255
|
||||
const green = (decimalColor >> 8) & 255
|
||||
const blue = decimalColor & 255
|
||||
|
||||
return `rgba(${red}, ${green}, ${blue}, ${alpha})`
|
||||
}
|
||||
|
||||
const chartData = ref({
|
||||
labels: props.data.data.map((project) => project.title),
|
||||
datasets: [
|
||||
{
|
||||
label: props.data.title,
|
||||
backgroundColor: props.data.data.map((project) => decimalToRgba(project.color, 0.5)),
|
||||
borderColor: props.data.data.map((project) => decimalToRgba(project.color)),
|
||||
data: props.data.data.map((project) => project.data),
|
||||
fill: true,
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
const chartOptions = ref({
|
||||
responsive: true,
|
||||
elements: {
|
||||
point: {
|
||||
radius: 0,
|
||||
},
|
||||
},
|
||||
plugins: {
|
||||
legend: {
|
||||
position: 'right',
|
||||
labels: {
|
||||
color: getComputedStyle(document.documentElement).getPropertyValue('--color-base'),
|
||||
font: {
|
||||
size: 12,
|
||||
family: 'Inter',
|
||||
},
|
||||
},
|
||||
},
|
||||
tooltip: {
|
||||
position: 'nearest',
|
||||
backgroundColor: getComputedStyle(document.documentElement).getPropertyValue(
|
||||
'--color-raised-bg'
|
||||
),
|
||||
borderColor: getComputedStyle(document.documentElement).getPropertyValue('--color-button-bg'),
|
||||
borderWidth: 1,
|
||||
titleColor: getComputedStyle(document.documentElement).getPropertyValue('--color-contrast'),
|
||||
titleFont: {
|
||||
size: 16,
|
||||
family: 'Inter',
|
||||
},
|
||||
bodyColor: getComputedStyle(document.documentElement).getPropertyValue('--color-base'),
|
||||
bodyFont: {
|
||||
size: 12,
|
||||
family: 'Inter',
|
||||
},
|
||||
boxPadding: 8,
|
||||
intersect: false,
|
||||
padding: 12,
|
||||
displayColors: false,
|
||||
},
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Pie :options="chartOptions" :data="chartData" />
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
@@ -30,9 +30,8 @@ export { default as TextLogo } from './brand/TextLogo.vue'
|
||||
export { default as FourOhFourNotFound } from '@/assets/branding/404.svg?component'
|
||||
|
||||
// Charts
|
||||
export { default as BarChart } from './chart/BarChart.vue'
|
||||
export { default as LineChart } from './chart/LineChart.vue'
|
||||
export { default as PieChart } from './chart/PieChart.vue'
|
||||
export { default as Chart } from './chart/Chart.vue'
|
||||
export { default as CompactChart } from './chart/CompactChart.vue'
|
||||
|
||||
// Modals
|
||||
export { default as Modal } from './modal/Modal.vue'
|
||||
@@ -185,6 +184,8 @@ export { default as VersionIcon } from '@/assets/icons/version.svg?component'
|
||||
export { default as WikiIcon } from '@/assets/icons/wiki.svg?component'
|
||||
export { default as XIcon } from '@/assets/icons/x.svg?component'
|
||||
export { default as XCircleIcon } from '@/assets/icons/x-circle.svg?component'
|
||||
export { default as ZoomInIcon } from '@/assets/icons/zoom-in.svg?component'
|
||||
export { default as ZoomOutIcon } from '@/assets/icons/zoom-out.svg?component'
|
||||
|
||||
// Editor Icons
|
||||
export { default as BoldIcon } from '@/assets/icons/bold.svg?component'
|
||||
|
||||
Reference in New Issue
Block a user