Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add 'Color-Coded Heatmap' widget #4628

Closed
Closed
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
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,9 @@ export const WIDGET_COMPONENTS: Record<WidgetConfigKey, AsyncComponent> = {
gauge: () => ({
component: import('@/common/modules/widgets/_widgets/gauge/Gauge.vue'),
}),
colorCodedHeatmap: () => ({
component: import('@/common/modules/widgets/_widgets/color-coded-heatmap/ColorCodedHeatmap.vue'),
}),
// progressCard: () => ({
// component: import('@/common/modules/widgets/_widgets/progress-card/ProgressCard.vue'),
// }),
Expand All @@ -58,5 +61,6 @@ export const WIDGET_COMPONENT_ICON_MAP: Record<WidgetConfigKey, string> = {
table: 'ic_chart-table',
stackedAreaChart: 'ic_chart-area',
gauge: 'ic_chart-gauge',
colorCodedHeatmap: 'ic_chart-color-heatmap',
// progressCard: 'ic_chart-progress-card',
};
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import clusteredColumnChart from '@/common/modules/widgets/_widgets/clustered-column-chart/widget-config';
import colorCodedHeatmap from '@/common/modules/widgets/_widgets/color-coded-heatmap/widget-config';
import gauge from '@/common/modules/widgets/_widgets/gauge/widget-config';
import geoMap from '@/common/modules/widgets/_widgets/geo-map/widget-config';
import heatmap from '@/common/modules/widgets/_widgets/heatmap/widget-config';
Expand All @@ -24,6 +25,7 @@ export const CONSOLE_WIDGET_CONFIG_KEYS = [
'pieChart',
'treemap',
'heatmap',
'colorCodedHeatmap',
'geoMap',
'table',
'stackedAreaChart',
Expand All @@ -40,6 +42,7 @@ export const CONSOLE_WIDGET_CONFIG: Record<WidgetConfigKey, Partial<WidgetConfig
pieChart,
treemap,
heatmap,
colorCodedHeatmap,
geoMap,
table,
stackedAreaChart,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<script lang="ts" setup>
import {
computed, reactive, watch,
computed, onMounted, reactive, watch,
} from 'vue';

import { cloneDeep } from 'lodash';
Expand Down Expand Up @@ -87,6 +87,13 @@ watch(() => labelsMenuItem.value, (menuItem) => {
state.proxyValue = { ...state.proxyValue, field: _selectedValue };
}
}, { immediate: true });
onMounted(() => {
state.proxyValue = {
...state.proxyValue,
value: state.proxyValue?.value ?? [],
baseColor: state.proxyValue?.baseColor ?? props.widgetFieldSchema?.options?.baseColor ?? DEFAULT_BASE_COLOR,
};
});
</script>

<template>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
<script setup lang="ts">
import {
computed, defineExpose, reactive, ref,
} from 'vue';

import { max, orderBy } from 'lodash';

import { SpaceConnector } from '@cloudforet/core-lib/space-connector';
import { numberFormatter } from '@cloudforet/utils';

import type { ListResponse } from '@/schema/_common/api-verbs/list';
import type { PrivateWidgetLoadParameters } from '@/schema/dashboard/private-widget/api-verbs/load';
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 { useWidgetFrame } from '@/common/modules/widgets/_composables/use-widget-frame';
import { useWidgetInitAndRefresh } from '@/common/modules/widgets/_composables/use-widget-init-and-refresh';
import {
getApiQueryDateRange,
getReferenceLabel,
getWidgetBasedOnDate,
getWidgetDateRange,
} from '@/common/modules/widgets/_helpers/widget-date-helper';
import type { DateRange } from '@/common/modules/widgets/types/widget-data-type';
import type { WidgetEmit, WidgetExpose, WidgetProps } from '@/common/modules/widgets/types/widget-display-type';
import type { AdvancedFormatRulesValue, GroupByValue } from '@/common/modules/widgets/types/widget-field-value-type';


type Data = ListResponse<{
[key: string]: string|number;
}>;

const boxWrapperRef = ref<any|null>(null);
const BOX_MIN_WIDTH = 112;
const MAX_COUNT = 80;
const props = defineProps<WidgetProps>();
const emit = defineEmits<WidgetEmit>();
const state = reactive({
loading: false,
errorMessage: undefined as string|undefined,
data: null as Data | null,
heatmapMaxValue: computed(() => max(state.chartData.map((d) => d?.[2] || 0)) ?? 1),
unit: computed<string|undefined>(() => widgetFrameProps.value.unitMap?.[state.dataField]),
boxWidth: computed<number>(() => {
if (!props.width) return BOX_MIN_WIDTH;
const widgetContentWidth = props.width;
if (props.width >= 990) return widgetContentWidth / 10;
return widgetContentWidth / 8 < BOX_MIN_WIDTH ? BOX_MIN_WIDTH : widgetContentWidth / 8;
}),
boxWrapperHeight: computed(() => (boxWrapperRef.value?.scrollHeight <= (30 * 16) ? 'auto' : '30rem')),
refinedData: computed(() => {
if (!state.data) return [];
const _orderedData = orderBy(state.data.results, [state.dataField], ['desc']);
return _orderedData.map((d) => ({
name: d[state.groupByField],
value: numberFormatter(d[state.dataField], { minimumFractionDigits: 2 }),
color: getColor(d[state.formatRulesField], state.formatRulesField),
}));
}),
// required fields
granularity: computed<string>(() => props.widgetOptions?.granularity as string),
dataField: computed<string|undefined>(() => props.widgetOptions?.dataField as string),
basedOnDate: computed(() => getWidgetBasedOnDate(state.granularity, props.dashboardOptions?.date_range?.end)),
groupByField: computed<string|undefined>(() => (props.widgetOptions?.groupBy as GroupByValue)?.value as string),
formatRulesValue: computed<AdvancedFormatRulesValue>(() => props.widgetOptions?.advancedFormatRules as AdvancedFormatRulesValue),
formatRulesField: computed<string|undefined>(() => state.formatRulesValue?.field),
dateRange: computed<DateRange>(() => {
const [_start, _end] = getWidgetDateRange(state.granularity, state.basedOnDate, 1);
return { start: _start, end: _end };
}),
});
const { widgetFrameProps, widgetFrameEventHandlers } = useWidgetFrame(props, emit, {
dateRange: computed(() => state.dateRange),
errorMessage: computed(() => state.errorMessage),
widgetLoading: computed(() => state.loading),
noData: computed(() => (state.data ? !state.data.results?.length : false)),
});

/* Api */
const fetchWidget = async (): Promise<Data|APIErrorToast|undefined> => {
if (props.widgetState === 'INACTIVE') return undefined;
try {
const _isPrivate = props.widgetId.startsWith('private');
const _fetcher = _isPrivate
? SpaceConnector.clientV2.dashboard.privateWidget.load<PrivateWidgetLoadParameters, Data>
: SpaceConnector.clientV2.dashboard.publicWidget.load<PublicWidgetLoadParameters, Data>;
const _queryDateRange = getApiQueryDateRange(state.granularity, state.dateRange);
const res = await _fetcher({
widget_id: props.widgetId,
query: {
granularity: state.granularity,
start: _queryDateRange.start,
end: _queryDateRange.end,
group_by: [state.groupByField, state.formatRulesField],
fields: {
[state.dataField]: {
key: state.dataField,
operator: 'sum',
},
},
// field_group: [state.formatRulesField],
// sort: [{ key: `_total_${state.dataField}`, desc: true }],
page: { start: 1, limit: MAX_COUNT },
},
vars: props.dashboardVars,
});
state.errorMessage = undefined;
return res;
} catch (e: any) {
state.loading = false;
state.errorMessage = e.message;
ErrorHandler.handleError(e);
return ErrorHandler.makeAPIErrorToast(e);
}
};

/* Util */
const loadWidget = async (): Promise<Data|APIErrorToast> => {
state.loading = true;
const res = await fetchWidget();
if (typeof res === 'function') return res;
state.data = res;
state.loading = false;
return state.data;
};
const getColor = (val: string, field: string): string => {
const _label = getReferenceLabel(props.allReferenceTypeInfo, field, val);
return state.formatRulesValue.value.find((d) => d.text === _label)?.color ?? state.formatRulesValue.baseColor;
};

useWidgetInitAndRefresh({ props, emit, loadWidget });
defineExpose<WidgetExpose<Data>>({
loadWidget,
});
</script>

<template>
<widget-frame v-bind="widgetFrameProps"
class="color-coded-heatmap"
v-on="widgetFrameEventHandlers"
>
<!--Do not delete div element below. It's defense code for redraw-->
<div class="content-wrapper">
<div class="box-wrapper"
:style="{'grid-template-columns': `repeat(auto-fill, ${state.boxWidth-4}px)`,
}"
>
<div v-for="(data, idx) in state.refinedData"
:key="`box-${idx}`"
v-tooltip.bottom="`${data.name}: ${data.value}`"
class="value-box"
:style="{'background-color': data.color}"
>
<span class="value-text">{{ data.name }}</span>
</div>
</div>
</div>
</widget-frame>
</template>

<style lang="postcss" scoped>
.color-coded-heatmap {
.content-wrapper {
height: 96%;
overflow-y: auto;
}
.box-wrapper {
display: grid;
grid-auto-flow: row;
gap: 1px;
.value-box {
height: 3.125rem;
font-weight: 500;
padding: 0.5rem;
.value-text {
@apply text-label-sm;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
overflow: hidden;
text-overflow: ellipsis;
text-align: center;
}
}
}
}
</style>
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { ADVANCED_FORMAT_RULE_TYPE } from '@/common/modules/widgets/_constants/widget-field-constant';
import type { WidgetConfig } from '@/common/modules/widgets/types/widget-config-type';


const colorCodedHeatmap: WidgetConfig = {
widgetName: 'colorCodedHeatmap',
meta: {
title: 'Color Coded Heatmap',
sizes: ['full'],
defaultValidationConfig: {
defaultMaxCount: 2,
},
},
requiredFieldsSchema: {
granularity: {},
dataField: {},
groupBy: {
options: {
dataTarget: 'labels_info',
hideCount: true,
defaultIndex: 0,
excludeDateField: true,
},
},
advancedFormatRules: {
options: {
formatRulesType: ADVANCED_FORMAT_RULE_TYPE.field,
description: 'COMMON.WIDGETS.ADVANCED_FORMAT_RULES.COLOR_CODED_HEATMAP_DESC',
},
},
},
optionalFieldsSchema: {},
};


export default colorCodedHeatmap;
26 changes: 26 additions & 0 deletions packages/language-pack/console-translation-2.8.babel
Original file line number Diff line number Diff line change
Expand Up @@ -24073,6 +24073,32 @@
</translation>
</translations>
</concept_node>
<folder_node>
<name>ADVANCED_FORMAT_RULES</name>
<children>
<concept_node>
<name>COLOR_CODED_HEATMAP_DESC</name>
<definition_loaded>false</definition_loaded>
<description/>
<comment/>
<default_text/>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>ja-JP</language>
<approved>false</approved>
</translation>
<translation>
<language>ko-KR</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
</children>
</folder_node>
<folder_node>
<name>ALL_SUMMARY</name>
<children>
Expand Down
3 changes: 3 additions & 0 deletions packages/language-pack/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -1363,6 +1363,9 @@
"ACTUAL": "Actual",
"ADD_WIDGET": "Add Widget",
"ADD_WIDGET_TO_DASHBOARD": "Add Widget to Dashboard",
"ADVANCED_FORMAT_RULES": {
"COLOR_CODED_HEATMAP_DESC": "Heatmap color changes \baccording to the specified text in the legend."
},
"ALL_SUMMARY": {
"ADD_SERVICE_ACCOUNTS": "Add Service Accounts",
"ALL": "All",
Expand Down
3 changes: 3 additions & 0 deletions packages/language-pack/ja.json
Original file line number Diff line number Diff line change
Expand Up @@ -1363,6 +1363,9 @@
"ACTUAL": "実際のサイズ",
"ADD_WIDGET": "ウィジェットを追加する",
"ADD_WIDGET_TO_DASHBOARD": "ダッシュボードにウィジェットを追加する",
"ADVANCED_FORMAT_RULES": {
"COLOR_CODED_HEATMAP_DESC": ""
},
"ALL_SUMMARY": {
"ADD_SERVICE_ACCOUNTS": "サービスアカウントの追加",
"ALL": "合計",
Expand Down
3 changes: 3 additions & 0 deletions packages/language-pack/ko.json
Original file line number Diff line number Diff line change
Expand Up @@ -1363,6 +1363,9 @@
"ACTUAL": "실제 크기",
"ADD_WIDGET": "위젯 추가",
"ADD_WIDGET_TO_DASHBOARD": "대시보드에 위젯 추가",
"ADVANCED_FORMAT_RULES": {
"COLOR_CODED_HEATMAP_DESC": ""
},
"ALL_SUMMARY": {
"ADD_SERVICE_ACCOUNTS": "서비스 어카운트 추가",
"ALL": "전체 서비스",
Expand Down
Loading