Skip to content

Commit

Permalink
feat: add 'Color-Coded Heatmap' widget (#4629)
Browse files Browse the repository at this point in the history
* feat: add `Color-Coded Heatmap` widget

Signed-off-by: yuda <[email protected]>

* feat: add to widget config list

Signed-off-by: yuda <[email protected]>

* fix(advanced-format-rules): set init value when mounted

Signed-off-by: yuda <[email protected]>

* chore: update language

Signed-off-by: yuda <[email protected]>

* chore: delete unused variable

Signed-off-by: yuda <[email protected]>

* chore: add icon

Signed-off-by: yuda <[email protected]>

* chore: apply code review

Signed-off-by: yuda <[email protected]>

---------

Signed-off-by: yuda <[email protected]>
  • Loading branch information
yuda110 authored Sep 2, 2024
1 parent 10ae75a commit 0f3fe6c
Show file tree
Hide file tree
Showing 12 changed files with 290 additions and 1 deletion.
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: props.value?.value ?? [],
baseColor: props.value?.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,188 @@
<script setup lang="ts">
import {
computed, defineExpose, reactive, ref,
} from 'vue';
import { 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,
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
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
/* eslint-disable */
/* tslint:disable */
// @ts-ignore
import icon from 'vue-svgicon'
icon.register({
'ic_chart-heatmap-table': {
width: 24,
height: 24,
viewBox: '0 0 24 24',
data: '<path pid="0" d="M20.25 2.25H3.75a1.5 1.5 0 00-1.5 1.5v16.5a1.5 1.5 0 001.5 1.5h16.5a1.5 1.5 0 001.5-1.5V3.75a1.5 1.5 0 00-1.5-1.5zm-13.5 13.5h-3v-3h3v3zm1.5 1.5h3v3h-3v-3zm4.5 0h3v3h-3v-3zm0-6v-3h3v3h-3zm3-4.5h-3v-3h3v3zm1.5 1.5h3v3h-3v-3zm-6-4.5v7.5h-7.5v-7.5h7.5zm6 16.5v-7.5h3v7.5h-3z" _fill="#232533"/>'
}
})
1 change: 1 addition & 0 deletions packages/mirinae/src/foundation/icons/p-icons/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ import './ic_chart-column'
import './ic_chart-donut'
import './ic_chart-gauge'
import './ic_chart-geomap'
import './ic_chart-heatmap-table'
import './ic_chart-heatmap'
import './ic_chart-line'
import './ic_chart-number-card'
Expand Down

0 comments on commit 0f3fe6c

Please sign in to comment.