@datagouv/components-next 1.0.2-dev.4 → 1.0.2-dev.6

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.
@@ -1,4 +1,4 @@
1
- import { b as Ke } from "./main-ByqZlhiZ.js";
1
+ import { b as Ke } from "./main-B2kXxWRG.js";
2
2
  import We from "vue";
3
3
  function Fe(I, 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.4",
3
+ "version": "1.0.2-dev.6",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "engines": {
@@ -17,15 +17,14 @@
17
17
  ],
18
18
  "dependencies": {
19
19
  "@floating-ui/vue": "^1.1.8",
20
+ "@types/leaflet": "^1.9.17",
20
21
  "@headlessui/vue": "^1.7.23",
21
22
  "@remixicon/vue": "^4.5.0",
22
23
  "@types/hast": "^3.0.4",
23
- "@types/leaflet": "^1.9.17",
24
24
  "@vueuse/core": "^13.1.0",
25
25
  "@vueuse/router": "^13.1.0",
26
26
  "chart.js": "^4.4.8",
27
27
  "dompurify": "^3.2.5",
28
- "echarts": "^6.0.0",
29
28
  "geopf-extensions-openlayers": "^1.0.0-beta.5",
30
29
  "leaflet": "^1.9.4",
31
30
  "maplibre-gl": "^5.6.2",
@@ -51,7 +50,6 @@
51
50
  "unist-util-visit": "^5.0.0",
52
51
  "vue": "^3.5.13",
53
52
  "vue-content-loader": "^2.0.1",
54
- "vue-echarts": "^8.0.1",
55
53
  "vue-router": "^4.5.0",
56
54
  "vue-sonner": "^2.0.9",
57
55
  "vue3-json-viewer": "^2.4.1",
@@ -147,7 +147,7 @@ async function getTableInfos(page: number, sortConfig?: SortConfig) {
147
147
  try {
148
148
  // Check that this function return wanted data
149
149
  const response = await getData(config, props.resource.id, page, sortConfig)
150
- if ('data' in response && response.data && 0 in response.data) {
150
+ if ('data' in response && response.data && response.data.length > 0) {
151
151
  // Update existing rows
152
152
  rows.value = response.data
153
153
  columns.value = Object.keys(response.data[0]).filter(item => item !== '__id')
@@ -6,92 +6,15 @@ export type SortConfig = {
6
6
  type: string
7
7
  } | null
8
8
 
9
- export type TabularDataResponse = {
10
- data: Array<Record<string, unknown>>
11
- meta: { total: number }
12
- }
13
-
14
- export type TabularAggregateType = 'avg' | 'sum' | 'count' | 'min' | 'max'
15
-
16
- export type FetchTabularDataOptions = {
17
- resourceId: string
18
- page?: number
19
- pageSize?: number
20
- columns?: Array<string> | undefined
21
- sort?: SortConfig
22
- groupBy?: string | undefined
23
- aggregation?: {
24
- column: string
25
- type: TabularAggregateType
26
- } | undefined
27
- }
28
-
29
- export type TabularProfileResponse = {
30
- profile: {
31
- header: Array<string>
32
- columns: Record<string, {
33
- score: number
34
- format: string
35
- python_type: string
36
- }>
37
- formats: Record<string, Array<string>>
38
- profile: Record<string, {
39
- tops: Array<{ count: number, value: string }>
40
- nb_distinct: number
41
- nb_missing_values: number
42
- min?: number
43
- max?: number
44
- std?: number
45
- mean?: number
46
- }>
47
- encoding: string
48
- separator: string
49
- categorical: Array<string>
50
- total_lines: number
51
- nb_duplicates: number
52
- columns_fields: Record<string, {
53
- score: number
54
- format: string
55
- python_type: string
56
- }>
57
- columns_labels: Record<string, {
58
- score: number
59
- format: string
60
- python_type: string
61
- }>
62
- header_row_idx: number
63
- heading_columns: number
64
- trailing_columns: number
65
- }
66
- deleted_at: string | null
67
- dataset_id: string
68
- indexes: null
69
- }
70
-
71
- /**
72
- * Call Tabular-api to get table content with options object
73
- */
74
- export async function fetchTabularData(config: PluginConfig, options: FetchTabularDataOptions): Promise<TabularDataResponse> {
75
- const page = options.page ?? 1
76
- const pageSize = options.pageSize ?? config.tabularApiPageSize ?? 15
77
- let url = `${config.tabularApiUrl}/api/resources/${options.resourceId}/data/?page=${page}&page_size=${pageSize}`
78
- if (options.columns) {
79
- url += `&columns=${options.columns.join(',')}`
80
- }
81
- if (options.sort) {
82
- url += `&${options.sort.column}__sort=${options.sort.type}`
83
- }
84
- if (options.groupBy && options.aggregation?.type) {
85
- url += `&${options.groupBy}__groupby&${options.aggregation.column}__${options.aggregation.type}`
86
- }
87
- return await ofetch<TabularDataResponse>(url)
88
- }
89
-
90
9
  /**
91
10
  * Call Tabular-api to get table content
92
11
  */
93
- export function getData(config: PluginConfig, id: string, page: number, sortConfig?: SortConfig) {
94
- return fetchTabularData(config, { resourceId: id, page, sort: sortConfig })
12
+ export async function getData(config: PluginConfig, id: string, page: number, sortConfig?: SortConfig) {
13
+ let url = `${config.tabularApiUrl}/api/resources/${id}/data/?page=${page}&page_size=${config.tabularApiPageSize || 15}`
14
+ if (sortConfig) {
15
+ url = url + `&${sortConfig.column}__sort=${sortConfig.type}`
16
+ }
17
+ return await ofetch(url)
95
18
  }
96
19
 
97
20
  /**
@@ -99,5 +22,5 @@ export function getData(config: PluginConfig, id: string, page: number, sortConf
99
22
  */
100
23
  export function useGetProfile() {
101
24
  const config = useComponentsConfig()
102
- return (id: string) => ofetch<TabularProfileResponse>(`${config.tabularApiUrl}/api/resources/${id}/profile/`)
25
+ return (id: string) => ofetch(`${config.tabularApiUrl}/api/resources/${id}/profile/`)
103
26
  }
package/src/main.ts CHANGED
@@ -23,7 +23,6 @@ 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, getDefaultGlobalSearchConfig, defaultDatasetSortOptions, defaultDataserviceSortOptions, defaultReuseSortOptions, defaultOrganizationSortOptions } from './types/search'
29
28
 
@@ -101,7 +100,6 @@ import { configKey, useComponentsConfig, type PluginConfig } from './config.js'
101
100
  export { Toaster, toast } from 'vue-sonner'
102
101
 
103
102
  export * from './composables/useActiveDescendant'
104
- export * from './composables/useDebouncedRef'
105
103
  export * from './composables/useMetrics'
106
104
  export * from './composables/useReuseType'
107
105
  export * from './composables/useTranslation'
@@ -123,7 +121,6 @@ export * from './functions/resources'
123
121
  export * from './functions/reuses'
124
122
  export * from './functions/schemas'
125
123
  export * from './functions/users'
126
- export * from './functions/tabularApi'
127
124
  export * from './types/access_types'
128
125
 
129
126
  export type {
@@ -221,23 +218,6 @@ export type {
221
218
  ValidataError,
222
219
  Weight,
223
220
  WellType,
224
- Chart,
225
- ChartForm,
226
- ChartForApi,
227
- FilterCondition,
228
- Filter,
229
- AndFilters,
230
- GenericFilter,
231
- XAxisType,
232
- XAxisSortBy,
233
- SortDirection,
234
- XAxis,
235
- XAxisForm,
236
- UnitPosition,
237
- YAxis,
238
- DataSeriesType,
239
- DataSeries,
240
- DataSeriesForm,
241
221
  }
242
222
 
243
223
  export {
@@ -1,7 +1,7 @@
1
1
  import type { User } from './users'
2
2
  import type { Badges } from './badges'
3
3
 
4
- export type MemberRole = 'admin' | 'editor'
4
+ export type MemberRole = 'admin' | 'editor' | 'partial_editor'
5
5
 
6
6
  export type Member = {
7
7
  role: MemberRole
@@ -1,4 +0,0 @@
1
- import { i as f } from "./main-ByqZlhiZ.js";
2
- export {
3
- f as default
4
- };
@@ -1,152 +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, DataSeries, XAxis, YAxis, ChartForm, XAxisForm } from '../../types/visualizations'
18
-
19
- use([CanvasRenderer, LineChart, BarChart, TitleComponent, TooltipComponent, LegendComponent, GridComponent, DatasetComponent])
20
-
21
- const props = defineProps<{
22
- chart: Chart | ChartForm
23
- series: {
24
- data: Record<string, Array<Record<string, unknown>>>
25
- columns: Record<string, Array<string>>
26
- }
27
- }>()
28
-
29
- function mapSeriesType(type: DataSeries['type']): 'line' | 'bar' {
30
- return (type ?? 'line') === 'histogram' ? 'bar' : 'line'
31
- }
32
-
33
- function mapXAxisType(xAxis: XAxis | XAxisForm): 'category' | 'value' {
34
- if (!xAxis) return 'category'
35
- return (xAxis.type ?? 'discrete') === 'continuous' ? 'value' : 'category'
36
- }
37
-
38
- function buildYAxisFormatter(yAxis: YAxis): ((value: number) => string) | undefined {
39
- return (value: number) => {
40
- const v = summarize(value)
41
- if (!yAxis.unit) return v
42
- if (yAxis.unit_position === 'prefix') return `${yAxis.unit} ${v}`
43
- return `${v} ${yAxis.unit}`
44
- }
45
- }
46
-
47
- const echartsOption = computed(() => {
48
- const seriesCount = props.chart.series.length
49
- if (!props.chart.series || seriesCount === 0) return
50
-
51
- // Create series configuration with data mapping
52
- const seriesData = props.chart.series.map((s) => {
53
- const xColumn = s.column_x_name_override ?? props.chart.x_axis.column_x
54
- const yColumn = s.aggregate_y ? `${s.column_y}__${s.aggregate_y}` : s.column_y
55
- const resourceId = s.resource_id
56
- const seriesType = s.type
57
-
58
- if (!xColumn || !yColumn || !resourceId || !seriesType || !props.series.data[resourceId] || !props.series.columns[resourceId]) {
59
- return null
60
- }
61
-
62
- // Sort data before passing to ECharts to avoid transform issues
63
- const sortedData = [...props.series.data[resourceId]]
64
- const sortBy = props.chart.x_axis.sort_x_by
65
- const sortDirection = props.chart.x_axis.sort_x_direction ?? 'asc'
66
-
67
- if (sortBy && sortDirection && props.chart.x_axis.column_x) {
68
- const sortKey = sortBy === 'axis_x' ? xColumn : yColumn
69
- sortedData.sort((a, b) => {
70
- const valA = a[sortKey] as number
71
- const valB = b[sortKey] as number
72
- if (valA < valB) return sortDirection === 'asc' ? -1 : 1
73
- if (valA > valB) return sortDirection === 'asc' ? 1 : -1
74
- return 0
75
- })
76
- }
77
-
78
- return {
79
- series: {
80
- type: mapSeriesType(seriesType),
81
- dimensions: s.aggregate_y ? [xColumn, yColumn] : props.series.columns[resourceId],
82
- name: s.column_y,
83
- encode: {
84
- x: xColumn,
85
- y: yColumn,
86
- },
87
- } as LineSeriesOption | BarSeriesOption,
88
- data: {
89
- source: sortedData,
90
- dimensions: s.aggregate_y ? [xColumn, yColumn] : props.series.columns[resourceId],
91
- },
92
- }
93
- }).filter(Boolean).reduce((acc: { series: Array<LineSeriesOption | BarSeriesOption>, data: Array<Record<string, unknown>> }, curr) => {
94
- if (curr) {
95
- acc.series.push(curr.series)
96
- acc.data.push(curr.data)
97
- }
98
- return acc
99
- }, {
100
- series: [],
101
- data: [],
102
- })
103
-
104
- return {
105
- dataset: [...seriesData.data],
106
- title: {
107
- text: props.chart.title,
108
- left: 'center',
109
- },
110
- tooltip: {
111
- trigger: 'axis' as const,
112
- formatter: (params: Array<{ value: Record<string, unknown>, axisValueLabel: string, seriesName: string }>) => {
113
- let tooltip = ''
114
- for (const param of params) {
115
- const keys = Object.keys(param.value)
116
- const col = keys.find(key => key.startsWith(param.seriesName))!
117
- const formatter = new Intl.NumberFormat('fr-FR')
118
- tooltip += `${format.encodeHTML(param.axisValueLabel)}: <strong>${formatter.format(Number(param.value[col]))}</strong><br>`
119
- }
120
- return tooltip
121
- },
122
- },
123
- legend: {
124
- bottom: 0,
125
- },
126
- grid: {
127
- top: 60,
128
- bottom: 40,
129
- left: 20,
130
- right: 20,
131
- containLabel: true,
132
- },
133
- xAxis: {
134
- type: mapXAxisType(props.chart.x_axis),
135
- name: (props.chart.x_axis as XAxis).column_x,
136
- },
137
- yAxis: {
138
- type: 'value' as const,
139
- name: props.chart.y_axis.label ?? undefined,
140
- min: props.chart.y_axis.min ?? undefined,
141
- max: props.chart.y_axis.max ?? undefined,
142
- axisLabel: {
143
- formatter: buildYAxisFormatter(props.chart.y_axis),
144
- },
145
- },
146
- series: seriesData.series,
147
- } satisfies ComposeOption<
148
- | BarSeriesOption
149
- | LineSeriesOption
150
- >
151
- })
152
- </script>
@@ -1,194 +0,0 @@
1
- <template>
2
- <LoadingBlock
3
- :status="status"
4
- :data="series"
5
- >
6
- <template #default="{ data }">
7
- <ChartViewer
8
- :chart="chart"
9
- :series="data"
10
- />
11
- </template>
12
- <template #error>
13
- <div class="text-center py-8 text-gray-500">
14
- Une erreur est survenue lors du chargement des données du graphique.
15
- </div>
16
- </template>
17
- </LoadingBlock>
18
- </template>
19
-
20
- <script setup lang="ts">
21
- import { reactive, ref, watch } from 'vue'
22
- import ChartViewer from './ChartViewer.vue'
23
- import LoadingBlock from '../../components/LoadingBlock.vue'
24
- import { useComponentsConfig } from '../../config'
25
- import { fetchTabularData, useGetProfile, type TabularDataResponse, type TabularProfileResponse } from '../../functions/tabularApi'
26
- import type { Chart, ChartForm } from '../../types/visualizations'
27
-
28
- const chart = defineModel<Chart | ChartForm>({ required: true })
29
-
30
- const emit = defineEmits<{
31
- columns: [columns: Record<string, Array<string>>]
32
- }>()
33
-
34
- const config = useComponentsConfig()
35
- const getProfile = useGetProfile()
36
-
37
- // Loading and error states
38
- const status = ref<'idle' | 'pending' | 'success' | 'error'>('idle')
39
- const error = ref<Error | null>(null)
40
-
41
- // Dataset source for the chart
42
- const series = reactive<{
43
- data: Record<string, Array<Record<string, unknown>>>
44
- columns: Record<string, Array<string>>
45
- }>({
46
- data: {},
47
- columns: {},
48
- })
49
-
50
- async function fetchSeriesProfile() {
51
- status.value = 'pending'
52
- error.value = null
53
-
54
- try {
55
- if (chart.value.series.length === 0) {
56
- status.value = 'success'
57
- series.data = {}
58
- series.columns = {}
59
- return
60
- }
61
-
62
- // Fetch data for all series in parallel
63
- const fetchPromises = chart.value.series.map(async (serie) => {
64
- if (!serie.resource_id) return
65
- return {
66
- id: serie.resource_id,
67
- profile: await getProfile(serie.resource_id),
68
- }
69
- }).filter(Boolean) as Array<Promise<{
70
- id: string
71
- profile: TabularProfileResponse
72
- }>>
73
-
74
- const results = (await Promise.allSettled(fetchPromises))
75
- .filter(r => r.status === 'fulfilled')
76
- .map(r => r.value)
77
- series.columns = Object.fromEntries(results.map(result => [
78
- result.id,
79
- result.profile.profile.header,
80
- ]))
81
- if (results.length > 0) {
82
- const columns = results[0]?.profile.profile.header ?? []
83
- const firstColumn = columns.filter(c => c !== '__id')[0]
84
- if (!chart.value.x_axis.column_x && firstColumn) {
85
- chart.value.x_axis.column_x = firstColumn
86
- }
87
- }
88
- status.value = 'success'
89
- }
90
- catch (err) {
91
- error.value = err instanceof Error ? err : new Error('Failed to fetch series profile')
92
- status.value = 'error'
93
- console.log(err)
94
- series.columns = {}
95
- }
96
- }
97
-
98
- async function fetchSeriesData() {
99
- status.value = 'pending'
100
- error.value = null
101
-
102
- try {
103
- if (chart.value.series.length === 0 || !chart.value.x_axis.column_x) {
104
- status.value = 'success'
105
- series.data = {}
106
- return
107
- }
108
-
109
- // Fetch data for all series in parallel
110
- const fetchPromises = chart.value.series.map(async (serie) => {
111
- const xColumn = serie.column_x_name_override ?? chart.value.x_axis.column_x
112
- if (!xColumn || !serie.resource_id || !serie.column_y) return
113
- return {
114
- id: serie.resource_id,
115
- profile: await getProfile(serie.resource_id),
116
- data: await fetchTabularData(config, {
117
- columns: serie.aggregate_y ? undefined : [xColumn, serie.column_y],
118
- resourceId: serie.resource_id,
119
- page: 1,
120
- pageSize: 100,
121
- groupBy: xColumn,
122
- aggregation: serie.column_y && serie.aggregate_y
123
- ? {
124
- column: serie.column_y,
125
- type: serie.aggregate_y,
126
- }
127
- : undefined,
128
- }),
129
- }
130
- }).filter(Boolean) as Array<Promise<{
131
- id: string
132
- data: TabularDataResponse
133
- }>>
134
-
135
- const results = (await Promise.allSettled(fetchPromises))
136
- .filter(r => r.status === 'fulfilled')
137
- .map(r => r.value)
138
- // Transform data into echarts format
139
- series.data = Object.fromEntries(results.map(result => [
140
- result.id,
141
- result.data.data,
142
- ]))
143
- status.value = 'success'
144
- }
145
- catch (err) {
146
- error.value = err instanceof Error ? err : new Error('Failed to fetch series data')
147
- status.value = 'error'
148
- series.data = {}
149
- }
150
- }
151
-
152
- // Watch for changes in the chart or its series
153
- watch(() => chart.value.series, async () => {
154
- await fetchSeriesProfile()
155
- }, { immediate: true, deep: true })
156
-
157
- // Watch for changes in the chart or its series
158
- watch([() => chart.value.series, () => chart.value.x_axis.column_x], async () => {
159
- await fetchSeriesData()
160
- }, { immediate: true, deep: true })
161
-
162
- watch(() => series.columns, () => {
163
- emit('columns', series.columns)
164
- })
165
-
166
- /**
167
- * Combines data from multiple series into a single echarts dataset source
168
- */
169
- // function combineSeriesData(seriesData: Array<TabularDataResponse | null>): Array<Array<unknown>> {
170
- // if (!seriesData.length || seriesData.every(data => !data || !data.data || data.data.length === 0)) {
171
- // return []
172
- // }
173
-
174
- // // Get all unique columns from all series
175
- // const allColumns = Array.from(new Set(seriesData.flatMap(data =>
176
- // data?.data?.flatMap(Object.keys) || []
177
- // )))
178
-
179
- // // Build the source array with headers
180
- // const source: Array<Array<unknown>> = [allColumns]
181
-
182
- // // Add data rows - for simplicity, we'll use data from the first series
183
- // // In a real implementation, you'd want to merge/combine the data appropriately
184
- // const firstData = seriesData.find(data => data?.data?.length)?.data
185
- // if (firstData) {
186
- // firstData.forEach(row => {
187
- // const rowValues = allColumns.map(column => row[column])
188
- // source.push(rowValues)
189
- // })
190
- // }
191
-
192
- // return source
193
- // }
194
- </script>
@@ -1,84 +0,0 @@
1
- import type { TabularAggregateType } from '../functions/tabularApi'
2
- import type { Owned, OwnedWithId } from './owned'
3
-
4
- export type FilterCondition = 'equal' | 'greater'
5
-
6
- export type Filter = {
7
- column: string
8
- condition: FilterCondition
9
- value?: string
10
- }
11
-
12
- export type AndFilters = {
13
- filters: Array<Filter | AndFilters>
14
- }
15
-
16
- export type GenericFilter = Filter | AndFilters
17
-
18
- export type XAxisType = 'discrete' | 'continuous'
19
-
20
- export type XAxisSortBy = 'axis_x' | 'axis_y'
21
-
22
- export type SortDirection = 'asc' | 'desc'
23
-
24
- export type XAxis = {
25
- column_x: string
26
- sort_x_by: XAxisSortBy | null
27
- sort_x_direction: SortDirection | null
28
- type: XAxisType
29
- }
30
-
31
- export type XAxisForm = Omit<XAxis, 'sort_x_by'> & { sort_x_by: XAxisSortBy | '' | null }
32
-
33
- export type UnitPosition = 'prefix' | 'suffix'
34
-
35
- export type YAxis = {
36
- min: number | null
37
- max: number | null
38
- label: string | null
39
- unit: string | null
40
- unit_position: UnitPosition | null
41
- }
42
-
43
- export type DataSeriesType = 'line' | 'histogram'
44
-
45
- export type DataSeries = {
46
- type: DataSeriesType
47
- column_y: string
48
- aggregate_y: TabularAggregateType | null
49
- resource_id: string
50
- column_x_name_override: string | null
51
- filters: GenericFilter | null
52
- }
53
-
54
- // Type for form where aggregate_y can be empty string for select binding
55
- export type DataSeriesForm = Omit<DataSeries, 'aggregate_y'> & { aggregate_y: TabularAggregateType | '' | null }
56
-
57
- export type Chart = Owned & {
58
- id: string
59
- title: string
60
- slug: string
61
- description: string
62
- private: boolean
63
- created_at: string
64
- last_modified: string
65
- deleted_at: string | null
66
- uri: string
67
- page: string
68
- x_axis: XAxis
69
- y_axis: YAxis
70
- series: Array<DataSeries>
71
- extras: Record<string, unknown>
72
- permissions: { delete: boolean, edit: boolean, read: boolean }
73
- metrics: {
74
- views: number
75
- }
76
- }
77
-
78
- export type ChartForApi = OwnedWithId & Pick<Chart, 'title' | 'description' | 'private' | 'x_axis' | 'y_axis' | 'series' | 'extras'>
79
-
80
- export type ChartForm = Omit<ChartForApi, 'x_axis' | 'series' | 'owner' | 'organization'> & {
81
- owned: OwnedWithId
82
- x_axis: XAxisForm
83
- series: Array<DataSeriesForm>
84
- }