Skip to content

Commit

Permalink
fix(heatmap): refactor heatmap widget & apply vue query (#5296)
Browse files Browse the repository at this point in the history
Signed-off-by: samuel.park <[email protected]>
  • Loading branch information
piggggggggy authored Dec 22, 2024
1 parent 429f632 commit 62638ce
Show file tree
Hide file tree
Showing 2 changed files with 101 additions and 127 deletions.
221 changes: 95 additions & 126 deletions apps/web/src/common/modules/widgets/_widgets/heatmap/Heatmap.vue
Original file line number Diff line number Diff line change
@@ -1,48 +1,49 @@
<script setup lang="ts">
import { useResizeObserver } from '@vueuse/core';
import {
computed, defineExpose, reactive, ref, watch,
computed, defineExpose, onMounted, reactive, ref, watch,
} from 'vue';
import { useQuery } from '@tanstack/vue-query';
import dayjs from 'dayjs';
import type { HeatmapSeriesOption } from 'echarts/charts';
import type { EChartsType } from 'echarts/core';
import { init } from 'echarts/core';
import { isEmpty, max, throttle } from 'lodash';
import { SpaceConnector } from '@cloudforet/core-lib/space-connector';
import { getCancellableFetcher } from '@cloudforet/core-lib/space-connector/cancellable-fetcher';
import { numberFormatter } from '@cloudforet/utils';
import type { ListResponse } from '@/schema/_common/api-verbs/list';
import { GRANULARITY } from '@/schema/dashboard/_constants/widget-constant';
import type { PrivateDataTableModel } from '@/schema/dashboard/private-data-table/model';
import type { PrivateWidgetLoadParameters } from '@/schema/dashboard/private-widget/api-verbs/load';
import type { PublicDataTableModel } from '@/schema/dashboard/public-data-table/model';
import type { PublicWidgetLoadParameters } from '@/schema/dashboard/public-widget/api-verbs/load';
import type { APIErrorToast } from '@/common/composables/error/errorHandler';
import ErrorHandler from '@/common/composables/error/errorHandler';
import WidgetFrame from '@/common/modules/widgets/_components/WidgetFrame.vue';
import { useWidgetDateRange } from '@/common/modules/widgets/_composables/use-widget-date-range';
import { useWidgetFrame } from '@/common/modules/widgets/_composables/use-widget-frame';
import { useWidgetInitAndRefresh } from '@/common/modules/widgets/_composables/use-widget-init-and-refresh';
import { DATE_FIELD } from '@/common/modules/widgets/_constants/widget-constant';
import { DATE_FORMAT } from '@/common/modules/widgets/_constants/widget-field-constant';
import { sortObjectByKeys } from '@/common/modules/widgets/_helpers/widget-data-table-helper';
import {
getReferenceLabel,
getWidgetDateFields,
getWidgetDateRange,
} from '@/common/modules/widgets/_helpers/widget-date-helper';
import { isDateField } from '@/common/modules/widgets/_helpers/widget-field-helper';
import { getWidgetDataTable } from '@/common/modules/widgets/_helpers/widget-helper';
import {
getWidgetLoadApiQueryDateRange,
} from '@/common/modules/widgets/_helpers/widget-load-helper';
import type { ColorSchemaValue, ColorValue } from '@/common/modules/widgets/_widget-fields/color-schema/type';
import type { ColorSchemaValue } from '@/common/modules/widgets/_widget-fields/color-schema/type';
import type { DataFieldValue } from '@/common/modules/widgets/_widget-fields/data-field/type';
import type { DateFormatValue } from '@/common/modules/widgets/_widget-fields/date-format/type';
import type { DateRangeValue } from '@/common/modules/widgets/_widget-fields/date-range/type';
import type { GranularityValue } from '@/common/modules/widgets/_widget-fields/granularity/type';
import type { LegendValue } from '@/common/modules/widgets/_widget-fields/legend/type';
import type { XAxisValue } from '@/common/modules/widgets/_widget-fields/x-axis/type';
import type { DateRange, DynamicFieldData, StaticFieldData } from '@/common/modules/widgets/types/widget-data-type';
import type { DateRange, StaticFieldData } from '@/common/modules/widgets/types/widget-data-type';
import type { WidgetEmit, WidgetExpose, WidgetProps } from '@/common/modules/widgets/types/widget-display-type';
Expand All @@ -60,26 +61,31 @@ const { dateRange } = useWidgetDateRange({
granularity: computed<GranularityValue>(() => props.widgetOptions?.granularity?.value as GranularityValue),
});
const chartContext = ref<HTMLElement|null>(null);
const state = reactive({
runQueries: false,
isPrivateWidget: computed<boolean>(() => props.widgetId.startsWith('private')),
dataTable: undefined as PublicDataTableModel|PrivateDataTableModel|undefined,
loading: false,
errorMessage: undefined as string|undefined,
data: null as Data | null,
chart: null as EChartsType | null,
xAxisData: computed<string[]>(() => {
if (!state.data?.results?.length) return [];
if (isDateField(state.xAxisField)) {
const _isSeparatedDate = state.xAxisField !== DATE_FIELD.DATE;
return getWidgetDateFields(state.granularity, state.widgetDateRange.start, state.widgetDateRange.end, _isSeparatedDate);
if (isDateField(widgetOptionsState.xAxisInfo?.data)) {
const _isSeparatedDate = widgetOptionsState.xAxisInfo.data !== DATE_FIELD.DATE;
return getWidgetDateFields(widgetOptionsState.granularityInfo?.granularity, state.widgetDateRange.start, state.widgetDateRange.end, _isSeparatedDate);
}
return state.data.results.map((d) => d[state.xAxisField] as string) || [];
return state.data.results.map((d) => d[widgetOptionsState.xAxisInfo?.data as string] as string) || [];
}),
yAxisData: computed<string[]>(() => {
if (!state.data?.results?.length) return [];
return state.dataField;
return (widgetOptionsState.dataFieldInfo?.data ?? []) as string[];
}),
chartData: [],
heatmapMaxValue: computed(() => max(state.chartData.map((d) => d?.[2] || 0)) ?? 1),
unit: computed<string|undefined>(() => widgetFrameProps.value.unitMap?.[state.dataField]),
// unit: computed<string|undefined>(() => widgetFrameProps.value.unitMap?.[state.dataField]),
chartOptions: computed<HeatmapSeriesOption>(() => ({
grid: {
left: 0,
Expand All @@ -91,10 +97,10 @@ const state = reactive({
data: state.xAxisData,
axisLabel: {
formatter: (val) => {
if (state.xAxisField === DATE_FIELD.DATE) {
return dayjs.utc(val).format(state.dateFormat);
if (widgetOptionsState.xAxisInfo.data === DATE_FIELD.DATE) {
return dayjs.utc(val).format(widgetOptionsState.dateFormatInfo?.format);
}
return getReferenceLabel(props.allReferenceTypeInfo, state.xAxisField, val);
return getReferenceLabel(props.allReferenceTypeInfo, widgetOptionsState.xAxisInfo?.data, val);
},
},
},
Expand All @@ -105,37 +111,29 @@ const state = reactive({
show: true,
},
axisLabel: {
formatter: (val) => {
if (state.dataFieldInfo.fieldType === 'staticField') {
return val;
}
if (state.dynamicFieldInfo?.fieldValue === DATE_FIELD.DATE) {
return dayjs.utc(val).format(state.dateFormat);
}
return getReferenceLabel(props.allReferenceTypeInfo, state.dataField, val);
},
formatter: (val) => val,
},
},
tooltip: {
position: 'top',
confine: true,
formatter: (params) => {
let _name = getReferenceLabel(props.allReferenceTypeInfo, state.xAxisField, params.name);
if (state.xAxisField === DATE_FIELD.DATE) _name = dayjs.utc(_name).format(state.dateFormat);
if (state.unit) _name = `${_name} (${state.unit})`;
let _name = getReferenceLabel(props.allReferenceTypeInfo, widgetOptionsState.xAxisInfo?.data, params.name);
if (widgetOptionsState.xAxisInfo?.data === DATE_FIELD.DATE) _name = dayjs.utc(_name).format(widgetOptionsState.dateFormatInfo?.format);
// if (state.unit) _name = `${_name} (${state.unit})`;
const _value = numberFormatter(params.value[2]) || '';
return `${params.marker} ${_name}: <b>${_value}</b>`;
},
},
visualMap: {
show: state.showLegends,
show: widgetOptionsState.legendInfo?.toggleValue,
calculable: true,
orient: 'horizontal',
left: 'left',
bottom: 0,
max: state.heatmapMaxValue,
inRange: {
color: state.colorValue,
color: widgetOptionsState.colorSchemaInfo?.colorValue,
},
outOfRange: {
color: '#999',
Expand All @@ -154,127 +152,87 @@ const state = reactive({
},
})),
// required fields
granularity: computed<string>(() => props.widgetOptions?.granularity as string),
xAxisField: computed<string|undefined>(() => (props.widgetOptions?.xAxis as XAxisValue)?.data),
xAxisCount: computed<number|undefined>(() => (props.widgetOptions?.xAxis as XAxisValue)?.count),
dataFieldInfo: computed(() => props.widgetOptions?.tableDataField),
dynamicFieldInfo: computed(() => state.dataFieldInfo?.dynamicFieldInfo),
staticFieldInfo: computed(() => state.dataFieldInfo?.staticFieldInfo),
dataField: computed<string|string[]|undefined>(() => {
if (state.dataFieldInfo?.fieldType === 'staticField') return state.staticFieldInfo?.fieldValue;
return state.dynamicFieldInfo?.fieldValue;
}),
dynamicFieldValue: computed<string[]>(() => state.dynamicFieldInfo?.fixedValue || []),
colorValue: computed<ColorValue>(() => (props.widgetOptions?.colorSchema as ColorSchemaValue)?.colorValue),
widgetDateRange: computed<DateRange>(() => {
let _start = dateRange.value.start;
let _end = dateRange.value.end;
if (isDateField(state.xAxisField)) {
[_start, _end] = getWidgetDateRange(state.granularity, _end, state.xAxisCount);
} else if (isDateField(state.dataField)) {
let subtract = state.dynamicFieldInfo.count;
if (state.dynamicFieldInfo?.valueType === 'fixed') {
if (state.granularity === GRANULARITY.YEARLY) subtract = 3;
if (state.granularity === GRANULARITY.MONTHLY) subtract = 12;
if (state.granularity === GRANULARITY.DAILY) subtract = 30;
}
[_start, _end] = getWidgetDateRange(state.granularity, _end, subtract);
if (isDateField(widgetOptionsState.xAxisInfo?.data)) {
[_start, _end] = getWidgetDateRange(widgetOptionsState.granularityInfo?.granularity, _end, widgetOptionsState.xAxisInfo?.count);
}
return { start: _start, end: _end };
}),
// optional fields
showLegends: computed<boolean>(() => (props.widgetOptions?.legend as LegendValue)?.toggleValue),
dateFormat: computed<string|undefined>(() => {
const _dateFormat = (props.widgetOptions?.dateFormat?.value as DateFormatValue)?.format || 'MMM DD, YYYY';
return DATE_FORMAT?.[_dateFormat]?.[state.granularity];
}),
});
const { widgetFrameProps, widgetFrameEventHandlers } = useWidgetFrame(props, emit, {
dateRange,
errorMessage: computed(() => state.errorMessage),
widgetLoading: computed(() => state.loading),
noData: computed(() => (state.data ? !state.data.results?.length : false)),
const widgetOptionsState = reactive({
granularityInfo: computed<GranularityValue>(() => props.widgetOptions?.granularity?.value as GranularityValue),
dataFieldInfo: computed<DataFieldValue>(() => props.widgetOptions?.dataField?.value as DataFieldValue),
xAxisInfo: computed<XAxisValue>(() => props.widgetOptions?.xAxis?.value as XAxisValue),
colorSchemaInfo: computed<ColorSchemaValue>(() => props.widgetOptions?.colorSchema?.value as ColorSchemaValue),
legendInfo: computed<LegendValue>(() => props.widgetOptions?.legend?.value as LegendValue),
dateFormatInfo: computed<DateFormatValue>(() => props.widgetOptions?.dateFormat?.value as DateFormatValue),
});
/* Api */
const privateWidgetFetcher = getCancellableFetcher<PrivateWidgetLoadParameters, Data>(SpaceConnector.clientV2.dashboard.privateWidget.load);
const publicWidgetFetcher = getCancellableFetcher<PublicWidgetLoadParameters, Data>(SpaceConnector.clientV2.dashboard.publicWidget.load);
const fetchWidget = async (): Promise<Data|APIErrorToast|undefined> => {
if (props.widgetState === 'INACTIVE') return undefined;
try {
state.loading = true;
const _isPrivate = props.widgetId.startsWith('private');
const _fetcher = _isPrivate ? privateWidgetFetcher : publicWidgetFetcher;
const { status, response } = await _fetcher({
const fetchWidgetData = async (params: PrivateWidgetLoadParameters|PublicWidgetLoadParameters): Promise<Data> => {
const defaultFetcher = state.isPrivateWidget
? SpaceConnector.clientV2.dashboard.privateWidget.load<PrivateWidgetLoadParameters, Data>
: SpaceConnector.clientV2.dashboard.publicWidget.load<PublicWidgetLoadParameters, Data>;
const res = await defaultFetcher(params);
return res;
};
const queryKey = computed(() => [
'widget-load-heatmap',
props.widgetId,
{
start: dateRange.value.start,
end: dateRange.value.end,
granularity: widgetOptionsState.granularityInfo?.granularity,
dataTableId: state.dataTable?.data_table_id,
dataTableOptions: JSON.stringify(sortObjectByKeys(state.dataTable?.options) ?? {}),
groupBy: [widgetOptionsState.xAxisInfo?.data as string],
count: widgetOptionsState.xAxisInfo.count,
},
]);
const queryResult = useQuery({
queryKey,
queryFn: async () => {
const results = await fetchWidgetData({
widget_id: props.widgetId,
granularity: state.granularity,
...getWidgetLoadApiQueryDateRange(state.granularity, dateRange.value),
// query: {
// granularity: state.granularity,
// ...(!isDateField(state.xAxisField) && { page: { start: 1, limit: state.xAxisCount } }),
// ...getWidgetLoadApiQueryDateRange(state.granularity, dateRange.value),
// ...getWidgetLoadApiQuery(state.dataFieldInfo, state.xAxisField),
// },
granularity: widgetOptionsState.granularityInfo?.granularity,
group_by: [widgetOptionsState.xAxisInfo?.data as string],
...getWidgetLoadApiQueryDateRange(widgetOptionsState.granularityInfo?.granularity, dateRange.value),
...(!isDateField(widgetOptionsState.xAxisInfo.data) && { page: { start: 1, limit: widgetOptionsState.xAxisInfo.count } }),
vars: props.dashboardVars,
});
if (status === 'succeed') {
state.errorMessage = undefined;
state.loading = false;
return response;
}
return undefined;
} catch (e: any) {
state.loading = false;
state.errorMessage = e.message;
ErrorHandler.handleError(e);
return ErrorHandler.makeAPIErrorToast(e);
}
};
state.data = results;
drawChart(state.data);
return results;
},
enabled: computed(() => props.widgetState !== 'INACTIVE' && !!state.dataTable && state.runQueries),
});
/* Util */
const getDynamicFieldData = (rawData: DynamicFieldData): any[] => {
const _criteria = state.dynamicFieldInfo?.criteria;
const _seriesData: any[] = [];
state.xAxisData.forEach((x, xIdx) => {
const _targetData = rawData.results?.find((d) => d[state.xAxisField] === x);
state.yAxisData.forEach((y, yIdx) => {
const _data = _targetData?.[_criteria].find((v) => v[state.dataField] === y);
_seriesData.push([xIdx, yIdx, _data ? _data.value : 0]);
});
});
const loading = computed(() => queryResult.isLoading);
const errorMessage = computed(() => queryResult.error?.value?.message);
return _seriesData;
};
const getStaticFieldData = (rawData: StaticFieldData): any[] => {
/* Util */
const getFieldData = (rawData: StaticFieldData): any[] => {
const _seriesData: any[] = [];
state.xAxisData.forEach((x, xIdx) => {
state.yAxisData.forEach((y, yIdx) => {
const _data = rawData.results?.find((v) => v[state.xAxisField] === x);
const _data = rawData.results?.find((v) => v[widgetOptionsState.xAxisInfo?.data as string] === x);
_seriesData.push([xIdx, yIdx, _data ? _data[y] : 0]);
});
});
return _seriesData;
};
const drawChart = (rawData: Data|null) => {
if (isEmpty(rawData)) return;
// get converted chart data
let _seriesData: any[];
if (state.dataFieldInfo?.fieldType === 'staticField') {
_seriesData = getStaticFieldData(rawData);
} else {
_seriesData = getDynamicFieldData(rawData);
}
state.chartData = _seriesData;
state.chartData = getFieldData(rawData);
};
const loadWidget = async (): Promise<Data|APIErrorToast> => {
const res = await fetchWidget();
if (!res) return state.data;
if (typeof res === 'function') return res;
state.data = res;
drawChart(state.data);
return state.data;
const loadWidget = () => {
state.runQueries = true;
};
/* Watcher */
Expand All @@ -285,11 +243,22 @@ watch([() => state.chartData, () => chartContext.value], ([, chartCtx]) => {
}
});
const { widgetFrameProps, widgetFrameEventHandlers } = useWidgetFrame(props, emit, {
dateRange,
errorMessage: errorMessage.value,
widgetLoading: loading.value,
noData: computed(() => (state.data ? !state.data.results?.length : false)),
});
useResizeObserver(chartContext, throttle(() => {
state.chart?.resize();
}, 500));
useWidgetInitAndRefresh({ props, emit, loadWidget });
defineExpose<WidgetExpose<Data>>({
onMounted(async () => {
if (!props.dataTableId) return;
state.dataTable = await getWidgetDataTable(props.dataTableId);
});
defineExpose<WidgetExpose>({
loadWidget,
});
</script>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,12 @@ const heatmap: WidgetConfig = {
defaultIndex: 0,
},
},
dataField: {},
dataField: {
options: {
multiSelectable: true,
allSelected: true,
},
},
colorSchema: {},
},
optionalFieldsSchema: {
Expand Down

0 comments on commit 62638ce

Please sign in to comment.