@datagouv/components-next 1.0.2-dev.52 → 1.0.2-dev.54
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.
- package/assets/main.css +4 -0
- package/dist/{Datafair.client-CBE0uebV.js → Datafair.client-qm_JoZUL.js} +1 -1
- package/dist/{JsonPreview.client-B-kw70ep.js → JsonPreview.client-BpovqdDN.js} +2 -2
- package/dist/{MapContainer.client-dUUInlHa.js → MapContainer.client-6Y5RJxtw.js} +2 -2
- package/dist/{PdfPreview.client-Y2qTpKFd.js → PdfPreview.client-Drv5EwJe.js} +2 -2
- package/dist/{Pmtiles.client-BOPJ59Cq.js → Pmtiles.client-B3dUb4iS.js} +1 -1
- package/dist/{PreviewWrapper.vue_vue_type_script_setup_true_lang-DtgGpKmM.js → PreviewWrapper.vue_vue_type_script_setup_true_lang-BmRAxeK4.js} +1 -1
- package/dist/{XmlPreview.client-CtW1m4O8.js → XmlPreview.client-CXF1N-AI.js} +3 -3
- package/dist/components-next.css +1 -1
- package/dist/components-next.js +144 -145
- package/dist/components.css +1 -1
- package/dist/{index-B8siUkxs.js → index-lCAbcwQm.js} +1 -1
- package/dist/{main-DaWGX8hL.js → main-5ZJvZtsQ.js} +31672 -52962
- package/dist/{vue3-xml-viewer.common-S9Mg9vGE.js → vue3-xml-viewer.common-X_gxbf2s.js} +1 -1
- package/package.json +1 -3
- package/src/components/InfiniteLoader.vue +53 -0
- package/src/components/ResourceAccordion/Preview.vue +11 -11
- package/src/components/ResourceAccordion/ResourceAccordion.vue +1 -1
- package/src/components/ResourceExplorer/ResourceExplorer.vue +60 -3
- package/src/components/ResourceExplorer/ResourceExplorerSidebar.vue +2 -2
- package/src/components/ResourceExplorer/ResourceExplorerViewer.vue +4 -3
- package/src/components/Search/GlobalSearch.vue +45 -4
- package/src/components/TabularExplorer/TabularCell.vue +51 -0
- package/src/components/TabularExplorer/TabularCellPopover.vue +170 -0
- package/src/components/TabularExplorer/TabularExplorer.vue +870 -0
- package/src/components/TabularExplorer/TabularFilterContent.vue +351 -0
- package/src/components/TabularExplorer/TabularFilterPopover.vue +111 -0
- package/src/components/TabularExplorer/types.ts +83 -0
- package/src/composables/useHasTabularData.ts +1 -9
- package/src/composables/useSearchFilter.ts +90 -0
- package/src/composables/useStableQueryParams.ts +28 -3
- package/src/functions/api.ts +34 -33
- package/src/functions/api.types.ts +1 -0
- package/src/functions/tabular.ts +60 -0
- package/src/functions/tabularApi.ts +9 -137
- package/src/main.ts +9 -27
- package/src/components/Chart/ChartViewer.vue +0 -147
- package/src/components/Chart/ChartViewerWrapper.vue +0 -224
- package/src/components/Form/Listbox.vue +0 -101
- package/src/functions/charts.ts +0 -68
- package/src/types/visualizations.ts +0 -89
|
@@ -1,11 +1,13 @@
|
|
|
1
|
-
import { ref, watch, type Ref } from 'vue'
|
|
1
|
+
import { computed, ref, watch, type Ref } from 'vue'
|
|
2
2
|
import type { SearchTypeConfig } from '../types/search'
|
|
3
|
+
import { forEachActiveCustomFilter, type CustomFilterEntry } from './useSearchFilter'
|
|
3
4
|
|
|
4
5
|
type FilterRefs = Record<string, Ref<unknown>>
|
|
5
6
|
|
|
6
7
|
interface StableQueryParamsOptions {
|
|
7
8
|
typeConfig: SearchTypeConfig | undefined
|
|
8
9
|
allFilters: FilterRefs
|
|
10
|
+
customFilterRegistry: Map<string, CustomFilterEntry>
|
|
9
11
|
q: Ref<string>
|
|
10
12
|
sort: Ref<string | undefined>
|
|
11
13
|
page: Ref<number>
|
|
@@ -17,7 +19,7 @@ interface StableQueryParamsOptions {
|
|
|
17
19
|
* Applies hiddenFilters first, then user filters (which can override hiddenFilters).
|
|
18
20
|
*/
|
|
19
21
|
export function useStableQueryParams(options: StableQueryParamsOptions) {
|
|
20
|
-
const { typeConfig, allFilters, q, sort, page, pageSize } = options
|
|
22
|
+
const { typeConfig, allFilters, customFilterRegistry, q, sort, page, pageSize } = options
|
|
21
23
|
const stableParams = ref<Record<string, unknown>>({})
|
|
22
24
|
|
|
23
25
|
const buildParams = () => {
|
|
@@ -50,6 +52,19 @@ export function useStableQueryParams(options: StableQueryParamsOptions) {
|
|
|
50
52
|
}
|
|
51
53
|
}
|
|
52
54
|
|
|
55
|
+
// 3.5. Apply custom filter values. Concatenate into an array on collision
|
|
56
|
+
// so a custom filter mapped onto a built-in apiParam (e.g. theme → tag)
|
|
57
|
+
// combines with an existing built-in value instead of overwriting it.
|
|
58
|
+
forEachActiveCustomFilter(customFilterRegistry, (apiParam, value) => {
|
|
59
|
+
const existing = params[apiParam]
|
|
60
|
+
if (existing === undefined) {
|
|
61
|
+
params[apiParam] = value
|
|
62
|
+
}
|
|
63
|
+
else {
|
|
64
|
+
params[apiParam] = Array.isArray(existing) ? [...existing, value] : [existing, value]
|
|
65
|
+
}
|
|
66
|
+
})
|
|
67
|
+
|
|
53
68
|
// 4. Always include q, sort (if valid for this type), page, page_size
|
|
54
69
|
if (q.value) {
|
|
55
70
|
params.q = q.value
|
|
@@ -66,9 +81,19 @@ export function useStableQueryParams(options: StableQueryParamsOptions) {
|
|
|
66
81
|
return params
|
|
67
82
|
}
|
|
68
83
|
|
|
84
|
+
// Computed that reads all custom filter values, establishing reactive dependencies
|
|
85
|
+
// on both the Map mutations (shallowReactive) and each entry's ref.value.
|
|
86
|
+
const customFilterValues = computed(() => {
|
|
87
|
+
const snapshot: Record<string, unknown> = {}
|
|
88
|
+
for (const [urlParam, entry] of customFilterRegistry) {
|
|
89
|
+
snapshot[urlParam] = entry.ref.value
|
|
90
|
+
}
|
|
91
|
+
return snapshot
|
|
92
|
+
})
|
|
93
|
+
|
|
69
94
|
// Watch all dependencies and update only if content changed
|
|
70
95
|
watch(
|
|
71
|
-
[q, sort, page, ...Object.values(allFilters)],
|
|
96
|
+
[q, sort, page, ...Object.values(allFilters), customFilterValues],
|
|
72
97
|
() => {
|
|
73
98
|
const newParams = buildParams()
|
|
74
99
|
// JSON.stringify comparison is safe here because buildParams() builds the object deterministically
|
package/src/functions/api.ts
CHANGED
|
@@ -24,6 +24,7 @@ export async function useFetch<DataT, ErrorT = never>(
|
|
|
24
24
|
return await config.customUseFetch(url, options)
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
+
const isRaw = options?.raw
|
|
27
28
|
const data: Ref<DataT | null> = ref(null)
|
|
28
29
|
const error: Ref<ErrorT | null> = ref(null)
|
|
29
30
|
const status = ref<AsyncDataRequestStatus>('idle')
|
|
@@ -35,37 +36,39 @@ export async function useFetch<DataT, ErrorT = never>(
|
|
|
35
36
|
const query = deepToValue(options?.query)
|
|
36
37
|
status.value = 'pending'
|
|
37
38
|
try {
|
|
38
|
-
data.value =
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
config.onRequest
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
39
|
+
data.value = isRaw
|
|
40
|
+
? await ofetch<DataT | null>(urlValue, { params: params ?? query })
|
|
41
|
+
: await ofetch<DataT | null>(urlValue, {
|
|
42
|
+
baseURL: config.apiBase,
|
|
43
|
+
params: params ?? query,
|
|
44
|
+
onRequest(param) {
|
|
45
|
+
if (config.onRequest) {
|
|
46
|
+
if (Array.isArray(config.onRequest)) {
|
|
47
|
+
config.onRequest.forEach(r => r(param))
|
|
48
|
+
}
|
|
49
|
+
else {
|
|
50
|
+
config.onRequest(param)
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
const { options } = param
|
|
54
|
+
options.headers.set('Content-Type', 'application/json')
|
|
55
|
+
options.headers.set('Accept', 'application/json')
|
|
56
|
+
options.credentials = 'include'
|
|
57
|
+
if (config.devApiKey) {
|
|
58
|
+
options.headers.set('X-API-KEY', config.devApiKey)
|
|
59
|
+
}
|
|
57
60
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
61
|
+
if (locale) {
|
|
62
|
+
if (!options.params) {
|
|
63
|
+
options.params = {}
|
|
64
|
+
}
|
|
65
|
+
options.params['lang'] = locale
|
|
66
|
+
}
|
|
67
|
+
},
|
|
68
|
+
onRequestError: config.onRequestError,
|
|
69
|
+
onResponse: config.onResponse,
|
|
70
|
+
onResponseError: config.onResponseError,
|
|
71
|
+
})
|
|
69
72
|
status.value = 'success'
|
|
70
73
|
}
|
|
71
74
|
catch (e) {
|
|
@@ -90,9 +93,7 @@ export async function useFetch<DataT, ErrorT = never>(
|
|
|
90
93
|
|
|
91
94
|
return {
|
|
92
95
|
data,
|
|
93
|
-
refresh:
|
|
94
|
-
execute()
|
|
95
|
-
},
|
|
96
|
+
refresh: () => execute(),
|
|
96
97
|
execute,
|
|
97
98
|
clear: () => {
|
|
98
99
|
data.value = null
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import type { Component } from 'vue'
|
|
2
|
+
import {
|
|
3
|
+
RiHashtag,
|
|
4
|
+
RiPriceTag3Line,
|
|
5
|
+
RiText,
|
|
6
|
+
RiCalendarLine,
|
|
7
|
+
RiCheckboxLine,
|
|
8
|
+
} from '@remixicon/vue'
|
|
9
|
+
import { useTranslation } from '../composables/useTranslation'
|
|
10
|
+
import type { ColumnFilters, ColumnType } from '../components/TabularExplorer/types'
|
|
11
|
+
|
|
12
|
+
export function hasFilterForColumn(filters: Record<string, ColumnFilters>, column: string): boolean {
|
|
13
|
+
const f = filters[column]
|
|
14
|
+
if (!f) return false
|
|
15
|
+
return !!(f.in?.length || f.exact != null || f.contains || f.null || f.min != null || f.max != null)
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function buildTypeConfig(t: (s: string) => string): Record<ColumnType, { icon: Component, label: string }> {
|
|
19
|
+
return {
|
|
20
|
+
number: { icon: RiHashtag, label: t('Nombre') },
|
|
21
|
+
categorical: { icon: RiPriceTag3Line, label: t('Catégoriel') },
|
|
22
|
+
text: { icon: RiText, label: t('Texte') },
|
|
23
|
+
date: { icon: RiCalendarLine, label: t('Date') },
|
|
24
|
+
boolean: { icon: RiCheckboxLine, label: t('Booléen') },
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function useFormatTabular() {
|
|
29
|
+
const { locale } = useTranslation()
|
|
30
|
+
|
|
31
|
+
function formatNumber(value: unknown): string {
|
|
32
|
+
const num = Number(value)
|
|
33
|
+
if (Number.isNaN(num)) return String(value)
|
|
34
|
+
return num.toLocaleString(locale)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function formatCellDate(value: unknown): string {
|
|
38
|
+
if (value == null || value === '') return '–'
|
|
39
|
+
const d = new Date(String(value))
|
|
40
|
+
if (Number.isNaN(d.getTime())) return String(value)
|
|
41
|
+
return new Intl.DateTimeFormat(locale, { day: '2-digit', month: '2-digit', year: 'numeric' }).format(d)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return { formatNumber, formatCellDate }
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const TRUTHY_VALUES = ['true', '1', 'oui', 'yes']
|
|
48
|
+
const FALSY_VALUES = ['false', '0', 'non', 'no']
|
|
49
|
+
|
|
50
|
+
export function isTruthy(value: unknown): boolean {
|
|
51
|
+
if (typeof value === 'boolean') return value
|
|
52
|
+
if (typeof value === 'string') return TRUTHY_VALUES.includes(value.toLowerCase())
|
|
53
|
+
return Boolean(value)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function isFalsy(value: unknown): boolean {
|
|
57
|
+
if (typeof value === 'boolean') return !value
|
|
58
|
+
if (typeof value === 'string') return FALSY_VALUES.includes(value.toLowerCase())
|
|
59
|
+
return !value
|
|
60
|
+
}
|
|
@@ -1,146 +1,18 @@
|
|
|
1
1
|
import { ofetch } from 'ofetch'
|
|
2
2
|
import { useComponentsConfig, type PluginConfig } from '../config'
|
|
3
|
-
import type {
|
|
3
|
+
import type { SortConfig } from '../components/TabularExplorer/types'
|
|
4
4
|
|
|
5
|
-
export type SortConfig
|
|
6
|
-
column: string
|
|
7
|
-
type: string
|
|
8
|
-
} | null
|
|
9
|
-
|
|
10
|
-
export type TabularDataResponse = {
|
|
11
|
-
data: Array<Record<string, unknown>>
|
|
12
|
-
links: {
|
|
13
|
-
profile: string
|
|
14
|
-
swagger: string
|
|
15
|
-
next: string
|
|
16
|
-
}
|
|
17
|
-
meta: { total: number }
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export type TabularAggregateType = 'avg' | 'sum' | 'count' | 'min' | 'max'
|
|
21
|
-
|
|
22
|
-
export type FetchTabularDataOptions = {
|
|
23
|
-
resourceId: string
|
|
24
|
-
page?: number
|
|
25
|
-
pageSize?: number
|
|
26
|
-
columns?: Array<string> | undefined
|
|
27
|
-
sort?: SortConfig
|
|
28
|
-
groupBy?: string | undefined
|
|
29
|
-
aggregation?: {
|
|
30
|
-
column: string
|
|
31
|
-
type: TabularAggregateType
|
|
32
|
-
} | undefined
|
|
33
|
-
filters?: GenericFilter | undefined
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
export type TabularProfileResponse = {
|
|
37
|
-
profile: {
|
|
38
|
-
header: Array<string>
|
|
39
|
-
columns: Record<string, {
|
|
40
|
-
score: number
|
|
41
|
-
format: string
|
|
42
|
-
python_type: string
|
|
43
|
-
}>
|
|
44
|
-
formats: Record<string, Array<string>>
|
|
45
|
-
profile: Record<string, {
|
|
46
|
-
tops: Array<{ count: number, value: string }>
|
|
47
|
-
nb_distinct: number
|
|
48
|
-
nb_missing_values: number
|
|
49
|
-
min?: number
|
|
50
|
-
max?: number
|
|
51
|
-
std?: number
|
|
52
|
-
mean?: number
|
|
53
|
-
}>
|
|
54
|
-
encoding: string
|
|
55
|
-
separator: string
|
|
56
|
-
categorical: Array<string>
|
|
57
|
-
total_lines: number
|
|
58
|
-
nb_duplicates: number
|
|
59
|
-
columns_fields: Record<string, {
|
|
60
|
-
score: number
|
|
61
|
-
format: string
|
|
62
|
-
python_type: string
|
|
63
|
-
}>
|
|
64
|
-
columns_labels: Record<string, {
|
|
65
|
-
score: number
|
|
66
|
-
format: string
|
|
67
|
-
python_type: string
|
|
68
|
-
}>
|
|
69
|
-
header_row_idx: number
|
|
70
|
-
heading_columns: number
|
|
71
|
-
trailing_columns: number
|
|
72
|
-
}
|
|
73
|
-
deleted_at: string | null
|
|
74
|
-
dataset_id: string
|
|
75
|
-
indexes: null
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
/**
|
|
79
|
-
* Call Tabular-api to get table content with options object
|
|
80
|
-
*/
|
|
81
|
-
export async function fetchTabularData(config: PluginConfig, options: FetchTabularDataOptions): Promise<TabularDataResponse> {
|
|
82
|
-
const page = options.page ?? 1
|
|
83
|
-
const pageSize = options.pageSize ?? config.tabularApiPageSize ?? 15
|
|
84
|
-
console.log(options)
|
|
85
|
-
let url = `${config.tabularApiUrl}/api/resources/${options.resourceId}/data/?page=${page}&page_size=${pageSize}`
|
|
86
|
-
if (options.columns) {
|
|
87
|
-
url += `&columns=${options.columns.join(',')}`
|
|
88
|
-
}
|
|
89
|
-
if (options.sort) {
|
|
90
|
-
url += `&${options.sort.column}__sort=${options.sort.type}`
|
|
91
|
-
}
|
|
92
|
-
if (options.groupBy && options.aggregation?.type) {
|
|
93
|
-
url += `&${options.groupBy}__groupby&${options.aggregation.column}__${options.aggregation.type}`
|
|
94
|
-
}
|
|
95
|
-
if (options.filters) {
|
|
96
|
-
const filterQuery = buildFilterQuery(options.filters)
|
|
97
|
-
if (filterQuery) {
|
|
98
|
-
url += `&${filterQuery}`
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
return await ofetch<TabularDataResponse>(url)
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
function buildFilterQuery(filters: GenericFilter): string {
|
|
105
|
-
const params: Array<string> = []
|
|
106
|
-
if ('filters' in filters) {
|
|
107
|
-
for (const filter of filters.filters) {
|
|
108
|
-
if ('filters' in filter) {
|
|
109
|
-
params.push(buildFilterQuery(filter))
|
|
110
|
-
}
|
|
111
|
-
else {
|
|
112
|
-
if (filter.condition === 'is_null') {
|
|
113
|
-
params.push(`${filter.column}__isnull`)
|
|
114
|
-
}
|
|
115
|
-
else if (filter.condition === 'is_not_null') {
|
|
116
|
-
params.push(`${filter.column}__isnotnull`)
|
|
117
|
-
}
|
|
118
|
-
else if (filter.value !== null && filter.value !== undefined && filter.value !== '') {
|
|
119
|
-
params.push(`${filter.column}__${filter.condition}=${encodeURIComponent(filter.value)}`)
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
else {
|
|
125
|
-
const filter = filters
|
|
126
|
-
if (filter.condition === 'is_null') {
|
|
127
|
-
params.push(`${filter.column}__isnull`)
|
|
128
|
-
}
|
|
129
|
-
else if (filter.condition === 'is_not_null') {
|
|
130
|
-
params.push(`${filter.column}__isnotnull`)
|
|
131
|
-
}
|
|
132
|
-
else if (filter.value !== null && filter.value !== undefined && filter.value !== '') {
|
|
133
|
-
params.push(`${filter.column}__${filter.condition}=${encodeURIComponent(filter.value)}`)
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
return params.join('&')
|
|
137
|
-
}
|
|
5
|
+
export type { SortConfig }
|
|
138
6
|
|
|
139
7
|
/**
|
|
140
8
|
* Call Tabular-api to get table content
|
|
141
9
|
*/
|
|
142
|
-
export function getData(config: PluginConfig, id: string, page: number, sortConfig?: SortConfig) {
|
|
143
|
-
|
|
10
|
+
export async function getData(config: PluginConfig, id: string, page: number, sortConfig?: SortConfig | null) {
|
|
11
|
+
let url = `${config.tabularApiUrl}/api/resources/${id}/data/?page=${page}&page_size=${config.tabularApiPageSize || 15}`
|
|
12
|
+
if (sortConfig) {
|
|
13
|
+
url = url + `&${sortConfig.column}__sort=${sortConfig.direction}`
|
|
14
|
+
}
|
|
15
|
+
return await ofetch(url)
|
|
144
16
|
}
|
|
145
17
|
|
|
146
18
|
/**
|
|
@@ -148,5 +20,5 @@ export function getData(config: PluginConfig, id: string, page: number, sortConf
|
|
|
148
20
|
*/
|
|
149
21
|
export function useGetProfile() {
|
|
150
22
|
const config = useComponentsConfig()
|
|
151
|
-
return (id: string) => ofetch
|
|
23
|
+
return (id: string) => ofetch(`${config.tabularApiUrl}/api/resources/${id}/profile/`)
|
|
152
24
|
}
|
package/src/main.ts
CHANGED
|
@@ -23,9 +23,10 @@ import type { Site } from './types/site'
|
|
|
23
23
|
import type { Weight, WellType } from './types/ui'
|
|
24
24
|
import type { User, UserReference } from './types/users'
|
|
25
25
|
import type { Report, ReportSubject, ReportReason } from './types/reports'
|
|
26
|
-
import type { Chart, ChartForm, ChartForApi, FilterCondition, Filter, AndFilters, GenericFilter, XAxisType, XAxisSortBy, SortDirection, XAxis, XAxisForm, UnitPosition, YAxis, DataSeriesType, DataSeries, DataSeriesForm } from './types/visualizations'
|
|
27
26
|
import type { GlobalSearchConfig, SearchType, SortOption } from './types/search'
|
|
28
27
|
import { getDefaultDatasetConfig, getDefaultDataserviceConfig, getDefaultReuseConfig, getDefaultOrganizationConfig, getDefaultTopicConfig, getDefaultGlobalSearchConfig, defaultDatasetSortOptions, defaultDataserviceSortOptions, defaultReuseSortOptions, defaultOrganizationSortOptions } from './types/search'
|
|
28
|
+
import { useSearchFilter } from './composables/useSearchFilter'
|
|
29
|
+
import type { UseSearchFilterOptions } from './composables/useSearchFilter'
|
|
29
30
|
|
|
30
31
|
import ActivityList from './components/ActivityList/ActivityList.vue'
|
|
31
32
|
import UserActivityList from './components/ActivityList/UserActivityList.vue'
|
|
@@ -81,8 +82,6 @@ import ReuseDetails from './components/ReuseDetails.vue'
|
|
|
81
82
|
import SchemaCard from './components/SchemaCard.vue'
|
|
82
83
|
import SimpleBanner from './components/SimpleBanner.vue'
|
|
83
84
|
import SmallChart from './components/SmallChart.vue'
|
|
84
|
-
import ChartViewer from './components/Chart/ChartViewer.vue'
|
|
85
|
-
import ChartViewerWrapper from './components/Chart/ChartViewerWrapper.vue'
|
|
86
85
|
import StatBox from './components/StatBox.vue'
|
|
87
86
|
import Tab from './components/Tabs/Tab.vue'
|
|
88
87
|
import TabGroup from './components/Tabs/TabGroup.vue'
|
|
@@ -97,14 +96,14 @@ import GlobalSearch from './components/Search/GlobalSearch.vue'
|
|
|
97
96
|
import SearchInput from './components/Search/SearchInput.vue'
|
|
98
97
|
import SearchableSelect from './components/Form/SearchableSelect.vue'
|
|
99
98
|
import SelectGroup from './components/Form/SelectGroup.vue'
|
|
100
|
-
import
|
|
99
|
+
import InfiniteLoader from './components/InfiniteLoader.vue'
|
|
100
|
+
import TabularExplorer from './components/TabularExplorer/TabularExplorer.vue'
|
|
101
101
|
import type { UseFetchFunction } from './functions/api.types'
|
|
102
102
|
import { configKey, useComponentsConfig, type PluginConfig } from './config.js'
|
|
103
103
|
|
|
104
104
|
export { Toaster, toast } from 'vue-sonner'
|
|
105
105
|
|
|
106
106
|
export * from './composables/useActiveDescendant'
|
|
107
|
-
export * from './composables/useDebouncedRef'
|
|
108
107
|
export * from './composables/useMetrics'
|
|
109
108
|
export * from './composables/useReuseType'
|
|
110
109
|
export * from './composables/useTranslation'
|
|
@@ -124,15 +123,15 @@ export * from './functions/owned'
|
|
|
124
123
|
export * from './functions/resources'
|
|
125
124
|
export * from './functions/reuses'
|
|
126
125
|
export * from './functions/schemas'
|
|
126
|
+
export * from './functions/tabular'
|
|
127
127
|
export * from './functions/users'
|
|
128
|
-
export * from './functions/tabularApi'
|
|
129
|
-
export * from './functions/charts'
|
|
130
128
|
export * from './types/access_types'
|
|
131
129
|
|
|
132
130
|
export type {
|
|
133
131
|
GlobalSearchConfig,
|
|
134
132
|
SearchType,
|
|
135
133
|
SortOption,
|
|
134
|
+
UseSearchFilterOptions,
|
|
136
135
|
UseFetchFunction,
|
|
137
136
|
AccessType,
|
|
138
137
|
AccessAudience,
|
|
@@ -223,23 +222,6 @@ export type {
|
|
|
223
222
|
ValidataError,
|
|
224
223
|
Weight,
|
|
225
224
|
WellType,
|
|
226
|
-
Chart,
|
|
227
|
-
ChartForm,
|
|
228
|
-
ChartForApi,
|
|
229
|
-
FilterCondition,
|
|
230
|
-
Filter,
|
|
231
|
-
AndFilters,
|
|
232
|
-
GenericFilter,
|
|
233
|
-
XAxisType,
|
|
234
|
-
XAxisSortBy,
|
|
235
|
-
SortDirection,
|
|
236
|
-
XAxis,
|
|
237
|
-
XAxisForm,
|
|
238
|
-
UnitPosition,
|
|
239
|
-
YAxis,
|
|
240
|
-
DataSeriesType,
|
|
241
|
-
DataSeries,
|
|
242
|
-
DataSeriesForm,
|
|
243
225
|
}
|
|
244
226
|
|
|
245
227
|
export {
|
|
@@ -253,6 +235,7 @@ export {
|
|
|
253
235
|
defaultDataserviceSortOptions,
|
|
254
236
|
defaultReuseSortOptions,
|
|
255
237
|
defaultOrganizationSortOptions,
|
|
238
|
+
useSearchFilter,
|
|
256
239
|
}
|
|
257
240
|
|
|
258
241
|
// Vue Plugin
|
|
@@ -325,8 +308,6 @@ export {
|
|
|
325
308
|
SchemaCard,
|
|
326
309
|
SimpleBanner,
|
|
327
310
|
SmallChart,
|
|
328
|
-
ChartViewer,
|
|
329
|
-
ChartViewerWrapper,
|
|
330
311
|
StatBox,
|
|
331
312
|
OpenApiViewer,
|
|
332
313
|
Tab,
|
|
@@ -343,5 +324,6 @@ export {
|
|
|
343
324
|
SearchInput,
|
|
344
325
|
SearchableSelect,
|
|
345
326
|
SelectGroup,
|
|
346
|
-
|
|
327
|
+
InfiniteLoader,
|
|
328
|
+
TabularExplorer,
|
|
347
329
|
}
|
|
@@ -1,147 +0,0 @@
|
|
|
1
|
-
<template>
|
|
2
|
-
<VChart
|
|
3
|
-
class="w-full min-h-96"
|
|
4
|
-
:option="echartsOption"
|
|
5
|
-
autoresize
|
|
6
|
-
/>
|
|
7
|
-
</template>
|
|
8
|
-
|
|
9
|
-
<script setup lang="ts">
|
|
10
|
-
import { format, use, type ComposeOption } from 'echarts/core'
|
|
11
|
-
import { CanvasRenderer } from 'echarts/renderers'
|
|
12
|
-
import { LineChart, BarChart, type BarSeriesOption, type LineSeriesOption } from 'echarts/charts'
|
|
13
|
-
import { TitleComponent, TooltipComponent, LegendComponent, GridComponent, DatasetComponent } from 'echarts/components'
|
|
14
|
-
import VChart from 'vue-echarts'
|
|
15
|
-
import { computed } from 'vue'
|
|
16
|
-
import { summarize } from '../../functions/helpers'
|
|
17
|
-
import type { Chart, XAxis, YAxis, XAxisForm, ChartForApi } from '../../types/visualizations'
|
|
18
|
-
|
|
19
|
-
use([CanvasRenderer, LineChart, BarChart, TitleComponent, TooltipComponent, LegendComponent, GridComponent, DatasetComponent])
|
|
20
|
-
|
|
21
|
-
const props = defineProps<{
|
|
22
|
-
chart: Chart | ChartForApi
|
|
23
|
-
series: {
|
|
24
|
-
data: Record<string, Array<Record<string, unknown>>>
|
|
25
|
-
columns: Record<string, Array<string>>
|
|
26
|
-
}
|
|
27
|
-
}>()
|
|
28
|
-
|
|
29
|
-
function mapXAxisType(xAxis: XAxis | XAxisForm): 'category' | 'value' {
|
|
30
|
-
if (!xAxis) return 'category'
|
|
31
|
-
return (xAxis.type ?? 'discrete') === 'continuous' ? 'value' : 'category'
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
function buildYAxisFormatter(yAxis: YAxis): ((value: number) => string) | undefined {
|
|
35
|
-
return (value: number) => {
|
|
36
|
-
const v = summarize(value)
|
|
37
|
-
if (!yAxis.unit) return v
|
|
38
|
-
if (yAxis.unit_position === 'prefix') return `${yAxis.unit} ${v}`
|
|
39
|
-
return `${v} ${yAxis.unit}`
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
const echartsOption = computed(() => {
|
|
44
|
-
const seriesCount = props.chart.series.length
|
|
45
|
-
if (!props.chart.series || seriesCount === 0) return
|
|
46
|
-
|
|
47
|
-
const seriesData = props.chart.series.map((s) => {
|
|
48
|
-
const xColumn = s.column_x_name_override ?? props.chart.x_axis.column_x
|
|
49
|
-
const yColumn = s.aggregate_y ? `${s.column_y}__${s.aggregate_y}` : s.column_y
|
|
50
|
-
const resourceId = s.resource_id
|
|
51
|
-
|
|
52
|
-
if (!xColumn || !yColumn || !resourceId || !s.type || !props.series.data[resourceId] || !props.series.columns[resourceId]) {
|
|
53
|
-
return null
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
const sortedData = [...props.series.data[resourceId]]
|
|
57
|
-
let sortBy: string | null = null
|
|
58
|
-
let sortDirection: 'asc' | 'desc' = 'asc'
|
|
59
|
-
sortBy = props.chart.x_axis.sort_x_by
|
|
60
|
-
sortDirection = props.chart.x_axis.sort_x_direction ?? 'asc'
|
|
61
|
-
|
|
62
|
-
if (sortBy && sortDirection && props.chart.x_axis.column_x) {
|
|
63
|
-
const sortKey = sortBy === 'axis_x' ? xColumn : yColumn
|
|
64
|
-
sortedData.sort((a, b) => {
|
|
65
|
-
const valA = a[sortKey] as number
|
|
66
|
-
const valB = b[sortKey] as number
|
|
67
|
-
if (valA < valB) return sortDirection === 'asc' ? -1 : 1
|
|
68
|
-
if (valA > valB) return sortDirection === 'asc' ? 1 : -1
|
|
69
|
-
return 0
|
|
70
|
-
})
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
return {
|
|
74
|
-
series: {
|
|
75
|
-
type: s.type === 'histogram' ? 'bar' : 'line',
|
|
76
|
-
dimensions: s.aggregate_y ? [xColumn, yColumn] : props.series.columns[resourceId],
|
|
77
|
-
name: s.column_y,
|
|
78
|
-
encode: {
|
|
79
|
-
x: xColumn,
|
|
80
|
-
y: yColumn,
|
|
81
|
-
},
|
|
82
|
-
} as LineSeriesOption | BarSeriesOption,
|
|
83
|
-
data: {
|
|
84
|
-
source: sortedData,
|
|
85
|
-
dimensions: s.aggregate_y ? [xColumn, yColumn] : props.series.columns[resourceId],
|
|
86
|
-
},
|
|
87
|
-
}
|
|
88
|
-
}).filter(Boolean).reduce((acc: { series: Array<LineSeriesOption | BarSeriesOption>, data: Array<Record<string, unknown>> }, curr) => {
|
|
89
|
-
if (curr) {
|
|
90
|
-
acc.series.push(curr.series)
|
|
91
|
-
acc.data.push(curr.data)
|
|
92
|
-
}
|
|
93
|
-
return acc
|
|
94
|
-
}, {
|
|
95
|
-
series: [],
|
|
96
|
-
data: [],
|
|
97
|
-
})
|
|
98
|
-
|
|
99
|
-
return {
|
|
100
|
-
dataset: [...seriesData.data],
|
|
101
|
-
title: {
|
|
102
|
-
text: props.chart.title,
|
|
103
|
-
left: 'center',
|
|
104
|
-
},
|
|
105
|
-
tooltip: {
|
|
106
|
-
trigger: 'axis' as const,
|
|
107
|
-
formatter: (params: Array<{ value: Record<string, unknown>, axisValueLabel: string, seriesName: string }>) => {
|
|
108
|
-
let tooltip = ''
|
|
109
|
-
for (const param of params) {
|
|
110
|
-
const keys = Object.keys(param.value)
|
|
111
|
-
const col = keys.find(key => key.startsWith(param.seriesName))!
|
|
112
|
-
const formatter = new Intl.NumberFormat('fr-FR')
|
|
113
|
-
tooltip += `${format.encodeHTML(param.axisValueLabel)}: <strong>${formatter.format(Number(param.value[col]))}</strong><br>`
|
|
114
|
-
}
|
|
115
|
-
return tooltip
|
|
116
|
-
},
|
|
117
|
-
},
|
|
118
|
-
legend: {
|
|
119
|
-
bottom: 0,
|
|
120
|
-
},
|
|
121
|
-
grid: {
|
|
122
|
-
top: 60,
|
|
123
|
-
bottom: 40,
|
|
124
|
-
left: 20,
|
|
125
|
-
right: 20,
|
|
126
|
-
containLabel: true,
|
|
127
|
-
},
|
|
128
|
-
xAxis: {
|
|
129
|
-
type: mapXAxisType(props.chart.x_axis),
|
|
130
|
-
name: (props.chart.x_axis as XAxis).column_x,
|
|
131
|
-
},
|
|
132
|
-
yAxis: {
|
|
133
|
-
type: 'value' as const,
|
|
134
|
-
name: props.chart.y_axis.label ?? undefined,
|
|
135
|
-
min: props.chart.y_axis.min ?? undefined,
|
|
136
|
-
max: props.chart.y_axis.max ?? undefined,
|
|
137
|
-
axisLabel: {
|
|
138
|
-
formatter: buildYAxisFormatter(props.chart.y_axis),
|
|
139
|
-
},
|
|
140
|
-
},
|
|
141
|
-
series: seriesData.series,
|
|
142
|
-
} satisfies ComposeOption<
|
|
143
|
-
| BarSeriesOption
|
|
144
|
-
| LineSeriesOption
|
|
145
|
-
>
|
|
146
|
-
})
|
|
147
|
-
</script>
|