Merge pull request #10461 from dataease/pr@dev-v2@chart-map-style-add-gradient-color-selector

feat(图表-地图): 地图颜色支持设置渐变色及自定义渐变色
This commit is contained in:
jianneng-fit2cloud 2024-06-24 14:08:40 +08:00 committed by GitHub
commit 4bb41cea58
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 455 additions and 18 deletions

View File

@ -218,6 +218,7 @@ onMounted(() => {
<custom-color-style-select
v-model="state"
:themes="themes"
:property-inner="propertyInner"
@change-basic-style="changeBasicStyle('colors')"
/>
</template>

View File

@ -3,6 +3,9 @@ import { ElColorPicker, ElPopover } from 'element-plus-secondary'
import { computed, ref } from 'vue'
import { useI18n } from '@/hooks/web/useI18n'
import { COLOR_CASES, COLOR_PANEL } from '@/views/chart/components/editor/util/chart'
import GradientColorSelector from '@/views/chart/components/editor/editor-style/components/GradientColorSelector.vue'
import { getMapColorCases, stepsColor } from '@/views/chart/components/js/util'
const { t } = useI18n()
const props = withDefaults(
@ -13,6 +16,7 @@ const props = withDefaults(
customColor: any
colorIndex: number
}
propertyInner: Array<string>
}>(),
{
themes: 'light'
@ -41,30 +45,48 @@ const customColorPickerRef = ref<InstanceType<typeof ElColorPicker>>()
function selectColorCase(option) {
state.value.basicStyleForm.colorScheme = option.value
colorCaseSelectorRef.value?.hide()
changeColorOption()
}
const changeColorOption = () => {
const items = colorCases.filter(ele => {
return ele.value === state.value.basicStyleForm.colorScheme
})
state.value.basicStyleForm.colors = [...items[0].colors]
state.value.customColor = state.value.basicStyleForm.colors[0]
state.value.colorIndex = 0
changeBasicStyle()
changeColorOption(option)
}
const changeColorOption = (option?) => {
let isGradient = option?.value?.endsWith('_split_gradient') || isColorGradient.value
const getColorItems = isGradient ? getMapColorCases(colorCases) : colorCases
const items = getColorItems.filter(ele => ele.value === state.value.basicStyleForm.colorScheme)
if (items.length > 0) {
state.value.basicStyleForm.colors = [...items[0].colors]
state.value.customColor = state.value.basicStyleForm.colors[0]
state.value.colorIndex = 0
changeBasicStyle()
}
}
const resetCustomColor = () => {
changeColorOption()
}
const switchColorCase = () => {
state.value.basicStyleForm.colors[state.value.colorIndex] = state.value.customColor
const { colorIndex, customColor, basicStyleForm } = state.value
const colors = basicStyleForm.colors
if (isColorGradient.value) {
let startColor = colorIndex === 0 ? customColor : colors[0]
let endColor = colorIndex === 0 ? colors[8] : customColor
basicStyleForm.colors = stepsColor(startColor, endColor, 9, 1)
} else {
colors[colorIndex] = customColor
}
changeBasicStyle()
}
const isColorGradient = computed(() =>
state.value.basicStyleForm.colorScheme.endsWith('_split_gradient')
)
const showColorGradientIndex = index => {
return index === 0 || index === state.value.basicStyleForm.colors.length - 1
}
const switchColor = (index, c) => {
if (isColorGradient.value && !showColorGradientIndex(index)) {
return
}
state.value.colorIndex = index
state.value.customColor = c
customColorPickerRef.value?.show()
@ -81,6 +103,21 @@ function onPopoverShow() {
function onPopoverHide() {
_popoverShow.value = false
}
const showProperty = prop => props.propertyInner?.includes(prop)
const colorItemBorderColor = (index, state) => {
const isCurrentColorActive = state.colorIndex === index
if (isColorGradient.value) {
if (showColorGradientIndex(index)) {
//
return isCurrentColorActive ? 'var(--ed-color-primary)' : 'rgb(230,230,230)'
} else {
//
return 'rgb(230,230,230,0.01)'
}
}
//
return isCurrentColorActive ? 'var(--ed-color-primary)' : ''
}
</script>
<template>
@ -90,6 +127,20 @@ function onPopoverHide() {
>
<el-row>
<el-form-item
v-if="showProperty('gradient-color')"
:label="$t('chart.color_case')"
class="form-item"
:class="'form-item-' + themes"
style="flex: 1; padding-right: 8px; margin-bottom: 16px"
>
<gradient-color-selector
v-model="state"
:themes="themes"
@select-color-case="selectColorCase"
/>
</el-form-item>
<el-form-item
v-if="!showProperty('gradient-color')"
:label="t('chart.color_case')"
class="form-item"
:class="'form-item-' + themes"
@ -187,14 +238,34 @@ function onPopoverHide() {
:key="index"
@click="switchColor(index, c)"
class="color-item"
:class="{ active: state.colorIndex === index }"
:class="{
active: state.colorIndex === index,
hover: isColorGradient ? showColorGradientIndex(index) : true
}"
:style="{
'border-color': colorItemBorderColor(index, state)
}"
>
<div
class="color-item__inner"
:style="{
backgroundColor: c
}"
></div>
>
<el-icon
v-if="isColorGradient && showColorGradientIndex(index)"
class="input-arrow-icon"
:style="{
color: 'white',
'font-size': 'x-small',
left: '2px',
bottom: '2px'
}"
:class="{ reverse: _popoverShow }"
>
<ArrowDown />
</el-icon>
</div>
</div>
<div class="inner-selector">
<el-color-picker
@ -298,7 +369,9 @@ function onPopoverHide() {
height: 14px;
border-radius: 1px;
}
&:not(.hover) {
cursor: initial;
}
&:hover {
border-color: var(--ed-color-primary-99, rgba(51, 112, 255, 0.6));
}

View File

@ -0,0 +1,306 @@
<script lang="tsx" setup>
import { computed, nextTick, onMounted, reactive, ref } from 'vue'
import { useI18n } from '@/hooks/web/useI18n'
import { COLOR_PANEL, COLOR_CASES } from '@/views/chart/components/editor/util/chart'
import { ElPopover } from 'element-plus-secondary'
import { getMapColorCases } from '@/views/chart/components/js/util'
const { t } = useI18n()
const props = withDefaults(
defineProps<{
themes?: EditorTheme
modelValue: {
basicStyleForm: ChartBasicStyle
customColor: any
colorIndex: number
}
propertyInner: Array<string>
}>(),
{
themes: 'light'
}
)
const colorCases = JSON.parse(JSON.stringify(COLOR_CASES))
const predefineColors = JSON.parse(JSON.stringify(COLOR_PANEL))
const emits = defineEmits(['update:modelValue', 'selectColorCase'])
const state = computed({
get() {
return props.modelValue
},
set(v) {
emits('update:modelValue', v)
}
})
const form = reactive({
value: null,
activeName: 'simple',
enableCustom: false,
tabPanes: [
{
label: t('chart.page_pager_general'),
name: 'simple',
data: JSON.parse(JSON.stringify(colorCases))
},
{
label: t('chart.gradient'),
name: 'split_gradient',
data: JSON.parse(JSON.stringify(getMapColorCases(colorCases)))
}
]
})
const scrollToSelected = () => {
const index = form.activeName === 'simple' ? 0 : 1
const parents = document.getElementById('color-tab-content-' + index)
if (!parents) return
const items = parents.getElementsByClassName('color-div-base selected')
if (items && items.length) {
const top = items[0].offsetTop || 0
parents.scrollTo(0, top)
}
}
const handleClick = () => {
form.enableCustom = false
nextTick(() => {
scrollToSelected()
})
const widget = ref['de-color-picker']
if (!widget) return
if (Array.isArray(widget)) {
widget.forEach(item => {
item.triggerHide && item.triggerHide()
})
return
}
widget.triggerHide && widget.triggerHide()
}
const selectNode = option => {
state.value.basicStyleForm.colors = option.colors
state.value.basicStyleForm.colorScheme = option.value
emits('selectColorCase', option)
}
const colorCaseSelectorRef = ref<InstanceType<typeof ElPopover>>()
const _popoverShow = ref(false)
function onPopoverShow() {
_popoverShow.value = true
}
function onPopoverHide() {
_popoverShow.value = false
}
onMounted(() => {
form.activeName = state.value.basicStyleForm.colorScheme.endsWith('_split_gradient')
? 'split_gradient'
: 'simple'
})
</script>
<template>
<el-popover
placement="bottom-start"
ref="colorCaseSelectorRef"
width="268"
:offset="4"
trigger="click"
:persistent="false"
:show-arrow="false"
@show="onPopoverShow"
@hide="onPopoverHide"
:popper-style="{ padding: 0 }"
:effect="themes"
>
<template #reference>
<el-input :effect="themes" readonly class="custom-color-selector">
<template #prefix>
<div class="custom-color-selector-container">
<div
v-for="(c, index) in state.basicStyleForm.colors"
:key="index"
:style="{
flex: 1,
height: '100%',
backgroundColor: c
}"
></div>
</div>
</template>
<template #suffix>
<el-icon class="input-arrow-icon" :class="{ reverse: _popoverShow }">
<ArrowDown />
</el-icon>
</template>
</el-input>
</template>
<template #default>
<el-tabs v-model="form.activeName" class="tab-header" @tab-click="handleClick">
<el-tab-pane
class="padding-tab"
v-for="(pane, i) in form.tabPanes"
:key="i"
:label="pane.label"
:name="pane.name"
>
<div class="pane_content">
<el-scrollbar
max-height="274px"
class="cases-list"
:class="{ dark: 'dark' === themes }"
>
<div
v-for="option in pane.data"
:key="option.value"
class="select-color-item"
:class="{ active: state.basicStyleForm.colorScheme === option.value }"
@click="selectNode(option)"
>
<div style="float: left">
<span
v-for="(c, index) in option.colors"
:key="index"
:style="{
width: '20px',
height: '20px',
float: 'left',
backgroundColor: c
}"
/>
</div>
<span class="cases-list__text">{{ option.name }}</span>
</div>
</el-scrollbar>
</div>
</el-tab-pane>
</el-tabs>
</template>
</el-popover>
</template>
<style scoped lang="less">
.custom-color-selector {
:deep(.ed-input__prefix) {
width: calc(100% - 22px);
.ed-input__prefix-inner {
width: 100%;
}
}
:deep(.ed-input__wrapper) {
cursor: pointer;
}
.custom-color-selector-container {
border-radius: 2px;
overflow: hidden;
width: 100%;
height: 16px;
display: flex;
flex-direction: row;
flex-wrap: nowrap;
align-items: center;
justify-content: space-evenly;
}
}
.cases-list {
margin: 6px 0;
.select-color-item {
width: 100%;
font-size: var(--ed-font-size-base);
padding: 0 32px 0 20px;
position: relative;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
color: var(--ed-text-color-regular);
height: 34px;
line-height: 34px;
box-sizing: border-box;
cursor: pointer;
display: flex;
align-items: center;
&:hover {
background-color: var(--ed-fill-color-light);
}
&.active {
color: var(--ed-color-primary);
font-weight: 500;
}
}
&.dark {
.select-color-item {
color: #ebebeb;
&:hover {
background-color: rgba(235, 235, 235, 0.1);
}
}
}
.cases-list__text {
margin-left: 4px;
}
}
.tab-header {
--ed-tabs-header-height: 34px;
--custom-tab-color: #646a73;
:deep(.ed-tabs__nav-wrap::after) {
background-color: unset;
}
&.dark {
--custom-tab-color: #a6a6a6;
}
height: 100%;
:deep(.ed-tabs__item) {
font-weight: 400;
font-size: 12px;
padding: 0 8px !important;
margin-right: 12px;
color: var(--custom-tab-color);
}
:deep(.is-active) {
font-weight: 500;
color: var(--ed-color-primary, #3370ff);
}
:deep(.ed-tabs__nav-scroll) {
padding-left: 0 !important;
}
:deep(.ed-tabs__header) {
margin: 0 !important;
}
:deep(.ed-tabs__content) {
height: calc(100% - 35px);
overflow-y: auto;
overflow-x: hidden;
}
}
.padding-tab {
padding: 0;
height: 100%;
width: 100%;
display: flex;
:deep(.ed-scrollbar) {
&.has-footer {
height: calc(100% - 81px);
}
}
:deep(.ed-footer) {
padding: 0;
height: 114px;
}
}
</style>

View File

@ -33,6 +33,7 @@ export class Map extends L7PlotChartView<ChoroplethOptions, Choropleth> {
properties: EditorProperty[] = [...MAP_EDITOR_PROPERTY, 'legend-selector']
propertyInner: EditorPropertyInner = {
...MAP_EDITOR_PROPERTY_INNER,
'basic-style-selector': ['colors', 'alpha', 'areaBorderColor', 'zoom', 'gradient-color'],
'legend-selector': ['icon', 'fontSize', 'color']
}
axis = MAP_AXIS_TYPE
@ -177,6 +178,7 @@ export class Map extends L7PlotChartView<ChoroplethOptions, Choropleth> {
const colors = basicStyle.colors.map(item => hexColorToRGBA(item, basicStyle.alpha))
const { legend } = parseJson(chart.customStyle)
let data = []
data = sourceData
let colorScale = []
if (legend.show) {
let minValue = misc.mapLegendMin
@ -196,7 +198,6 @@ export class Map extends L7PlotChartView<ChoroplethOptions, Choropleth> {
// 定义最大值最小值区间数量和对应的颜色
colorScale = getDynamicColorScale(minValue, maxValue, mapLegendNumber, colors)
} else {
data = sourceData
colorScale = colors
}
const areaMap = data.reduce((obj, value) => {

View File

@ -592,3 +592,59 @@ export const setMapChartDefaultMaxAndMinValueByData = (
callback(maxResult, minResult)
}
}
export const stepsColor = (start, end, steps, gamma) => {
let i
let j
let ms
let me
const output = []
const so = []
gamma = gamma || 1
const normalize = function (channel) {
return Math.pow(channel / 255, gamma)
}
start = parseColor(start).map(normalize)
end = parseColor(end).map(normalize)
for (i = 0; i < steps; i++) {
ms = steps - 1 === 0 ? 0 : i / (steps - 1)
me = 1 - ms
for (j = 0; j < 3; j++) {
so[j] = pad(Math.round(Math.pow(start[j] * me + end[j] * ms, 1 / gamma) * 255).toString(16))
}
output.push('#' + so.join(''))
}
function parseColor(hexStr) {
return hexStr.length === 4
? hexStr
.substr(1)
.split('')
.map(function (s) {
return 0x11 * parseInt(s, 16)
})
: [hexStr.substr(1, 2), hexStr.substr(3, 2), hexStr.substr(5, 2)].map(function (s) {
return parseInt(s, 16)
})
}
function pad(s) {
return s.length === 1 ? '0' + s : s
}
return output
}
export const getMapColorCases = colorCases => {
const cloneColorCases = JSON.parse(JSON.stringify(colorCases))
return cloneColorCases.map(colorItem => {
const curColors = colorItem.colors
const len = curColors.length
const start = curColors[0]
const end = curColors[len - 1]
const itemResult = {
name: colorItem.name,
value: colorItem.value + '_split_gradient',
baseColors: [start, end],
colors: stepsColor(start, end, 9, 1)
}
return itemResult
})
}