feat(analytics): add frappe-charts components and type definitions
- commit
- 1eb5fcd572670301bc3898ac0b3de4d4d57514e6
- author
- bnema · · 2025-12-06 20:53 UTC
- parent
- 377b106
- signature
- unsigned
4 files · +292 -0
A gui/src/pages/homepage/analytics/DailyVisitsChart.svelte +119 -0
| @@ -0,0 +1,119 @@ | |||
| 1 | + | <script lang="ts"> | |
| 2 | + | import { Chart } from 'frappe-charts'; | |
| 3 | + | import { onMount } from 'svelte'; | |
| 4 | + | import type { DailyVisitCount } from '../types'; | |
| 5 | + | ||
| 6 | + | interface Props { | |
| 7 | + | dailyVisits: DailyVisitCount[]; | |
| 8 | + | } | |
| 9 | + | ||
| 10 | + | let { dailyVisits }: Props = $props(); | |
| 11 | + | ||
| 12 | + | let container: HTMLDivElement; | |
| 13 | + | let chart: Chart | null = null; | |
| 14 | + | ||
| 15 | + | // Convert data to Frappe Charts format | |
| 16 | + | const chartData = $derived.by(() => { | |
| 17 | + | if (!dailyVisits || dailyVisits.length === 0) { | |
| 18 | + | return { labels: [], datasets: [{ values: [] }] }; | |
| 19 | + | } | |
| 20 | + | ||
| 21 | + | const labels = dailyVisits.map(d => { | |
| 22 | + | const date = new Date(d.day + 'T12:00:00'); | |
| 23 | + | return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric' }); | |
| 24 | + | }); | |
| 25 | + | const values = dailyVisits.map(d => d.visits); | |
| 26 | + | ||
| 27 | + | return { | |
| 28 | + | labels, | |
| 29 | + | datasets: [{ name: 'Visits', values }] | |
| 30 | + | }; | |
| 31 | + | }); | |
| 32 | + | ||
| 33 | + | const totalVisits = $derived(dailyVisits.reduce((sum, d) => sum + d.visits, 0)); | |
| 34 | + | ||
| 35 | + | // Initialize chart on mount | |
| 36 | + | onMount(() => { | |
| 37 | + | if (chartData.labels.length === 0) return; | |
| 38 | + | ||
| 39 | + | chart = new Chart(container, { | |
| 40 | + | data: chartData, | |
| 41 | + | type: 'line', | |
| 42 | + | height: 160, | |
| 43 | + | colors: ['#4ade80'], | |
| 44 | + | axisOptions: { xAxisMode: 'tick', xIsSeries: true }, | |
| 45 | + | lineOptions: { regionFill: 1, hideDots: 0, dotSize: 4 }, | |
| 46 | + | tooltipOptions: { formatTooltipY: (d: number) => `${d} visits` } | |
| 47 | + | }); | |
| 48 | + | ||
| 49 | + | return () => { | |
| 50 | + | chart?.destroy(); | |
| 51 | + | chart = null; | |
| 52 | + | }; | |
| 53 | + | }); | |
| 54 | + | ||
| 55 | + | // Update chart when data changes (after initial mount) | |
| 56 | + | $effect(() => { | |
| 57 | + | // Skip if chart not initialized yet or no data | |
| 58 | + | if (!chart) return; | |
| 59 | + | ||
| 60 | + | // Read chartData to track changes | |
| 61 | + | const data = chartData; | |
| 62 | + | if (data.labels.length === 0) return; | |
| 63 | + | ||
| 64 | + | chart.update(data); | |
| 65 | + | }); | |
| 66 | + | </script> | |
| 67 | + | ||
| 68 | + | <div class="chart-wrapper"> | |
| 69 | + | <div class="chart-header"> | |
| 70 | + | <span class="chart-label">Visits</span> | |
| 71 | + | <span class="chart-value">{totalVisits} total</span> | |
| 72 | + | </div> | |
| 73 | + | <div bind:this={container} class="chart-container"></div> | |
| 74 | + | </div> | |
| 75 | + | ||
| 76 | + | <style> | |
| 77 | + | .chart-wrapper { | |
| 78 | + | width: 100%; | |
| 79 | + | display: flex; | |
| 80 | + | flex-direction: column; | |
| 81 | + | } | |
| 82 | + | ||
| 83 | + | .chart-header { | |
| 84 | + | display: flex; | |
| 85 | + | align-items: center; | |
| 86 | + | gap: 0.5rem; | |
| 87 | + | padding: 0 0.25rem 0.5rem; | |
| 88 | + | font-size: 0.65rem; | |
| 89 | + | font-family: ui-monospace, 'Fira Code', 'Cascadia Code', Menlo, Monaco, Consolas, monospace; | |
| 90 | + | } | |
| 91 | + | ||
| 92 | + | .chart-label { | |
| 93 | + | color: #4ade80; | |
| 94 | + | font-weight: 600; | |
| 95 | + | } | |
| 96 | + | ||
| 97 | + | .chart-value { | |
| 98 | + | color: var(--dynamic-muted, #737373); | |
| 99 | + | } | |
| 100 | + | ||
| 101 | + | .chart-container { | |
| 102 | + | width: 100%; | |
| 103 | + | } | |
| 104 | + | ||
| 105 | + | .chart-container :global(.frappe-chart) { | |
| 106 | + | font-family: ui-monospace, 'Fira Code', 'Cascadia Code', Menlo, Monaco, Consolas, monospace; | |
| 107 | + | } | |
| 108 | + | ||
| 109 | + | .chart-container :global(.frappe-chart .axis text), | |
| 110 | + | .chart-container :global(.frappe-chart .chart-label) { | |
| 111 | + | fill: #737373; | |
| 112 | + | font-size: 10px; | |
| 113 | + | } | |
| 114 | + | ||
| 115 | + | .chart-container :global(.frappe-chart .axis line), | |
| 116 | + | .chart-container :global(.frappe-chart .chart-strokes) { | |
| 117 | + | stroke: #333; | |
| 118 | + | } | |
| 119 | + | </style> | |
| @@ -0,0 +1,119 @@ | |||||
| 1 | + | <script lang="ts"> | |||
| 2 | + | import { Chart } from 'frappe-charts'; | |||
| 3 | + | import { onMount } from 'svelte'; | |||
| 4 | + | import type { DailyVisitCount } from '../types'; | |||
| 5 | + | ||||
| 6 | + | interface Props { | |||
| 7 | + | dailyVisits: DailyVisitCount[]; | |||
| 8 | + | } | |||
| 9 | + | ||||
| 10 | + | let { dailyVisits }: Props = $props(); | |||
| 11 | + | ||||
| 12 | + | let container: HTMLDivElement; | |||
| 13 | + | let chart: Chart | null = null; | |||
| 14 | + | ||||
| 15 | + | // Convert data to Frappe Charts format | |||
| 16 | + | const chartData = $derived.by(() => { | |||
| 17 | + | if (!dailyVisits || dailyVisits.length === 0) { | |||
| 18 | + | return { labels: [], datasets: [{ values: [] }] }; | |||
| 19 | + | } | |||
| 20 | + | ||||
| 21 | + | const labels = dailyVisits.map(d => { | |||
| 22 | + | const date = new Date(d.day + 'T12:00:00'); | |||
| 23 | + | return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric' }); | |||
| 24 | + | }); | |||
| 25 | + | const values = dailyVisits.map(d => d.visits); | |||
| 26 | + | ||||
| 27 | + | return { | |||
| 28 | + | labels, | |||
| 29 | + | datasets: [{ name: 'Visits', values }] | |||
| 30 | + | }; | |||
| 31 | + | }); | |||
| 32 | + | ||||
| 33 | + | const totalVisits = $derived(dailyVisits.reduce((sum, d) => sum + d.visits, 0)); | |||
| 34 | + | ||||
| 35 | + | // Initialize chart on mount | |||
| 36 | + | onMount(() => { | |||
| 37 | + | if (chartData.labels.length === 0) return; | |||
| 38 | + | ||||
| 39 | + | chart = new Chart(container, { | |||
| 40 | + | data: chartData, | |||
| 41 | + | type: 'line', | |||
| 42 | + | height: 160, | |||
| 43 | + | colors: ['#4ade80'], | |||
| 44 | + | axisOptions: { xAxisMode: 'tick', xIsSeries: true }, | |||
| 45 | + | lineOptions: { regionFill: 1, hideDots: 0, dotSize: 4 }, | |||
| 46 | + | tooltipOptions: { formatTooltipY: (d: number) => `${d} visits` } | |||
| 47 | + | }); | |||
| 48 | + | ||||
| 49 | + | return () => { | |||
| 50 | + | chart?.destroy(); | |||
| 51 | + | chart = null; | |||
| 52 | + | }; | |||
| 53 | + | }); | |||
| 54 | + | ||||
| 55 | + | // Update chart when data changes (after initial mount) | |||
| 56 | + | $effect(() => { | |||
| 57 | + | // Skip if chart not initialized yet or no data | |||
| 58 | + | if (!chart) return; | |||
| 59 | + | ||||
| 60 | + | // Read chartData to track changes | |||
| 61 | + | const data = chartData; | |||
| 62 | + | if (data.labels.length === 0) return; | |||
| 63 | + | ||||
| 64 | + | chart.update(data); | |||
| 65 | + | }); | |||
| 66 | + | </script> | |||
| 67 | + | ||||
| 68 | + | <div class="chart-wrapper"> | |||
| 69 | + | <div class="chart-header"> | |||
| 70 | + | <span class="chart-label">Visits</span> | |||
| 71 | + | <span class="chart-value">{totalVisits} total</span> | |||
| 72 | + | </div> | |||
| 73 | + | <div bind:this={container} class="chart-container"></div> | |||
| 74 | + | </div> | |||
| 75 | + | ||||
| 76 | + | <style> | |||
| 77 | + | .chart-wrapper { | |||
| 78 | + | width: 100%; | |||
| 79 | + | display: flex; | |||
| 80 | + | flex-direction: column; | |||
| 81 | + | } | |||
| 82 | + | ||||
| 83 | + | .chart-header { | |||
| 84 | + | display: flex; | |||
| 85 | + | align-items: center; | |||
| 86 | + | gap: 0.5rem; | |||
| 87 | + | padding: 0 0.25rem 0.5rem; | |||
| 88 | + | font-size: 0.65rem; | |||
| 89 | + | font-family: ui-monospace, 'Fira Code', 'Cascadia Code', Menlo, Monaco, Consolas, monospace; | |||
| 90 | + | } | |||
| 91 | + | ||||
| 92 | + | .chart-label { | |||
| 93 | + | color: #4ade80; | |||
| 94 | + | font-weight: 600; | |||
| 95 | + | } | |||
| 96 | + | ||||
| 97 | + | .chart-value { | |||
| 98 | + | color: var(--dynamic-muted, #737373); | |||
| 99 | + | } | |||
| 100 | + | ||||
| 101 | + | .chart-container { | |||
| 102 | + | width: 100%; | |||
| 103 | + | } | |||
| 104 | + | ||||
| 105 | + | .chart-container :global(.frappe-chart) { | |||
| 106 | + | font-family: ui-monospace, 'Fira Code', 'Cascadia Code', Menlo, Monaco, Consolas, monospace; | |||
| 107 | + | } | |||
| 108 | + | ||||
| 109 | + | .chart-container :global(.frappe-chart .axis text), | |||
| 110 | + | .chart-container :global(.frappe-chart .chart-label) { | |||
| 111 | + | fill: #737373; | |||
| 112 | + | font-size: 10px; | |||
| 113 | + | } | |||
| 114 | + | ||||
| 115 | + | .chart-container :global(.frappe-chart .axis line), | |||
| 116 | + | .chart-container :global(.frappe-chart .chart-strokes) { | |||
| 117 | + | stroke: #333; | |||
| 118 | + | } | |||
| 119 | + | </style> | |||
A gui/src/pages/homepage/analytics/HourlyActivityChart.svelte +124 -0
| @@ -0,0 +1,124 @@ | |||
| 1 | + | <script lang="ts"> | |
| 2 | + | import { Chart } from 'frappe-charts'; | |
| 3 | + | import { onMount } from 'svelte'; | |
| 4 | + | import type { HourlyDistribution } from '../types'; | |
| 5 | + | ||
| 6 | + | interface Props { | |
| 7 | + | hourlyDistribution: HourlyDistribution[]; | |
| 8 | + | } | |
| 9 | + | ||
| 10 | + | let { hourlyDistribution }: Props = $props(); | |
| 11 | + | ||
| 12 | + | let container: HTMLDivElement; | |
| 13 | + | let chart: Chart | null = null; | |
| 14 | + | ||
| 15 | + | // Convert data to Frappe Charts format (ensure all 24 hours) | |
| 16 | + | const chartData = $derived.by(() => { | |
| 17 | + | const visitsByHour = new Map<number, number>(); | |
| 18 | + | hourlyDistribution.forEach(h => visitsByHour.set(h.hour, h.visit_count)); | |
| 19 | + | ||
| 20 | + | const labels = Array.from({ length: 24 }, (_, i) => | |
| 21 | + | i % 6 === 0 ? `${String(i).padStart(2, '0')}h` : '' | |
| 22 | + | ); | |
| 23 | + | const values = Array.from({ length: 24 }, (_, i) => visitsByHour.get(i) ?? 0); | |
| 24 | + | ||
| 25 | + | return { | |
| 26 | + | labels, | |
| 27 | + | datasets: [{ name: 'Activity', values }] | |
| 28 | + | }; | |
| 29 | + | }); | |
| 30 | + | ||
| 31 | + | // Peak hour for display | |
| 32 | + | const peakHour = $derived.by(() => { | |
| 33 | + | let maxVisits = 0; | |
| 34 | + | let peak = 0; | |
| 35 | + | hourlyDistribution.forEach(h => { | |
| 36 | + | if (h.visit_count > maxVisits) { | |
| 37 | + | maxVisits = h.visit_count; | |
| 38 | + | peak = h.hour; | |
| 39 | + | } | |
| 40 | + | }); | |
| 41 | + | return `${String(peak).padStart(2, '0')}:00`; | |
| 42 | + | }); | |
| 43 | + | ||
| 44 | + | // Initialize chart on mount | |
| 45 | + | onMount(() => { | |
| 46 | + | chart = new Chart(container, { | |
| 47 | + | data: chartData, | |
| 48 | + | type: 'bar', | |
| 49 | + | height: 120, | |
| 50 | + | colors: ['#4ade80'], | |
| 51 | + | axisOptions: { xAxisMode: 'tick' }, | |
| 52 | + | barOptions: { spaceRatio: 0.3 }, | |
| 53 | + | tooltipOptions: { formatTooltipY: (d: number) => `${d} visits` } | |
| 54 | + | }); | |
| 55 | + | ||
| 56 | + | return () => { | |
| 57 | + | chart?.destroy(); | |
| 58 | + | chart = null; | |
| 59 | + | }; | |
| 60 | + | }); | |
| 61 | + | ||
| 62 | + | // Update chart when data changes (after initial mount) | |
| 63 | + | $effect(() => { | |
| 64 | + | if (!chart) return; | |
| 65 | + | ||
| 66 | + | const data = chartData; | |
| 67 | + | if (data.labels.length === 0) return; | |
| 68 | + | ||
| 69 | + | chart.update(data); | |
| 70 | + | }); | |
| 71 | + | </script> | |
| 72 | + | ||
| 73 | + | <div class="chart-wrapper"> | |
| 74 | + | <div class="chart-header"> | |
| 75 | + | <span class="chart-label">Activity</span> | |
| 76 | + | <span class="chart-value">peak at {peakHour}</span> | |
| 77 | + | </div> | |
| 78 | + | <div bind:this={container} class="chart-container"></div> | |
| 79 | + | </div> | |
| 80 | + | ||
| 81 | + | <style> | |
| 82 | + | .chart-wrapper { | |
| 83 | + | width: 100%; | |
| 84 | + | display: flex; | |
| 85 | + | flex-direction: column; | |
| 86 | + | } | |
| 87 | + | ||
| 88 | + | .chart-header { | |
| 89 | + | display: flex; | |
| 90 | + | align-items: center; | |
| 91 | + | gap: 0.5rem; | |
| 92 | + | padding: 0 0.25rem 0.5rem; | |
| 93 | + | font-size: 0.65rem; | |
| 94 | + | font-family: ui-monospace, 'Fira Code', 'Cascadia Code', Menlo, Monaco, Consolas, monospace; | |
| 95 | + | } | |
| 96 | + | ||
| 97 | + | .chart-label { | |
| 98 | + | color: #4ade80; | |
| 99 | + | font-weight: 600; | |
| 100 | + | } | |
| 101 | + | ||
| 102 | + | .chart-value { | |
| 103 | + | color: var(--dynamic-muted, #737373); | |
| 104 | + | } | |
| 105 | + | ||
| 106 | + | .chart-container { | |
| 107 | + | width: 100%; | |
| 108 | + | } | |
| 109 | + | ||
| 110 | + | .chart-container :global(.frappe-chart) { | |
| 111 | + | font-family: ui-monospace, 'Fira Code', 'Cascadia Code', Menlo, Monaco, Consolas, monospace; | |
| 112 | + | } | |
| 113 | + | ||
| 114 | + | .chart-container :global(.frappe-chart .axis text), | |
| 115 | + | .chart-container :global(.frappe-chart .chart-label) { | |
| 116 | + | fill: #737373; | |
| 117 | + | font-size: 10px; | |
| 118 | + | } | |
| 119 | + | ||
| 120 | + | .chart-container :global(.frappe-chart .axis line), | |
| 121 | + | .chart-container :global(.frappe-chart .chart-strokes) { | |
| 122 | + | stroke: #333; | |
| 123 | + | } | |
| 124 | + | </style> | |
| @@ -0,0 +1,124 @@ | |||||
| 1 | + | <script lang="ts"> | |||
| 2 | + | import { Chart } from 'frappe-charts'; | |||
| 3 | + | import { onMount } from 'svelte'; | |||
| 4 | + | import type { HourlyDistribution } from '../types'; | |||
| 5 | + | ||||
| 6 | + | interface Props { | |||
| 7 | + | hourlyDistribution: HourlyDistribution[]; | |||
| 8 | + | } | |||
| 9 | + | ||||
| 10 | + | let { hourlyDistribution }: Props = $props(); | |||
| 11 | + | ||||
| 12 | + | let container: HTMLDivElement; | |||
| 13 | + | let chart: Chart | null = null; | |||
| 14 | + | ||||
| 15 | + | // Convert data to Frappe Charts format (ensure all 24 hours) | |||
| 16 | + | const chartData = $derived.by(() => { | |||
| 17 | + | const visitsByHour = new Map<number, number>(); | |||
| 18 | + | hourlyDistribution.forEach(h => visitsByHour.set(h.hour, h.visit_count)); | |||
| 19 | + | ||||
| 20 | + | const labels = Array.from({ length: 24 }, (_, i) => | |||
| 21 | + | i % 6 === 0 ? `${String(i).padStart(2, '0')}h` : '' | |||
| 22 | + | ); | |||
| 23 | + | const values = Array.from({ length: 24 }, (_, i) => visitsByHour.get(i) ?? 0); | |||
| 24 | + | ||||
| 25 | + | return { | |||
| 26 | + | labels, | |||
| 27 | + | datasets: [{ name: 'Activity', values }] | |||
| 28 | + | }; | |||
| 29 | + | }); | |||
| 30 | + | ||||
| 31 | + | // Peak hour for display | |||
| 32 | + | const peakHour = $derived.by(() => { | |||
| 33 | + | let maxVisits = 0; | |||
| 34 | + | let peak = 0; | |||
| 35 | + | hourlyDistribution.forEach(h => { | |||
| 36 | + | if (h.visit_count > maxVisits) { | |||
| 37 | + | maxVisits = h.visit_count; | |||
| 38 | + | peak = h.hour; | |||
| 39 | + | } | |||
| 40 | + | }); | |||
| 41 | + | return `${String(peak).padStart(2, '0')}:00`; | |||
| 42 | + | }); | |||
| 43 | + | ||||
| 44 | + | // Initialize chart on mount | |||
| 45 | + | onMount(() => { | |||
| 46 | + | chart = new Chart(container, { | |||
| 47 | + | data: chartData, | |||
| 48 | + | type: 'bar', | |||
| 49 | + | height: 120, | |||
| 50 | + | colors: ['#4ade80'], | |||
| 51 | + | axisOptions: { xAxisMode: 'tick' }, | |||
| 52 | + | barOptions: { spaceRatio: 0.3 }, | |||
| 53 | + | tooltipOptions: { formatTooltipY: (d: number) => `${d} visits` } | |||
| 54 | + | }); | |||
| 55 | + | ||||
| 56 | + | return () => { | |||
| 57 | + | chart?.destroy(); | |||
| 58 | + | chart = null; | |||
| 59 | + | }; | |||
| 60 | + | }); | |||
| 61 | + | ||||
| 62 | + | // Update chart when data changes (after initial mount) | |||
| 63 | + | $effect(() => { | |||
| 64 | + | if (!chart) return; | |||
| 65 | + | ||||
| 66 | + | const data = chartData; | |||
| 67 | + | if (data.labels.length === 0) return; | |||
| 68 | + | ||||
| 69 | + | chart.update(data); | |||
| 70 | + | }); | |||
| 71 | + | </script> | |||
| 72 | + | ||||
| 73 | + | <div class="chart-wrapper"> | |||
| 74 | + | <div class="chart-header"> | |||
| 75 | + | <span class="chart-label">Activity</span> | |||
| 76 | + | <span class="chart-value">peak at {peakHour}</span> | |||
| 77 | + | </div> | |||
| 78 | + | <div bind:this={container} class="chart-container"></div> | |||
| 79 | + | </div> | |||
| 80 | + | ||||
| 81 | + | <style> | |||
| 82 | + | .chart-wrapper { | |||
| 83 | + | width: 100%; | |||
| 84 | + | display: flex; | |||
| 85 | + | flex-direction: column; | |||
| 86 | + | } | |||
| 87 | + | ||||
| 88 | + | .chart-header { | |||
| 89 | + | display: flex; | |||
| 90 | + | align-items: center; | |||
| 91 | + | gap: 0.5rem; | |||
| 92 | + | padding: 0 0.25rem 0.5rem; | |||
| 93 | + | font-size: 0.65rem; | |||
| 94 | + | font-family: ui-monospace, 'Fira Code', 'Cascadia Code', Menlo, Monaco, Consolas, monospace; | |||
| 95 | + | } | |||
| 96 | + | ||||
| 97 | + | .chart-label { | |||
| 98 | + | color: #4ade80; | |||
| 99 | + | font-weight: 600; | |||
| 100 | + | } | |||
| 101 | + | ||||
| 102 | + | .chart-value { | |||
| 103 | + | color: var(--dynamic-muted, #737373); | |||
| 104 | + | } | |||
| 105 | + | ||||
| 106 | + | .chart-container { | |||
| 107 | + | width: 100%; | |||
| 108 | + | } | |||
| 109 | + | ||||
| 110 | + | .chart-container :global(.frappe-chart) { | |||
| 111 | + | font-family: ui-monospace, 'Fira Code', 'Cascadia Code', Menlo, Monaco, Consolas, monospace; | |||
| 112 | + | } | |||
| 113 | + | ||||
| 114 | + | .chart-container :global(.frappe-chart .axis text), | |||
| 115 | + | .chart-container :global(.frappe-chart .chart-label) { | |||
| 116 | + | fill: #737373; | |||
| 117 | + | font-size: 10px; | |||
| 118 | + | } | |||
| 119 | + | ||||
| 120 | + | .chart-container :global(.frappe-chart .axis line), | |||
| 121 | + | .chart-container :global(.frappe-chart .chart-strokes) { | |||
| 122 | + | stroke: #333; | |||
| 123 | + | } | |||
| 124 | + | </style> | |||
A gui/src/pages/homepage/analytics/index.ts +2 -0
| @@ -0,0 +1,2 @@ | |||
| 1 | + | export { default as DailyVisitsChart } from './DailyVisitsChart.svelte'; | |
| 2 | + | export { default as HourlyActivityChart } from './HourlyActivityChart.svelte'; | |
| @@ -0,0 +1,2 @@ | |||||
| 1 | + | export { default as DailyVisitsChart } from './DailyVisitsChart.svelte'; | |||
| 2 | + | export { default as HourlyActivityChart } from './HourlyActivityChart.svelte'; | |||
A gui/src/types/frappe-charts.d.ts +47 -0
| @@ -0,0 +1,47 @@ | |||
| 1 | + | declare module 'frappe-charts' { | |
| 2 | + | export interface ChartData { | |
| 3 | + | labels: string[]; | |
| 4 | + | datasets: Array<{ | |
| 5 | + | name?: string; | |
| 6 | + | values: number[]; | |
| 7 | + | chartType?: string; | |
| 8 | + | }>; | |
| 9 | + | } | |
| 10 | + | ||
| 11 | + | export interface ChartOptions { | |
| 12 | + | data: ChartData; | |
| 13 | + | type: 'line' | 'bar' | 'axis-mixed' | 'pie' | 'percentage' | 'heatmap'; | |
| 14 | + | height?: number; | |
| 15 | + | colors?: string[]; | |
| 16 | + | axisOptions?: { | |
| 17 | + | xAxisMode?: 'tick' | 'span'; | |
| 18 | + | xIsSeries?: boolean; | |
| 19 | + | yAxisMode?: 'tick' | 'span'; | |
| 20 | + | }; | |
| 21 | + | barOptions?: { | |
| 22 | + | spaceRatio?: number; | |
| 23 | + | stacked?: boolean; | |
| 24 | + | }; | |
| 25 | + | lineOptions?: { | |
| 26 | + | regionFill?: number; | |
| 27 | + | hideDots?: number; | |
| 28 | + | hideLine?: number; | |
| 29 | + | heatline?: number; | |
| 30 | + | dotSize?: number; | |
| 31 | + | }; | |
| 32 | + | tooltipOptions?: { | |
| 33 | + | formatTooltipX?: (d: string) => string; | |
| 34 | + | formatTooltipY?: (d: number) => string; | |
| 35 | + | }; | |
| 36 | + | isNavigable?: boolean; | |
| 37 | + | valuesOverPoints?: boolean; | |
| 38 | + | maxSlices?: number; | |
| 39 | + | } | |
| 40 | + | ||
| 41 | + | export class Chart { | |
| 42 | + | constructor(container: HTMLElement, options: ChartOptions); | |
| 43 | + | update(data: ChartData): void; | |
| 44 | + | destroy(): void; | |
| 45 | + | export(): void; | |
| 46 | + | } | |
| 47 | + | } | |
| @@ -0,0 +1,47 @@ | |||||
| 1 | + | declare module 'frappe-charts' { | |||
| 2 | + | export interface ChartData { | |||
| 3 | + | labels: string[]; | |||
| 4 | + | datasets: Array<{ | |||
| 5 | + | name?: string; | |||
| 6 | + | values: number[]; | |||
| 7 | + | chartType?: string; | |||
| 8 | + | }>; | |||
| 9 | + | } | |||
| 10 | + | ||||
| 11 | + | export interface ChartOptions { | |||
| 12 | + | data: ChartData; | |||
| 13 | + | type: 'line' | 'bar' | 'axis-mixed' | 'pie' | 'percentage' | 'heatmap'; | |||
| 14 | + | height?: number; | |||
| 15 | + | colors?: string[]; | |||
| 16 | + | axisOptions?: { | |||
| 17 | + | xAxisMode?: 'tick' | 'span'; | |||
| 18 | + | xIsSeries?: boolean; | |||
| 19 | + | yAxisMode?: 'tick' | 'span'; | |||
| 20 | + | }; | |||
| 21 | + | barOptions?: { | |||
| 22 | + | spaceRatio?: number; | |||
| 23 | + | stacked?: boolean; | |||
| 24 | + | }; | |||
| 25 | + | lineOptions?: { | |||
| 26 | + | regionFill?: number; | |||
| 27 | + | hideDots?: number; | |||
| 28 | + | hideLine?: number; | |||
| 29 | + | heatline?: number; | |||
| 30 | + | dotSize?: number; | |||
| 31 | + | }; | |||
| 32 | + | tooltipOptions?: { | |||
| 33 | + | formatTooltipX?: (d: string) => string; | |||
| 34 | + | formatTooltipY?: (d: number) => string; | |||
| 35 | + | }; | |||
| 36 | + | isNavigable?: boolean; | |||
| 37 | + | valuesOverPoints?: boolean; | |||
| 38 | + | maxSlices?: number; | |||
| 39 | + | } | |||
| 40 | + | ||||
| 41 | + | export class Chart { | |||
| 42 | + | constructor(container: HTMLElement, options: ChartOptions); | |||
| 43 | + | update(data: ChartData): void; | |||
| 44 | + | destroy(): void; | |||
| 45 | + | export(): void; | |||
| 46 | + | } | |||
| 47 | + | } | |||