Skip to content

Commit 0da616b

Browse files
committed
Merge branch 'next' of github.com:devforth/adminforth into next
2 parents 5fc2106 + a5523fb commit 0da616b

File tree

5 files changed

+275
-0
lines changed

5 files changed

+275
-0
lines changed

adminforth/documentation/docs/tutorial/03-Customization/15-afcl.md

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2120,8 +2120,87 @@ import { PieChart } from '@/afcl'
21202120
</div>
21212121
</div>
21222122

2123+
## Treemap Chart
2124+
2125+
This example is based on the ApexCharts treemap demo “color range”.
2126+
2127+
<div class="split-screen" >
2128+
<div >
2129+
2130+
```ts
2131+
import { TreeMapChart } from '@/afcl'
2132+
2133+
const deltaToColor = (delta: number) => {
2134+
if (delta < -10) return '#B91C1C' // bright red
2135+
if (delta < 0) return '#EF4444' // red
2136+
if (delta <= 10) return '#22C55E' // green
2137+
return '#15803D' // very green
2138+
}
2139+
2140+
const formatDelta = (delta: number) => (delta > 0 ? `+${delta}%` : `${delta}%`)
2141+
2142+
const data = [
2143+
{ x: 'New Delhi', value: 218, delta: 12 },
2144+
{ x: 'Kolkata', value: 149, delta: -4 },
2145+
{ x: 'Mumbai', value: 184, delta: -14 },
2146+
{ x: 'Ahmedabad', value: 55, delta: 6 },
2147+
{ x: 'Bangalore', value: 84, delta: 9 },
2148+
{ x: 'Pune', value: 31, delta: -2 },
2149+
{ x: 'Chennai', value: 70, delta: 0 }
2150+
]
2151+
2152+
// Optional: precompute per-point color using delta
2153+
const dataWithColors = data.map((item) => ({
2154+
...item,
2155+
fillColor: deltaToColor(item.delta),
2156+
}))
2157+
2158+
const series = [
2159+
{ name: 'Desktops', fieldName: 'value' },
2160+
]
2161+
```
2162+
2163+
```html
2164+
<TreeMapChart
2165+
:data="dataWithColors"
2166+
:series="series"
2167+
:options="{
2168+
chart: { height: 350 },
2169+
dataLabels: {
2170+
formatter: (text, { seriesIndex, dataPointIndex, w }) => {
2171+
const point = w?.config?.series?.[seriesIndex]?.data?.[dataPointIndex]
2172+
if (!point) return text
2173+
return `${text} ${formatDelta(point.delta)}`
2174+
},
2175+
},
2176+
plotOptions: {
2177+
treemap: {
2178+
distributed: false,
2179+
enableShades: false,
2180+
},
2181+
},
2182+
tooltip: {
2183+
y: {
2184+
formatter: (value, { seriesIndex, dataPointIndex, w }) => {
2185+
const point = w?.config?.series?.[seriesIndex]?.data?.[dataPointIndex]
2186+
if (!point) return value
2187+
return `${point.value} (${formatDelta(point.delta)})`
2188+
},
2189+
},
2190+
},
2191+
}"
2192+
/>
2193+
```
2194+
2195+
</div>
2196+
<div>
2197+
![Treemap Chart](image-28.png)
2198+
</div>
2199+
</div>
2200+
21232201
## Mixed Chart
21242202

2203+
21252204
```ts
21262205
import { MixedChart } from '@/afcl'
21272206
```
32.3 KB
Loading
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
<template>
2+
<div class="afcl-treemap -mb-2" ref="chart"></div>
3+
</template>
4+
5+
<script setup lang="ts">
6+
import ApexCharts, { type ApexOptions } from 'apexcharts';
7+
import { ref, type Ref, watch, computed, onUnmounted } from 'vue';
8+
9+
const chart: Ref<HTMLDivElement | null> = ref(null);
10+
11+
const props = defineProps<{
12+
data: {
13+
x: string,
14+
[key: string]: any,
15+
}[],
16+
series: {
17+
name: string,
18+
fieldName: string,
19+
}[],
20+
options?: ApexOptions,
21+
}>();
22+
23+
const optionsBase: ApexOptions = {
24+
chart: {
25+
height: 350,
26+
type: 'treemap',
27+
fontFamily: 'Inter, sans-serif',
28+
toolbar: {
29+
show: false,
30+
},
31+
},
32+
legend: {
33+
show: false,
34+
},
35+
dataLabels: {
36+
enabled: true,
37+
style: {
38+
fontFamily: 'Inter, sans-serif',
39+
colors: ['#FFFFFF'],
40+
},
41+
},
42+
plotOptions: {
43+
treemap: {
44+
distributed: true,
45+
enableShades: false,
46+
},
47+
},
48+
};
49+
50+
const options = computed(() => {
51+
if (props.data?.length > 0) {
52+
props.series.forEach((s) => {
53+
if (props.data[0][s.fieldName] === undefined) {
54+
throw new Error(
55+
`Field ${s.fieldName} not found even in first data point ${JSON.stringify(props.data[0])}, something is wrong`,
56+
);
57+
}
58+
});
59+
}
60+
61+
const nextOptions: ApexOptions = {
62+
...optionsBase,
63+
series: props.series.map((s) => ({
64+
name: s.name,
65+
data: (props.data ?? []).map((item: any) => {
66+
const { x, y: _ignoredY, ...rest } = item ?? {};
67+
return {
68+
x,
69+
y: item?.[s.fieldName],
70+
...rest,
71+
};
72+
}),
73+
})),
74+
};
75+
76+
function mergeOptions(target: any, source: any) {
77+
if (!source) {
78+
return;
79+
}
80+
for (const key in source) {
81+
if (typeof source[key] === 'object' && !Array.isArray(source[key])) {
82+
if (!target[key]) {
83+
target[key] = {};
84+
}
85+
mergeOptions(target[key], source[key]);
86+
} else {
87+
target[key] = source[key];
88+
}
89+
}
90+
}
91+
92+
mergeOptions(nextOptions, props.options);
93+
return nextOptions;
94+
});
95+
96+
let apexChart: ApexCharts | null = null;
97+
98+
watch(() => [options.value, chart.value], (value) => {
99+
if (!value || !chart.value) {
100+
return;
101+
}
102+
103+
if (apexChart) {
104+
apexChart.updateOptions(options.value);
105+
} else {
106+
apexChart = new ApexCharts(chart.value, options.value);
107+
apexChart.render();
108+
}
109+
});
110+
111+
onUnmounted(() => {
112+
if (apexChart) {
113+
apexChart.destroy();
114+
}
115+
});
116+
</script>
117+
118+
<style lang="scss">
119+
:root {
120+
--afcl-treemap-text: #FFFFFF;
121+
}
122+
123+
[data-theme='dark'] {
124+
--afcl-treemap-text: #FFFFFF;
125+
}
126+
127+
.afcl-treemap {
128+
.apexcharts-datalabel {
129+
fill: var(--afcl-treemap-text);
130+
}
131+
132+
.apexcharts-legend-text {
133+
color: var(--afcl-treemap-text) !important;
134+
}
135+
}
136+
</style>

adminforth/spa/src/afcl/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ export { default as Dropzone } from './Dropzone.vue';
1212
export { default as AreaChart } from './AreaChart.vue';
1313
export { default as BarChart } from './BarChart.vue';
1414
export { default as PieChart } from './PieChart.vue';
15+
export { default as TreeMapChart } from './TreeMapChart.vue';
1516
export { default as Table } from './Table.vue';
1617
export { default as ProgressBar } from './ProgressBar.vue';
1718
export { default as Spinner } from './Spinner.vue';

dev-demo/custom/AfComponents.vue

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -224,6 +224,15 @@
224224
>
225225
</Table>
226226

227+
<div class="w-full">
228+
<p class="text-sm font-semibold text-lightPrimary dark:text-darkPrimary mb-2">TreeMapChart (value + delta)</p>
229+
<TreeMapChart
230+
:data="treemapData"
231+
:series="treemapSeries"
232+
:options="treemapOptions"
233+
/>
234+
</div>
235+
227236
<Spinner class="w-10 h-10" />
228237
</div>
229238

@@ -336,6 +345,7 @@ import { Toggle } from '@/afcl';
336345
import { Modal } from '@/afcl';
337346
import { IconSearchOutline } from '@iconify-prerendered/vue-flowbite'
338347
import { DatePicker } from '@/afcl';
348+
import { TreeMapChart } from '@/afcl';
339349
import CustomRangePicker from "@/components/CustomRangePicker.vue";
340350
import Toast from '@/components/Toast.vue';
341351
import { useAdminforth } from '@/adminforth';
@@ -358,6 +368,55 @@ const selected = ref(null)
358368
const selected2 = ref([])
359369
const valueStart = ref()
360370
371+
const deltaToColor = (delta: number) => {
372+
if (delta < -10) return '#B91C1C' // bright red
373+
if (delta < 0) return '#EF4444' // red
374+
if (delta <= 10) return '#22C55E' // green
375+
return '#15803D' // very green
376+
}
377+
378+
const formatDelta = (delta: number) => (delta > 0 ? `+${delta}%` : `${delta}%`)
379+
380+
const treemapData = [
381+
{ x: 'New Delhi', value: 218, delta: 12 },
382+
{ x: 'Kolkata', value: 149, delta: -4 },
383+
{ x: 'Mumbai', value: 184, delta: -14 },
384+
{ x: 'Ahmedabad', value: 55, delta: 6 },
385+
{ x: 'Bangalore', value: 84, delta: 9 },
386+
{ x: 'Pune', value: 31, delta: -2 },
387+
].map((item) => ({
388+
...item,
389+
fillColor: deltaToColor(item.delta),
390+
}))
391+
392+
const treemapSeries = [
393+
{ name: 'Value', fieldName: 'value' },
394+
]
395+
396+
const treemapOptions: any = {
397+
chart: { height: 350 },
398+
dataLabels: {
399+
formatter: (text: string, { seriesIndex, dataPointIndex, w }: any) => {
400+
const point = w?.config?.series?.[seriesIndex]?.data?.[dataPointIndex]
401+
return `${text} ${formatDelta(point.delta)}`
402+
},
403+
},
404+
plotOptions: {
405+
treemap: {
406+
distributed: false,
407+
enableShades: false,
408+
},
409+
},
410+
tooltip: {
411+
y: {
412+
formatter: (value: any, { seriesIndex, dataPointIndex, w }: any) => {
413+
const point = w?.config?.series?.[seriesIndex]?.data?.[dataPointIndex]
414+
return `${point.value} (${formatDelta(point.delta)})`
415+
},
416+
},
417+
},
418+
}
419+
361420
362421
watch(valueStart, (newVal) => {
363422
console.log('New start value:', newVal);

0 commit comments

Comments
 (0)