forked from frappe/insights
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
d4e4b99
commit 6ca7357
Showing
11 changed files
with
411 additions
and
547 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,158 @@ | ||
<script setup> | ||
import Autocomplete from '@/components/Controls/Autocomplete.vue' | ||
import Checkbox from '@/components/Controls/Checkbox.vue' | ||
import Color from '@/components/Controls/Color.vue' | ||
import DraggableList from '@/components/DraggableList.vue' | ||
import DraggableListItemMenu from '@/components/DraggableListItemMenu.vue' | ||
import { FIELDTYPES } from '@/utils' | ||
import { computed } from 'vue' | ||
import SeriesOption from '../SeriesOption.vue' | ||
const emit = defineEmits(['update:modelValue']) | ||
const options = defineModel('options') | ||
const props = defineProps({ | ||
seriesType: { type: String }, | ||
options: { type: Object, required: true }, | ||
columns: { type: Array, required: true }, | ||
}) | ||
if (!options.value.xAxis) options.value.xAxis = [] | ||
if (!options.value.yAxis) options.value.yAxis = [] | ||
if (typeof options.value.xAxis === 'string') { | ||
options.value.xAxis = [{ column: options.value.xAxis }] | ||
} | ||
if (typeof options.value.yAxis === 'string') { | ||
options.value.yAxis = [{ column: options.value.yAxis }] | ||
} | ||
if (Array.isArray(options.value.xAxis) && typeof options.value.xAxis[0] === 'string') { | ||
options.value.xAxis = options.value.xAxis.map((column) => ({ column })) | ||
} | ||
if (Array.isArray(options.value.yAxis) && typeof options.value.yAxis[0] === 'string') { | ||
options.value.yAxis = options.value.yAxis.map((column) => ({ column })) | ||
} | ||
options.value.yAxis.forEach((item) => { | ||
if (!item.series_options) item.series_options = {} | ||
}) | ||
const indexOptions = computed(() => { | ||
return props.columns | ||
?.filter((column) => !FIELDTYPES.NUMBER.includes(column.type)) | ||
.map((column) => ({ | ||
label: column.label, | ||
value: column.label, | ||
description: column.type, | ||
})) | ||
}) | ||
const valueOptions = computed(() => { | ||
return props.columns | ||
?.filter((column) => FIELDTYPES.NUMBER.includes(column.type)) | ||
.map((column) => ({ | ||
label: column.label, | ||
value: column.label, | ||
description: column.type, | ||
})) | ||
}) | ||
function updateYAxis(columnOptions) { | ||
if (!columnOptions) { | ||
options.value.yAxis = [] | ||
return | ||
} | ||
options.value.yAxis = columnOptions.map((option) => { | ||
const existingColumn = options.value.yAxis?.find((c) => c.column === option.value) | ||
const series_options = existingColumn ? existingColumn.series_options : {} | ||
return { column: option.value, series_options } | ||
}) | ||
} | ||
function updateXAxis(columnOptions) { | ||
if (!columnOptions) { | ||
options.value.xAxis = [] | ||
return | ||
} | ||
options.value.xAxis = columnOptions.map((option) => { | ||
return { column: option.value } | ||
}) | ||
} | ||
</script> | ||
<template> | ||
<FormControl | ||
type="text" | ||
label="Title" | ||
class="w-full" | ||
v-model="options.title" | ||
placeholder="Title" | ||
/> | ||
<div> | ||
<div class="mb-1 flex items-center justify-between"> | ||
<label class="block text-xs text-gray-600">X Axis</label> | ||
<Autocomplete | ||
:multiple="true" | ||
:options="indexOptions" | ||
:modelValue="options.xAxis?.map((item) => item.column) || []" | ||
@update:model-value="updateXAxis" | ||
> | ||
<template #target="{ togglePopover }"> | ||
<Button variant="ghost" icon="plus" @click="togglePopover" /> | ||
</template> | ||
</Autocomplete> | ||
</div> | ||
<DraggableList | ||
group="xAxis" | ||
item-key="column" | ||
empty-text="No columns selected" | ||
v-model:items="options.xAxis" | ||
/> | ||
</div> | ||
<div> | ||
<div class="mb-1 flex items-center justify-between"> | ||
<label class="block text-xs text-gray-600">Y Axis</label> | ||
<Autocomplete | ||
:multiple="true" | ||
:options="valueOptions" | ||
:modelValue="options.yAxis?.map((item) => item.column) || []" | ||
@update:model-value="updateYAxis" | ||
> | ||
<template #target="{ togglePopover }"> | ||
<Button variant="ghost" icon="plus" @click="togglePopover" /> | ||
</template> | ||
</Autocomplete> | ||
</div> | ||
<DraggableList | ||
group="yAxis" | ||
item-key="column" | ||
empty-text="No columns selected" | ||
v-model:items="options.yAxis" | ||
> | ||
<template #item-suffix="{ item, index }"> | ||
<DraggableListItemMenu> | ||
<SeriesOption | ||
:seriesType="props.seriesType" | ||
:modelValue="item.series_options || {}" | ||
@update:modelValue="options.yAxis[index].series_options = $event" | ||
/> | ||
</DraggableListItemMenu> | ||
</template> | ||
</DraggableList> | ||
</div> | ||
<div v-if="options.yAxis?.length == 2" class="space-y-2 text-gray-600"> | ||
<Checkbox v-model="options.splitYAxis" label="Split Y Axis" /> | ||
</div> | ||
<div> | ||
<label class="mb-1.5 block text-xs text-gray-600">Reference Line</label> | ||
<Autocomplete | ||
:modelValue="options.referenceLine" | ||
:options="['Average', 'Median', 'Min', 'Max']" | ||
@update:modelValue="options.referenceLine = $event?.value" | ||
/> | ||
</div> | ||
<Color label="Colors" v-model="options.colors" :max="options.yAxis?.length || 1" multiple /> | ||
</template> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,215 @@ | ||
import { formatNumber, getShortNumber } from '@/utils' | ||
import { getColors as getDefaultColors } from '@/utils/colors' | ||
import { graphic } from 'echarts/core' | ||
|
||
export default function getAxisChartOptions({ chartType, options, data }) { | ||
const xAxisColumns = getXAxisColumns(options, data) | ||
const xAxisValues = getXAxisValues(xAxisColumns, data) | ||
const datasets = makeDatasets(options, data, xAxisColumns, xAxisValues) | ||
return makeOptions(chartType, xAxisValues, datasets, options) | ||
} | ||
|
||
function getXAxisColumns(options, data) { | ||
if (!options.xAxis || !options.xAxis.length) return [] | ||
const xAxisOptions = handleLegacyAxisOptions(options.xAxis) | ||
// remove the columns that might be removed from the query but not the chart | ||
return xAxisOptions | ||
.filter((xAxisOption) => data[0]?.hasOwnProperty(xAxisOption.column)) | ||
.map((op) => op.column) | ||
} | ||
|
||
function getXAxisValues(xAxisColumns, data) { | ||
if (!data?.length) return [] | ||
if (!xAxisColumns.length) return [] | ||
|
||
let firsXAxisColumn = xAxisColumns[0] | ||
if (typeof firsXAxisColumn !== 'string') { | ||
return console.warn('Invalid X-Axis option. Please re-select the X-Axis option.') | ||
} | ||
|
||
const values = data.map((d) => d[firsXAxisColumn]) | ||
return [...new Set(values)] | ||
} | ||
|
||
function makeDatasets(options, data, xAxisColumns, xAxisValues) { | ||
let yAxis = options.yAxis | ||
if (!data?.length || !yAxis?.length) return [] | ||
yAxis = handleLegacyAxisOptions(yAxis) | ||
|
||
const validYAxisOptions = yAxis | ||
// to exclude the columns that might be removed from the query but not the chart | ||
.filter( | ||
(yAxisOption) => | ||
data[0].hasOwnProperty(yAxisOption?.column) || data[0].hasOwnProperty(yAxisOption) | ||
) | ||
|
||
if (xAxisColumns.length == 1) { | ||
// even if data has multiple points for each xAxisColumn | ||
// for eg. Oct has 3 price for each category | ||
// Month Category Price | ||
// Oct A 10 | ||
// Oct B 20 | ||
// Oct C 30 | ||
// so, we need to sum up the prices of each unique month | ||
const xAxisColumn = xAxisColumns[0] | ||
return validYAxisOptions.map((option) => { | ||
const column = option.column || option | ||
const seriesOptions = option.series_options || {} | ||
const _data = xAxisValues.map((xAxisValue) => { | ||
const points = data.filter((d) => d[xAxisColumn] === xAxisValue) | ||
const sum = points.reduce((acc, curr) => acc + curr[column], 0) | ||
return sum | ||
}) | ||
return { | ||
label: column, | ||
data: _data, | ||
series_options: seriesOptions, | ||
} | ||
}) | ||
} | ||
|
||
// if multiple xAxis columns are selected | ||
// then consider first xAxis column as labels | ||
// and other xAxis columns' values as series | ||
const datasets = [] | ||
const firstAxisColumn = xAxisColumns[0] | ||
const restAxisColumns = xAxisColumns.slice(1) | ||
|
||
for (let yAxisOption of validYAxisOptions) { | ||
const column = yAxisOption.column || yAxisOption | ||
const seriesOptions = yAxisOption.series_options || {} | ||
for (let xAxis of restAxisColumns) { | ||
const axisData = data.map((d) => d[xAxis]) | ||
const uniqueAxisData = [...new Set(axisData)] | ||
for (let axis of uniqueAxisData) { | ||
const _data = data.filter((d) => d[xAxis] === axis).map((d) => d[column]) | ||
datasets.push({ | ||
label: axis, | ||
data: _data, | ||
name: axis, | ||
series_options: seriesOptions, | ||
}) | ||
} | ||
} | ||
} | ||
|
||
return datasets | ||
} | ||
|
||
function makeOptions(chartType, labels, datasets, options) { | ||
if (!datasets?.length) return {} | ||
|
||
const colors = options.colors?.length | ||
? [...options.colors, ...getDefaultColors()] | ||
: getDefaultColors() | ||
|
||
return { | ||
animation: false, | ||
color: colors, | ||
grid: { | ||
top: 15, | ||
bottom: 35, | ||
left: 25, | ||
right: 35, | ||
containLabel: true, | ||
}, | ||
xAxis: { | ||
axisType: 'xAxis', | ||
type: 'category', | ||
axisTick: false, | ||
data: labels, | ||
splitLine: { | ||
show: chartType == 'scatter', | ||
lineStyle: { type: 'dashed' }, | ||
}, | ||
axisLabel: { | ||
rotate: options.rotateLabels, | ||
formatter: (value, _) => (!isNaN(value) ? getShortNumber(value, 1) : value), | ||
}, | ||
}, | ||
yAxis: datasets.map((dataset) => ({ | ||
name: options.splitYAxis ? dataset.label : undefined, | ||
nameGap: 45, | ||
nameLocation: 'middle', | ||
nameTextStyle: { color: 'transparent' }, | ||
type: 'value', | ||
splitLine: { | ||
lineStyle: { type: 'dashed' }, | ||
}, | ||
axisLabel: { | ||
formatter: (value, _) => (!isNaN(value) ? getShortNumber(value, 1) : value), | ||
}, | ||
})), | ||
series: datasets.map((dataset, index) => ({ | ||
name: dataset.label, | ||
data: dataset.data, | ||
type: chartType || dataset.series_options.type || 'bar', | ||
yAxisIndex: options.splitYAxis ? index : 0, | ||
color: dataset.series_options.color || colors[index], | ||
markLine: getMarkLineOption(options), | ||
// line styles | ||
smoothMonotone: 'x', | ||
smooth: dataset.series_options.smoothLines || options.smoothLines ? 0.4 : false, | ||
showSymbol: dataset.series_options.showPoints || options.showPoints, | ||
symbolSize: chartType == 'scatter' ? 10 : 5, | ||
areaStyle: | ||
dataset.series_options.showArea || options.showArea | ||
? { | ||
color: new graphic.LinearGradient(0, 0, 0, 1, [ | ||
{ offset: 0, color: dataset.series_options.color || colors[index] }, | ||
{ offset: 1, color: '#fff' }, | ||
]), | ||
opacity: 0.2, | ||
} | ||
: undefined, | ||
// bar styles | ||
itemStyle: { | ||
borderRadius: [4, 4, 0, 0], | ||
}, | ||
barMaxWidth: 50, | ||
stack: options.stack ? 'stack' : null, | ||
})), | ||
legend: { | ||
icon: 'circle', | ||
type: 'scroll', | ||
bottom: 'bottom', | ||
pageIconSize: 12, | ||
pageIconColor: '#64748B', | ||
pageIconInactiveColor: '#C0CCDA', | ||
pageFormatter: '{current}', | ||
pageButtonItemGap: 2, | ||
}, | ||
tooltip: { | ||
trigger: 'axis', | ||
confine: true, | ||
appendToBody: false, | ||
valueFormatter: (value) => (isNaN(value) ? value : formatNumber(value)), | ||
}, | ||
} | ||
} | ||
|
||
function getMarkLineOption(options) { | ||
return options.referenceLine | ||
? { | ||
data: [ | ||
{ | ||
name: options.referenceLine, | ||
type: options.referenceLine.toLowerCase(), | ||
label: { position: 'middle', formatter: '{b}: {c}' }, | ||
}, | ||
], | ||
} | ||
: {} | ||
} | ||
|
||
function handleLegacyAxisOptions(axisOptions) { | ||
// if axisOptions = 'column1' | ||
if (typeof axisOptions === 'string') { | ||
axisOptions = [{ column: axisOptions }] | ||
} | ||
// if axisOptions = ['column1', 'column2'] | ||
if (Array.isArray(axisOptions) && typeof axisOptions[0] === 'string') { | ||
axisOptions = axisOptions.map((column) => ({ column })) | ||
} | ||
return axisOptions | ||
} |
Oops, something went wrong.