@datagouv/components-next 1.0.2-dev.154 → 1.0.2-dev.156

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 (23) hide show
  1. package/dist/{Datafair.client-CUK3pS6u.js → Datafair.client-bp15NLRY.js} +1 -1
  2. package/dist/{JsonPreview.client-DXCVNsnV.js → JsonPreview.client-yqVpZTmF.js} +2 -2
  3. package/dist/{MapContainer.client-BTwgsANA.js → MapContainer.client-2lywLnVm.js} +2 -2
  4. package/dist/{PdfPreview.client-B-W-HknR.js → PdfPreview.client-BG7MXWMB.js} +2 -2
  5. package/dist/{Pmtiles.client-CCNaGHFD.js → Pmtiles.client-5ghpJ7mq.js} +5647 -5500
  6. package/dist/{PreviewWrapper.vue_vue_type_script_setup_true_lang-Cu7pYpdZ.js → PreviewWrapper.vue_vue_type_script_setup_true_lang-C-sK1BJV.js} +1 -1
  7. package/dist/{XmlPreview.client-CalqyfB_.js → XmlPreview.client-DWzKxXoL.js} +3 -3
  8. package/dist/components-next.js +98 -99
  9. package/dist/components.css +1 -1
  10. package/dist/{index-CeKxF_3e.js → index-B6iEXWF6.js} +148 -137
  11. package/dist/{leaflet-src-zZByibj1.js → leaflet-src-CpoKI-p2.js} +1 -1
  12. package/dist/{main-B4HZYPjy.js → main-uIKdSuDJ.js} +12426 -12478
  13. package/dist/{vue3-xml-viewer.common-omEYjVTS.js → vue3-xml-viewer.common-C2hQ2aXh.js} +1 -1
  14. package/package.json +2 -2
  15. package/src/components/Chart/ChartViewer.vue +97 -10
  16. package/src/components/TabularExplorer/TabularCell.vue +0 -9
  17. package/src/components/TabularExplorer/TabularCellPopover.vue +1 -1
  18. package/src/components/TabularExplorer/TabularExplorer.vue +4 -80
  19. package/src/components/TabularExplorer/TabularFilterContent.vue +3 -3
  20. package/src/components/TabularExplorer/types.ts +5 -5
  21. package/src/functions/charts.ts +18 -2
  22. package/src/functions/tabular.ts +1 -26
  23. package/src/types/visualizations.ts +1 -1
@@ -1,4 +1,4 @@
1
- import { g as Ke } from "./main-B4HZYPjy.js";
1
+ import { g as Ke } from "./main-uIKdSuDJ.js";
2
2
  import We from "vue";
3
3
  function Fe($, K) {
4
4
  for (var V = 0; V < K.length; V++) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@datagouv/components-next",
3
- "version": "1.0.2-dev.154",
3
+ "version": "1.0.2-dev.156",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "engines": {
@@ -31,7 +31,7 @@
31
31
  "@vueuse/core": "^14.3.0",
32
32
  "@vueuse/router": "^14.3.0",
33
33
  "chart.js": "^4.5.1",
34
- "dompurify": "^3.4.7",
34
+ "dompurify": "^3.4.11",
35
35
  "echarts": "^6.1.0",
36
36
  "geopf-extensions-openlayers": "1.0.0-beta.11",
37
37
  "leaflet": "^1.9.4",
@@ -13,7 +13,8 @@ import { TitleComponent, TooltipComponent, LegendComponent, GridComponent, Datas
13
13
  import { init, type ECharts as EChartsType } from 'echarts'
14
14
  import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
15
15
  import { summarize } from '../../functions/helpers'
16
- import type { Chart, XAxis, YAxis, XAxisForm, ChartForApi } from '../../types/visualizations'
16
+ import { useFormatDate } from '../../functions/dates'
17
+ import type { Chart, ColumnDefinition, ColumnsDefinition, XAxis, YAxis, XAxisForm, ChartForApi } from '../../types/visualizations'
17
18
  import { useTranslation } from '../../composables/useTranslation'
18
19
 
19
20
  use([CanvasRenderer, LineChart, BarChart, TitleComponent, TooltipComponent, LegendComponent, GridComponent, DatasetComponent])
@@ -22,11 +23,12 @@ const props = defineProps<{
22
23
  chart: Chart | ChartForApi
23
24
  series: {
24
25
  data: Record<string, Array<Record<string, unknown>>>
25
- columns: Record<string, Array<{ name: string, type: string }>>
26
+ columns: ColumnsDefinition
26
27
  }
27
28
  }>()
28
29
 
29
30
  const { locale } = useTranslation()
31
+ const { formatDate } = useFormatDate()
30
32
  const chartContainer = ref<HTMLElement | null>(null)
31
33
  let echartsInstance: EChartsType | null = null
32
34
 
@@ -36,9 +38,11 @@ function capture(): string | null {
36
38
 
37
39
  defineExpose({ capture })
38
40
 
39
- function mapXAxisType(xAxis: XAxis | XAxisForm): 'category' | 'value' {
41
+ // ECharts has a dedicated 'time' axis type for continuous date values.
42
+ function mapXAxisType(xAxis: XAxis | XAxisForm): 'category' | 'value' | 'time' {
40
43
  if (!xAxis) return 'category'
41
- return xAxis.type === 'continuous' ? 'value' : 'category'
44
+ if (xAxis.type !== 'continuous') return 'category'
45
+ return isDateAxis.value ? 'time' : 'value'
42
46
  }
43
47
 
44
48
  function buildYAxisFormatter(yAxis: YAxis): ((value: number) => string) | undefined {
@@ -50,6 +54,77 @@ function buildYAxisFormatter(yAxis: YAxis): ((value: number) => string) | undefi
50
54
  }
51
55
  }
52
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
+
53
128
  const echartsOption = computed(() => {
54
129
  const seriesCount = props.chart.series.length
55
130
  if (!props.chart.series || seriesCount === 0) return
@@ -64,12 +139,15 @@ const echartsOption = computed(() => {
64
139
  return null
65
140
  }
66
141
 
67
- const sortedData = [...props.series.data[resourceId]]
68
- const sortBy = props.chart.x_axis.sort_x_by
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)
69
147
  const sortDirection = props.chart.x_axis.sort_x_direction ?? 'asc'
70
148
 
71
149
  if (sortBy && sortDirection && props.chart.x_axis.column_x) {
72
- const sortKey = sortBy === 'axis_x' ? xColumn : yColumn
150
+ const sortKey = sortBy === 'axis_y' ? yColumn : xColumn
73
151
  sortedData.sort((a, b) => {
74
152
  const valA = a[sortKey]
75
153
  const valB = b[sortKey]
@@ -142,6 +220,8 @@ const echartsOption = computed(() => {
142
220
  seriesData.data.push(curr.data)
143
221
  }
144
222
 
223
+ const xAxisType = mapXAxisType(props.chart.x_axis)
224
+
145
225
  return {
146
226
  dataset: [...seriesData.data],
147
227
  textStyle: {
@@ -152,10 +232,12 @@ const echartsOption = computed(() => {
152
232
  trigger: 'axis' as const,
153
233
  formatter: (params: Array<{ value: Record<string, unknown>, axisValueLabel: string, seriesName: string }>) => {
154
234
  let tooltip = ''
155
- const formatter = new Intl.NumberFormat('fr-FR')
235
+ const valueFormatter = new Intl.NumberFormat(locale)
236
+ const xAxisFormatter = buildXAxisFormatter(xAxisColumn.value, { dateStyle: 'medium' })
156
237
  for (const param of params) {
157
238
  const seriesName = param.seriesName
158
- tooltip += `${format.encodeHTML(param.axisValueLabel)}: <strong>${formatter.format(Number(param.value[seriesName]))}</strong><br>`
239
+ const axisLabel = xAxisFormatter ? xAxisFormatter(param.axisValueLabel) : param.axisValueLabel
240
+ tooltip += `${format.encodeHTML(String(axisLabel))}: <strong>${valueFormatter.format(Number(param.value[seriesName]))}</strong><br>`
159
241
  }
160
242
  return tooltip
161
243
  },
@@ -172,8 +254,13 @@ const echartsOption = computed(() => {
172
254
  containLabel: true,
173
255
  },
174
256
  xAxis: {
175
- type: mapXAxisType(props.chart.x_axis),
257
+ type: xAxisType,
176
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,
177
264
  },
178
265
  yAxis: {
179
266
  type: 'value' as const,
@@ -20,10 +20,6 @@
20
20
  class="inline-block rounded font-medium px-2 py-0.5 text-xs max-w-full truncate"
21
21
  :style="categoryBadgeStyle ? { backgroundColor: categoryBadgeStyle.backgroundColor, color: categoryBadgeStyle.color } : undefined"
22
22
  >{{ value }}</span>
23
- <span
24
- v-else-if="columnType === 'number' && noNumberFormat"
25
- :class="compact ? 'font-mono tabular-nums text-xs text-gray-title' : ''"
26
- >{{ value }}</span>
27
23
  <span
28
24
  v-else-if="columnType === 'number'"
29
25
  :class="compact ? 'font-mono tabular-nums text-xs text-gray-title' : ''"
@@ -32,10 +28,6 @@
32
28
  v-else-if="columnType === 'date'"
33
29
  :class="compact ? 'font-mono tabular-nums text-xs text-gray-title' : ''"
34
30
  >{{ formatCellDate(value) }}</span>
35
- <span
36
- v-else-if="columnType === 'year'"
37
- :class="compact ? 'font-mono tabular-nums text-xs text-gray-title' : ''"
38
- >{{ value }}</span>
39
31
  <span
40
32
  v-else
41
33
  class="text-gray-title truncate block text-xs"
@@ -52,7 +44,6 @@ defineProps<{
52
44
  columnType: ColumnType
53
45
  categoryBadgeStyle?: BadgeStyle
54
46
  compact?: boolean
55
- noNumberFormat?: boolean
56
47
  }>()
57
48
 
58
49
  const { t } = useTranslation()
@@ -128,7 +128,7 @@ function filterByValue() {
128
128
  const val = String(cell.value.value ?? '')
129
129
  const col = cell.value.column
130
130
  const existing = filters.value[col] ?? {}
131
- if (cell.value.columnType === 'categorical' || cell.value.columnType === 'text' || cell.value.columnType === 'date' || cell.value.columnType === 'year') {
131
+ if (cell.value.columnType === 'categorical' || cell.value.columnType === 'text' || cell.value.columnType === 'date') {
132
132
  const current = existing.in ?? []
133
133
  if (!current.includes(val)) {
134
134
  filters.value = { ...filters.value, [col]: { ...existing, in: [...current, val] } }
@@ -63,7 +63,6 @@
63
63
  ExploreResourceView), so they line up with the resource title /
64
64
  download button above. Only the table itself goes edge-to-edge
65
65
  (via -mx-4 below). -->
66
- <slot name="toolbar-top" />
67
66
  <div>
68
67
  <div class="flex items-center py-3 gap-2">
69
68
  <!-- Mobile: filter & sort button -->
@@ -380,32 +379,15 @@
380
379
  v-for="col in displayedColumns"
381
380
  :key="col"
382
381
  data-cell
383
- class="p-2 align-middle whitespace-nowrap border-r border-gray-default last:border-r-0 overflow-hidden"
384
- :class="[
385
- !props.disablePopover && 'cursor-pointer',
386
- { 'text-right font-mono tabular-nums text-sm': getColumnType(col) === 'number' || getColumnType(col) === 'date' || getColumnType(col) === 'year' },
387
- ]"
382
+ class="p-2 align-middle whitespace-nowrap border-r border-gray-default last:border-r-0 overflow-hidden cursor-pointer hover:bg-gray-200/50"
383
+ :class="{ 'text-right font-mono tabular-nums text-sm': getColumnType(col) === 'number' || getColumnType(col) === 'date' }"
388
384
  :style="columnWidths[col] ? { maxWidth: columnWidths[col] + 'px' } : { maxWidth: '300px' }"
389
385
  @click="onCellClick(col, row[col], $event)"
390
386
  >
391
- <a
392
- v-if="linkColumns?.has(col)"
393
- :href="getRowHref(row) ?? ''"
394
- class="link"
395
- >
396
- <TabularCell
397
- :value="row[col]"
398
- :column-type="getColumnType(col)"
399
- :category-badge-style="getColumnType(col) === 'categorical' ? getCategoryBadgeStyle(col, String(row[col])) : undefined"
400
- :no-number-format="props.noFormatColumns?.includes(col)"
401
- />
402
- </a>
403
387
  <TabularCell
404
- v-else
405
388
  :value="row[col]"
406
389
  :column-type="getColumnType(col)"
407
390
  :category-badge-style="getColumnType(col) === 'categorical' ? getCategoryBadgeStyle(col, String(row[col])) : undefined"
408
- :no-number-format="props.noFormatColumns?.includes(col)"
409
391
  />
410
392
  </td>
411
393
  </tr>
@@ -420,7 +402,6 @@
420
402
 
421
403
  <!-- Cell popover -->
422
404
  <TabularCellPopover
423
- v-if="!props.disablePopover"
424
405
  v-model:cell="activeCell"
425
406
  v-model:filters="filters"
426
407
  />
@@ -469,25 +450,11 @@
469
450
  class="min-w-0 pl-4 cursor-pointer"
470
451
  @click="onCellClick(col, row[col], $event)"
471
452
  >
472
- <a
473
- v-if="linkColumns?.has(col)"
474
- :href="getRowHref(row) ?? ''"
475
- >
476
- <TabularCell
477
- :value="row[col]"
478
- :column-type="getColumnType(col)"
479
- :category-badge-style="getColumnType(col) === 'categorical' ? getCategoryBadgeStyle(col, String(row[col])) : undefined"
480
- compact
481
- :no-number-format="props.noFormatColumns?.includes(col)"
482
- />
483
- </a>
484
453
  <TabularCell
485
- v-else
486
454
  :value="row[col]"
487
455
  :column-type="getColumnType(col)"
488
456
  :category-badge-style="getColumnType(col) === 'categorical' ? getCategoryBadgeStyle(col, String(row[col])) : undefined"
489
457
  compact
490
- :no-number-format="props.noFormatColumns?.includes(col)"
491
458
  />
492
459
  </div>
493
460
  </div>
@@ -647,7 +614,7 @@ import { useFetch } from '../../functions/api'
647
614
  import { useComponentsConfig } from '../../config'
648
615
  import { useTranslation } from '../../composables/useTranslation'
649
616
  import { injectTabularProfile } from '../../composables/useTabularProfile'
650
- import { buildTypeConfig, buildFormatConfig, humanizeFormat, GENERIC_FORMATS, hasFilterForColumn as _hasFilterForColumn, isTruthy, isFalsy, resolveColumnType, buildGlobalSearchConditions } from '../../functions/tabular'
617
+ import { buildTypeConfig, buildFormatConfig, humanizeFormat, GENERIC_FORMATS, hasFilterForColumn as _hasFilterForColumn, isTruthy, isFalsy, resolveColumnType } from '../../functions/tabular'
651
618
  import ClientOnly from '../ClientOnly.vue'
652
619
  import SimpleBanner from '../SimpleBanner.vue'
653
620
  import BrandedButton from '../BrandedButton.vue'
@@ -666,37 +633,8 @@ const props = defineProps<{
666
633
  // the host's padding. Used on the standalone explore page where the table
667
634
  // should span the whole screen while the toolbar stays inside the container.
668
635
  fullBleed?: boolean
669
- // When set, searches across multiple columns using the Tabular API's or(...)
670
- // parameter. Text and categorical columns get a __contains filter; number
671
- // columns get a __exact filter (since __contains is not supported for numbers
672
- // by the API). Date and boolean columns are excluded.
673
- // Note: combined via AND with any existing column-specific `contains` filters,
674
- // so it acts as an additional narrowing constraint, not a replacement.
675
- globalSearch?: string
676
- // When true, clicking a cell does not open the popover. Use when row-click
677
- // navigation is the primary interaction.
678
- disablePopover?: boolean
679
- // Initial filters applied on mount, e.g. { 'Administration': { contains: 'Ministère' } }.
680
- // Used when navigating from the detail page badge with query params.
681
- initialFilters?: Record<string, ColumnFilters>
682
- // When set, renders <a> tags inside the specified column cells for native
683
- // browser UX (hover URL, ctrl+click, middle-click).
684
- rowHref?: { columns: string[], href: (row: TabularRow) => string }
685
- // Columns listed here render their values raw (no number formatting).
686
- // Useful for identifier columns.
687
- noFormatColumns?: string[]
688
636
  }>()
689
637
 
690
- const rowHrefFn = computed(() => props.rowHref?.href ?? null)
691
-
692
- function getRowHref(row: TabularRow): string | null {
693
- return rowHrefFn.value?.(row) ?? null
694
- }
695
-
696
- const linkColumns = computed(() =>
697
- props.rowHref ? new Set(props.rowHref.columns) : null,
698
- )
699
-
700
638
  const { t } = useTranslation()
701
639
  const config = useComponentsConfig()
702
640
 
@@ -720,10 +658,6 @@ const dataUrl = computed(() =>
720
658
  const sort = ref<SortConfig | null>(null)
721
659
  const filters = ref<Record<string, ColumnFilters>>({})
722
660
 
723
- if (props.initialFilters) {
724
- filters.value = { ...props.initialFilters }
725
- }
726
-
727
661
  const PAGE_SIZE = 50
728
662
 
729
663
  const dataQuery = computed(() => {
@@ -754,10 +688,6 @@ const dataQuery = computed(() => {
754
688
  q[`${col}__isnotnull`] = ''
755
689
  }
756
690
  }
757
- if (props.globalSearch && profileData.value?.profile) {
758
- const conditions = buildGlobalSearchConditions(allColumns.value, getColumnType, props.globalSearch)
759
- q.or = '(' + conditions.join(',') + ')'
760
- }
761
691
  return q
762
692
  })
763
693
 
@@ -1008,7 +938,6 @@ onUnmounted(() => {
1008
938
  const activeCell = ref<CellInfo | null>(null)
1009
939
 
1010
940
  function onCellClick(col: string, value: unknown, event: MouseEvent) {
1011
- if (props.disablePopover) return
1012
941
  const el = (event.target as HTMLElement).closest('[data-cell]') as HTMLElement | null
1013
942
  if (!el) return
1014
943
  if (activeCell.value && activeCell.value.element === el) {
@@ -1032,12 +961,7 @@ const activeFilters = computed<ActiveFilter[]>(() => {
1032
961
  parts.push(`= ${filter.in.join(', ')}`)
1033
962
  }
1034
963
  if (filter.exact != null) {
1035
- if (getColumnType(col) === 'boolean') {
1036
- parts.push(`= ${filter.exact === 'true' ? t('Vrai') : t('Faux')}`)
1037
- }
1038
- else {
1039
- parts.push(`= ${filter.exact}`)
1040
- }
964
+ parts.push(`= ${filter.exact === 'true' ? t('Vrai') : t('Faux')}`)
1041
965
  }
1042
966
  if (filter.contains) {
1043
967
  parts.push(`${t('contient')} "${filter.contains}"`)
@@ -154,14 +154,14 @@
154
154
 
155
155
  <!-- Number range -->
156
156
  <form
157
- v-if="(columnType === 'number' || columnType === 'year') && columnProfile"
157
+ v-if="columnType === 'number' && columnProfile"
158
158
  class="px-3 py-2 border-b border-black/10 space-y-2"
159
159
  @submit.prevent="applyRange"
160
160
  >
161
161
  <div class="flex items-center gap-2 text-xs text-gray-plain">
162
- <span class="tabular-nums">{{ columnType === 'year' ? profileMin : formatNumber(profileMin) }}</span>
162
+ <span class="tabular-nums">{{ formatNumber(profileMin) }}</span>
163
163
  <span class="text-gray-medium">—</span>
164
- <span class="tabular-nums">{{ columnType === 'year' ? profileMax : formatNumber(profileMax) }}</span>
164
+ <span class="tabular-nums">{{ formatNumber(profileMax) }}</span>
165
165
  </div>
166
166
  <div class="flex items-center gap-2">
167
167
  <input
@@ -51,8 +51,8 @@ export type TabularColumnProfile = {
51
51
  tops: TabularTopValue[]
52
52
  nb_distinct: number
53
53
  nb_missing_values: number
54
- min?: number
55
- max?: number
54
+ min?: number | string
55
+ max?: number | string
56
56
  std?: number
57
57
  mean?: number
58
58
  }
@@ -62,13 +62,13 @@ export type TabularTopValue = {
62
62
  count: number
63
63
  }
64
64
 
65
- export type ColumnType = 'number' | 'categorical' | 'text' | 'date' | 'boolean' | 'year'
65
+ export type ColumnType = 'number' | 'categorical' | 'text' | 'date' | 'boolean'
66
66
 
67
67
  export type ColumnFilters = {
68
68
  in?: string[]
69
69
  exact?: string
70
- min?: number | string
71
- max?: number | string
70
+ min?: number
71
+ max?: number
72
72
  contains?: string
73
73
  null?: 'only' | 'exclude'
74
74
  }
@@ -1,5 +1,5 @@
1
1
  import type { Chart, ChartForm, ChartForApi, Filter, ColumnDefinition } from '../types/visualizations'
2
- import type { TabularProfile } from '../components/TabularExplorer/types'
2
+ import type { ColumnType, TabularProfile } from '../components/TabularExplorer/types'
3
3
  import { resolveColumnType } from './tabular'
4
4
 
5
5
  export function toChartForm(chart: Chart) {
@@ -74,6 +74,22 @@ export function buildColumnsFromProfile(profile: { profile: TabularProfile }): A
74
74
  const colInfo = profile.profile.columns[name]
75
75
  const isCategorical = profile.profile.categorical.includes(name)
76
76
  const colType = resolveColumnType(colInfo ?? { python_type: 'unknown', format: undefined }, isCategorical)
77
- return { name, type: colType }
77
+ const colProfile = profile.profile.profile[name]
78
+ return {
79
+ name,
80
+ type: colType,
81
+ min: normalizeProfileNumber(colProfile?.min, colType),
82
+ max: normalizeProfileNumber(colProfile?.max, colType),
83
+ }
78
84
  })
79
85
  }
86
+
87
+ function normalizeProfileNumber(value: number | string | undefined, type: ColumnType): number | undefined {
88
+ if (value === undefined || value === null) return undefined
89
+ if (type === 'date' && typeof value === 'string') {
90
+ const timestamp = new Date(value).getTime()
91
+ return Number.isNaN(timestamp) ? undefined : timestamp
92
+ }
93
+ const parsed = Number(value)
94
+ return Number.isNaN(parsed) ? undefined : parsed
95
+ }
@@ -42,13 +42,12 @@ export function buildTypeConfig(t: TranslationFunction): Record<ColumnType, Type
42
42
  text: { icon: RiText, label: t('Texte') },
43
43
  date: { icon: RiCalendarLine, label: t('Date') },
44
44
  boolean: { icon: RiCheckboxLine, label: t('Booléen') },
45
- year: { icon: RiCalendarLine, label: t('Année') },
46
45
  }
47
46
  }
48
47
 
49
48
  export function resolveColumnType(colInfo: { python_type: string, format?: string }, isCategorical: boolean): ColumnType {
50
- if (colInfo.format === 'year') return 'year'
51
49
  if (['int', 'float'].includes(colInfo.python_type)) return 'number'
50
+ if (colInfo.format === 'year') return 'date'
52
51
  if (['date', 'datetime'].includes(colInfo.python_type)) return 'date'
53
52
  if (colInfo.python_type === 'bool') return 'boolean'
54
53
  if (isCategorical) return 'categorical'
@@ -159,30 +158,6 @@ export function useFormatTabular() {
159
158
  const TRUTHY_VALUES = ['true', '1', 'oui', 'yes']
160
159
  const FALSY_VALUES = ['false', '0', 'non', 'no']
161
160
 
162
- /**
163
- * Builds the OR conditions for global search across all columns.
164
- * Text and categorical columns get a `__contains` filter; number columns
165
- * get a `__exact` filter only when the search value is numeric.
166
- * Date and boolean columns are excluded.
167
- */
168
- export function buildGlobalSearchConditions(
169
- allColumns: string[],
170
- getColumnType: (col: string) => ColumnType,
171
- searchValue: string,
172
- ): string[] {
173
- const conditions: string[] = []
174
- for (const col of allColumns) {
175
- const type = getColumnType(col)
176
- if (type === 'text' || type === 'categorical') {
177
- conditions.push(col + '__contains.' + encodeURIComponent(searchValue))
178
- }
179
- else if (type === 'number' && searchValue.length > 0 && isFinite(Number(searchValue))) {
180
- conditions.push(col + '__exact.' + encodeURIComponent(searchValue))
181
- }
182
- }
183
- return conditions
184
- }
185
-
186
161
  export function isTruthy(value: unknown): boolean {
187
162
  if (typeof value === 'boolean') return value
188
163
  if (typeof value === 'string') return TRUTHY_VALUES.includes(value.toLowerCase())
@@ -89,5 +89,5 @@ export type ChartForm = Omit<ChartForApi, 'x_axis' | 'series' | 'owner' | 'organ
89
89
  filter: GenericFilter | null
90
90
  }
91
91
 
92
- export type ColumnDefinition = { name: string, type: ColumnType }
92
+ export type ColumnDefinition = { name: string, type: ColumnType, min?: number, max?: number }
93
93
  export type ColumnsDefinition = Record<string, Array<ColumnDefinition>>