Skip to content

Commit

Permalink
refactor: common axis chart options
Browse files Browse the repository at this point in the history
  • Loading branch information
nextchamp-saqib committed Jan 26, 2024
1 parent d4e4b99 commit 6ca7357
Show file tree
Hide file tree
Showing 11 changed files with 411 additions and 547 deletions.
158 changes: 158 additions & 0 deletions frontend/src/widgets/AxisChart/AxisChartOptions.vue
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>
215 changes: 215 additions & 0 deletions frontend/src/widgets/AxisChart/getAxisChartOptions.js
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
}
Loading

0 comments on commit 6ca7357

Please sign in to comment.