|
@@ -3,7 +3,7 @@
|
|
|
:style="{
|
|
:style="{
|
|
|
...styleMap?.mainStyle
|
|
...styleMap?.mainStyle
|
|
|
}"
|
|
}"
|
|
|
- class="relative w-full h-full box-border relative"
|
|
|
|
|
|
|
+ class="relative w-full h-full box-border overflow-visible"
|
|
|
>
|
|
>
|
|
|
<ImageBg
|
|
<ImageBg
|
|
|
v-if="styleMap?.mainStyle?.imageSrc"
|
|
v-if="styleMap?.mainStyle?.imageSrc"
|
|
@@ -11,26 +11,14 @@
|
|
|
:imageStyle="styleMap?.mainStyle?.imageStyle"
|
|
:imageStyle="styleMap?.mainStyle?.imageStyle"
|
|
|
/>
|
|
/>
|
|
|
|
|
|
|
|
- <div class="absolute inset-0 w-full h-full">
|
|
|
|
|
|
|
+ <div class="absolute inset-0 w-full h-full overflow-visible">
|
|
|
<svg
|
|
<svg
|
|
|
:viewBox="`0 0 ${width} ${height}`"
|
|
:viewBox="`0 0 ${width} ${height}`"
|
|
|
preserveAspectRatio="xMidYMid meet"
|
|
preserveAspectRatio="xMidYMid meet"
|
|
|
- class="w-full h-full block"
|
|
|
|
|
|
|
+ class="w-full h-full block overflow-visible"
|
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
|
>
|
|
>
|
|
|
<defs></defs>
|
|
<defs></defs>
|
|
|
- <!-- 主轴直线:按 area 分段绘制,每段使用对应范围的直线颜色 -->
|
|
|
|
|
- <line
|
|
|
|
|
- v-for="(seg, segIdx) in axisSegments"
|
|
|
|
|
- :key="'axis-' + segIdx"
|
|
|
|
|
- :x1="seg.x1"
|
|
|
|
|
- :y1="seg.y1"
|
|
|
|
|
- :x2="seg.x2"
|
|
|
|
|
- :y2="seg.y2"
|
|
|
|
|
- :stroke="seg.stroke"
|
|
|
|
|
- :stroke-width="mainLineWidth"
|
|
|
|
|
- stroke-linecap="butt"
|
|
|
|
|
- />
|
|
|
|
|
<!-- 刻度线:所有刻度,按 area 使用对应直线颜色 -->
|
|
<!-- 刻度线:所有刻度,按 area 使用对应直线颜色 -->
|
|
|
<line
|
|
<line
|
|
|
v-for="(pos, idx) in tickPositions"
|
|
v-for="(pos, idx) in tickPositions"
|
|
@@ -43,23 +31,33 @@
|
|
|
:stroke-width="itemsLineWidth"
|
|
:stroke-width="itemsLineWidth"
|
|
|
stroke-linecap="butt"
|
|
stroke-linecap="butt"
|
|
|
/>
|
|
/>
|
|
|
- <!-- 标签(仅主刻度位置显示),按 area 使用对应文本颜色 -->
|
|
|
|
|
- <template v-if="enableLabels">
|
|
|
|
|
- <text
|
|
|
|
|
- v-for="(lb, idx) in labelList"
|
|
|
|
|
- :key="'label-' + idx"
|
|
|
|
|
- :x="lb.x"
|
|
|
|
|
- :y="lb.y"
|
|
|
|
|
- :text-anchor="lb.textAnchor"
|
|
|
|
|
- :dominant-baseline="lb.dominantBaseline"
|
|
|
|
|
- :fill="lb.fill"
|
|
|
|
|
- :font-size="mainTextSize"
|
|
|
|
|
- :font-family="mainFontFamily"
|
|
|
|
|
- >
|
|
|
|
|
- {{ lb.text }}
|
|
|
|
|
- </text>
|
|
|
|
|
- </template>
|
|
|
|
|
|
|
+ <!-- 主轴直线:按 area 分段绘制,每段使用对应范围的直线颜色 -->
|
|
|
|
|
+ <line
|
|
|
|
|
+ v-for="(seg, segIdx) in axisSegments"
|
|
|
|
|
+ :key="'axis-' + segIdx"
|
|
|
|
|
+ :x1="segIdx === 0 ? seg.x1 - itemsLineWidth / 2 : seg.x1"
|
|
|
|
|
+ :y1="seg.y1"
|
|
|
|
|
+ :x2="segIdx === axisSegments.length - 1 ? seg.x2 + itemsLineWidth / 2 : seg.x2"
|
|
|
|
|
+ :y2="seg.y2"
|
|
|
|
|
+ :stroke="seg.stroke"
|
|
|
|
|
+ :stroke-width="mainLineWidth"
|
|
|
|
|
+ stroke-linecap="butt"
|
|
|
|
|
+ />
|
|
|
</svg>
|
|
</svg>
|
|
|
|
|
+ <!-- 标签用 HTML 渲染在 SVG 外,避免被 viewBox 裁剪,全部展示 -->
|
|
|
|
|
+ <div
|
|
|
|
|
+ v-if="enableLabels && labelList.length"
|
|
|
|
|
+ class="absolute inset-0 pointer-events-none overflow-visible"
|
|
|
|
|
+ >
|
|
|
|
|
+ <span
|
|
|
|
|
+ v-for="(lb, idx) in labelList"
|
|
|
|
|
+ :key="'label-' + idx"
|
|
|
|
|
+ class="absolute whitespace-nowrap"
|
|
|
|
|
+ :style="labelSpanStyle(lb)"
|
|
|
|
|
+ >
|
|
|
|
|
+ {{ lb.text }}
|
|
|
|
|
+ </span>
|
|
|
|
|
+ </div>
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|
|
|
</template>
|
|
</template>
|
|
@@ -131,8 +129,19 @@ const itemsStyleConfig = computed(() =>
|
|
|
s.part?.name === 'items' && (s.part?.state === props.state || s.part?.state === 'default')
|
|
s.part?.name === 'items' && (s.part?.state === props.state || s.part?.state === 'default')
|
|
|
)
|
|
)
|
|
|
)
|
|
)
|
|
|
|
|
+// 从 styles 中读取 indicator 的 other.length(主刻度线长度)
|
|
|
|
|
+const indicatorStyleConfig = computed(() =>
|
|
|
|
|
+ props.styles?.find(
|
|
|
|
|
+ (s: any) =>
|
|
|
|
|
+ s.part?.name === 'indicator' && (s.part?.state === props.state || s.part?.state === 'default')
|
|
|
|
|
+ )
|
|
|
|
|
+)
|
|
|
const tickLength = computed(() => itemsStyleConfig.value?.other?.length ?? 5)
|
|
const tickLength = computed(() => itemsStyleConfig.value?.other?.length ?? 5)
|
|
|
-const mainTickLength = computed(() => Math.max(tickLength.value * 1.5, tickLength.value + 4))
|
|
|
|
|
|
|
+const mainTickLength = computed(
|
|
|
|
|
+ () =>
|
|
|
|
|
+ indicatorStyleConfig.value?.other?.length ??
|
|
|
|
|
+ Math.max(tickLength.value * 1.5, tickLength.value + 4)
|
|
|
|
|
+)
|
|
|
|
|
|
|
|
const mainLineColor = computed(() => styleMap.value?.mainStyle?.line?.color ?? '#212121')
|
|
const mainLineColor = computed(() => styleMap.value?.mainStyle?.line?.color ?? '#212121')
|
|
|
const mainLineWidth = computed(() => styleMap.value?.mainStyle?.line?.width ?? 2)
|
|
const mainLineWidth = computed(() => styleMap.value?.mainStyle?.line?.width ?? 2)
|
|
@@ -150,22 +159,27 @@ const isTopOrLeft = computed(
|
|
|
() => props.mode === 'horizontal_top' || props.mode === 'vertical_left'
|
|
() => props.mode === 'horizontal_top' || props.mode === 'vertical_left'
|
|
|
)
|
|
)
|
|
|
|
|
|
|
|
-// 刻度数量、主刻度数量
|
|
|
|
|
|
|
+// 刻度数量、主刻度间隔(空格数)
|
|
|
const tickCount = computed(() => Math.max(2, props.tick))
|
|
const tickCount = computed(() => Math.max(2, props.tick))
|
|
|
-const mainTickCount = computed(() => Math.max(1, Math.min(props.mainTick, tickCount.value)))
|
|
|
|
|
|
|
+// 主刻度间隔:表示两个主刻度之间包含的“空格”数量(即小刻度间的间隔数)
|
|
|
|
|
+const majorTickGap = computed(() => Math.max(1, Math.min(props.mainTick || 1, tickCount.value - 1)))
|
|
|
|
|
|
|
|
-// 主刻度在刻度数组中的索引集合
|
|
|
|
|
|
|
+// 主刻度在刻度数组中的索引集合:始终从 0 开始,按 mainTick 作为“空格数”间隔
|
|
|
|
|
+// 若最后剩余的间隔不足 mainTick,则末尾不再补主刻度(“下个大刻度不出来就不展示标签”)
|
|
|
const majorIndices = computed(() => {
|
|
const majorIndices = computed(() => {
|
|
|
const set = new Set<number>()
|
|
const set = new Set<number>()
|
|
|
const n = tickCount.value
|
|
const n = tickCount.value
|
|
|
- const m = mainTickCount.value
|
|
|
|
|
- if (m <= 1) {
|
|
|
|
|
|
|
+ const gap = majorTickGap.value
|
|
|
|
|
+ if (n <= 1) {
|
|
|
set.add(0)
|
|
set.add(0)
|
|
|
return set
|
|
return set
|
|
|
}
|
|
}
|
|
|
- for (let i = 0; i < m; i++) {
|
|
|
|
|
- const idx = Math.round((i * (n - 1)) / (m - 1))
|
|
|
|
|
- set.add(Math.min(idx, n - 1))
|
|
|
|
|
|
|
+
|
|
|
|
|
+ const lastIndex = n - 1
|
|
|
|
|
+ const maxStep = Math.floor(lastIndex / gap)
|
|
|
|
|
+ for (let step = 0; step <= maxStep; step++) {
|
|
|
|
|
+ const idx = step * gap
|
|
|
|
|
+ if (idx <= lastIndex) set.add(idx)
|
|
|
}
|
|
}
|
|
|
return set
|
|
return set
|
|
|
})
|
|
})
|
|
@@ -181,19 +195,24 @@ const tickNormPositions = computed(() => {
|
|
|
|
|
|
|
|
// 根据 mode 计算轴线坐标(主轴直线)
|
|
// 根据 mode 计算轴线坐标(主轴直线)
|
|
|
// horizontal_top: 主轴贴在最底部;horizontal_bottom: 主轴贴在最顶部
|
|
// horizontal_top: 主轴贴在最底部;horizontal_bottom: 主轴贴在最顶部
|
|
|
|
|
+// vertical_left: 主轴贴在最右侧;vertical_right: 主轴贴在最左侧
|
|
|
const axisLine = computed(() => {
|
|
const axisLine = computed(() => {
|
|
|
const { width, height } = props
|
|
const { width, height } = props
|
|
|
- const pad = mainTickLength.value + 8
|
|
|
|
|
const halfLine = mainLineWidth.value / 2
|
|
const halfLine = mainLineWidth.value / 2
|
|
|
if (isHorizontal.value) {
|
|
if (isHorizontal.value) {
|
|
|
const y = isTopOrLeft.value ? height - halfLine : halfLine
|
|
const y = isTopOrLeft.value ? height - halfLine : halfLine
|
|
|
return { x1: 0, y1: y, x2: width, y2: y }
|
|
return { x1: 0, y1: y, x2: width, y2: y }
|
|
|
}
|
|
}
|
|
|
- const x = isTopOrLeft.value ? pad : width - pad
|
|
|
|
|
|
|
+ // 垂直模式下:根据模式贴边放置主轴
|
|
|
|
|
+ const x =
|
|
|
|
|
+ props.mode === 'vertical_left'
|
|
|
|
|
+ ? width - halfLine // 主轴靠最右
|
|
|
|
|
+ : halfLine // 主轴靠最左(vertical_right)
|
|
|
return { x1: x, y1: 0, x2: x, y2: height }
|
|
return { x1: x, y1: 0, x2: x, y2: height }
|
|
|
})
|
|
})
|
|
|
|
|
|
|
|
// 主轴按 area 分段:根据范围边界拆成多段,每段使用对应 area 的直线颜色
|
|
// 主轴按 area 分段:根据范围边界拆成多段,每段使用对应 area 的直线颜色
|
|
|
|
|
+// 两端需要处理刻度线的宽度
|
|
|
const axisSegments = computed(() => {
|
|
const axisSegments = computed(() => {
|
|
|
const { width, height } = props
|
|
const { width, height } = props
|
|
|
const start = props.rangeStart
|
|
const start = props.rangeStart
|
|
@@ -251,7 +270,6 @@ const axisSegments = computed(() => {
|
|
|
// 刻度线端点:与主轴共线;每根刻度按数值取 area 直线颜色
|
|
// 刻度线端点:与主轴共线;每根刻度按数值取 area 直线颜色
|
|
|
const tickPositions = computed(() => {
|
|
const tickPositions = computed(() => {
|
|
|
const { width, height } = props
|
|
const { width, height } = props
|
|
|
- const pad = mainTickLength.value + 8
|
|
|
|
|
const axis = axisLine.value
|
|
const axis = axisLine.value
|
|
|
const norm = tickNormPositions.value
|
|
const norm = tickNormPositions.value
|
|
|
const major = majorIndices.value
|
|
const major = majorIndices.value
|
|
@@ -274,8 +292,9 @@ const tickPositions = computed(() => {
|
|
|
})
|
|
})
|
|
|
})
|
|
})
|
|
|
} else {
|
|
} else {
|
|
|
- const xAxis = isTopOrLeft.value ? pad : width - pad
|
|
|
|
|
- const sign = isTopOrLeft.value ? -1 : 1
|
|
|
|
|
|
|
+ // 垂直模式下,刻度与主轴共线:vertical_left 主轴在右侧,刻度向左伸展;vertical_right 相反
|
|
|
|
|
+ const xAxis = axis.x1
|
|
|
|
|
+ const sign = props.mode === 'vertical_left' ? -1 : 1
|
|
|
norm.forEach((t, idx) => {
|
|
norm.forEach((t, idx) => {
|
|
|
const y = t * height
|
|
const y = t * height
|
|
|
const len = major.has(idx) ? mainTickLength.value : tickLength.value
|
|
const len = major.has(idx) ? mainTickLength.value : tickLength.value
|
|
@@ -292,7 +311,7 @@ const tickPositions = computed(() => {
|
|
|
return list
|
|
return list
|
|
|
})
|
|
})
|
|
|
|
|
|
|
|
-// 标签文案:有设置内容用设置内容,否则用主刻度数值(根据范围变化),主刻度数为整数
|
|
|
|
|
|
|
+// 标签文案:有设置内容用设置内容,否则用主刻度对应的数值(根据范围变化)
|
|
|
const labelTexts = computed(() => {
|
|
const labelTexts = computed(() => {
|
|
|
const raw = (props.labels || '').trim()
|
|
const raw = (props.labels || '').trim()
|
|
|
if (raw) {
|
|
if (raw) {
|
|
@@ -301,24 +320,75 @@ const labelTexts = computed(() => {
|
|
|
.map((s) => s.trim())
|
|
.map((s) => s.trim())
|
|
|
.filter(Boolean)
|
|
.filter(Boolean)
|
|
|
}
|
|
}
|
|
|
- const m = mainTickCount.value
|
|
|
|
|
- const start = props.rangeStart
|
|
|
|
|
- const end = props.rangeEnd
|
|
|
|
|
|
|
+ const indices = Array.from(majorIndices.value).sort((a, b) => a - b)
|
|
|
|
|
+ if (!indices.length) return []
|
|
|
|
|
+ const n = tickCount.value
|
|
|
const list: string[] = []
|
|
const list: string[] = []
|
|
|
- for (let i = 0; i < m; i++) {
|
|
|
|
|
- const v = m <= 1 ? start : start + ((end - start) * i) / (m - 1)
|
|
|
|
|
- list.push(String(Math.round(v)))
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ indices.forEach((idx) => {
|
|
|
|
|
+ const t = n <= 1 ? 0.5 : idx / (n - 1)
|
|
|
|
|
+ const v = valueAt(t)
|
|
|
|
|
+ list.push(String(Math.floor(v)))
|
|
|
|
|
+ })
|
|
|
return list
|
|
return list
|
|
|
})
|
|
})
|
|
|
|
|
|
|
|
|
|
+// 标签的 HTML 定位样式:根据不同模式,按主刻度长度 + 5 偏移
|
|
|
|
|
+function labelSpanStyle(lb: {
|
|
|
|
|
+ x: number
|
|
|
|
|
+ y: number
|
|
|
|
|
+ textAnchor: string
|
|
|
|
|
+ dominantBaseline: string
|
|
|
|
|
+ fill: string
|
|
|
|
|
+}) {
|
|
|
|
|
+ const style: Record<string, string> = {
|
|
|
|
|
+ color: lb.fill as string,
|
|
|
|
|
+ fontSize: String(mainTextSize.value),
|
|
|
|
|
+ fontFamily: String(mainFontFamily.value)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const offset = `${mainTickLength.value + 5}px`
|
|
|
|
|
+
|
|
|
|
|
+ switch (props.mode) {
|
|
|
|
|
+ // horizontal_top:不设置 top,bottom 为 主刻度长度 + 5
|
|
|
|
|
+ case 'horizontal_top': {
|
|
|
|
|
+ style.left = props.width > 0 ? `${(lb.x / props.width) * 100}%` : '0'
|
|
|
|
|
+ style.bottom = offset
|
|
|
|
|
+ style.transform = 'translateX(-50%)'
|
|
|
|
|
+ break
|
|
|
|
|
+ }
|
|
|
|
|
+ // horizontal_bottom:不设置 bottom,top 为 主刻度长度 + 5
|
|
|
|
|
+ case 'horizontal_bottom': {
|
|
|
|
|
+ style.left = props.width > 0 ? `${(lb.x / props.width) * 100}%` : '0'
|
|
|
|
|
+ style.top = offset
|
|
|
|
|
+ style.transform = 'translateX(-50%)'
|
|
|
|
|
+ break
|
|
|
|
|
+ }
|
|
|
|
|
+ // vertical_right:top 按刻度位置动态计算,left 为 主刻度长度 + 5
|
|
|
|
|
+ case 'vertical_right': {
|
|
|
|
|
+ style.top = props.height > 0 ? `${(lb.y / props.height) * 100}%` : '0'
|
|
|
|
|
+ style.left = offset
|
|
|
|
|
+ style.transform = 'translateY(-50%)'
|
|
|
|
|
+ break
|
|
|
|
|
+ }
|
|
|
|
|
+ // vertical_left:不设置 left,right 为 主刻度长度 + 5
|
|
|
|
|
+ case 'vertical_left': {
|
|
|
|
|
+ style.top = props.height > 0 ? `${(lb.y / props.height) * 100}%` : '0'
|
|
|
|
|
+ style.right = offset
|
|
|
|
|
+ style.transform = 'translateY(-50%)'
|
|
|
|
|
+ break
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return style
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
// 标签位置与对齐(仅主刻度位置),按 area 使用对应文本颜色
|
|
// 标签位置与对齐(仅主刻度位置),按 area 使用对应文本颜色
|
|
|
const labelList = computed(() => {
|
|
const labelList = computed(() => {
|
|
|
if (!props.enableLabels || labelTexts.value.length === 0) return []
|
|
if (!props.enableLabels || labelTexts.value.length === 0) return []
|
|
|
const { width, height } = props
|
|
const { width, height } = props
|
|
|
- const pad = mainTickLength.value + 8
|
|
|
|
|
const labelGap = 4
|
|
const labelGap = 4
|
|
|
- const m = mainTickCount.value
|
|
|
|
|
|
|
+ const indices = Array.from(majorIndices.value).sort((a, b) => a - b)
|
|
|
|
|
+ const m = indices.length
|
|
|
const texts = labelTexts.value
|
|
const texts = labelTexts.value
|
|
|
const list: {
|
|
const list: {
|
|
|
x: number
|
|
x: number
|
|
@@ -336,7 +406,8 @@ const labelList = computed(() => {
|
|
|
? yAxis - mainTickLength.value - labelGap
|
|
? yAxis - mainTickLength.value - labelGap
|
|
|
: yAxis + mainTickLength.value + labelGap
|
|
: yAxis + mainTickLength.value + labelGap
|
|
|
for (let i = 0; i < m; i++) {
|
|
for (let i = 0; i < m; i++) {
|
|
|
- const t = m <= 1 ? 0.5 : i / (m - 1)
|
|
|
|
|
|
|
+ const idx = indices[i]
|
|
|
|
|
+ const t = tickCount.value <= 1 ? 0.5 : idx / (tickCount.value - 1)
|
|
|
const x = t * width
|
|
const x = t * width
|
|
|
const value = valueAt(t)
|
|
const value = valueAt(t)
|
|
|
list.push({
|
|
list.push({
|
|
@@ -349,12 +420,14 @@ const labelList = computed(() => {
|
|
|
})
|
|
})
|
|
|
}
|
|
}
|
|
|
} else {
|
|
} else {
|
|
|
- const xAxis = isTopOrLeft.value ? pad : width - pad
|
|
|
|
|
|
|
+ // 垂直模式下,标签位置基于主轴位置再偏移主刻度长度和间距
|
|
|
|
|
+ const xAxis = axisLine.value.x1
|
|
|
const labelX = isTopOrLeft.value
|
|
const labelX = isTopOrLeft.value
|
|
|
? xAxis - mainTickLength.value - labelGap
|
|
? xAxis - mainTickLength.value - labelGap
|
|
|
: xAxis + mainTickLength.value + labelGap
|
|
: xAxis + mainTickLength.value + labelGap
|
|
|
for (let i = 0; i < m; i++) {
|
|
for (let i = 0; i < m; i++) {
|
|
|
- const t = m <= 1 ? 0.5 : i / (m - 1)
|
|
|
|
|
|
|
+ const idx = indices[i]
|
|
|
|
|
+ const t = tickCount.value <= 1 ? 0.5 : idx / (tickCount.value - 1)
|
|
|
const y = t * height
|
|
const y = t * height
|
|
|
const value = valueAt(t)
|
|
const value = valueAt(t)
|
|
|
list.push({
|
|
list.push({
|