• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

keplergl / kepler.gl / 21753132461

06 Feb 2026 01:58PM UTC coverage: 61.667% (+0.006%) from 61.661%
21753132461

Pull #3299

github

web-flow
Merge ec1357ce5 into cbb3204cf
Pull Request #3299: fix: truncate map tooltips

6384 of 12280 branches covered (51.99%)

Branch coverage included in aggregate %.

3 of 3 new or added lines in 1 file covered. (100.0%)

20 existing lines in 1 file now uncovered.

13070 of 19267 relevant lines covered (67.84%)

82.03 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

74.12
/src/common-utils/src/data-type.ts
1
// SPDX-License-Identifier: MIT
2
// Copyright contributors to the kepler.gl project
3

4
import {Analyzer, DATA_TYPES as AnalyzerDATA_TYPES} from 'type-analyzer';
5
import {ArrowTableInterface, ApacheVectorInterface, RowData, Field} from '@kepler.gl/types';
6
import {ALL_FIELD_TYPES} from '@kepler.gl/constants';
7
import {console as globalConsole} from 'global/window';
8
import {range} from 'd3-array';
9
import {isHexWkb, notNullorUndefined} from './data';
10
import {h3IsValid} from './h3-utils';
11

12
const H3_ANALYZER_TYPE = 'H3';
15✔
13

14
// Returns true if the value is likely a WKT geometry string (heuristic check).
15
const WKT_PREFIX_RE =
16
  /^(?:SRID=\d+\s*;\s*)?(?:POINT|LINESTRING|POLYGON|MULTIPOINT|MULTILINESTRING|MULTIPOLYGON|GEOMETRYCOLLECTION)(?:\s+(?:Z|M|ZM))?\s*\(/i;
15✔
17

18
export function isWkt(value: unknown): boolean {
19
  if (typeof value !== 'string') {
1,471✔
20
    return false;
60✔
21
  }
22

23
  const s = value.trim();
1,411✔
24
  if (s.length < 10) {
1,411✔
25
    return false;
475✔
26
  }
27

28
  if (!s.includes('(') || !s.includes(')')) {
936!
29
    return false;
936✔
30
  }
31

UNCOV
32
  return WKT_PREFIX_RE.test(s);
×
33
}
34

35
export const ACCEPTED_ANALYZER_TYPES = [
15✔
36
  AnalyzerDATA_TYPES.DATE,
37
  AnalyzerDATA_TYPES.TIME,
38
  AnalyzerDATA_TYPES.DATETIME,
39
  AnalyzerDATA_TYPES.NUMBER,
40
  AnalyzerDATA_TYPES.INT,
41
  AnalyzerDATA_TYPES.FLOAT,
42
  AnalyzerDATA_TYPES.BOOLEAN,
43
  AnalyzerDATA_TYPES.STRING,
44
  AnalyzerDATA_TYPES.GEOMETRY,
45
  AnalyzerDATA_TYPES.GEOMETRY_FROM_STRING,
46
  AnalyzerDATA_TYPES.PAIR_GEOMETRY_FROM_STRING,
47
  AnalyzerDATA_TYPES.ZIPCODE,
48
  AnalyzerDATA_TYPES.ARRAY,
49
  AnalyzerDATA_TYPES.OBJECT,
50
  H3_ANALYZER_TYPE
51
];
52

53
const IGNORE_DATA_TYPES = Object.keys(AnalyzerDATA_TYPES).filter(
15✔
54
  type => !ACCEPTED_ANALYZER_TYPES.includes(type)
270✔
55
);
56

57
/**
58
 * Getting sample data for analyzing field type.
59
 */
60
export function getSampleForTypeAnalyze({
61
  fields,
62
  rows,
63
  sampleCount = 50
118✔
64
}: {
65
  fields: string[];
66
  rows: unknown[][] | RowData;
67
  sampleCount?: number;
68
}): RowData {
69
  const total = Math.min(sampleCount, rows.length);
119✔
70
  // const fieldOrder = fields.map(f => f.name);
71
  const sample = range(0, total, 1).map(() => ({}));
1,362✔
72

73
  if (rows.length < 1) {
119!
UNCOV
74
    return [];
×
75
  }
76
  const isRowObject = !Array.isArray(rows[0]);
119✔
77

78
  // collect sample data for each field
79
  fields.forEach((field, fieldIdx) => {
119✔
80
    // row counter
81
    let i = 0;
947✔
82
    // sample counter
83
    let j = 0;
947✔
84

85
    while (j < total) {
947✔
86
      if (i >= rows.length) {
15,078✔
87
        // if depleted data pool
88
        sample[j][field] = null;
1,000✔
89
        j++;
1,000✔
90
      } else if (notNullorUndefined(rows[i][isRowObject ? field : fieldIdx])) {
14,078!
91
        const value = rows[i][isRowObject ? field : fieldIdx];
13,073!
92
        sample[j][field] = typeof value === 'string' ? value.trim() : value;
13,073✔
93
        j++;
13,073✔
94
        i++;
13,073✔
95
      } else {
96
        i++;
1,005✔
97
      }
98
    }
99
  });
100

101
  return sample;
119✔
102
}
103

104
/**
105
 * Getting sample data for analyzing field type for Arrow tables.
106
 * @param table Arrow table or an array of vectors.
107
 * @param fields Field names.
108
 * @param sampleCount Number of sample rows to get.
109
 * @returns Sample rows.
110
 */
111
export function getSampleForTypeAnalyzeArrow(
112
  table: ArrowTableInterface | ApacheVectorInterface[],
113
  fields: string[],
114
  sampleCount = 50
×
115
): any[] {
116
  const isTable = !Array.isArray(table);
×
117

118
  const numRows = isTable ? table.numRows : table[0].length;
×
119
  const getVector = isTable ? index => table.getChildAt(index) : index => table[index];
×
120

121
  const total = Math.min(sampleCount, numRows);
×
UNCOV
122
  const sample = range(0, total, 1).map(() => ({}));
×
123

UNCOV
124
  if (numRows < 1) {
×
UNCOV
125
    return [];
×
126
  }
127

128
  // collect sample data for each field
UNCOV
129
  fields.forEach((field, fieldIdx) => {
×
UNCOV
130
    let rowIndex = 0;
×
UNCOV
131
    let sampleIndex = 0;
×
132

UNCOV
133
    while (sampleIndex < total) {
×
UNCOV
134
      if (rowIndex >= numRows) {
×
135
        // if depleted data pool
UNCOV
136
        sample[sampleIndex][field] = null;
×
UNCOV
137
        sampleIndex++;
×
UNCOV
138
      } else if (notNullorUndefined(getVector(fieldIdx)?.get(rowIndex))) {
×
UNCOV
139
        const value = getVector(fieldIdx)?.get(rowIndex);
×
UNCOV
140
        sample[sampleIndex][field] = typeof value === 'string' ? value.trim() : value;
×
UNCOV
141
        sampleIndex++;
×
UNCOV
142
        rowIndex++;
×
143
      } else {
UNCOV
144
        rowIndex++;
×
145
      }
146
    }
147
  });
148

UNCOV
149
  return sample;
×
150
}
151

152
/**
153
 * Convert type-analyzer output to kepler.gl field types
154
 *
155
 * @param aType
156
 * @returns corresponding type in `ALL_FIELD_TYPES`
157
 */
158
/* eslint-disable complexity */
159
export function analyzerTypeToFieldType(aType: string): string {
160
  const {
161
    DATE,
162
    TIME,
163
    DATETIME,
164
    NUMBER,
165
    INT,
166
    FLOAT,
167
    BOOLEAN,
168
    STRING,
169
    GEOMETRY,
170
    GEOMETRY_FROM_STRING,
171
    PAIR_GEOMETRY_FROM_STRING,
172
    ZIPCODE,
173
    ARRAY,
174
    OBJECT
175
  } = AnalyzerDATA_TYPES;
973✔
176

177
  // TODO: un recognized types
178
  // CURRENCY PERCENT NONE
179
  switch (aType) {
973✔
180
    case DATE:
181
      return ALL_FIELD_TYPES.date;
19✔
182
    case TIME:
183
    case DATETIME:
184
      return ALL_FIELD_TYPES.timestamp;
133✔
185
    case FLOAT:
186
      return ALL_FIELD_TYPES.real;
332✔
187
    case INT:
188
      return ALL_FIELD_TYPES.integer;
193✔
189
    case BOOLEAN:
190
      return ALL_FIELD_TYPES.boolean;
48✔
191
    case GEOMETRY:
192
    case GEOMETRY_FROM_STRING:
193
    case PAIR_GEOMETRY_FROM_STRING:
194
      return ALL_FIELD_TYPES.geojson;
70✔
195
    case ARRAY:
196
      return ALL_FIELD_TYPES.array;
21✔
197
    case OBJECT:
198
      return ALL_FIELD_TYPES.object;
10✔
199
    case NUMBER:
200
    case STRING:
201
    case ZIPCODE:
202
      return ALL_FIELD_TYPES.string;
127✔
203
    case H3_ANALYZER_TYPE:
204
      return ALL_FIELD_TYPES.h3;
16✔
205
    default:
206
      globalConsole.warn(`Unsupported analyzer type: ${aType}`);
4✔
207
      return ALL_FIELD_TYPES.string;
4✔
208
  }
209
}
210

211
/**
212
 * Analyze field types from data in `string` format, e.g. uploaded csv.
213
 * Assign `type`, `fieldIdx` and `format` (timestamp only) to each field
214
 *
215
 * @param data array of row object
216
 * @param fieldOrder array of field names as string
217
 * @returns formatted fields
218
 * @public
219
 * @example
220
 *
221
 * import {getFieldsFromData} from '@kepler.gl/common-utils';
222
 * const data = [{
223
 *   time: '2016-09-17 00:09:55',
224
 *   value: '4',
225
 *   surge: '1.2',
226
 *   isTrip: 'true',
227
 *   zeroOnes: '0'
228
 * }, {
229
 *   time: '2016-09-17 00:30:08',
230
 *   value: '3',
231
 *   surge: null,
232
 *   isTrip: 'false',
233
 *   zeroOnes: '1'
234
 * }, {
235
 *   time: null,
236
 *   value: '2',
237
 *   surge: '1.3',
238
 *   isTrip: null,
239
 *   zeroOnes: '1'
240
 * }];
241
 *
242
 * const fieldOrder = ['time', 'value', 'surge', 'isTrip', 'zeroOnes'];
243
 * const fields = getFieldsFromData(data, fieldOrder);
244
 * // fields = [
245
 * // {name: 'time', format: 'YYYY-M-D H:m:s', fieldIdx: 1, type: 'timestamp'},
246
 * // {name: 'value', format: '', fieldIdx: 4, type: 'integer'},
247
 * // {name: 'surge', format: '', fieldIdx: 5, type: 'real'},
248
 * // {name: 'isTrip', format: '', fieldIdx: 6, type: 'boolean'},
249
 * // {name: 'zeroOnes', format: '', fieldIdx: 7, type: 'integer'}];
250
 *
251
 */
252
export function getFieldsFromData(data: RowData, fieldOrder: string[]): Field[] {
253
  // add a check for epoch timestamp
254
  const metadata = Analyzer.computeColMeta(
119✔
255
    data,
256
    [
257
      {regex: /.*geojson|all_points/g, dataType: 'GEOMETRY'},
258
      {regex: /.*census/g, dataType: 'STRING'}
259
    ],
260
    {ignoredDataTypes: IGNORE_DATA_TYPES}
261
  );
262

263
  const {fieldByIndex} = renameDuplicateFields(fieldOrder);
119✔
264

265
  const result = fieldOrder.map((field, index) => {
119✔
266
    const name = fieldByIndex[index];
955✔
267

268
    const fieldMeta = metadata.find(m => m.key === field);
5,416✔
269

270
    // fieldMeta could be undefined if the field has no data and Analyzer.computeColMeta
271
    // will ignore the field. In this case, we will simply assign the field type to STRING
272
    // since dropping the column in the RowData could be expensive
273
    let type = fieldMeta?.type || 'STRING';
955✔
274
    const format = fieldMeta?.format || '';
955✔
275

276
    // quick check if first valid string in column is H3
277
    if (type === AnalyzerDATA_TYPES.STRING) {
955✔
278
      for (let i = 0, n = data.length; i < n; ++i) {
141✔
279
        if (notNullorUndefined(data[i][name])) {
146✔
280
          type = h3IsValid(data[i][name] || '') ? H3_ANALYZER_TYPE : type;
138!
281
          break;
138✔
282
        }
283
      }
284
    }
285

286
    // quick check if string is hex wkb
287
    if (type === AnalyzerDATA_TYPES.STRING) {
955✔
288
      type = data.some(d => isHexWkb(d[name])) ? AnalyzerDATA_TYPES.GEOMETRY : type;
1,472✔
289
    }
290

291
    // quick check if string is wkt
292
    if (type === AnalyzerDATA_TYPES.STRING) {
955✔
293
      type = data.some(d => isWkt(d[name])) ? AnalyzerDATA_TYPES.GEOMETRY_FROM_STRING : type;
1,471!
294
    }
295

296
    return {
955✔
297
      name,
298
      id: name,
299
      displayName: name,
300
      format,
301
      fieldIdx: index,
302
      type: analyzerTypeToFieldType(type),
303
      analyzerType: type,
304
      valueAccessor: dc => d => {
17✔
UNCOV
305
        return dc.valueAt(d.index, index);
×
306
      }
307
    };
308
  });
309

310
  return result;
119✔
311
}
312

313
/**
314
 * pass in an array of field names, rename duplicated one
315
 * and return a map from old field index to new name
316
 *
317
 * @param fieldOrder
318
 * @returns new field name by index
319
 */
320
export function renameDuplicateFields(fieldOrder: string[]): {
321
  allNames: string[];
322
  fieldByIndex: string[];
323
} {
324
  return fieldOrder.reduce<{allNames: string[]; fieldByIndex: string[]}>(
119✔
325
    (accu, field, i) => {
326
      const {allNames} = accu;
955✔
327
      let fieldName = field;
955✔
328

329
      // add a counter to duplicated names
330
      if (allNames.includes(field)) {
955✔
331
        let counter = 0;
2✔
332
        while (allNames.includes(`${field}-${counter}`)) {
2✔
333
          counter++;
1✔
334
        }
335
        fieldName = `${field}-${counter}`;
2✔
336
      }
337

338
      accu.fieldByIndex[i] = fieldName;
955✔
339
      accu.allNames.push(fieldName);
955✔
340

341
      return accu;
955✔
342
    },
343
    {allNames: [], fieldByIndex: []}
344
  );
345
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc