Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
99 changes: 47 additions & 52 deletions frontend/src2/charts/components/BaseChart.vue
Original file line number Diff line number Diff line change
Expand Up @@ -5,80 +5,75 @@ import { wheneverChanges } from '../../helpers'
import ChartTitle from './ChartTitle.vue'

const props = defineProps({
title: { type: String, required: false },
subtitle: { type: String, required: false },
options: { type: Object, required: true },
onClick: { type: Function, required: false },
title: { type: String, required: false },
subtitle: { type: String, required: false },
options: { type: Object, required: true },
onClick: { type: Function, required: false },
})

let eChart = null
const chartRef = ref(null)
let resizeObserver = null

onMounted(async () => {
const series = props.options?.series?.find((s) => s.type === 'map')
const isMap = series && series.type === 'map'
const renderer = isMap ? 'canvas' : 'svg'
eChart = echarts.init(chartRef.value, 'light', { renderer })
// Choose renderer (map requires canvas)
const isMap = props.options?.series?.some(s => s.type === 'map')
const renderer = isMap ? 'canvas' : 'svg'

await setChartOptions()
props.onClick && eChart.on('click', props.onClick)
eChart = echarts.init(chartRef.value, 'light', { renderer })

resizeObserver = new ResizeObserver(() => eChart.resize())
setTimeout(
() => chartRef.value && resizeObserver && resizeObserver.observe(chartRef.value),
1000,
)
if (Object.keys(props.options).length) {
eChart.setOption(props.options)
}

if (props.onClick) {
eChart.on('click', props.onClick)
}

// Auto-resize chart
resizeObserver = new ResizeObserver(() => {
try {
eChart?.resize()
} catch (_) {}
})

setTimeout(() => {
chartRef.value && resizeObserver.observe(chartRef.value)
}, 500)
})

onBeforeUnmount(() => {
if (chartRef.value && resizeObserver) resizeObserver.unobserve(chartRef.value)
chartRef.value && resizeObserver?.unobserve(chartRef.value)
})

wheneverChanges(() => props.options, setChartOptions, { deep: true })

async function setChartOptions() {
if (!eChart) return
const series = props.options?.series?.find((s) => s.type === 'map')
const isMap = series && series.type === 'map'
if (isMap) {
await registerMap(series.map)
}
eChart.setOption({ ...props.options })
}
if (!eChart) return

async function registerMap(mapName) {
if (!mapName) return
if (mapName === 'india') {
const mapJson = await import('../../assets/maps_json/india.json')
echarts.registerMap('india', mapJson.default)
} else if (mapName === 'world') {
const mapJson = await import('../../assets/maps_json/world_map.json')
echarts.registerMap('world', mapJson.default)
}
const mapSeries = props.options?.series?.find(s => s.type === 'map')
if (mapSeries) {
await registerMap(mapSeries.map)
}

eChart.setOption({ ...props.options })
}

defineExpose({ downloadChart })
function downloadChart() {
const image = new Image()
const type = 'png'
image.src = eChart.getDataURL({
type,
pixelRatio: 2,
backgroundColor: '#fff',
})
const link = document.createElement('a')
link.href = image.src
link.download = `${props.title}.${type}`
link.click()
// Load map JSON file when required
async function registerMap(mapName) {
try {
const res = await fetch(`/assets/insights/maps/${mapName}.json`)
const geoJson = await res.json()
echarts.registerMap(mapName, geoJson)
} catch (e) {
console.warn("Map load failed:", mapName)
}
}
</script>

<template>
<div class="flex h-full w-full flex-col rounded">
<ChartTitle v-if="title" :title="title" />
<div ref="chartRef" class="w-full flex-1 overflow-hidden">
<slot></slot>
</div>
</div>
<div class="flex h-full w-full flex-col rounded">
<ChartTitle v-if="title" :title="title" :subtitle="subtitle" />
<div ref="chartRef" class="w-full flex-1 overflow-hidden"></div>
</div>
</template>
117 changes: 64 additions & 53 deletions frontend/src2/charts/components/YAxisConfig.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
import ColorInput from '@/components/Controls/ColorInput.vue'
import { debounce } from 'frappe-ui'
import { watchEffect } from 'vue'
import Checkbox from '../../components/Checkbox.vue'
import DraggableList from '../../components/DraggableList.vue'
import InlineFormControlLabel from '../../components/InlineFormControlLabel.vue'
import { copy } from '../../helpers'
Expand All @@ -12,13 +11,14 @@ import CollapsibleSection from './CollapsibleSection.vue'
import MeasurePicker from './MeasurePicker.vue'

const props = defineProps<{ columnOptions: ColumnOption[] }>()

// y-axis config
const y_axis = defineModel<AxisChartConfig['y_axis']>({
required: true,
default: () => ({
series: [],
}),
default: () => ({ series: [] }),
})

// Ensure at least one series exists
const emptySeries = { measure: {} as MeasureOption }
watchEffect(() => {
if (!y_axis.value?.series?.length) {
Expand All @@ -30,73 +30,83 @@ function addSeries() {
y_axis.value.series.push(copy(emptySeries))
}

// Color update handler
const updateColor = debounce((color: string, idx: number) => {
if (!y_axis.value.series[idx].color) {
y_axis.value.series[idx].color = []
}
y_axis.value.series[idx].color = color ? [color] : []
}, 500)

// Label position options
const labelPositionOptions = [
{ label: 'Top', value: 'top' },
{ label: 'Center', value: 'inside' },
{ label: 'Bottom', value: 'bottom' },
]
</script>

<template>
<CollapsibleSection title="Y Axis">
<div class="flex flex-col gap-3 pt-1">

<!-- Series configuration -->
<div>
<p class="mb-1.5 text-xs text-gray-600">Series</p>
<div>
<DraggableList v-model:items="y_axis.series" group="series">
<template #item="{ item, index }">
<MeasurePicker
:model-value="item.measure"
:column-options="props.columnOptions"
@update:model-value="Object.assign(item.measure, $event || {})"
@remove="y_axis.series.splice(index, 1)"
>
<template #config-fields>
<InlineFormControlLabel label="Type">
<FormControl
type="select"
v-model="item.type"
:options="['Line', 'Bar']"
/>
</InlineFormControlLabel>
<InlineFormControlLabel label="Align">
<FormControl
type="select"
v-model="item.align"
:options="['Left', 'Right']"
/>
</InlineFormControlLabel>
<InlineFormControlLabel label="Color">
<ColorInput
:model-value="item.color?.[0]"
@update:model-value="updateColor($event, index)"
placement="left-start"
/>
</InlineFormControlLabel>
<Toggle
label="Show Data Labels"
v-model="item.show_data_labels"

<DraggableList v-model:items="y_axis.series" group="series">
<template #item="{ item, index }">
<MeasurePicker
:model-value="item.measure"
:column-options="props.columnOptions"
@update:model-value="Object.assign(item.measure, $event || {})"
@remove="y_axis.series.splice(index, 1)"
>
<template #config-fields>
<InlineFormControlLabel label="Type">
<FormControl type="select" v-model="item.type" :options="['Line', 'Bar']" />
</InlineFormControlLabel>

<InlineFormControlLabel label="Align">
<FormControl type="select" v-model="item.align" :options="['Left', 'Right']" />
</InlineFormControlLabel>

<InlineFormControlLabel label="Color">
<ColorInput
:model-value="item.color?.[0]"
@update:model-value="updateColor($event, index)"
placement="left-start"
/>
</InlineFormControlLabel>

<Toggle label="Show Data Labels" v-model="item.show_data_labels" />

<slot name="series-settings" :series="item" :idx="index" />
</template>
</MeasurePicker>
</template>
</DraggableList>
<button
class="mt-1.5 text-left text-xs text-gray-600 hover:underline"
@click="addSeries"
>
+ Add series
</button>
</div>
<slot name="series-settings" :series="item" :idx="index" />
</template>
</MeasurePicker>
</template>
</DraggableList>

<button class="mt-1.5 text-left text-xs text-gray-600 hover:underline" @click="addSeries">
+ Add series
</button>
</div>

<!-- General Y-axis settings -->
<slot name="y-axis-settings" :y_axis="y_axis" />
<Toggle label="Show Data Labels" v-model="y_axis.show_data_labels" />

<!-- Label position -->
<div v-if="y_axis.show_data_labels">
<InlineFormControlLabel label="Label Position">
<FormControl
type="select"
v-model="y_axis.label_position"
:options="labelPositionOptions"
/>
</InlineFormControlLabel>
</div>

<Toggle label="Show Axis Label" v-model="y_axis.show_axis_label" />
<Toggle label="Show Scrollbar" v-model="y_axis.show_scrollbar" />

<FormControl
v-if="y_axis.show_axis_label"
v-model="y_axis.axis_label"
Expand All @@ -106,6 +116,7 @@ const updateColor = debounce((color: string, idx: number) => {
<InlineFormControlLabel label="Y-Min" class="w-1/2">
<FormControl type="number" v-model="y_axis.min" placeholder="Min" />
</InlineFormControlLabel>

<InlineFormControlLabel label="Y-Max" class="w-1/2">
<FormControl type="number" v-model="y_axis.max" placeholder="Max" />
</InlineFormControlLabel>
Expand Down
Loading