forked from github/dataease
Merge pull request #10461 from dataease/pr@dev-v2@chart-map-style-add-gradient-color-selector
feat(图表-地图): 地图颜色支持设置渐变色及自定义渐变色
This commit is contained in:
commit
4bb41cea58
@ -218,6 +218,7 @@ onMounted(() => {
|
||||
<custom-color-style-select
|
||||
v-model="state"
|
||||
:themes="themes"
|
||||
:property-inner="propertyInner"
|
||||
@change-basic-style="changeBasicStyle('colors')"
|
||||
/>
|
||||
</template>
|
||||
|
@ -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));
|
||||
}
|
||||
|
@ -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>
|
@ -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) => {
|
||||
|
@ -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
|
||||
})
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user