-
Notifications
You must be signed in to change notification settings - Fork 37
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: add 'Color-Coded Heatmap' widget (#4629)
* 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
Showing
12 changed files
with
290 additions
and
1 deletion.
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
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
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
188 changes: 188 additions & 0 deletions
188
apps/web/src/common/modules/widgets/_widgets/color-coded-heatmap/ColorCodedHeatmap.vue
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,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> |
36 changes: 36 additions & 0 deletions
36
apps/web/src/common/modules/widgets/_widgets/color-coded-heatmap/widget-config.ts
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,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; |
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
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
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
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
3 changes: 3 additions & 0 deletions
3
packages/mirinae/src/foundation/icons/icon-assets/ic_chart-heatmap-table.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
12 changes: 12 additions & 0 deletions
12
packages/mirinae/src/foundation/icons/p-icons/ic_chart-heatmap-table.js
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,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"/>' | ||
} | ||
}) |
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