@cfasim-ui/docs 0.5.0 → 0.6.0

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