@datagouv/components-next 1.0.2-dev.20 → 1.0.2-dev.200

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.
Files changed (165) hide show
  1. package/README.md +7 -6
  2. package/assets/main.css +4 -0
  3. package/dist/{Control-DuZJdKV_.js → Control-B9WQxlpJ.js} +1 -1
  4. package/dist/{Datafair.client-Ds3VQrRi.js → Datafair.client-CFTmBFoY.js} +9 -9
  5. package/dist/{Event--kp8kMdJ.js → Event-iXDrp-fy.js} +31 -31
  6. package/dist/{Image-34hvypZI.js → Image-mNMBOofs.js} +9 -9
  7. package/dist/ImagePreview.client-BvDM3qjE.js +36 -0
  8. package/dist/JsonPreview.client-BP6gPRxf.js +40 -0
  9. package/dist/{Map-BjUnLyj8.js → Map-BfqKMw0S.js} +1739 -1599
  10. package/dist/{MapContainer.client-B2BijMx5.js → MapContainer.client-D9N3664r.js} +41 -42
  11. package/dist/{OSM-s40W6sQ2.js → OSM-CcrUhItr.js} +15 -23
  12. package/dist/{PdfPreview.client-DJBMzrlC.js → PdfPreview.client-C6A6ZiKl.js} +835 -856
  13. package/dist/Pmtiles.client-B_sDZoqu.js +26333 -0
  14. package/dist/PreviewWrapper.vue_vue_type_script_setup_true_lang-DGLXBys_.js +60 -0
  15. package/dist/{ScaleLine-KW-nXqp3.js → ScaleLine-VGQ7nkMS.js} +2 -2
  16. package/dist/{Tile-DbNFNPfU.js → Tile-CSpn0Ca8.js} +160 -163
  17. package/dist/{TileImage-BsXBxMtq.js → TileImage-CpbaEFwd.js} +143 -131
  18. package/dist/{View-BR92hTWP.js → View-BYBpt-AU.js} +483 -422
  19. package/dist/XmlPreview.client-DFQ1T-LN.js +34 -0
  20. package/dist/{common-PJfpC179.js → common-BoeTUnDZ.js} +52 -52
  21. package/dist/components-next.css +1 -52
  22. package/dist/components-next.js +196 -150
  23. package/dist/components.css +2 -2
  24. package/dist/{index-BFQX7Ccz.js → index-B_tPYi5Z.js} +34094 -27864
  25. package/dist/{leaflet-src-7m1mB8LI.js → leaflet-src-PeylHk0o.js} +162 -164
  26. package/dist/{main-vqhkVhVS.js → main-CVOZp_Nx.js} +41939 -73132
  27. package/dist/{proj-DsetBcW7.js → proj-DxGbobp7.js} +662 -626
  28. package/dist/{text-clamp.esm-B5kW_XMt.js → text-clamp.esm-DVseBqZZ.js} +4 -4
  29. package/dist/{tilecoord-Db24Px13.js → tilecoord-DvX7WWiA.js} +273 -264
  30. package/dist/{vue3-json-viewer-BXwup7nO.js → vue3-json-viewer-CJLQFzXZ.js} +10 -16
  31. package/dist/{vue3-xml-viewer.common-BcNJo35o.js → vue3-xml-viewer.common-CcF3Xd23.js} +1 -1
  32. package/package.json +47 -38
  33. package/src/chart.ts +5 -0
  34. package/src/components/ActivityList/ActivityList.vue +24 -14
  35. package/src/components/AppLink.vue +3 -0
  36. package/src/components/BannerAction.vue +6 -2
  37. package/src/components/Chart/ChartViewer.vue +320 -0
  38. package/src/components/Chart/ChartViewerWrapper.vue +182 -0
  39. package/src/components/CopyButton.vue +6 -2
  40. package/src/components/DataserviceCard.vue +24 -5
  41. package/src/components/DataserviceQuality.vue +82 -0
  42. package/src/components/DataserviceQualityInline.vue +31 -0
  43. package/src/components/DataserviceQualityTooltipContent.vue +79 -0
  44. package/src/components/DatasetCard.vue +24 -9
  45. package/src/components/DatasetInformation/DatasetTemporalitySection.vue +7 -4
  46. package/src/components/DatasetQuality.vue +11 -11
  47. package/src/components/DatasetQualityInline.vue +2 -2
  48. package/src/components/DatasetQualityTooltipContent.vue +1 -1
  49. package/src/components/DiscussionMessageCard.vue +14 -15
  50. package/src/components/Form/Listbox.vue +101 -0
  51. package/src/components/Form/SchemaSelect.vue +2 -0
  52. package/src/components/FormattedDate.vue +81 -0
  53. package/src/components/InfiniteLoader.vue +53 -0
  54. package/src/components/LoadingBlock.vue +4 -1
  55. package/src/components/ObjectCardHeader.vue +11 -4
  56. package/src/components/OpenApiViewer/ContentTypeSelect.vue +48 -0
  57. package/src/components/OpenApiViewer/EndpointRequest.vue +164 -0
  58. package/src/components/OpenApiViewer/EndpointResponses.vue +149 -0
  59. package/src/components/OpenApiViewer/OpenApiViewer.vue +308 -0
  60. package/src/components/OpenApiViewer/SchemaPanel.vue +53 -0
  61. package/src/components/OpenApiViewer/SchemaTree.vue +77 -0
  62. package/src/components/OpenApiViewer/openapi.ts +150 -0
  63. package/src/components/OrganizationNameWithCertificate.vue +3 -2
  64. package/src/components/Pagination.vue +25 -62
  65. package/src/components/PostCard.vue +13 -15
  66. package/src/components/RadioInput.vue +7 -2
  67. package/src/components/ResourceAccordion/DataStructure.vue +11 -33
  68. package/src/components/ResourceAccordion/Datafair.client.vue +1 -1
  69. package/src/components/ResourceAccordion/Downloads.vue +172 -0
  70. package/src/components/ResourceAccordion/ImagePreview.client.vue +48 -0
  71. package/src/components/ResourceAccordion/JsonPreview.client.vue +23 -104
  72. package/src/components/ResourceAccordion/MapContainer.client.vue +2 -11
  73. package/src/components/ResourceAccordion/Metadata.vue +4 -6
  74. package/src/components/ResourceAccordion/PdfPreview.client.vue +58 -97
  75. package/src/components/ResourceAccordion/PdfSkeleton.vue +44 -0
  76. package/src/components/ResourceAccordion/Pmtiles.client.vue +28 -13
  77. package/src/components/ResourceAccordion/Preview.vue +19 -15
  78. package/src/components/ResourceAccordion/PreviewWrapper.vue +89 -0
  79. package/src/components/ResourceAccordion/ResourceAccordion.vue +36 -117
  80. package/src/components/ResourceAccordion/XmlPreview.client.vue +16 -98
  81. package/src/components/ResourceExplorer/ResourceExplorer.vue +105 -210
  82. package/src/components/ResourceExplorer/ResourceExplorerHeader.vue +79 -0
  83. package/src/components/ResourceExplorer/ResourceExplorerSidebar.vue +104 -71
  84. package/src/components/ResourceExplorer/ResourceExplorerViewer.vue +143 -291
  85. package/src/components/ResourceExplorer/ResourceMainAction.vue +240 -0
  86. package/src/components/ResourceExplorer/ResourceSelector.vue +51 -0
  87. package/src/components/ResourceExplorer/ResourceViewerHeader.vue +122 -0
  88. package/src/components/ResourceExplorer/ResourceViewerSkeleton.vue +94 -0
  89. package/src/components/ResourceListItem.vue +168 -0
  90. package/src/components/ReuseCard.vue +28 -9
  91. package/src/components/ReuseDetails.vue +2 -2
  92. package/src/components/ReuseHorizontalCard.vue +2 -2
  93. package/src/components/SchemaCard.vue +10 -1
  94. package/src/components/Search/Filter/AccessTypeFilter.vue +2 -2
  95. package/src/components/Search/GlobalSearch.vue +229 -123
  96. package/src/components/Search/SearchInput.vue +5 -4
  97. package/src/components/Search/Sidemenu.vue +4 -3
  98. package/src/components/SmallChart.vue +11 -10
  99. package/src/components/StatBox.vue +13 -5
  100. package/src/components/TabularExplorer/TabularActiveFilters.vue +79 -0
  101. package/src/components/TabularExplorer/TabularCell.vue +50 -0
  102. package/src/components/TabularExplorer/TabularCellPopover.vue +170 -0
  103. package/src/components/TabularExplorer/TabularColumnsMenu.vue +112 -0
  104. package/src/components/TabularExplorer/TabularExplorer.vue +299 -0
  105. package/src/components/TabularExplorer/TabularFilterContent.vue +362 -0
  106. package/src/components/TabularExplorer/TabularFilterPopover.vue +106 -0
  107. package/src/components/TabularExplorer/TabularMobileFilterButton.vue +21 -0
  108. package/src/components/TabularExplorer/TabularMobileFilters.vue +150 -0
  109. package/src/components/TabularExplorer/TabularRowsInfo.vue +31 -0
  110. package/src/components/TabularExplorer/TabularSkeleton.vue +92 -0
  111. package/src/components/TabularExplorer/TabularTable.vue +481 -0
  112. package/src/components/TabularExplorer/TabularToolbar.vue +25 -0
  113. package/src/components/TabularExplorer/types.ts +84 -0
  114. package/src/components/TabularExplorer/useColumnMetadata.ts +98 -0
  115. package/src/components/TabularExplorer/useTabularContext.ts +82 -0
  116. package/src/components/TopicCard.vue +13 -14
  117. package/src/composables/useActiveDescendant.ts +5 -0
  118. package/src/composables/useDatasetResources.ts +164 -0
  119. package/src/composables/useHasTabularData.ts +15 -1
  120. package/src/composables/useMetrics.ts +1 -1
  121. package/src/composables/useResizable.ts +46 -0
  122. package/src/composables/useResourceCapabilities.ts +48 -40
  123. package/src/composables/useSearchFilter.ts +120 -0
  124. package/src/composables/useStableQueryParams.ts +38 -6
  125. package/src/composables/useTabularProfile.ts +70 -0
  126. package/src/composables/useTranslation.ts +24 -9
  127. package/src/config.ts +21 -3
  128. package/src/functions/activities.ts +3 -3
  129. package/src/functions/api.ts +20 -42
  130. package/src/functions/api.types.ts +1 -0
  131. package/src/functions/charts.ts +99 -0
  132. package/src/functions/datasets.ts +0 -17
  133. package/src/functions/dates.ts +6 -7
  134. package/src/functions/helpers.ts +15 -0
  135. package/src/functions/metrics.ts +63 -60
  136. package/src/functions/paginate.ts +46 -0
  137. package/src/functions/resourceCapabilities.ts +34 -0
  138. package/src/functions/resources.ts +136 -1
  139. package/src/functions/schemas.ts +27 -0
  140. package/src/functions/scroll.ts +20 -0
  141. package/src/functions/tabular.ts +214 -0
  142. package/src/functions/tabularApi.ts +96 -11
  143. package/src/main.ts +119 -14
  144. package/src/types/dataservices.ts +17 -0
  145. package/src/types/organizations.ts +10 -0
  146. package/src/types/pages.ts +0 -5
  147. package/src/types/posts.ts +2 -2
  148. package/src/types/reports.ts +5 -1
  149. package/src/types/resources.ts +0 -1
  150. package/src/types/reuses.ts +12 -1
  151. package/src/types/search.ts +63 -1
  152. package/src/types/site.ts +5 -3
  153. package/src/types/ui.ts +2 -0
  154. package/src/types/users.ts +2 -1
  155. package/src/types/visualizations.ts +93 -0
  156. package/assets/swagger-themes/newspaper.css +0 -1670
  157. package/dist/JsonPreview.client-CYxmz0yV.js +0 -72
  158. package/dist/Pmtiles.client-C_hvRDuy.js +0 -25185
  159. package/dist/Swagger.client-B4y0hXME.js +0 -4
  160. package/dist/XmlPreview.client-LETEfNBH.js +0 -64
  161. package/src/components/ResourceAccordion/Swagger.client.vue +0 -48
  162. package/src/functions/pagination.ts +0 -9
  163. /package/src/components/{DatasetQualityItem.vue → QualityItem.vue} +0 -0
  164. /package/src/components/{DatasetQualityItemWarning.vue → QualityItemWarning.vue} +0 -0
  165. /package/src/components/{DatasetQualityScore.vue → QualityScore.vue} +0 -0
@@ -0,0 +1,320 @@
1
+ <template>
2
+ <div
3
+ ref="chartContainer"
4
+ class="w-full min-h-96"
5
+ />
6
+ </template>
7
+
8
+ <script setup lang="ts">
9
+ import { format, use, type ComposeOption } from 'echarts/core'
10
+ import { CanvasRenderer } from 'echarts/renderers'
11
+ import { LineChart, BarChart, type BarSeriesOption, type LineSeriesOption } from 'echarts/charts'
12
+ import { TitleComponent, TooltipComponent, LegendComponent, GridComponent, DatasetComponent } from 'echarts/components'
13
+ import { init, type ECharts as EChartsType } from 'echarts'
14
+ import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
15
+ import { summarize } from '../../functions/helpers'
16
+ import { useFormatDate } from '../../functions/dates'
17
+ import type { Chart, ColumnDefinition, ColumnsDefinition, XAxis, YAxis, XAxisForm, ChartForApi } from '../../types/visualizations'
18
+ import { useTranslation } from '../../composables/useTranslation'
19
+
20
+ use([CanvasRenderer, LineChart, BarChart, TitleComponent, TooltipComponent, LegendComponent, GridComponent, DatasetComponent])
21
+
22
+ const props = defineProps<{
23
+ chart: Chart | ChartForApi
24
+ series: {
25
+ data: Record<string, Array<Record<string, unknown>>>
26
+ columns: ColumnsDefinition
27
+ }
28
+ }>()
29
+
30
+ const { locale } = useTranslation()
31
+ const { formatDate } = useFormatDate()
32
+ const chartContainer = ref<HTMLElement | null>(null)
33
+ let echartsInstance: EChartsType | null = null
34
+
35
+ function capture(): string | null {
36
+ return echartsInstance?.getDataURL() ?? null
37
+ }
38
+
39
+ defineExpose({ capture })
40
+
41
+ // ECharts has a dedicated 'time' axis type for continuous date values.
42
+ function mapXAxisType(xAxis: XAxis | XAxisForm): 'category' | 'value' | 'time' {
43
+ if (!xAxis) return 'category'
44
+ if (xAxis.type !== 'continuous') return 'category'
45
+ return isDateAxis.value ? 'time' : 'value'
46
+ }
47
+
48
+ function buildYAxisFormatter(yAxis: YAxis): ((value: number) => string) | undefined {
49
+ return (value: number) => {
50
+ const v = summarize(value)
51
+ if (!yAxis.unit) return v
52
+ if (yAxis.unit_position === 'prefix') return `${yAxis.unit} ${v}`
53
+ return `${v} ${yAxis.unit}`
54
+ }
55
+ }
56
+
57
+ function buildXAxisFormatter(column: ColumnDefinition | null, options?: Intl.DateTimeFormatOptions): ((value: string | number | Date) => string) | undefined {
58
+ if (column?.type !== 'date') return undefined
59
+ return (value: string | number | Date) => {
60
+ if (value === null || value === undefined) return ''
61
+ const date = value instanceof Date ? value : new Date(value)
62
+ if (Number.isNaN(date.getTime())) return String(value)
63
+ return formatDate(date, options ?? { dateStyle: 'short' })
64
+ }
65
+ }
66
+
67
+ function parseXValueAsDate(row: Record<string, unknown>, xColumn: string): Record<string, unknown> {
68
+ const value = row[xColumn]
69
+ if (value === null || value === undefined || value instanceof Date) return row
70
+ if (typeof value === 'string') {
71
+ const date = new Date(value)
72
+ if (!Number.isNaN(date.getTime())) {
73
+ return { ...row, [xColumn]: date }
74
+ }
75
+ }
76
+ return row
77
+ }
78
+
79
+ const xAxisColumn = computed(() => {
80
+ const columnName = props.chart.x_axis.column_x
81
+ if (!columnName) return null
82
+ for (const columns of Object.values(props.series.columns)) {
83
+ const column = columns.find(c => c.name === columnName)
84
+ if (column) return column
85
+ }
86
+ return null
87
+ })
88
+
89
+ const isDateAxis = computed(() => xAxisColumn.value?.type === 'date')
90
+
91
+ const xAxisBounds = computed(() => {
92
+ const columnName = props.chart.x_axis.column_x
93
+ if (!columnName) return { min: undefined, max: undefined }
94
+
95
+ let min: number | undefined
96
+ let max: number | undefined
97
+
98
+ for (const s of props.chart.series) {
99
+ const xColumn = s.column_x_name_override ?? columnName
100
+ const data = props.series.data[s.resource_id]
101
+ if (!data) continue
102
+
103
+ for (const row of data) {
104
+ const value = row[xColumn]
105
+ if (value === null || value === undefined) continue
106
+
107
+ let numericValue: number | undefined
108
+ if (value instanceof Date) {
109
+ numericValue = value.getTime()
110
+ }
111
+ else if (isDateAxis.value && typeof value === 'string') {
112
+ numericValue = new Date(value).getTime()
113
+ }
114
+ else {
115
+ numericValue = Number(value)
116
+ }
117
+
118
+ if (numericValue === undefined || Number.isNaN(numericValue)) continue
119
+
120
+ if (min === undefined || numericValue < min) min = numericValue
121
+ if (max === undefined || numericValue > max) max = numericValue
122
+ }
123
+ }
124
+
125
+ return { min, max }
126
+ })
127
+
128
+ const echartsOption = computed(() => {
129
+ const seriesCount = props.chart.series.length
130
+ if (!props.chart.series || seriesCount === 0) return
131
+
132
+ const seriesArray = props.chart.series.map((s) => {
133
+ const xColumn = s.column_x_name_override ?? props.chart.x_axis.column_x
134
+ const yColumn = s.aggregate_y ? `${s.column_y}__${s.aggregate_y}` : s.column_y
135
+ const resourceId = s.resource_id
136
+
137
+ const resourceColumns = props.series.columns[resourceId]
138
+ if (!xColumn || !yColumn || !resourceId || !s.type || !props.series.data[resourceId] || !resourceColumns) {
139
+ return null
140
+ }
141
+
142
+ const rawData = props.series.data[resourceId]
143
+ const sortedData = isDateAxis.value
144
+ ? rawData.map(row => parseXValueAsDate(row, xColumn))
145
+ : [...rawData]
146
+ const sortBy = props.chart.x_axis.sort_x_by ?? (isDateAxis.value ? 'axis_x' : null)
147
+ const sortDirection = props.chart.x_axis.sort_x_direction ?? 'asc'
148
+
149
+ if (sortBy && sortDirection && props.chart.x_axis.column_x) {
150
+ const sortKey = sortBy === 'axis_y' ? yColumn : xColumn
151
+ sortedData.sort((a, b) => {
152
+ const valA = a[sortKey]
153
+ const valB = b[sortKey]
154
+
155
+ const aNullish = valA === null || valA === undefined
156
+ const bNullish = valB === null || valB === undefined
157
+ if (aNullish && bNullish) return 0
158
+ if (aNullish) return sortDirection === 'asc' ? -1 : 1
159
+ if (bNullish) return sortDirection === 'asc' ? 1 : -1
160
+
161
+ if (valA instanceof Date && valB instanceof Date) {
162
+ const timeA = valA.getTime()
163
+ const timeB = valB.getTime()
164
+ if (timeA < timeB) return sortDirection === 'asc' ? -1 : 1
165
+ if (timeA > timeB) return sortDirection === 'asc' ? 1 : -1
166
+ return 0
167
+ }
168
+
169
+ if (typeof valA === 'boolean' && typeof valB === 'boolean') {
170
+ if (valA === valB) return 0
171
+ // false comes before true in ascending order
172
+ if (valB) return sortDirection === 'asc' ? -1 : 1
173
+ return sortDirection === 'asc' ? 1 : -1
174
+ }
175
+
176
+ const numA = Number(valA)
177
+ const numB = Number(valB)
178
+ const bothNumeric = !isNaN(numA) && !isNaN(numB)
179
+
180
+ if (bothNumeric) {
181
+ if (numA < numB) return sortDirection === 'asc' ? -1 : 1
182
+ if (numA > numB) return sortDirection === 'asc' ? 1 : -1
183
+ return 0
184
+ }
185
+
186
+ const strA = String(valA)
187
+ const strB = String(valB)
188
+ return strA.localeCompare(strB, locale, {
189
+ sensitivity: 'base',
190
+ numeric: true,
191
+ }) * (sortDirection === 'asc' ? 1 : -1)
192
+ })
193
+ }
194
+
195
+ return {
196
+ series: {
197
+ type: s.type === 'histogram' ? 'bar' : 'line',
198
+ dimensions: s.aggregate_y ? [xColumn, yColumn] : resourceColumns.map(c => c.name),
199
+ name: yColumn,
200
+ encode: {
201
+ x: xColumn,
202
+ y: yColumn,
203
+ },
204
+ } as LineSeriesOption | BarSeriesOption,
205
+ data: {
206
+ source: sortedData,
207
+ dimensions: s.aggregate_y ? [xColumn, yColumn] : resourceColumns.map(c => c.name),
208
+ },
209
+ }
210
+ })
211
+
212
+ const seriesData = {
213
+ series: [] as Array<LineSeriesOption | BarSeriesOption>,
214
+ data: [] as Array<Record<string, unknown>>,
215
+ }
216
+
217
+ for (const curr of seriesArray) {
218
+ if (!curr) continue
219
+ seriesData.series.push(curr.series)
220
+ seriesData.data.push(curr.data)
221
+ }
222
+
223
+ const xAxisType = mapXAxisType(props.chart.x_axis)
224
+
225
+ return {
226
+ dataset: [...seriesData.data],
227
+ textStyle: {
228
+ fontFamily: 'Marianne, arial, sans-serif',
229
+ },
230
+ color: '#000091',
231
+ tooltip: {
232
+ trigger: 'axis' as const,
233
+ formatter: (params: Array<{ value: Record<string, unknown>, axisValueLabel: string, seriesName: string }>) => {
234
+ let tooltip = ''
235
+ const valueFormatter = new Intl.NumberFormat(locale)
236
+ const xAxisFormatter = buildXAxisFormatter(xAxisColumn.value, { dateStyle: 'medium' })
237
+ for (const param of params) {
238
+ const seriesName = param.seriesName
239
+ const axisLabel = xAxisFormatter ? xAxisFormatter(param.axisValueLabel) : param.axisValueLabel
240
+ tooltip += `${format.encodeHTML(String(axisLabel))}: <strong>${valueFormatter.format(Number(param.value[seriesName]))}</strong><br>`
241
+ }
242
+ return tooltip
243
+ },
244
+ },
245
+ legend: {
246
+ show: false,
247
+ bottom: 0,
248
+ },
249
+ grid: {
250
+ top: 60,
251
+ bottom: 40,
252
+ left: 20,
253
+ right: 20,
254
+ containLabel: true,
255
+ },
256
+ xAxis: {
257
+ type: xAxisType,
258
+ name: (props.chart.x_axis as XAxis).column_x,
259
+ min: xAxisType !== 'category' ? xAxisColumn.value?.min ?? xAxisBounds.value.min ?? undefined : undefined,
260
+ max: xAxisType !== 'category' ? xAxisColumn.value?.max ?? xAxisBounds.value.max ?? undefined : undefined,
261
+ axisLabel: xAxisType === 'time'
262
+ ? { formatter: buildXAxisFormatter(xAxisColumn.value) }
263
+ : undefined,
264
+ },
265
+ yAxis: {
266
+ type: 'value' as const,
267
+ name: props.chart.y_axis.label ?? undefined,
268
+ min: props.chart.y_axis.min ?? undefined,
269
+ max: props.chart.y_axis.max ?? undefined,
270
+ axisLabel: {
271
+ formatter: buildYAxisFormatter(props.chart.y_axis),
272
+ },
273
+ },
274
+ series: seriesData.series,
275
+ } satisfies ComposeOption<
276
+ | BarSeriesOption
277
+ | LineSeriesOption
278
+ >
279
+ })
280
+
281
+ onMounted(() => {
282
+ if (chartContainer.value) {
283
+ echartsInstance = init(chartContainer.value)
284
+ updateChart()
285
+ }
286
+ })
287
+
288
+ onUnmounted(() => {
289
+ if (echartsInstance) {
290
+ echartsInstance.dispose()
291
+ echartsInstance = null
292
+ }
293
+ })
294
+
295
+ watch(() => props.chart, updateChart, { deep: true })
296
+
297
+ watch(() => props.series, updateChart, { deep: true })
298
+
299
+ function updateChart() {
300
+ if (!echartsInstance) return
301
+ const option = echartsOption.value
302
+ if (option) {
303
+ echartsInstance.setOption(option, { notMerge: true })
304
+ }
305
+ }
306
+
307
+ const handleResize = () => {
308
+ if (echartsInstance) {
309
+ echartsInstance.resize()
310
+ }
311
+ }
312
+
313
+ onMounted(() => {
314
+ window.addEventListener('resize', handleResize)
315
+ })
316
+
317
+ onUnmounted(() => {
318
+ window.removeEventListener('resize', handleResize)
319
+ })
320
+ </script>
@@ -0,0 +1,182 @@
1
+ <template>
2
+ <LoadingBlock
3
+ :status="status"
4
+ :data="series"
5
+ >
6
+ <template #default="{ data }">
7
+ <ChartViewer
8
+ ref="chartViewerRef"
9
+ :chart="chart"
10
+ :series="data"
11
+ />
12
+ </template>
13
+ <template #error>
14
+ <div class="text-center py-8 text-gray-500">
15
+ {{ t('Une erreur est survenue lors du chargement des données du graphique.') }}
16
+ </div>
17
+ </template>
18
+ </LoadingBlock>
19
+ </template>
20
+
21
+ <script setup lang="ts">
22
+ import { computed, reactive, ref, watch } from 'vue'
23
+ import ChartViewer from './ChartViewer.vue'
24
+ import LoadingBlock from '../../components/LoadingBlock.vue'
25
+ import { useComponentsConfig } from '../../config'
26
+ import { fetchTabularData, useGetProfile } from '../../functions/tabularApi'
27
+ import type { Chart, ChartForApi, ColumnsDefinition } from '../../types/visualizations'
28
+ import { useTranslation } from '../../composables/useTranslation'
29
+ import { watchDebounced } from '@vueuse/shared'
30
+ import { buildColumnsFromProfile } from '../../functions/charts'
31
+
32
+ const props = defineProps<{
33
+ chart: Chart | ChartForApi
34
+ }>()
35
+
36
+ const { t } = useTranslation()
37
+ const chartViewerRef = ref<InstanceType<typeof ChartViewer> | null>(null)
38
+
39
+ function capture(): string | null {
40
+ return chartViewerRef.value?.capture() ?? null
41
+ }
42
+
43
+ defineExpose({ capture })
44
+ const config = useComponentsConfig()
45
+ const getProfile = useGetProfile()
46
+
47
+ const status = ref<'idle' | 'pending' | 'success' | 'error'>('idle')
48
+ const error = ref<Error | null>(null)
49
+
50
+ const pendingOperations = ref(0)
51
+
52
+ const series = reactive<{
53
+ data: Record<string, Array<Record<string, unknown>>>
54
+ columns: ColumnsDefinition
55
+ }>({
56
+ data: {},
57
+ columns: {},
58
+ })
59
+
60
+ const resourceIds = computed(() => {
61
+ return props.chart.series
62
+ .filter(s => s.resource_id)
63
+ .map(s => s.resource_id)
64
+ })
65
+
66
+ async function fetchSeriesProfile() {
67
+ pendingOperations.value++
68
+ status.value = 'pending'
69
+ error.value = null
70
+
71
+ try {
72
+ if (props.chart.series.length === 0) {
73
+ status.value = 'success'
74
+ series.data = {}
75
+ series.columns = {}
76
+ return
77
+ }
78
+
79
+ const fetchPromises = props.chart.series
80
+ .filter(serie => serie.resource_id)
81
+ .map(async (serie) => {
82
+ return {
83
+ id: serie.resource_id,
84
+ profile: await getProfile(serie.resource_id),
85
+ }
86
+ })
87
+
88
+ const results = (await Promise.allSettled(fetchPromises))
89
+ .filter(r => r.status === 'fulfilled')
90
+ .map(r => r.value)
91
+ series.columns = Object.fromEntries(results.map(result => [
92
+ result.id,
93
+ buildColumnsFromProfile(result.profile),
94
+ ]))
95
+ }
96
+ catch (err) {
97
+ error.value = err instanceof Error ? err : new Error('Failed to fetch series profile')
98
+ status.value = 'error'
99
+ console.error(err)
100
+ series.columns = {}
101
+ series.data = {}
102
+ }
103
+ finally {
104
+ pendingOperations.value--
105
+ }
106
+ }
107
+
108
+ async function fetchSeriesData() {
109
+ pendingOperations.value++
110
+ status.value = 'pending'
111
+ error.value = null
112
+
113
+ try {
114
+ if (props.chart.series.length === 0 || !props.chart.x_axis.column_x) {
115
+ status.value = 'success'
116
+ series.data = {}
117
+ return
118
+ }
119
+
120
+ const fetchPromises = props.chart.series
121
+ .filter((serie) => {
122
+ const xColumn = serie.column_x_name_override ?? props.chart.x_axis.column_x
123
+ return xColumn && serie.resource_id && serie.column_y
124
+ })
125
+ .map(async (serie) => {
126
+ const xColumn = serie.column_x_name_override ?? props.chart.x_axis.column_x
127
+ return {
128
+ id: serie.resource_id,
129
+ data: await fetchTabularData(config, {
130
+ columns: serie.aggregate_y ? undefined : [xColumn, serie.column_y],
131
+ resourceId: serie.resource_id,
132
+ page: 1,
133
+ pageSize: 100,
134
+ groupBy: xColumn,
135
+ aggregation: serie.column_y && serie.aggregate_y
136
+ ? {
137
+ column: serie.column_y,
138
+ type: serie.aggregate_y,
139
+ }
140
+ : undefined,
141
+ filters: serie.filters ?? undefined,
142
+ }),
143
+ }
144
+ })
145
+
146
+ const results = (await Promise.allSettled(fetchPromises))
147
+ .filter(r => r.status === 'fulfilled')
148
+ .map(r => r.value)
149
+ series.data = Object.fromEntries(results.map(result => [
150
+ result.id,
151
+ result.data.data,
152
+ ]))
153
+ }
154
+ catch (err) {
155
+ error.value = err instanceof Error ? err : new Error('Failed to fetch series data')
156
+ status.value = 'error'
157
+ series.data = {}
158
+ }
159
+ finally {
160
+ pendingOperations.value--
161
+ }
162
+ }
163
+
164
+ watch(resourceIds, async () => {
165
+ await fetchSeriesProfile()
166
+ }, { immediate: true })
167
+
168
+ watchDebounced([
169
+ () => props.chart.series,
170
+ () => props.chart.x_axis.column_x,
171
+ ], async () => {
172
+ await fetchSeriesData()
173
+ }, { debounce: config.searchDebounce ?? 300, immediate: true, deep: true })
174
+
175
+ watch(pendingOperations, (count) => {
176
+ if (count === 0) {
177
+ if (error.value === null && status.value === 'pending') {
178
+ status.value = 'success'
179
+ }
180
+ }
181
+ })
182
+ </script>
@@ -17,7 +17,7 @@
17
17
  />
18
18
  <span
19
19
  class="copy-label"
20
- :class="{ 'sr-only': hideLabel }"
20
+ :class="{ 'sr-only': hideLabel || iconOnly }"
21
21
  >{{ copiedLabel }}</span>
22
22
  </span>
23
23
  <span
@@ -32,7 +32,7 @@
32
32
  />
33
33
  <span
34
34
  class="copy-link copy-label"
35
- :class="{ 'sr-only': hideLabel }"
35
+ :class="{ 'sr-only': hideLabel || iconOnly }"
36
36
  >{{ label }}</span>
37
37
  </span>
38
38
  </button>
@@ -49,9 +49,13 @@ const props = withDefaults(defineProps<{
49
49
  label: string
50
50
  copiedLabel: string
51
51
  hideLabel?: boolean
52
+ // Like hideLabel but keeps the default icon and has no bordered box — just the
53
+ // plain copy icon with an sr-only label.
54
+ iconOnly?: boolean
52
55
  reverse?: boolean
53
56
  }>(), {
54
57
  hideLabel: false,
58
+ iconOnly: false,
55
59
  reverse: false,
56
60
  })
57
61
 
@@ -53,6 +53,7 @@
53
53
  <ObjectCardHeader
54
54
  :icon="isTabularApi ? RiSparklingLine : RiTerminalLine"
55
55
  :url="dataserviceUrl || dataservice.self_web_url"
56
+ :title-tag="titleTag"
56
57
  >
57
58
  {{ dataservice.title }}
58
59
  </ObjectCardHeader>
@@ -70,9 +71,19 @@
70
71
  class="size-4 flex-none fill-gray-medium"
71
72
  />
72
73
  <!-- https://github.com/datagouv/cdata/issues/653 -->
73
- <p class="text-sm whitespace-nowrap mb-0 text-gray-medium">
74
- {{ t('Mis à jour {date}', { date: formatRelativeIfRecentDate(dataservice.metadata_modified_at, { dateStyle: 'medium' }) }) }}
75
- </p>
74
+ <TranslationT
75
+ tag="p"
76
+ class="text-sm whitespace-nowrap mb-0 text-gray-medium"
77
+ keypath="Mis à jour {date}"
78
+ >
79
+ <template #date>
80
+ <FormattedDate
81
+ :date="dataservice.metadata_modified_at"
82
+ format="relative"
83
+ :options="{ dateStyle: 'medium' }"
84
+ />
85
+ </template>
86
+ </TranslationT>
76
87
  <RiSubtractLine
77
88
  aria-hidden="true"
78
89
  class="size-4 flex-none fill-gray-medium"
@@ -98,6 +109,11 @@
98
109
  </p>
99
110
  </div>
100
111
  </div>
112
+ <DataserviceQualityInline
113
+ v-if="dataservice.quality"
114
+ :quality="dataservice.quality"
115
+ />
116
+
101
117
  <ObjectCardShortDescription
102
118
  v-if="showDescription"
103
119
  :text="dataservice.description"
@@ -112,9 +128,9 @@ import { computed } from 'vue'
112
128
  import type { RouteLocationRaw } from 'vue-router'
113
129
  import { RiArchiveLine, RiEyeLine, RiLockLine, RiPassValidLine, RiSparklingLine, RiStarLine, RiSubtractLine, RiTerminalLine } from '@remixicon/vue'
114
130
  import { useComponentsConfig } from '../config'
115
- import { useFormatDate } from '../functions/dates'
116
131
  import { summarize } from '../functions/helpers'
117
132
  import type { Dataservice } from '../types/dataservices'
133
+ import type { TitleTag } from '../types/ui'
118
134
  import { useTranslation } from '../composables/useTranslation'
119
135
  import OrganizationLogo from './OrganizationLogo.vue'
120
136
  import Avatar from './Avatar.vue'
@@ -124,12 +140,16 @@ import ObjectCardBadge from './ObjectCardBadge.vue'
124
140
  import ObjectCardHeader from './ObjectCardHeader.vue'
125
141
  import ObjectCardOwner from './ObjectCardOwner.vue'
126
142
  import ObjectCardShortDescription from './ObjectCardShortDescription.vue'
143
+ import DataserviceQualityInline from './DataserviceQualityInline.vue'
144
+ import FormattedDate from './FormattedDate.vue'
145
+ import TranslationT from './TranslationT.vue'
127
146
 
128
147
  type Props = {
129
148
  dataservice: Dataservice
130
149
  dataserviceUrl?: RouteLocationRaw
131
150
  organizationUrl?: RouteLocationRaw
132
151
  showDescription?: boolean
152
+ titleTag?: TitleTag
133
153
  }
134
154
 
135
155
  const props = withDefaults(defineProps<Props>(), {
@@ -137,7 +157,6 @@ const props = withDefaults(defineProps<Props>(), {
137
157
  })
138
158
 
139
159
  const { t } = useTranslation()
140
- const { formatRelativeIfRecentDate } = useFormatDate()
141
160
 
142
161
  const config = useComponentsConfig()
143
162
  const isTabularApi = computed(() => {
@@ -0,0 +1,82 @@
1
+ <template>
2
+ <Toggletip
3
+ :styled-button="false"
4
+ button-class="flex items-center gap-1 rounded-sm p-1.5 -m-1.5 border-transparent -outline-offset-2 hover:bg-gray-some transition-colors w-full text-left"
5
+ :button-props="{ title: t('Qualité des métadonnées') }"
6
+ >
7
+ <QualityScore
8
+ :score="quality.score"
9
+ class="w-full"
10
+ />
11
+ <RiInformationLine
12
+ class="size-5 shrink-0"
13
+ aria-hidden="true"
14
+ />
15
+ <template #toggletip>
16
+ <DataserviceQualityTooltipContent :quality />
17
+ </template>
18
+ </Toggletip>
19
+ <template v-if="!hideWarnings">
20
+ <ul class="list-none pl-0">
21
+ <QualityItemWarning
22
+ :quality-item="quality.has_description"
23
+ :message="t('Description non renseignée')"
24
+ />
25
+ <QualityItemWarning
26
+ :quality-item="quality.has_machine_documentation"
27
+ :message="t('Documentation machine (OpenAPI) manquante')"
28
+ />
29
+ <QualityItemWarning
30
+ :quality-item="quality.has_technical_documentation"
31
+ :message="t('Documentation technique manquante')"
32
+ />
33
+ <QualityItemWarning
34
+ :quality-item="quality.has_business_documentation"
35
+ :message="t('Documentation métier manquante')"
36
+ />
37
+ <QualityItemWarning
38
+ :quality-item="quality.license"
39
+ :message="t('Licence non renseignée')"
40
+ />
41
+ <QualityItemWarning
42
+ :quality-item="quality.has_contact_point"
43
+ :message="t('Point de contact non renseigné')"
44
+ />
45
+ <QualityItemWarning
46
+ :quality-item="quality.has_base_api_url"
47
+ :message="t(`URL de base de l'API non renseignée`)"
48
+ />
49
+ <QualityItemWarning
50
+ :quality-item="quality.availability_documented"
51
+ :message="t('Disponibilité non documentée')"
52
+ />
53
+ <QualityItemWarning
54
+ :quality-item="quality.rate_limiting_documented"
55
+ :message="t(`Limite d'appels non documentée`)"
56
+ />
57
+ <QualityItemWarning
58
+ :quality-item="quality.access_conditions_clear"
59
+ :message="t(`Conditions d'accès non précisées`)"
60
+ />
61
+ </ul>
62
+ </template>
63
+ </template>
64
+
65
+ <script setup lang="ts">
66
+ import { RiInformationLine } from '@remixicon/vue'
67
+ import type { DataserviceQuality } from '../types/dataservices'
68
+ import QualityScore from './QualityScore.vue'
69
+ import QualityItemWarning from './QualityItemWarning.vue'
70
+ import DataserviceQualityTooltipContent from './DataserviceQualityTooltipContent.vue'
71
+ import Toggletip from './Toggletip.vue'
72
+ import { useTranslation } from '../composables/useTranslation'
73
+
74
+ withDefaults(defineProps<{
75
+ quality: DataserviceQuality
76
+ hideWarnings?: boolean
77
+ }>(), {
78
+ hideWarnings: false,
79
+ })
80
+
81
+ const { t } = useTranslation()
82
+ </script>