@cfasim-ui/docs 0.5.1 → 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -87,6 +87,17 @@ export interface Series extends LineMarkStyle {
87
87
  * effect when `line` is `false`.
88
88
  */
89
89
  outline?: boolean;
90
+ /**
91
+ * Outline stroke color. Defaults to the page background
92
+ * (`var(--color-bg-0, #fff)`). Only applies when `outline` is true.
93
+ */
94
+ outlineColor?: string;
95
+ /**
96
+ * Total extra width added to the line's `strokeWidth` for the outline
97
+ * (split evenly on each side). Defaults to `4`. Only applies when
98
+ * `outline` is true.
99
+ */
100
+ outlineWidth?: number;
90
101
  dotFill?: string;
91
102
  dotStroke?: string;
92
103
  /**
@@ -103,6 +114,12 @@ export interface Area {
103
114
  x?: LineChartXInput;
104
115
  color?: string;
105
116
  opacity?: number;
117
+ /**
118
+ * CSS `mix-blend-mode` applied to the area fill. Lets overlapping
119
+ * areas (e.g. confidence bands) combine their colors instead of one
120
+ * obscuring the other.
121
+ */
122
+ blendMode?: BlendMode;
106
123
  }
107
124
 
108
125
  export interface AreaSection {
@@ -223,6 +240,10 @@ const props = withDefaults(defineProps<LineChartProps>(), {
223
240
  yScaleType: "linear",
224
241
  });
225
242
 
243
+ // The template root is a <Teleport>, so fallthrough attrs (class, style,
244
+ // data-*, id…) can't auto-inherit — forward them onto the wrapper manually.
245
+ defineOptions({ inheritAttrs: false });
246
+
226
247
  const emit = defineEmits<{
227
248
  (e: "hover", payload: ChartHoverPayload): void;
228
249
  }>();
@@ -933,6 +954,9 @@ const {
933
954
  triggerCsvDownload,
934
955
  menuFilename,
935
956
  isFullscreen,
957
+ fullscreenStyle,
958
+ teleportTarget,
959
+ exitFullscreen,
936
960
  } = useChartFoundation({
937
961
  width: () => props.width,
938
962
  height: () => props.height,
@@ -947,6 +971,7 @@ const {
947
971
  filename: () => props.filename,
948
972
  downloadLink: () => props.downloadLink,
949
973
  downloadButton: () => props.downloadButton,
974
+ fullscreenTarget: () => props.fullscreenTarget,
950
975
  chartPadding: () => props.chartPadding,
951
976
  inlineLegendLabels: () => inlineLegendLabels.value,
952
977
  hasTooltipSlot: () => hasTooltipSlot.value,
@@ -1016,405 +1041,418 @@ const positionedLegendItems = computed(() => {
1016
1041
  </script>
1017
1042
 
1018
1043
  <template>
1019
- <div
1020
- ref="containerRef"
1021
- class="line-chart-wrapper"
1022
- :class="{ 'is-fullscreen': isFullscreen }"
1023
- >
1024
- <ChartMenu v-if="menu" :items="menuItems" />
1025
- <div class="chart-sr-only" aria-live="polite">
1026
- {{ isFullscreen ? "Chart expanded to fill window" : "" }}
1027
- </div>
1028
- <svg ref="svgRef" :width="width" :height="totalHeight">
1029
- <!-- title -->
1030
- <text
1031
- v-if="title"
1032
- :x="titleResolved.x"
1033
- :y="titleResolved.lineHeight"
1034
- :text-anchor="titleResolved.anchor"
1035
- :font-size="titleResolved.fontSize"
1036
- :font-weight="titleResolved.fontWeight"
1037
- :fill="titleResolved.color"
1038
- >
1039
- <tspan
1040
- v-for="(line, i) in titleResolved.lines"
1041
- :key="i"
1044
+ <Teleport :to="teleportTarget" :disabled="!isFullscreen">
1045
+ <div
1046
+ ref="containerRef"
1047
+ v-bind="$attrs"
1048
+ class="line-chart-wrapper"
1049
+ :class="{ 'is-fullscreen': isFullscreen }"
1050
+ :style="fullscreenStyle"
1051
+ >
1052
+ <ChartMenu
1053
+ v-if="menu"
1054
+ :items="menuItems"
1055
+ :is-fullscreen="isFullscreen"
1056
+ @close="exitFullscreen"
1057
+ />
1058
+ <div class="chart-sr-only" aria-live="polite">
1059
+ {{ isFullscreen ? "Chart expanded to fill window" : "" }}
1060
+ </div>
1061
+ <svg ref="svgRef" :width="width" :height="totalHeight">
1062
+ <!-- title -->
1063
+ <text
1064
+ v-if="title"
1042
1065
  :x="titleResolved.x"
1043
- :dy="i === 0 ? 0 : titleResolved.lineHeight"
1066
+ :y="titleResolved.lineHeight"
1067
+ :text-anchor="titleResolved.anchor"
1068
+ :font-size="titleResolved.fontSize"
1069
+ :font-weight="titleResolved.fontWeight"
1070
+ :fill="titleResolved.color"
1044
1071
  >
1045
- {{ line }}
1046
- </tspan>
1047
- </text>
1048
- <!-- inline legend -->
1049
- <g v-if="positionedLegendItems.length > 0">
1050
- <template v-for="(item, i) in positionedLegendItems" :key="'ileg' + i">
1051
- <!-- series indicator: line -->
1052
- <line
1053
- v-if="item.type === 'series'"
1054
- :x1="item.x"
1055
- :y1="item.y"
1056
- :x2="item.x + 12"
1057
- :y2="item.y"
1058
- :stroke="item.color"
1059
- stroke-width="2"
1060
- :stroke-dasharray="item.dashed ? '4 2' : undefined"
1061
- />
1062
- <!-- section indicator: filled circle -->
1063
- <circle
1064
- v-else
1065
- :cx="item.x + 4"
1066
- :cy="item.y"
1067
- r="4"
1068
- :fill="item.color"
1069
- :fill-opacity="item.fillOpacity"
1070
- :stroke="item.color"
1071
- stroke-width="1.5"
1072
- />
1073
- <text
1074
- :x="item.x + 18"
1075
- :y="item.y + 4"
1076
- :font-size="legendResolved.fontSize"
1077
- :fill="legendResolved.fill"
1078
- :font-weight="legendResolved.fontWeight"
1072
+ <tspan
1073
+ v-for="(line, i) in titleResolved.lines"
1074
+ :key="i"
1075
+ :x="titleResolved.x"
1076
+ :dy="i === 0 ? 0 : titleResolved.lineHeight"
1079
1077
  >
1080
- {{ item.label }}
1081
- </text>
1082
- </template>
1083
- </g>
1084
- <!-- axes -->
1085
- <line
1086
- :x1="snap(padding.left)"
1087
- :y1="snap(padding.top)"
1088
- :x2="snap(padding.left)"
1089
- :y2="snap(padding.top + innerH)"
1090
- stroke="currentColor"
1091
- stroke-opacity="0.3"
1092
- />
1093
- <line
1094
- :x1="snap(padding.left)"
1095
- :y1="snap(padding.top + innerH)"
1096
- :x2="snap(padding.left + innerW)"
1097
- :y2="snap(padding.top + innerH)"
1098
- stroke="currentColor"
1099
- stroke-opacity="0.3"
1100
- />
1101
- <!-- y grid lines -->
1102
- <template v-if="yGrid">
1078
+ {{ line }}
1079
+ </tspan>
1080
+ </text>
1081
+ <!-- inline legend -->
1082
+ <g v-if="positionedLegendItems.length > 0">
1083
+ <template
1084
+ v-for="(item, i) in positionedLegendItems"
1085
+ :key="'ileg' + i"
1086
+ >
1087
+ <!-- series indicator: line -->
1088
+ <line
1089
+ v-if="item.type === 'series'"
1090
+ :x1="item.x"
1091
+ :y1="item.y"
1092
+ :x2="item.x + 12"
1093
+ :y2="item.y"
1094
+ :stroke="item.color"
1095
+ stroke-width="2"
1096
+ :stroke-dasharray="item.dashed ? '4 2' : undefined"
1097
+ />
1098
+ <!-- section indicator: filled circle -->
1099
+ <circle
1100
+ v-else
1101
+ :cx="item.x + 4"
1102
+ :cy="item.y"
1103
+ r="4"
1104
+ :fill="item.color"
1105
+ :fill-opacity="item.fillOpacity"
1106
+ :stroke="item.color"
1107
+ stroke-width="1.5"
1108
+ />
1109
+ <text
1110
+ :x="item.x + 18"
1111
+ :y="item.y + 4"
1112
+ :font-size="legendResolved.fontSize"
1113
+ :fill="legendResolved.fill"
1114
+ :font-weight="legendResolved.fontWeight"
1115
+ >
1116
+ {{ item.label }}
1117
+ </text>
1118
+ </template>
1119
+ </g>
1120
+ <!-- axes -->
1103
1121
  <line
1104
- v-for="(tick, i) in yTickItems"
1105
- :key="'yg' + i"
1106
- :x1="padding.left"
1107
- :y1="tick.y"
1108
- :x2="padding.left + innerW"
1109
- :y2="tick.y"
1122
+ :x1="snap(padding.left)"
1123
+ :y1="snap(padding.top)"
1124
+ :x2="snap(padding.left)"
1125
+ :y2="snap(padding.top + innerH)"
1110
1126
  stroke="currentColor"
1111
- stroke-opacity="0.1"
1127
+ stroke-opacity="0.3"
1112
1128
  />
1113
- </template>
1114
- <!-- x grid lines -->
1115
- <template v-if="xGrid">
1116
1129
  <line
1117
- v-for="(tick, i) in xTickItems"
1118
- :key="'xg' + i"
1119
- :x1="tick.x"
1120
- :y1="padding.top"
1121
- :x2="tick.x"
1122
- :y2="padding.top + innerH"
1130
+ :x1="snap(padding.left)"
1131
+ :y1="snap(padding.top + innerH)"
1132
+ :x2="snap(padding.left + innerW)"
1133
+ :y2="snap(padding.top + innerH)"
1123
1134
  stroke="currentColor"
1124
- stroke-opacity="0.1"
1125
- />
1126
- </template>
1127
- <!-- y tick labels -->
1128
- <text
1129
- v-for="(tick, i) in yTickItems"
1130
- :key="'y' + i"
1131
- data-testid="y-tick"
1132
- :x="padding.left - 6"
1133
- :y="tick.y"
1134
- text-anchor="end"
1135
- dominant-baseline="middle"
1136
- :font-size="tickLabelResolved.fontSize"
1137
- :fill="tickLabelResolved.fill"
1138
- :font-weight="tickLabelResolved.fontWeight"
1139
- :fill-opacity="tickLabelResolved.fillOpacity"
1140
- >
1141
- {{ tick.value }}
1142
- </text>
1143
- <!-- y axis label -->
1144
- <text
1145
- v-if="yLabel"
1146
- :x="0"
1147
- :y="0"
1148
- :transform="`translate(14, ${padding.top + innerH / 2}) rotate(-90)`"
1149
- text-anchor="middle"
1150
- :font-size="axisLabelResolved.fontSize"
1151
- :fill="axisLabelResolved.fill"
1152
- :font-weight="axisLabelResolved.fontWeight"
1153
- >
1154
- {{ yLabel }}
1155
- </text>
1156
- <!-- x tick labels -->
1157
- <text
1158
- v-for="(tick, i) in xTickItems"
1159
- :key="'x' + i"
1160
- data-testid="x-tick"
1161
- :x="tick.x"
1162
- :y="padding.top + innerH + 16"
1163
- :text-anchor="tick.anchor"
1164
- :font-size="tickLabelResolved.fontSize"
1165
- :fill="tickLabelResolved.fill"
1166
- :font-weight="tickLabelResolved.fontWeight"
1167
- :fill-opacity="tickLabelResolved.fillOpacity"
1168
- >
1169
- {{ tick.value }}
1170
- </text>
1171
- <!-- x axis label -->
1172
- <text
1173
- v-if="xLabel"
1174
- :x="padding.left + innerW / 2"
1175
- :y="height - 4"
1176
- text-anchor="middle"
1177
- :font-size="axisLabelResolved.fontSize"
1178
- :fill="axisLabelResolved.fill"
1179
- :font-weight="axisLabelResolved.fontWeight"
1180
- >
1181
- {{ xLabel }}
1182
- </text>
1183
- <!-- areas -->
1184
- <path
1185
- v-for="(a, i) in allAreas"
1186
- :key="'area' + i"
1187
- :d="toAreaPath(a)"
1188
- :fill="a.color ?? 'currentColor'"
1189
- :fill-opacity="a.opacity ?? 0.2"
1190
- stroke="none"
1191
- />
1192
- <!-- data lines and dots -->
1193
- <template v-for="(s, i) in allSeries" :key="i">
1194
- <path
1195
- v-if="s.line !== false && s.outline"
1196
- :d="toPath(s)"
1197
- fill="none"
1198
- stroke="var(--color-bg-0, #fff)"
1199
- :stroke-width="(s.strokeWidth ?? 1.5) + 4"
1200
- stroke-linecap="round"
1201
- stroke-linejoin="round"
1202
- data-testid="line-outline"
1135
+ stroke-opacity="0.3"
1203
1136
  />
1204
- <path
1205
- v-if="s.line !== false"
1206
- :d="toPath(s)"
1207
- fill="none"
1208
- :stroke="s.color ?? 'currentColor'"
1209
- :stroke-width="s.strokeWidth ?? 1.5"
1210
- :stroke-opacity="s.lineOpacity ?? s.opacity ?? lineOpacity"
1211
- :stroke-dasharray="s.dashed ? '6 3' : undefined"
1212
- :style="s.blendMode ? { mixBlendMode: s.blendMode } : undefined"
1213
- />
1214
- <template v-if="s.dots">
1215
- <circle
1216
- v-for="(pt, j) in toPoints(s)"
1217
- :key="j"
1218
- :cx="pt.x"
1219
- :cy="pt.y"
1220
- :r="s.dotRadius ?? (s.strokeWidth ?? 1.5) + 1"
1221
- :fill="s.dotFill ?? s.color ?? 'currentColor'"
1222
- :fill-opacity="s.dotOpacity ?? s.opacity ?? lineOpacity"
1223
- :stroke="s.dotStroke ?? 'none'"
1224
- :style="s.blendMode ? { mixBlendMode: s.blendMode } : undefined"
1137
+ <!-- y grid lines -->
1138
+ <template v-if="yGrid">
1139
+ <line
1140
+ v-for="(tick, i) in yTickItems"
1141
+ :key="'yg' + i"
1142
+ :x1="padding.left"
1143
+ :y1="tick.y"
1144
+ :x2="padding.left + innerW"
1145
+ :y2="tick.y"
1146
+ stroke="currentColor"
1147
+ stroke-opacity="0.1"
1148
+ />
1149
+ </template>
1150
+ <!-- x grid lines -->
1151
+ <template v-if="xGrid">
1152
+ <line
1153
+ v-for="(tick, i) in xTickItems"
1154
+ :key="'xg' + i"
1155
+ :x1="tick.x"
1156
+ :y1="padding.top"
1157
+ :x2="tick.x"
1158
+ :y2="padding.top + innerH"
1159
+ stroke="currentColor"
1160
+ stroke-opacity="0.1"
1225
1161
  />
1226
1162
  </template>
1227
- </template>
1228
- <!-- area sections (rendered above series) -->
1229
- <template v-for="(sec, i) in areaSections ?? []" :key="'areasec' + i">
1163
+ <!-- y tick labels -->
1164
+ <text
1165
+ v-for="(tick, i) in yTickItems"
1166
+ :key="'y' + i"
1167
+ data-testid="y-tick"
1168
+ :x="padding.left - 6"
1169
+ :y="tick.y"
1170
+ text-anchor="end"
1171
+ dominant-baseline="middle"
1172
+ :font-size="tickLabelResolved.fontSize"
1173
+ :fill="tickLabelResolved.fill"
1174
+ :font-weight="tickLabelResolved.fontWeight"
1175
+ :fill-opacity="tickLabelResolved.fillOpacity"
1176
+ >
1177
+ {{ tick.value }}
1178
+ </text>
1179
+ <!-- y axis label -->
1180
+ <text
1181
+ v-if="yLabel"
1182
+ :x="0"
1183
+ :y="0"
1184
+ :transform="`translate(14, ${padding.top + innerH / 2}) rotate(-90)`"
1185
+ text-anchor="middle"
1186
+ :font-size="axisLabelResolved.fontSize"
1187
+ :fill="axisLabelResolved.fill"
1188
+ :font-weight="axisLabelResolved.fontWeight"
1189
+ >
1190
+ {{ yLabel }}
1191
+ </text>
1192
+ <!-- x tick labels -->
1193
+ <text
1194
+ v-for="(tick, i) in xTickItems"
1195
+ :key="'x' + i"
1196
+ data-testid="x-tick"
1197
+ :x="tick.x"
1198
+ :y="padding.top + innerH + 16"
1199
+ :text-anchor="tick.anchor"
1200
+ :font-size="tickLabelResolved.fontSize"
1201
+ :fill="tickLabelResolved.fill"
1202
+ :font-weight="tickLabelResolved.fontWeight"
1203
+ :fill-opacity="tickLabelResolved.fillOpacity"
1204
+ >
1205
+ {{ tick.value }}
1206
+ </text>
1207
+ <!-- x axis label -->
1208
+ <text
1209
+ v-if="xLabel"
1210
+ :x="padding.left + innerW / 2"
1211
+ :y="height - 4"
1212
+ text-anchor="middle"
1213
+ :font-size="axisLabelResolved.fontSize"
1214
+ :fill="axisLabelResolved.fill"
1215
+ :font-weight="axisLabelResolved.fontWeight"
1216
+ >
1217
+ {{ xLabel }}
1218
+ </text>
1219
+ <!-- areas -->
1230
1220
  <path
1231
- :d="toSectionPath(sec)"
1232
- :fill="
1233
- sec.color ??
1234
- (sec.seriesIndex != null
1235
- ? (allSeries[sec.seriesIndex]?.color ?? 'currentColor')
1236
- : '#999')
1237
- "
1238
- :fill-opacity="sec.opacity ?? 0.15"
1221
+ v-for="(a, i) in allAreas"
1222
+ :key="'area' + i"
1223
+ :d="toAreaPath(a)"
1224
+ :fill="a.color ?? 'currentColor'"
1225
+ :fill-opacity="a.opacity ?? 0.2"
1239
1226
  stroke="none"
1227
+ :style="a.blendMode ? { mixBlendMode: a.blendMode } : undefined"
1240
1228
  />
1241
- <path
1242
- v-if="sec.seriesIndex != null"
1243
- :d="toSectionPath(sec, false)"
1244
- fill="none"
1245
- :stroke="
1246
- sec.color ?? allSeries[sec.seriesIndex]?.color ?? 'currentColor'
1247
- "
1248
- :stroke-width="sec.strokeWidth ?? 2"
1249
- :stroke-dasharray="sec.dashed ? '6 3' : undefined"
1250
- />
1251
- <!-- vertical edge lines for full-height sections -->
1252
- <template v-if="sec.seriesIndex == null">
1229
+ <!-- data lines and dots -->
1230
+ <template v-for="(s, i) in allSeries" :key="i">
1231
+ <path
1232
+ v-if="s.line !== false && s.outline"
1233
+ :d="toPath(s)"
1234
+ fill="none"
1235
+ :stroke="s.outlineColor ?? 'var(--color-bg-0, #fff)'"
1236
+ :stroke-width="(s.strokeWidth ?? 1.5) + (s.outlineWidth ?? 4)"
1237
+ stroke-linecap="round"
1238
+ stroke-linejoin="round"
1239
+ data-testid="line-outline"
1240
+ />
1241
+ <path
1242
+ v-if="s.line !== false"
1243
+ :d="toPath(s)"
1244
+ fill="none"
1245
+ :stroke="s.color ?? 'currentColor'"
1246
+ :stroke-width="s.strokeWidth ?? 1.5"
1247
+ :stroke-opacity="s.lineOpacity ?? s.opacity ?? lineOpacity"
1248
+ :stroke-dasharray="s.dashed ? '6 3' : undefined"
1249
+ :style="s.blendMode ? { mixBlendMode: s.blendMode } : undefined"
1250
+ />
1251
+ <template v-if="s.dots">
1252
+ <circle
1253
+ v-for="(pt, j) in toPoints(s)"
1254
+ :key="j"
1255
+ :cx="pt.x"
1256
+ :cy="pt.y"
1257
+ :r="s.dotRadius ?? (s.strokeWidth ?? 1.5) + 1"
1258
+ :fill="s.dotFill ?? s.color ?? 'currentColor'"
1259
+ :fill-opacity="s.dotOpacity ?? s.opacity ?? lineOpacity"
1260
+ :stroke="s.dotStroke ?? 'none'"
1261
+ :style="s.blendMode ? { mixBlendMode: s.blendMode } : undefined"
1262
+ />
1263
+ </template>
1264
+ </template>
1265
+ <!-- area sections (rendered above series) -->
1266
+ <template v-for="(sec, i) in areaSections ?? []" :key="'areasec' + i">
1267
+ <path
1268
+ :d="toSectionPath(sec)"
1269
+ :fill="
1270
+ sec.color ??
1271
+ (sec.seriesIndex != null
1272
+ ? (allSeries[sec.seriesIndex]?.color ?? 'currentColor')
1273
+ : '#999')
1274
+ "
1275
+ :fill-opacity="sec.opacity ?? 0.15"
1276
+ stroke="none"
1277
+ />
1278
+ <path
1279
+ v-if="sec.seriesIndex != null"
1280
+ :d="toSectionPath(sec, false)"
1281
+ fill="none"
1282
+ :stroke="
1283
+ sec.color ?? allSeries[sec.seriesIndex]?.color ?? 'currentColor'
1284
+ "
1285
+ :stroke-width="sec.strokeWidth ?? 2"
1286
+ :stroke-dasharray="sec.dashed ? '6 3' : undefined"
1287
+ />
1288
+ <!-- vertical edge lines for full-height sections -->
1289
+ <template v-if="sec.seriesIndex == null">
1290
+ <line
1291
+ :x1="snap(sectionXPixel(sec, 'start'))"
1292
+ :y1="padding.top"
1293
+ :x2="snap(sectionXPixel(sec, 'start'))"
1294
+ :y2="padding.top + innerH"
1295
+ :stroke="sec.color ?? '#999'"
1296
+ :stroke-width="sec.strokeWidth ?? 2"
1297
+ :stroke-dasharray="sec.dashed ? '6 3' : undefined"
1298
+ />
1299
+ <line
1300
+ :x1="snap(sectionXPixel(sec, 'end'))"
1301
+ :y1="padding.top"
1302
+ :x2="snap(sectionXPixel(sec, 'end'))"
1303
+ :y2="padding.top + innerH"
1304
+ :stroke="sec.color ?? '#999'"
1305
+ :stroke-width="sec.strokeWidth ?? 2"
1306
+ :stroke-dasharray="sec.dashed ? '6 3' : undefined"
1307
+ />
1308
+ </template>
1309
+ <!-- tick marks at section boundaries -->
1253
1310
  <line
1254
1311
  :x1="snap(sectionXPixel(sec, 'start'))"
1255
- :y1="padding.top"
1312
+ :y1="padding.top + innerH - 4"
1256
1313
  :x2="snap(sectionXPixel(sec, 'start'))"
1257
- :y2="padding.top + innerH"
1258
- :stroke="sec.color ?? '#999'"
1259
- :stroke-width="sec.strokeWidth ?? 2"
1260
- :stroke-dasharray="sec.dashed ? '6 3' : undefined"
1314
+ :y2="padding.top + innerH + 4"
1315
+ stroke="currentColor"
1316
+ stroke-opacity="0.4"
1261
1317
  />
1262
1318
  <line
1263
1319
  :x1="snap(sectionXPixel(sec, 'end'))"
1264
- :y1="padding.top"
1320
+ :y1="padding.top + innerH - 4"
1265
1321
  :x2="snap(sectionXPixel(sec, 'end'))"
1266
- :y2="padding.top + innerH"
1267
- :stroke="sec.color ?? '#999'"
1268
- :stroke-width="sec.strokeWidth ?? 2"
1269
- :stroke-dasharray="sec.dashed ? '6 3' : undefined"
1322
+ :y2="padding.top + innerH + 4"
1323
+ stroke="currentColor"
1324
+ stroke-opacity="0.4"
1270
1325
  />
1271
1326
  </template>
1272
- <!-- tick marks at section boundaries -->
1273
- <line
1274
- :x1="snap(sectionXPixel(sec, 'start'))"
1275
- :y1="padding.top + innerH - 4"
1276
- :x2="snap(sectionXPixel(sec, 'start'))"
1277
- :y2="padding.top + innerH + 4"
1278
- stroke="currentColor"
1279
- stroke-opacity="0.4"
1280
- />
1327
+ <!-- Tooltip: crosshair line -->
1281
1328
  <line
1282
- :x1="snap(sectionXPixel(sec, 'end'))"
1283
- :y1="padding.top + innerH - 4"
1284
- :x2="snap(sectionXPixel(sec, 'end'))"
1285
- :y2="padding.top + innerH + 4"
1329
+ v-if="hasTooltipSlot && hoverIndex !== null"
1330
+ :x1="snap(hoverX)"
1331
+ :y1="padding.top"
1332
+ :x2="snap(hoverX)"
1333
+ :y2="padding.top + innerH"
1286
1334
  stroke="currentColor"
1287
- stroke-opacity="0.4"
1335
+ stroke-opacity="0.3"
1336
+ stroke-dasharray="4 2"
1337
+ pointer-events="none"
1288
1338
  />
1289
- </template>
1290
- <!-- Tooltip: crosshair line -->
1291
- <line
1292
- v-if="hasTooltipSlot && hoverIndex !== null"
1293
- :x1="snap(hoverX)"
1294
- :y1="padding.top"
1295
- :x2="snap(hoverX)"
1296
- :y2="padding.top + innerH"
1297
- stroke="currentColor"
1298
- stroke-opacity="0.3"
1299
- stroke-dasharray="4 2"
1300
- pointer-events="none"
1301
- />
1302
- <!-- Tooltip: hover dots -->
1303
- <circle
1304
- v-for="(dot, i) in hoverDots"
1305
- :key="'hd' + i"
1306
- :cx="dot.x"
1307
- :cy="dot.y"
1308
- r="4"
1309
- :fill="dot.color"
1310
- stroke="var(--color-bg-0, #fff)"
1311
- stroke-width="2"
1312
- pointer-events="none"
1313
- />
1314
- <!-- Tooltip: interaction overlay -->
1315
- <rect
1316
- v-if="hasTooltipSlot"
1317
- :x="padding.left"
1318
- :y="padding.top"
1319
- :width="innerW"
1320
- :height="innerH"
1321
- fill="transparent"
1322
- style="cursor: crosshair; touch-action: none"
1323
- v-on="tooltipHandlers"
1324
- />
1325
- <!-- annotations (top layer) -->
1326
- <ChartAnnotations
1327
- v-if="annotations && annotations.length > 0"
1328
- :annotations="annotations"
1329
- :project="projectAnnotation"
1330
- :bounds="bounds"
1331
- />
1332
- <!-- area section labels -->
1333
- <g v-for="(item, i) in sectionLabels.labels" :key="'seclab' + i">
1339
+ <!-- Tooltip: hover dots -->
1334
1340
  <circle
1335
- :cx="item.cx - item.textWidth / 2 - 2"
1336
- :cy="sectionLabelBaseY + item.row * SECTION_LABEL_ROW_HEIGHT + 4"
1341
+ v-for="(dot, i) in hoverDots"
1342
+ :key="'hd' + i"
1343
+ :cx="dot.x"
1344
+ :cy="dot.y"
1337
1345
  r="4"
1338
- :fill="item.color"
1339
- :fill-opacity="item.fillOpacity"
1340
- :stroke="item.color"
1341
- stroke-width="1.5"
1346
+ :fill="dot.color"
1347
+ stroke="var(--color-bg-0, #fff)"
1348
+ stroke-width="2"
1349
+ pointer-events="none"
1342
1350
  />
1343
- <text
1344
- v-if="item.labelText"
1345
- :x="item.cx - item.textWidth / 2 + 8"
1346
- :y="sectionLabelBaseY + item.row * SECTION_LABEL_ROW_HEIGHT + 8"
1347
- :font-size="item.labelStyle.fontSize"
1348
- :font-weight="item.labelStyle.fontWeight"
1349
- :fill="item.labelStyle.fill"
1350
- >
1351
- {{ item.labelText }}
1352
- </text>
1353
- <text
1354
- v-if="item.descText"
1355
- :x="item.cx - item.textWidth / 2 + 8"
1356
- :y="sectionLabelBaseY + item.row * SECTION_LABEL_ROW_HEIGHT + 22"
1357
- :font-size="item.descStyle.fontSize"
1358
- :font-weight="item.descStyle.fontWeight"
1359
- :fill="item.descStyle.fill"
1360
- :fill-opacity="item.descStyle.fillOpacity"
1361
- >
1362
- {{ item.descText }}
1363
- </text>
1364
- </g>
1365
- </svg>
1366
- <!-- Tooltip floating content -->
1367
- <div
1368
- v-if="hasTooltipSlot && hoverIndex !== null && hoverSlotProps"
1369
- ref="tooltipRef"
1370
- class="chart-tooltip-content"
1371
- :style="{
1372
- position: 'absolute',
1373
- top: '0',
1374
- left: '0',
1375
- willChange: 'transform',
1376
- transform: tooltipPos
1377
- ? `translate3d(${tooltipPos.left}px, ${tooltipPos.top}px, 0) translateY(-50%)`
1378
- : 'translateY(-50%)',
1379
- visibility: tooltipPos ? 'visible' : 'hidden',
1380
- }"
1381
- >
1382
- <slot name="tooltip" v-bind="hoverSlotProps">
1383
- <div class="line-chart-tooltip">
1384
- <div v-if="hoverSlotProps.xLabel" class="line-chart-tooltip-label">
1385
- {{ hoverSlotProps.xLabel }}
1386
- </div>
1387
- <div
1388
- v-for="v in hoverSlotProps.values"
1389
- :key="v.seriesIndex"
1390
- class="line-chart-tooltip-row"
1351
+ <!-- Tooltip: interaction overlay -->
1352
+ <rect
1353
+ v-if="hasTooltipSlot"
1354
+ :x="padding.left"
1355
+ :y="padding.top"
1356
+ :width="innerW"
1357
+ :height="innerH"
1358
+ fill="transparent"
1359
+ style="cursor: crosshair; touch-action: pan-y"
1360
+ v-on="tooltipHandlers"
1361
+ />
1362
+ <!-- annotations (top layer) -->
1363
+ <ChartAnnotations
1364
+ v-if="annotations && annotations.length > 0"
1365
+ :annotations="annotations"
1366
+ :project="projectAnnotation"
1367
+ :bounds="bounds"
1368
+ />
1369
+ <!-- area section labels -->
1370
+ <g v-for="(item, i) in sectionLabels.labels" :key="'seclab' + i">
1371
+ <circle
1372
+ :cx="item.cx - item.textWidth / 2 - 2"
1373
+ :cy="sectionLabelBaseY + item.row * SECTION_LABEL_ROW_HEIGHT + 4"
1374
+ r="4"
1375
+ :fill="item.color"
1376
+ :fill-opacity="item.fillOpacity"
1377
+ :stroke="item.color"
1378
+ stroke-width="1.5"
1379
+ />
1380
+ <text
1381
+ v-if="item.labelText"
1382
+ :x="item.cx - item.textWidth / 2 + 8"
1383
+ :y="sectionLabelBaseY + item.row * SECTION_LABEL_ROW_HEIGHT + 8"
1384
+ :font-size="item.labelStyle.fontSize"
1385
+ :font-weight="item.labelStyle.fontWeight"
1386
+ :fill="item.labelStyle.fill"
1391
1387
  >
1392
- <span
1393
- class="line-chart-tooltip-swatch"
1394
- :style="{ background: v.color }"
1395
- />
1396
- {{ isFinite(v.value) ? formatTooltipValue(v.value) : "—" }}
1388
+ {{ item.labelText }}
1389
+ </text>
1390
+ <text
1391
+ v-if="item.descText"
1392
+ :x="item.cx - item.textWidth / 2 + 8"
1393
+ :y="sectionLabelBaseY + item.row * SECTION_LABEL_ROW_HEIGHT + 22"
1394
+ :font-size="item.descStyle.fontSize"
1395
+ :font-weight="item.descStyle.fontWeight"
1396
+ :fill="item.descStyle.fill"
1397
+ :fill-opacity="item.descStyle.fillOpacity"
1398
+ >
1399
+ {{ item.descText }}
1400
+ </text>
1401
+ </g>
1402
+ </svg>
1403
+ <!-- Tooltip floating content -->
1404
+ <div
1405
+ v-if="hasTooltipSlot && hoverIndex !== null && hoverSlotProps"
1406
+ ref="tooltipRef"
1407
+ class="chart-tooltip-content"
1408
+ :style="{
1409
+ position: 'absolute',
1410
+ top: '0',
1411
+ left: '0',
1412
+ willChange: 'transform',
1413
+ transform: tooltipPos
1414
+ ? `translate3d(${tooltipPos.left}px, ${tooltipPos.top}px, 0) translateY(-50%)`
1415
+ : 'translateY(-50%)',
1416
+ visibility: tooltipPos ? 'visible' : 'hidden',
1417
+ }"
1418
+ >
1419
+ <slot name="tooltip" v-bind="hoverSlotProps">
1420
+ <div class="line-chart-tooltip">
1421
+ <div v-if="hoverSlotProps.xLabel" class="line-chart-tooltip-label">
1422
+ {{ hoverSlotProps.xLabel }}
1423
+ </div>
1424
+ <div
1425
+ v-for="v in hoverSlotProps.values"
1426
+ :key="v.seriesIndex"
1427
+ class="line-chart-tooltip-row"
1428
+ >
1429
+ <span
1430
+ class="line-chart-tooltip-swatch"
1431
+ :style="{ background: v.color }"
1432
+ />
1433
+ {{ isFinite(v.value) ? formatTooltipValue(v.value) : "—" }}
1434
+ </div>
1397
1435
  </div>
1398
- </div>
1399
- </slot>
1436
+ </slot>
1437
+ </div>
1438
+ <a
1439
+ v-if="downloadLinkText"
1440
+ class="line-chart-download-link"
1441
+ :href="csvHref!"
1442
+ :download="`${menuFilename()}.csv`"
1443
+ >
1444
+ {{ downloadLinkText }}
1445
+ </a>
1446
+ <button
1447
+ v-if="downloadButtonText"
1448
+ type="button"
1449
+ class="line-chart-download-button"
1450
+ @click="triggerCsvDownload"
1451
+ >
1452
+ {{ downloadButtonText }}
1453
+ </button>
1400
1454
  </div>
1401
- <a
1402
- v-if="downloadLinkText"
1403
- class="line-chart-download-link"
1404
- :href="csvHref!"
1405
- :download="`${menuFilename()}.csv`"
1406
- >
1407
- {{ downloadLinkText }}
1408
- </a>
1409
- <button
1410
- v-if="downloadButtonText"
1411
- type="button"
1412
- class="line-chart-download-button"
1413
- @click="triggerCsvDownload"
1414
- >
1415
- {{ downloadButtonText }}
1416
- </button>
1417
- </div>
1455
+ </Teleport>
1418
1456
  </template>
1419
1457
 
1420
1458
  <style scoped>