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:
Adrian O.V 2023-10-26 14:39:26 -04:00 committed by GitHub
parent 16a39b364c
commit c056c4e79e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 954 additions and 493 deletions

View File

@ -2,136 +2,168 @@
<DemoContainer>
<client-only>
<LineChart
:data="{
labels: [
'2021-01-01', '2021-01-02', '2021-01-03', '2021-01-04', '2021-01-05',
'2021-01-06', '2021-01-07', '2021-01-08', '2021-01-09', '2021-01-10',
'2021-01-11', '2021-01-12', '2021-01-13', '2021-01-14', '2021-01-15',
'2021-01-16', '2021-01-17'
],
data: [
{
title: 'Spirit',
color: 16711680,
data: [120, 130, 140, 150, 160, 170, 180, 190, 200, 210, 220, 230, 240, 250, 260, 270, 280],
},
{
title: 'Ad Astra',
color: 65280,
data: [150, 155, 160, 165, 170, 175, 180, 185, 190, 195, 200, 205, 210, 215, 220, 225, 230],
},
{
title: 'Tempad',
color: 255,
data: [180, 182, 184, 186, 188, 190, 192, 194, 196, 198, 200, 202, 204, 206, 208, 210, 212],
},
]
}"
/>
<BarChart
:data="{
labels: [
'2021-01-01', '2021-01-02', '2021-01-03', '2021-01-04', '2021-01-05',
'2021-01-06', '2021-01-07', '2021-01-08', '2021-01-09', '2021-01-10',
'2021-01-11', '2021-01-12', '2021-01-13', '2021-01-14', '2021-01-15',
'2021-01-16', '2021-01-17'
],
data: [
{
title: 'Spirit',
color: 16711680,
data: [120, 130, 140, 150, 160, 170, 180, 190, 200, 210, 220, 230, 240, 250, 260, 270, 280],
},
{
title: 'Ad Astra',
color: 65280,
data: [150, 155, 160, 165, 170, 175, 180, 185, 190, 195, 200, 205, 210, 215, 220, 225, 230],
},
{
title: 'Tempad',
color: 255,
data: [180, 182, 184, 186, 188, 190, 192, 194, 196, 198, 200, 202, 204, 206, 208, 210, 212],
},
]
}"
/>
<PieChart
:data="{
title: 'Downloads',
data: [
{
title: 'Spirit',
color: 16711680, // Red in decimal (equivalent to #FF0000 in hexadecimal)
data: 120, // Example download numbers for the three dates
},
{
title: 'Ad Astra',
color: 65280, // Green in decimal (equivalent to #00FF00 in hexadecimal)
data: 150, // Example download numbers for the three dates
},
{
title: 'Tempad',
color: 255, // Blue in decimal (equivalent to #0000FF in hexadecimal)
data: 180, // Example download numbers for the three dates
},
],
}"
/>
<Chart
name="Chart"
type="bar"
:stacked="true"
:labels="[
'2021-01-01', '2021-01-02', '2021-01-03', '2021-01-04', '2021-01-05',
'2021-01-06', '2021-01-07', '2021-01-08', '2021-01-09', '2021-01-10',
'2021-01-11', '2021-01-12', '2021-01-13', '2021-01-14', '2021-01-15',
'2021-01-16', '2021-01-17'
]"
:data="[
{
name: 'Spirit',
data: [120, 130, 140, 150, 160, 170, 180, 190, 200, 210, 220, 230, 240, 250, 260, 270, 280],
},
{
name: 'Ad Astra',
data: [150, 155, 160, 165, 170, 175, 180, 185, 190, 195, 200, 205, 210, 215, 220, 225, 230],
},
{
name: 'Tempad',
data: [180, 182, 184, 186, 188, 190, 192, 194, 196, 198, 200, 202, 204, 206, 208, 210, 212],
},
]"
:colors="['#FF0000', '#00FF00', '#0000FF']"
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>"
/>
</client-only>
</DemoContainer>
<DemoContainer>
<client-only>
<Chart
name="Chart"
type="line"
:labels="[
'2021-01-01', '2021-01-02', '2021-01-03', '2021-01-04', '2021-01-05',
'2021-01-06', '2021-01-07', '2021-01-08', '2021-01-09', '2021-01-10',
'2021-01-11', '2021-01-12', '2021-01-13', '2021-01-14', '2021-01-15',
'2021-01-16', '2021-01-17'
]"
:data="[
{
name: 'Spirit',
data: [120, 130, 140, 150, 160, 170, 180, 190, 200, 210, 220, 230, 240, 250, 260, 270, 1280],
},
{
name: 'Ad Astra',
data: [150, 155, 160, 165, 170, 175, 180, 185, 190, 195, 200, 205, 210, 215, 220, 225, 1230],
},
{
name: 'Tempad',
data: [180, 182, 184, 186, 188, 190, 192, 194, 196, 198, 200, 202, 204, 206, 208, 210, 212],
},
]"
:colors="['#FF0000', '#00FF00', '#0000FF']"
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>"
/>
</client-only>
</DemoContainer>
<DemoContainer>
<client-only>
<Chart
name="Chart"
:labels="[
'2021-01-01', '2021-01-02', '2021-01-03', '2021-01-04', '2021-01-05',
'2021-01-06', '2021-01-07', '2021-01-08', '2021-01-09', '2021-01-10',
'2021-01-11', '2021-01-12', '2021-01-13', '2021-01-14', '2021-01-15',
'2021-01-16', '2021-01-17'
]"
:data="[
{
name: 'Downloads',
data: [120, 130, 140, 150, 160, 170, 180, 190, 200, 210, 220, 230, 240, 250, 260, 270, 280],
},
{
name: 'Revenue',
data: [150, 155, 160, 165, 170, 175, 180, 185, 190, 195, 200, 205, 210, 215, 220, 225, 230],
},
{
name: 'Page views',
data: [180, 182, 184, 186, 188, 190, 192, 194, 196, 198, 200, 202, 204, 206, 208, 210, 212],
},
]"
hide-total
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>"
>
Slot for title stuff
<Chips :items="['option 1', 'option 3']" />
<template #toolbar>
<Button>
<PlusIcon />
Slot for toolbar stuff
</Button>
</template>
</Chart>
</client-only>
</DemoContainer>
<DemoContainer>
<client-only>
<div style="display: grid; grid-template-columns: 1fr 1fr; column-gap: var(--gap-md);">
<CompactChart
v-for="i in 4"
title="Downloads"
value="10,230"
:labels="[
'2021-01-01', '2021-01-02', '2021-01-03', '2021-01-04', '2021-01-05',
'2021-01-06', '2021-01-07', '2021-01-08', '2021-01-09', '2021-01-10',
'2021-01-11', '2021-01-12', '2021-01-13', '2021-01-14', '2021-01-15',
'2021-01-16', '2021-01-17'
]"
:data="[
{
name: 'Downloads',
data: [240, 180, 210, 160, 250, 130, 220, 270, 120, 260, 200, 230, 140, 280, 190, 150, 170],
}
]"
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>"
/>
</div>
</client-only>
</DemoContainer>
```vue
<LineChart
:formatLabel="(label) => doFormattingThings(label)"
:data="{
labels: [
'2021-01-01', '2021-01-02', '2021-01-03', '2021-01-04', '2021-01-05',
'2021-01-06', '2021-01-07', '2021-01-08', '2021-01-09', '2021-01-10',
'2021-01-11', '2021-01-12', '2021-01-13', '2021-01-14', '2021-01-15',
'2021-01-16', '2021-01-17'
],
data: [
{
title: 'Spirit',
color: 16711680,
data: [120, 130, 140, 150, 160, 170, 180, 190, 200, 210, 220, 230, 240, 250, 260, 270, 280],
},
{
title: 'Ad Astra',
color: 65280,
data: [150, 155, 160, 165, 170, 175, 180, 185, 190, 195, 200, 205, 210, 215, 220, 225, 230],
},
{
title: 'Tempad',
color: 255,
data: [180, 182, 184, 186, 188, 190, 192, 194, 196, 198, 200, 202, 204, 206, 208, 210, 212],
},
]
}"
/>
<Chart
name="Chart name"
:labels="['array', 'of', 'labels', 'for', 'x-axis', 'typically', 'dates']"
:data="[
{
name: 'Spirit',
data: ['array', 'of', 'data', 'equal', 'length', 'to', 'x-axis'],
},
...
]"
:colors="['array', 'of', 'colors', 'for', 'each', 'series/dataset']"
prefix="string or svg icon to append to each data point"
suffix="string or svg icon to append to each data point"
type="bar|line"
:stacked="true|false (default: false) (determines whether or not values overlap/sidebyside instead of stacked)"
:hideTotal="true|false (default: false) (hide total value in tooltip)"
:hideToolbar="true|false (default: false) (hide toolbar)"
:hideLegend="true|false (default: false) (hide legend)"
>
... slot for title stuff
<template #toolbar>
... slot for toolbar stuff
</template>
</Chart>
```
```vue
<PieChart
:data="{
title: 'Downloads',
data: [
{
title: 'Spirit',
color: 16711680, // Red in decimal (equivalent to #FF0000 in hexadecimal)
data: 120, // Example download numbers for the three dates
},
{
title: 'Ad Astra',
color: 65280, // Green in decimal (equivalent to #00FF00 in hexadecimal)
data: 150, // Example download numbers for the three dates
},
{
title: 'Tempad',
color: 255, // Blue in decimal (equivalent to #0000FF in hexadecimal)
data: 180, // Example download numbers for the three dates
},
],
}"
<CompactChart
title="Chart title"
value="Chart value"
:labels="['array', 'of', 'labels', 'for', 'x-axis', 'typically', 'dates']"
:data="[
{
name: 'Spirit',
data: ['array', 'of', 'data', 'equal', 'length', 'to', 'x-axis'],
},
...
]"
prefix="string or svg icon to append to each data point"
suffix="string or svg icon to append to each data point"
/>
```

View 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

View 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

View File

@ -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;

View File

@ -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>

View 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>

View 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>

View File

@ -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>

View File

@ -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>

View File

@ -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'

View File

@ -1,7 +1,7 @@
{
"name": "omorphia",
"type": "module",
"version": "0.6.2",
"version": "0.6.3",
"files": [
"dist"
],
@ -30,15 +30,15 @@
"@codemirror/language": "^6.9.1",
"@codemirror/state": "^6.3.0",
"@codemirror/view": "^6.21.3",
"chart.js": "^4.3.3",
"apexcharts": "^3.44.0",
"dayjs": "^1.11.7",
"floating-vue": "^2.0.0-beta.20",
"highlight.js": "^11.8.0",
"markdown-it": "^13.0.1",
"qrcode.vue": "^3.4.0",
"vue-chartjs": "^5.2.0",
"vue-router": "^4.2.1",
"vue-select": "^4.0.0-beta.6",
"vue3-apexcharts": "^1.4.4",
"xss": "^1.0.14"
},
"devDependencies": {

113
pnpm-lock.yaml generated
View File

@ -20,9 +20,9 @@ dependencies:
'@codemirror/view':
specifier: ^6.21.3
version: 6.21.3
chart.js:
specifier: ^4.3.3
version: 4.3.3
apexcharts:
specifier: ^3.44.0
version: 3.44.0
dayjs:
specifier: ^1.11.7
version: 1.11.7
@ -41,15 +41,15 @@ dependencies:
vue:
specifier: ^3.3.4
version: 3.3.4
vue-chartjs:
specifier: ^5.2.0
version: 5.2.0(chart.js@4.3.3)(vue@3.3.4)
vue-router:
specifier: ^4.2.1
version: 4.2.1(vue@3.3.4)
vue-select:
specifier: ^4.0.0-beta.6
version: 4.0.0-beta.6(vue@3.3.4)
vue3-apexcharts:
specifier: ^1.4.4
version: 1.4.4(apexcharts@3.44.0)(vue@3.3.4)
xss:
specifier: ^1.0.14
version: 1.0.14
@ -699,10 +699,6 @@ packages:
'@jridgewell/sourcemap-codec': 1.4.14
dev: true
/@kurkle/color@0.3.2:
resolution: {integrity: sha512-fuscdXJ9G1qb7W8VdHi+IwRqij3lBkosAm4ydQtEmbY58OzHXqQhvlxqEkoz0yssNVn38bcpRWgA9PP+OGoisw==}
dev: false
/@lezer/common@1.1.0:
resolution: {integrity: sha512-XPIN3cYDXsoJI/oDWoR2tD++juVrhgIago9xyKhZ7IhGlzdDM9QgC8D8saKNCz5pindGcznFr2HBSsEQSWnSjw==}
dev: false
@ -1364,6 +1360,10 @@ packages:
resolution: {integrity: sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==}
dev: true
/@yr/monotone-cubic-spline@1.0.3:
resolution: {integrity: sha512-FQXkOta0XBSUPHndIKON2Y9JeQz5ZeMqLYZVVK93FliNBFm7LNMIZmY6FrMEB9XPcDbE2bekMbZD6kzDkxwYjA==}
dev: false
/acorn-import-assertions@1.9.0(acorn@8.8.2):
resolution: {integrity: sha512-cmMwop9x+8KFhxvKrKfPYmN6/pKTYYHBqLa0DfvVZcKMJWNyWLnaqND7dx/qn66R7ewM1UX5XMaDVP5wlVTaVA==}
peerDependencies:
@ -1446,6 +1446,18 @@ packages:
picomatch: 2.3.1
dev: true
/apexcharts@3.44.0:
resolution: {integrity: sha512-u7Xzrbcxc2yWznN78Jh5NMCYVAsWDfBjRl5ea++rVzFAqjU2hLz4RgKIFwYOBDRQtW1e/Qz8azJTqIJ1+Vu9Qg==}
dependencies:
'@yr/monotone-cubic-spline': 1.0.3
svg.draggable.js: 2.2.2
svg.easing.js: 2.0.0
svg.filter.js: 2.0.2
svg.pathmorphing.js: 0.1.3
svg.resize.js: 1.4.3
svg.select.js: 3.0.1
dev: false
/argparse@1.0.10:
resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==}
dependencies:
@ -1529,13 +1541,6 @@ packages:
supports-color: 7.2.0
dev: true
/chart.js@4.3.3:
resolution: {integrity: sha512-aTk7pBw+x6sQYhon/NR3ikfUJuym/LdgpTlgZRe2PaEhjUMKBKyNaFCMVRAyTEWYFNO7qRu7iQVqOw/OqzxZxQ==}
engines: {pnpm: '>=7'}
dependencies:
'@kurkle/color': 0.3.2
dev: false
/chokidar@3.5.3:
resolution: {integrity: sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==}
engines: {node: '>= 8.10.0'}
@ -2784,6 +2789,60 @@ packages:
engines: {node: '>= 0.4'}
dev: true
/svg.draggable.js@2.2.2:
resolution: {integrity: sha512-JzNHBc2fLQMzYCZ90KZHN2ohXL0BQJGQimK1kGk6AvSeibuKcIdDX9Kr0dT9+UJ5O8nYA0RB839Lhvk4CY4MZw==}
engines: {node: '>= 0.8.0'}
dependencies:
svg.js: 2.7.1
dev: false
/svg.easing.js@2.0.0:
resolution: {integrity: sha512-//ctPdJMGy22YoYGV+3HEfHbm6/69LJUTAqI2/5qBvaNHZ9uUFVC82B0Pl299HzgH13rKrBgi4+XyXXyVWWthA==}
engines: {node: '>= 0.8.0'}
dependencies:
svg.js: 2.7.1
dev: false
/svg.filter.js@2.0.2:
resolution: {integrity: sha512-xkGBwU+dKBzqg5PtilaTb0EYPqPfJ9Q6saVldX+5vCRy31P6TlRCP3U9NxH3HEufkKkpNgdTLBJnmhDHeTqAkw==}
engines: {node: '>= 0.8.0'}
dependencies:
svg.js: 2.7.1
dev: false
/svg.js@2.7.1:
resolution: {integrity: sha512-ycbxpizEQktk3FYvn/8BH+6/EuWXg7ZpQREJvgacqn46gIddG24tNNe4Son6omdXCnSOaApnpZw6MPCBA1dODA==}
dev: false
/svg.pathmorphing.js@0.1.3:
resolution: {integrity: sha512-49HWI9X4XQR/JG1qXkSDV8xViuTLIWm/B/7YuQELV5KMOPtXjiwH4XPJvr/ghEDibmLQ9Oc22dpWpG0vUDDNww==}
engines: {node: '>= 0.8.0'}
dependencies:
svg.js: 2.7.1
dev: false
/svg.resize.js@1.4.3:
resolution: {integrity: sha512-9k5sXJuPKp+mVzXNvxz7U0uC9oVMQrrf7cFsETznzUDDm0x8+77dtZkWdMfRlmbkEEYvUn9btKuZ3n41oNA+uw==}
engines: {node: '>= 0.8.0'}
dependencies:
svg.js: 2.7.1
svg.select.js: 2.1.2
dev: false
/svg.select.js@2.1.2:
resolution: {integrity: sha512-tH6ABEyJsAOVAhwcCjF8mw4crjXSI1aa7j2VQR8ZuJ37H2MBUbyeqYr5nEO7sSN3cy9AR9DUwNg0t/962HlDbQ==}
engines: {node: '>= 0.8.0'}
dependencies:
svg.js: 2.7.1
dev: false
/svg.select.js@3.0.1:
resolution: {integrity: sha512-h5IS/hKkuVCbKSieR9uQCj9w+zLHoPh+ce19bBYyqF53g6mnPB8sAtIbe1s9dh2S2fCmYX2xel1Ln3PJBbK4kw==}
engines: {node: '>= 0.8.0'}
dependencies:
svg.js: 2.7.1
dev: false
/svgo@3.0.2:
resolution: {integrity: sha512-Z706C1U2pb1+JGP48fbazf3KxHrWOsLme6Rv7imFBn5EnuanDW1GPaA/P1/dvObE670JDePC3mnj0k0B7P0jjQ==}
engines: {node: '>=14.0.0'}
@ -3051,16 +3110,6 @@ packages:
resolution: {integrity: sha512-AFbieoL7a5LMqcnOF04ji+rpXadgOXnZsxQr//r83kLPr7biP7am3g9zbaZIaBGwBRWeSvoMD4mgPdX3e4NWBg==}
dev: true
/vue-chartjs@5.2.0(chart.js@4.3.3)(vue@3.3.4):
resolution: {integrity: sha512-d3zpKmGZr2OWHQ1xmxBcAn5ShTG917+/UCLaSpaCDDqT0U7DBsvFzTs69ZnHCgKoXT55GZDW8YEj9Av+dlONLA==}
peerDependencies:
chart.js: ^4.1.1
vue: ^3.0.0-0 || ^2.7.0
dependencies:
chart.js: 4.3.3
vue: 3.3.4
dev: false
/vue-demi@0.14.5(vue@3.3.4):
resolution: {integrity: sha512-o9NUVpl/YlsGJ7t+xuqJKx8EBGf1quRhCiT6D/J0pfwmk9zUwYkC7yrF4SZCe6fETvSM3UNL2edcbYrSyc4QHA==}
engines: {node: '>=12'}
@ -3138,6 +3187,16 @@ packages:
typescript: 5.2.2
dev: true
/vue3-apexcharts@1.4.4(apexcharts@3.44.0)(vue@3.3.4):
resolution: {integrity: sha512-TH89uZrxGjaDvkaYAISvj8+k6Bf1rUKFillc8oJirs5XZEPiwM1ELKZQ786wz0rfPqkSHHny2lqqUCK7Rw+LcQ==}
peerDependencies:
apexcharts: '> 3.0.0'
vue: '> 3.0.0'
dependencies:
apexcharts: 3.44.0
vue: 3.3.4
dev: false
/vue@3.3.4:
resolution: {integrity: sha512-VTyEYn3yvIeY1Py0WaYGZsXnz3y5UnGi62GjVEqvEGPl6nxbOrCXbVOTQWBEJUqAyTUk2uJ5JLVnYJ6ZzGbrSw==}
dependencies: