fix: convert areachart to working barchart in trend-chart.svelte and fixed empty data issue (#930)
Co-authored-by: richarddkenneth <[email protected]>
This commit is contained in:
co-authored by
richarddkenneth
parent
913db2a525
commit
ce663fe365
@@ -26,37 +26,80 @@
|
|||||||
const chartConfig = $derived({
|
const chartConfig = $derived({
|
||||||
views: {
|
views: {
|
||||||
label: $t('analytics.trend.views'),
|
label: $t('analytics.trend.views'),
|
||||||
color: 'var(--primary)'
|
color: 'var(--chart-4)'
|
||||||
},
|
},
|
||||||
enrollments: {
|
enrollments: {
|
||||||
label: $t('analytics.trend.enrollments'),
|
label: $t('analytics.trend.enrollments'),
|
||||||
color: 'var(--chart-2, var(--primary))'
|
color: 'var(--chart-2)'
|
||||||
}
|
}
|
||||||
} satisfies ChartConfig);
|
} satisfies ChartConfig);
|
||||||
|
|
||||||
const series = [
|
const series = $derived([
|
||||||
{ key: 'views', value: 'views', label: 'Views', color: 'var(--color-views)' },
|
{
|
||||||
{ key: 'enrollments', value: 'enrollments', label: 'Enrollments', color: 'var(--color-enrollments)' }
|
key: 'views',
|
||||||
];
|
label: chartConfig.views.label,
|
||||||
|
color: chartConfig.views.color
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'enrollments',
|
||||||
|
label: chartConfig.enrollments.label,
|
||||||
|
color: chartConfig.enrollments.color
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
|
||||||
const hasData = $derived(
|
let chartData = $derived(
|
||||||
(data?.sparkline.length ?? 0) > 0 && data?.sparkline.some((r) => r.views > 0 || r.enrollments > 0)
|
data?.sparkline.map((d) => {
|
||||||
|
const [year, month, day] = d.date.split('-');
|
||||||
|
const date = new Date(Number(year), Number(month) - 1, Number(day));
|
||||||
|
return { ...d, date };
|
||||||
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const hasData = $derived((chartData?.length ?? 0) > 0 && chartData?.some((r) => r.views > 0 || r.enrollments > 0));
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<AnalyticsPanelCard title={$t('analytics.trend.heading')} description={$t('analytics.trend.description')}>
|
<AnalyticsPanelCard title={$t('analytics.trend.heading')} description={$t('analytics.trend.description')}>
|
||||||
{#snippet children()}
|
{#snippet children()}
|
||||||
{#if loading && !data}
|
{#if loading && !chartData}
|
||||||
<div class="flex h-[280px] items-center justify-center">
|
<div class="flex h-[280px] items-center justify-center">
|
||||||
<Spinner class="ui:text-muted-foreground size-6" />
|
<Spinner class="ui:text-muted-foreground size-6" />
|
||||||
</div>
|
</div>
|
||||||
{:else if browser && hasData && data}
|
{:else if browser && hasData && chartData}
|
||||||
{#await loadChart() then C}
|
{#await loadChart() then C}
|
||||||
<C.ChartContainer class="h-[280px] w-full" config={chartConfig}>
|
<C.ChartContainer class="h-[280px] w-full" config={chartConfig}>
|
||||||
<C.AreaChart data={data.sparkline} x="date" axis="x" {series} />
|
<C.BarChart
|
||||||
|
data={chartData}
|
||||||
|
xScale={C.scaleBand().padding(0.25)}
|
||||||
|
x="date"
|
||||||
|
axis="x"
|
||||||
|
seriesLayout="group"
|
||||||
|
legend
|
||||||
|
{series}
|
||||||
|
props={{
|
||||||
|
xAxis: {
|
||||||
|
format: (d: Date) =>
|
||||||
|
d.toLocaleDateString('en-US', {
|
||||||
|
month: 'short',
|
||||||
|
day: 'numeric'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{#snippet tooltip()}
|
||||||
|
<C.Tooltip
|
||||||
|
labelFormatter={(d: Date) =>
|
||||||
|
d.toLocaleDateString('en-US', {
|
||||||
|
weekday: 'long',
|
||||||
|
year: 'numeric',
|
||||||
|
month: 'long',
|
||||||
|
day: 'numeric'
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
{/snippet}
|
||||||
|
</C.BarChart>
|
||||||
</C.ChartContainer>
|
</C.ChartContainer>
|
||||||
{/await}
|
{/await}
|
||||||
{:else if hasData && data}
|
{:else if hasData && chartData}
|
||||||
<div class="flex h-[280px] items-center justify-center">
|
<div class="flex h-[280px] items-center justify-center">
|
||||||
<Spinner class="ui:text-muted-foreground size-6" />
|
<Spinner class="ui:text-muted-foreground size-6" />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { onMount } from 'svelte';
|
|
||||||
import { t } from '$lib/utils/functions/translations';
|
import { t } from '$lib/utils/functions/translations';
|
||||||
import { currentOrg } from '$lib/utils/store/org';
|
import { currentOrg } from '$lib/utils/store/org';
|
||||||
import * as Page from '@cio/ui/base/page';
|
import * as Page from '@cio/ui/base/page';
|
||||||
@@ -15,9 +14,8 @@
|
|||||||
analyticsApi
|
analyticsApi
|
||||||
} from '$features/analytics';
|
} from '$features/analytics';
|
||||||
|
|
||||||
onMount(() => {
|
$effect(() => {
|
||||||
const orgId = $currentOrg.id;
|
analyticsApi.ensureFetched($currentOrg.id);
|
||||||
if (orgId) analyticsApi.ensureFetched(orgId);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
function handleRangeChange(days: 7 | 30 | 90) {
|
function handleRangeChange(days: 7 | 30 | 90) {
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { onMount } from 'svelte';
|
|
||||||
import { t } from '$lib/utils/functions/translations';
|
import { t } from '$lib/utils/functions/translations';
|
||||||
import { currentOrg } from '$lib/utils/store/org';
|
import { currentOrg } from '$lib/utils/store/org';
|
||||||
import * as Page from '@cio/ui/base/page';
|
import * as Page from '@cio/ui/base/page';
|
||||||
@@ -10,9 +9,8 @@
|
|||||||
import { StatusTiles, CourseBreakdown, LearnersTable, complianceApi } from '$features/compliance';
|
import { StatusTiles, CourseBreakdown, LearnersTable, complianceApi } from '$features/compliance';
|
||||||
import type { ComplianceLearnerRow } from '$features/compliance/utils/types';
|
import type { ComplianceLearnerRow } from '$features/compliance/utils/types';
|
||||||
|
|
||||||
onMount(() => {
|
$effect(() => {
|
||||||
const orgId = $currentOrg.id;
|
complianceApi.ensureFetched($currentOrg.id);
|
||||||
if (orgId) complianceApi.ensureFetched(orgId);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
function handleRefresh() {
|
function handleRefresh() {
|
||||||
|
|||||||
@@ -56,9 +56,6 @@
|
|||||||
"esbuild",
|
"esbuild",
|
||||||
"@tailwindcss/oxide"
|
"@tailwindcss/oxide"
|
||||||
],
|
],
|
||||||
"patchedDependencies": {
|
|
||||||
"[email protected]": "patches/[email protected]"
|
|
||||||
},
|
|
||||||
"overrides": {
|
"overrides": {
|
||||||
"zod": "4.2.1",
|
"zod": "4.2.1",
|
||||||
"miniflare>zod": "3.22.3",
|
"miniflare>zod": "3.22.3",
|
||||||
|
|||||||
@@ -32,7 +32,7 @@
|
|||||||
"bits-ui": "^2.16.1",
|
"bits-ui": "^2.16.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"jsrepo": "^3.5.1",
|
"jsrepo": "^3.5.1",
|
||||||
"layerchart": "2.0.0-next.27",
|
"layerchart": "2.2.0",
|
||||||
"mode-watcher": "^1.1.0",
|
"mode-watcher": "^1.1.0",
|
||||||
"runed": "^0.37.1",
|
"runed": "^0.37.1",
|
||||||
"shiki": "^3.22.0",
|
"shiki": "^3.22.0",
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import { getChartContext, Tooltip as TooltipPrimitive } from 'layerchart';
|
||||||
import { cn, type WithElementRef, type WithoutChildren } from '../../tools';
|
import { cn, type WithElementRef, type WithoutChildren } from '../../tools';
|
||||||
import type { HTMLAttributes } from 'svelte/elements';
|
import type { HTMLAttributes } from 'svelte/elements';
|
||||||
import { getPayloadConfigFromPayload, useChart, type TooltipPayload } from './chart-utils';
|
import { getPayloadConfigFromPayload, useChart, type TooltipPayload } from './chart-utils';
|
||||||
import { getTooltipContext, Tooltip as TooltipPrimitive } from 'layerchart';
|
|
||||||
import type { Snippet } from 'svelte';
|
import type { Snippet } from 'svelte';
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
@@ -48,27 +48,39 @@
|
|||||||
} = $props();
|
} = $props();
|
||||||
|
|
||||||
const chart = useChart();
|
const chart = useChart();
|
||||||
const tooltipCtx = getTooltipContext();
|
const chartCtx = getChartContext();
|
||||||
|
|
||||||
|
const visibleSeries = $derived(chartCtx.tooltip.series.filter((s) => s.value !== undefined));
|
||||||
|
|
||||||
const formattedLabel = $derived.by(() => {
|
const formattedLabel = $derived.by(() => {
|
||||||
if (hideLabel || !tooltipCtx.payload?.length) return null;
|
if (hideLabel || !visibleSeries?.length) return null;
|
||||||
|
|
||||||
const [item] = tooltipCtx.payload;
|
const [item] = visibleSeries;
|
||||||
const key = labelKey ?? item?.label ?? item?.name ?? 'value';
|
const tooltipData = chartCtx.tooltip.data;
|
||||||
|
|
||||||
const itemConfig = getPayloadConfigFromPayload(chart.config, item, key);
|
const dataLabel = tooltipData != null ? chartCtx.x(tooltipData) : undefined;
|
||||||
|
|
||||||
const value =
|
const key = labelKey ?? item?.label ?? 'value';
|
||||||
!labelKey && typeof label === 'string'
|
|
||||||
? (chart.config[label as keyof typeof chart.config]?.label ?? label)
|
let value: unknown;
|
||||||
: (itemConfig?.label ?? item.label);
|
if (!labelKey && typeof label === 'string') {
|
||||||
|
value = chart.config[label as keyof typeof chart.config]?.label ?? label;
|
||||||
|
} else {
|
||||||
|
const itemConfig = getPayloadConfigFromPayload(
|
||||||
|
chart.config,
|
||||||
|
item,
|
||||||
|
key,
|
||||||
|
tooltipData as Record<string, unknown> | null
|
||||||
|
);
|
||||||
|
value = itemConfig?.label ?? dataLabel;
|
||||||
|
}
|
||||||
|
|
||||||
if (value === undefined) return null;
|
if (value === undefined) return null;
|
||||||
if (!labelFormatter) return value;
|
if (!labelFormatter) return value;
|
||||||
return labelFormatter(value, tooltipCtx.payload);
|
return labelFormatter(value, visibleSeries);
|
||||||
});
|
});
|
||||||
|
|
||||||
const nestLabel = $derived(tooltipCtx.payload.length === 1 && indicator !== 'dot');
|
const nestLabel = $derived(visibleSeries.length === 1 && indicator !== 'dot');
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#snippet TooltipLabel()}
|
{#snippet TooltipLabel()}
|
||||||
@@ -85,6 +97,7 @@
|
|||||||
|
|
||||||
<TooltipPrimitive.Root variant="none">
|
<TooltipPrimitive.Root variant="none">
|
||||||
<div
|
<div
|
||||||
|
bind:this={ref}
|
||||||
class={cn(
|
class={cn(
|
||||||
'ui:border-border/50 ui:bg-background ui:grid ui:min-w-[9rem] ui:items-start ui:gap-1.5 ui:rounded-lg ui:border ui:px-2.5 ui:py-1.5 ui:text-xs ui:shadow-xl',
|
'ui:border-border/50 ui:bg-background ui:grid ui:min-w-[9rem] ui:items-start ui:gap-1.5 ui:rounded-lg ui:border ui:px-2.5 ui:py-1.5 ui:text-xs ui:shadow-xl',
|
||||||
className
|
className
|
||||||
@@ -95,23 +108,23 @@
|
|||||||
{@render TooltipLabel()}
|
{@render TooltipLabel()}
|
||||||
{/if}
|
{/if}
|
||||||
<div class="ui:grid ui:gap-1.5">
|
<div class="ui:grid ui:gap-1.5">
|
||||||
{#each tooltipCtx.payload as item, i (item.key + i)}
|
{#each visibleSeries as item, i (item.key + i)}
|
||||||
{@const key = `${nameKey || item.key || item.name || 'value'}`}
|
{@const key = `${nameKey || item.key || item.label || 'value'}`}
|
||||||
{@const itemConfig = getPayloadConfigFromPayload(chart.config, item, key)}
|
{@const itemConfig = getPayloadConfigFromPayload(chart.config, item, key, chartCtx.tooltip.data)}
|
||||||
{@const indicatorColor = color || item.payload?.color || item.color}
|
{@const indicatorColor = color || item.config?.color || item.color}
|
||||||
<div
|
<div
|
||||||
class={cn(
|
class={cn(
|
||||||
'ui:[&>svg]:text-muted-foreground ui:flex ui:w-full ui:flex-wrap ui:items-stretch ui:gap-2 ui:[&>svg]:size-2.5',
|
'ui:[&>svg]:text-muted-foreground ui:flex ui:w-full ui:flex-wrap ui:items-stretch ui:gap-2 ui:[&>svg]:size-2.5',
|
||||||
indicator === 'dot' && 'ui:items-center'
|
indicator === 'dot' && 'ui:items-center'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{#if formatter && item.value !== undefined && item.name}
|
{#if formatter && item.value !== undefined && item.label}
|
||||||
{@render formatter({
|
{@render formatter({
|
||||||
value: item.value,
|
value: item.value,
|
||||||
name: item.name,
|
name: item.label,
|
||||||
item,
|
item,
|
||||||
index: i,
|
index: i,
|
||||||
payload: tooltipCtx.payload
|
payload: visibleSeries
|
||||||
})}
|
})}
|
||||||
{:else}
|
{:else}
|
||||||
{#if itemConfig?.icon}
|
{#if itemConfig?.icon}
|
||||||
@@ -138,7 +151,7 @@
|
|||||||
{@render TooltipLabel()}
|
{@render TooltipLabel()}
|
||||||
{/if}
|
{/if}
|
||||||
<span class="ui:text-muted-foreground">
|
<span class="ui:text-muted-foreground">
|
||||||
{itemConfig?.label || item.name}
|
{itemConfig?.label || item.label}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
{#if item.value !== undefined}
|
{#if item.value !== undefined}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import type { Tooltip } from 'layerchart';
|
import type { Tooltip } from 'layerchart';
|
||||||
import { getContext, setContext, type Component, type ComponentProps, type Snippet } from 'svelte';
|
import { getContext, setContext, type Snippet } from 'svelte';
|
||||||
import type { ChartConfig } from './types';
|
import type { ChartConfig } from './types';
|
||||||
|
|
||||||
export type { ChartConfig } from './types';
|
export type { ChartConfig } from './types';
|
||||||
@@ -14,31 +14,37 @@ export type ChartLegendItem = {
|
|||||||
|
|
||||||
export type ExtractSnippetParams<T> = T extends Snippet<[infer P]> ? P : never;
|
export type ExtractSnippetParams<T> = T extends Snippet<[infer P]> ? P : never;
|
||||||
|
|
||||||
export type TooltipPayload = ExtractSnippetParams<ComponentProps<typeof Tooltip.Root>['children']>['payload'][number];
|
export type TooltipPayload = Tooltip.TooltipSeries;
|
||||||
|
|
||||||
// Helper to extract item config from a payload.
|
// Helper to extract item config from a payload.
|
||||||
export function getPayloadConfigFromPayload(config: ChartConfig, payload: TooltipPayload, key: string) {
|
export function getPayloadConfigFromPayload(
|
||||||
|
config: ChartConfig,
|
||||||
|
payload: TooltipPayload,
|
||||||
|
key: string,
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
data?: Record<string, any> | null
|
||||||
|
) {
|
||||||
if (typeof payload !== 'object' || payload === null) return undefined;
|
if (typeof payload !== 'object' || payload === null) return undefined;
|
||||||
|
|
||||||
const payloadPayload =
|
const payloadConfig =
|
||||||
'payload' in payload && typeof payload.payload === 'object' && payload.payload !== null
|
'config' in payload && typeof payload.config === 'object' && payload.config !== null ? payload.config : undefined;
|
||||||
? payload.payload
|
|
||||||
: undefined;
|
|
||||||
|
|
||||||
let configLabelKey: string = key;
|
let configLabelKey: string = key;
|
||||||
|
|
||||||
if (payload.key === key) {
|
if (payload.key === key) {
|
||||||
configLabelKey = payload.key;
|
configLabelKey = payload.key;
|
||||||
} else if (payload.name === key) {
|
} else if (payload.label === key) {
|
||||||
configLabelKey = payload.name;
|
configLabelKey = payload.label;
|
||||||
} else if (key in payload && typeof payload[key as keyof typeof payload] === 'string') {
|
} else if (key in payload && typeof payload[key as keyof typeof payload] === 'string') {
|
||||||
configLabelKey = payload[key as keyof typeof payload] as string;
|
configLabelKey = payload[key as keyof typeof payload] as string;
|
||||||
} else if (
|
} else if (
|
||||||
payloadPayload !== undefined &&
|
payloadConfig !== undefined &&
|
||||||
key in payloadPayload &&
|
key in payloadConfig &&
|
||||||
typeof payloadPayload[key as keyof typeof payloadPayload] === 'string'
|
typeof payloadConfig[key as keyof typeof payloadConfig] === 'string'
|
||||||
) {
|
) {
|
||||||
configLabelKey = payloadPayload[key as keyof typeof payloadPayload] as string;
|
configLabelKey = payloadConfig[key as keyof typeof payloadConfig] as string;
|
||||||
|
} else if (data != null && key in data && typeof data[key] === 'string') {
|
||||||
|
configLabelKey = data[key] as string;
|
||||||
}
|
}
|
||||||
|
|
||||||
return configLabelKey in config ? config[configLabelKey] : config[key as keyof typeof config];
|
return configLabelKey in config ? config[configLabelKey] : config[key as keyof typeof config];
|
||||||
|
|||||||
@@ -1,23 +0,0 @@
|
|||||||
diff --git a/dist/components/MonthPath.svelte b/dist/components/MonthPath.svelte
|
|
||||||
index 3ca6c1ec5bf10f2ebacceb0743d7844860bac057..5e3d4246a5d66b4643839c749eee00c8319c788c 100644
|
|
||||||
--- a/dist/components/MonthPath.svelte
|
|
||||||
+++ b/dist/components/MonthPath.svelte
|
|
||||||
@@ -30,7 +30,7 @@
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
- import { timeWeek, timeMonth, timeYear } from 'd3-time';
|
|
||||||
+ import { timeWeek, timeYear } from 'd3-time';
|
|
||||||
import { cls } from '@layerstack/tailwind';
|
|
||||||
import { endOfInterval } from '@layerstack/utils';
|
|
||||||
import { layerClass } from '../utils/attributes.js';
|
|
||||||
diff --git a/dist/utils/ticks.js b/dist/utils/ticks.js
|
|
||||||
index 9f82ad72a077c08b55f32c8fd8fa6c5d5f212d3f..5b56ed837beef7054324711853eee46c5819f9f9 100644
|
|
||||||
--- a/dist/utils/ticks.js
|
|
||||||
+++ b/dist/utils/ticks.js
|
|
||||||
@@ -1,4 +1,4 @@
|
|
||||||
-import { timeYear, timeMonth, timeDay, timeTicks } from 'd3-time';
|
|
||||||
+import { timeYear, timeDay, timeTicks } from 'd3-time';
|
|
||||||
import { format, Duration, isLiteralObject, DateToken, } from '@layerstack/utils';
|
|
||||||
import { isScaleBand, isScaleTime } from './scales.svelte.js';
|
|
||||||
export function getDurationFormat(duration, options = {
|
|
||||||
Generated
+98
-89
Reference in New Issue
Block a user