Skip to content

spatialnetworkslab/florence-datacontainer

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Florence DataContainer

A powerful yet light-weight interface to manage data. Designed to be used with florence.

Installation

In Node or browser applications, Datacontainer can be installed from NPM (npm install @snlab/florence-datacontainer). After installation, it can be imported into any project using ES6 import syntax.

import DataContainer from '@snlab/florence-datacontainer'

You can also load the library directly into an HTML page, which exports a DataContainer global:

<script src="https://unpkg.com/@snlab/florence-datacontainer"></script>

API Reference

Loading data

Loading data to DataContainer is done by passing a supported data structure as first argument to the DataContainer constructor:

const dataContainer = new DataContainer(supportedDataStructure)

DataContainer currently supports 3 data structures:

  • Column-oriented data
  • Row-oriented data
  • GeoJSON

More structures might be supported in the future. DataContainer internally stores data in a column-oriented format. This means that loading column-oriented data will be slightly faster than row-oriented data.

DataContainer supports 6 data types. These data types correspond to native JS data types/structures (see table below).

Data type JS equivalent Loadable Column name
quantitative Number yes NA
categorical String yes NA
temporal Date yes NA
interval Array of two Numbers yes NA
geometry GeoJSON geometry (except GeometryCollection) only by loading GeoJSON $geometry
grouped DataContainer no $grouped

Data of the types quantitative, categorical, temporal and interval are 'loadable', which means that they can be passed in either column-oriented or row-oriented format to the DataContainer constructor. Geometry data can only be loaded from GeoJSON. Geometry data can only exists in a column called $geometry. Grouped data is not loadable: it can only be generated by using .groupBy or .bin transformations (see Transformations). A column of grouped data is just a column of other DataContainers. Grouped data can only exist in a column called $grouped.

Loading column- and row-oriented data

// Column-oriented data
const columnOriented = new DataContainer({
  fruit: ['apple', 'banana', 'coconut', 'durian'],
  amount: [1, 2, 3, 4]
})

// Row-oriented data
const rowOriented = new DataContainer([
  { fruit: 'apple', amount: 1 },
  { fruit: 'banana', amount: 2 },
  { fruit: 'coconut', amount: 3 },
  { fruit: 'durian', amount: 4 }
])

const s = JSON.stringify

s(columnOriented.column('fruit')) === s(rowOriented.column('fruit')) // true
s(columnOriented.column('amount')) === s(rowOriented.column('amount')) // true

Loading GeoJSON data

When loading GeoJSON FeatureCollections, the geometry data will end up in a column called $geometry:

const geojson = new DataContainer({
  type: 'FeatureCollection',
  features: [
    {
      type: 'Feature',
      geometry: {
        type: 'Point', coordinates: [0, 0]
      },
      properties: {
        fruit: 'apple', amount: 1
      }
    }
  ]
})

geojson.column('$geometry') // [{ type: 'Point', coordinates: [0, 0] }]
geojson.column('fruit') // ['apple']

Options

The DataContainer constructor takes a second argument, which is an optional object of options:

const dataContainer = new DataContainer(data, { validate: true })
Option name Default value Type
validate true Boolean

Setting the validate option to false will disable column validation. This can save a bit of time when you are certain your data is completely valid (i.e. all columns have only one data type and contain at least one valid that is not NaN, undefined, null, or Infinity) or don't care if it is. More options might be added in the future.

Keying

The DataContainer automatically generates a key for each row when data is loaded. These keys are preserved during transformations like arrange and filter to keep track of which row is which:

const dataContainer = new DataContainer({ a: [2, 4, 6, 8, 10, 12, 14] })
dataContainer.keys() // ['0', '1', '2', '3', '4', '5', '6']
const transformed = dataContainer.filter(row => row.a > 10)
transformed.keys() // ['5', '6']

Furthermore, retrieving, updating or deleting rows (see accessing data) can be done either by index or by key:

const dataContainer = new DataContainer({ a: [2, 4, 6, 8, 10, 12, 14] })
const transformed = dataContainer.filter(row => row.a > 10)
transformed.row({ index: 0 }) // { a: 12, $key: '5' }
transformed.row({ key: '5' }) // { a: 12, $key: '5' }

Automatically generated keys are always strings. Besides using automatically generated keys, it is also possible to use a column present in the data as custom key (see setKey). When using a custom key, it is recommended to use a column with primitive values, like quantitative or categorical columns, containing respectively Numbers and Strings. Other types and objects like dates are possible too, since the key functionality internally uses a Map. Beware, however, that a reference to the exact same (and not an equivalent) object must be used in this case to retrieve, update or delete rows.

# DataContainer.keys()

Returns an array of keys:

const dataContainer = new DataContainer({ a: [100, 200, 300], b: ['a', 'b', 'c'] })
dataContainer.keys() // ['0', '1', '2']

# DataContainer.setKey(columnName)

Sets a column as key:

const dataContainer = new DataContainer({ a: [100, 200, 300], b: ['a', 'b', 'c'] })
dataContainer.setKey('b')
dataContainer.keys() // ['a', 'b', 'c']

# DataContainer.resetKey()

Resets the current keys:

const dataContainer = new DataContainer({ a: [100, 200, 300], b: ['a', 'b', 'c'] })
dataContainer.setKey('b')
dataContainer.keys() // ['a', 'b', 'c']
dataContainer.resetKeys()
dataContainer.keys() ['0', '1', '2']

Also works to reset keys after a transformation:

const dataContainer = new DataContainer({ a: [2, 4, 6, 8, 10, 12, 14] })
const transformed = dataContainer.filter(row => row.a > 10)
transformed.keys() // ['5', '6']
transformed.resetKey()
transformed.keys() // ['0', '1']

Accessing data

# DataContainer.data()

Returns whatever data is currently loaded to the DataContainer in a column-oriented format.

const dataContainer = new DataContainer([
  { fruit: 'apple', amount: 1 },
  { fruit: 'banana', amount: 2 }
])

dataContainer.data() // { fruit: ['apple', 'banana'], amount: [1, 2], $key: ['0', '1'] }

# DataContainer.row(accessorObject)

Returns an object representing a row. accessorObject is either { index: <Number> } or { key: <key value> }.

const dataContainer = new DataContainer({ fruit: ['apple', 'banana'], amount: [1, 2] })
dataContainer.row({ index: 0 }) // { fruit: 'apple', amount: 1, $key: '0' }

# DataContainer.rows()

Returns an Array of rows.

const dataContainer = new DataContainer({ fruit: ['apple', 'banana'], amount: [1, 2] })
dataContainer.rows() 
/* [
 *   { fruit: 'apple', amount: 1, $key: '0' },
 *   { fruit: 'banana', amount: 2, $key: '1' },
 * ] 
 */

# DataContainer.column(columnName)

Returns a column as an Array.

const dataContainer = new DataContainer({ fruit: ['apple', 'banana'], amount: [1, 2] })
dataContainer.column('fruit') // ['apple', 'banana']
dataContainer.keys() // ['0', '1']

# DataContainer.map(columnName, func)

Equivalent to .column(columnName).map(func)

# DataContainer.nrow()

Returns the number of rows.

# DataContainer.columnNames()

Returns the names of the currently loaded columns.

Domains and types

# DataContainer.domain(columnName)

Returns the domain of a column.

const dataContainer = new DataContainer({
  fruit: ['apple', 'banana', 'apple', 'banana'],
  quantity: [1, 2, 3, 4],
  dayOfSale: [new Date(2019, 4, 3), new Date(2019, 4, 4), new Date(2019, 4, 5), new Date(2019, 4, 6)]
})

dataContainer.domain('fruit') // ['apple', 'banana']
dataContainer.domain('quantity') // [1, 4]
dataContainer.domain('dayOfSale') // [Date Fri May 03 2019 ..., Date Mon May 06 2019 ...]

For geometry data (.domain('$geometry')), this will return the bounding box.

# DataContainer.min(columnName)

Equivalent to domain(columnName)[0]. Only works for quantitative and interval columns.

# DataContainer.max(columnName)

Equivalent to domain(columnName)[1]. Only works for quantitative and interval columns.

# DataContainer.bbox()

Equivalent to .domain('$geometry')

# DataContainer.type(columnName)

Returns the type of a column.

const dataContainer = new DataContainer({
  fruit: ['apple', 'banana', 'apple', 'banana'],
  quantity: [1, 2, 3, 4],
  dayOfSale: [new Date(2019, 4, 3), new Date(2019, 4, 4), new Date(2019, 4, 5), new Date(2019, 4, 6)]
})

dataContainer.type('fruit') // categorical
dataContainer.type('quantity') // quantitative
dataContainer.type('dayOfScale') // temporal

Checks

Some convenience functions to check data during development.

# DataContainer.hasColumn(columnName)

Checks if the DataContainer has a column.

# DataContainer.hasRow(accessorObject)

Checks if the DataContainer has a row. See .row for an explanation of the accessorObject.

const dataContainer = new DataContainer({ a: [1, 2, 3, 4] })
dataContainer.hasColumn('a') // true
dataContainer.hasColumn('b') // false

# DataContainer.columnIsValid(columnName)

Check if a column is valid.

const dataContainer = new DataContainer({ a: [1, NaN, 3] })
  .mutate({ b: () => NaN })

dataContainer.columnIsValid('a') // true
dataContainer.columnIsValid('b') // false

# DataContainer.validateColumn(columnName)

Similar to columnIsValid, but throws an error if a column is invalid, instead of returning false. ,

const dataContainer = new DataContainer({ a: [1, NaN, 3] })
  .mutate({ b: () => NaN })

dataContainer.validateColumn('a') // nothing happens
dataContainer.validateColumn('b') // throws error

# DataContainer.validateAllColumns()

When data is first loaded to the DataContainer, validateAllColumns is ran by default. To avoid wasting time on checks after every subsequent transformation, it is not ran after that. If you want DataContainer to throw an error if any data has somehow become invalid, you can call this method manually. Invalid here means that they contain mixed types, or only have invalid data like NaN.

Transformations

DataContainer's transformations are heavily inspired by R's dplyr (part of the tidyverse). All transformations will return a new DataContainer.

# DataContainer.select(selectInstructions)

select returns a DataContainer with only the columns specified in selectInstructions. selectInstructions can be a single String column name, or an Array of column names.

const dataContainer = new DataContainer({
  fruit: ['apple', 'banana'],
  quantity: [1, 2],
  dayOfSale: [new Date(2019, 4, 3), new Date(2019, 4, 4)]
})

const withoutDayOfSale = dataContainer.select(['fruit', 'quantity'])
withoutDayOfSale.data() // { fruit: ['apple', 'banana'], quantity: [1, 2], $key: ['0', '1'] }

# DataContainer.rename(renameInstructions)

rename is used to rename columns. renameInstructions must be an object with current column names as keys, and desired new column names as values.

const dataContainer = new DataContainer({ f: ['apple', 'banana'], a: [1, 2] })
const renamed = dataContainer.rename({ f: 'fruit', a: 'amount' })
renamed.column('fruit') // ['apple', 'banana']

# DataContainer.filter(filterFunction)

filter will throw away all rows that do not satisfy the condition expressed in the filterFunction.

const dataContainer = new DataContainer({ fruit: ['apple', 'banana'], amount: [1, 2] })
dataContainer.filter(row => row.fruit !== 'banana').data() // { fruit: ['apple'], amount: [1], $key: ['0'] }

# DataContainer.dropNA(dropNAInstructions)

dropNA is essentially a special case of filter that disposes of invalid values like NaN, null or undefined. dropNAInstructions can be

  • nothing, in which case it will dispose of all rows in all columns that contain invalid values
  • a String value with a column name. All rows that have invalid values in this column will be removed
  • an Array of column names (Strings). All rows that have invalid values in any of these columns will be removed
const dataContainer = new DataContainer(
  { a: [1, 2, undefined, 4], b: [5, null, 7, 8], c: [NaN, 10, 11, 12] }
)

dataContainer.dropNA().data() // { a: [4], b: [8], c: [12], $key: [3] }
dataContainer.dropNA(['a', 'b']).data() // { a: [1, 4], b: [5, 8], c: [NaN, 12], $key: ['0', '3'] }

# DataContainer.arrange(arrangeInstructions)

arrange is used to sort data. arrangeInstructions can be an

  • Object, with exactly one key (the column by which to sort) and one value (how to sort it, either 'ascending', 'descending' or a compareFunction)
  • Array, containing Objects as described in the line above
const dataContainer = new DataContainer({
  fruit: ['apple', 'banana', 'coconut' 'durian', 'coconut', 'banana'],
  amount: [4, 3, 7, 2, 4, 5]
})

const arranged = dataContainer.arrange([ { fruit: 'descending' }, { amount: 'ascending' } ])
arranged.data()
/* {
 *   fruit: ['durian', 'coconut', 'coconut', 'banana', 'banana', 'apple']
 *   value: [2, 4, 7, 3, 5, 4],
 *   $key: ['3', '4', '2', '1', '5', '0']
 * } */

# DataContainer.mutate(mutateInstructions)

mutate can be used to generate a new column based on existing rows. mutateInstructions must be an object with new column names as keys, and functions showing how to calculate the new column as values.

const dataContainer = new DataContainer({ a: [1, 2, 3, 4 ]})
const dataContainerWithASquared = dataContainer.mutate({ aSquared: row => row.a * row.a })
dataContainerWithASquared.column('aSquared') // [1, 4, 9, 16]

# DataContainer.transmute(mutateInstructions)

Same as mutate, except that it removes all the old columns.

# DataContainer.groupBy(groupByInstructions)

Used to split up a DataContainer in several DataContainers based on different categories. groupByInstructions can be a String containing a single column name, or an Array containing multiple column names.

const dataContainer = new DataContainer(
  { fruit: ['apple', 'banana', 'banana', 'apple'], amount: [10, 5, 13, 9] }
)

const grouped = dataContainer.groupBy('fruit')
grouped.column('fruit') // ['apple', 'banana']
grouped.column('$grouped') // [<DataContainer>, <DataContainer>]
grouped.map('$grouped', group => group.column('amount')) // [[10, 9], [5, 13]]

# DataContainer.bin(binInstructions)

Used to split up a DataContainer in several DataContainers based on classification of quantitative data.

const dataContainer = new DataContainer(
  { a: [1, 2, 3, 4, 5, 6, 7], b: [8, 9, 10, 11, 12, 13, 14] }
)

const binned = dataContainer.bin({ column: 'a', method: 'EqualInterval', numClasses: 3 })
binned.column('bins') // [[1, 3], [3, 5], [5, 7]]
binned.type('bins') // 'interval'
binned.row(1).$grouped.rows() // [{ a: 3, b: 10, $key: '2' }, { a: 4, b: 11, $key: '3' }]

Besides 'EqualInterval', other methods of classification are supported. Different methods might require different additional keys to be passed to binInstructions. See the table below for an overview.

Class. method option name
'EqualInterval' numClasses
'StandardDeviation' numClasses
'Quantile' numClasses
'Jenks' numClasses
'CKMeans' numClasses
'IntervalSize' binSize
'Manual' manualClasses

For 'Manual', manualClasses is required and must be an Array of intervals, which will become the bins. The classification is performed internally by classify-series.

It is also possible to bin over multiple dimensions by providing an Array of binInstructions. Instead of a single bins column, this will create multiple columns called bins_<original column name>:

const dataContainer = new DataContainer({
  a: [1, 2, 3, 5, 1, 2, 3, 5, 1, 2, 3, 5, 1, 2, 3, 5],
  b: [1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 5, 5, 5, 5]
})

const binned = dataContainer.bin([
  { column: 'a', method: 'IntervalSize', binSize: 2 },
  { column: 'b', method: 'IntervalSize', binSize: 2 }
])

console.log(binned.column('bins_a')) // [[1, 3], [1, 3], [3, 5], [3, 5]]
console.log(binned.column('bins_b')) // [[1, 3], [3, 5], [1, 3], [3, 5]]

# DataContainer.summarise(summariseInstructions)

Used to summarise columns. You can also use summarize if you prefer. summariseInstructions must be an Object with new column names as keys, and columnInstruction Objects as values. These columnInstructions must have the name of the column that is to be summarised as key, and the summaryMethod as value. When applying summarise to a grouped DataContainer, the summaries of the groups will be taken.

const dataContainer = new DataContainer({ a: [1, 2, 3, 4], b: ['a', 'b', 'a', 'b'] })
const grouped = dataContainer.groupBy('b')

dataContainer.summarise({ mean_a: { a: 'mean' }}).data() // { mean_a: [2.5], $key: ['0'] }
grouped.summarise({ mean_a: { a: 'mean' } }).data() // { b: ['a', 'b'], mean_a: [2, 3], $key: ['0', '1'] }

The following summaryMethods are available:

  • count
  • sum
  • mean
  • median
  • mode
  • min
  • max

It also possible to create your own summaryMethod by providing a function that receives the requested column as first argument, and returns a value that's either quantitative, categorical, temporal or an interval.

# DataContainer.mutarise(mutariseInstructions)

mutarise (or mutarize) is similar to summarise, but instead of collapsing the data to a single row, the summarised value will be added as a new column.

const dataContainer = new DataContainer({ a: [1, 2, 3, 4], b: ['a', 'b', 'a', 'b'] })
const mutarised = dataContainer.groupBy('b').mutarise({ mean_a: { a: 'mean' } })
mutarised.column('a') // [1, 2, 3, 4]
mutarised.column('mean_a') // [2, 3, 2, 3]

# DataContainer.transform(transformFunction)

Used for arbitrary transformations on the data. transformFunction receives an Object with all the columns currently loaded, and should return another object with columns.

const dataContainer = new DataContainer({ a: [1, 2, 3, 4] })
const transformed = dataContainer.transform(columns => {
  const b = columns.a.map(a => a ** 2)
  return { b }
})

transformed.column('b') // [1, 4, 9, 16]

# DataContainer.reproject(reprojectFunction)

Used to reproject data in the $geometry column. Can only be used when a $geometry column is present. reprojectFunction should be a function that accepts an Array of two Numbers and returns an Array of two Numbers. Particularly convenient to use with proj4:

const reprojectFunction = proj4('EPSG:4326', 'EPSG:3857').forward
const dataContainer = new DataContainer(geojson).reproject(reprojectFunction)

# DataContainer.cumsum(cumsumInstructions, { asInterval: false })

Calculates the cumulative sum of one or more columns. cumsumInstructions is an Object with new column names as keys, and the columns on which the cumulative sum should be based as values. Only quantitative columns can be used.

const dataContainer = new DataContainer({ a: [1, 2, 3, 4] })
dataContainer.cumsum({ cumsum_a: 'a' }).column('cumsum_a') // [1, 3, 6, 10]

If asInterval is set to true, intervals instead of integers will be returned (mimicking d3's stack):

const dataContainer = new DataContainer({ a: [1, 2, 3, 4] })
  .cumsum({ cumsum_a: 'a' }, { asInterval: true })
dataContainer.column('cumsum_a') // [[0, 1], [1, 3], [3, 6], [6, 10]]

# DataContainer.rowCumsum(cumsumInstructions, { asInterval: false })

Calculates the cumulative sum over all rows for the selected columns. cumsumInstructions is an Array with either:

  1. column names (Strings)
  2. Objects with only one key and one value, where the key is the new column name and the value the old.

In the case of 1., the old columns will be overwritten.

const dataContainer = new DataContainer({
  a: [1, 2, 3, 4],
  b: [1, 2, 3, 4],
  c: [1, 2, 3, 4]
}).rowCumsum(['a', 'b', { rowCumsum_c: 'c' }])

dataContainer.column('a') // [1, 2, 3, 4]
dataContainer.column('b') // [2, 4, 6, 8]
dataContainer.column('rowCumsum_c') // [3, 6, 9, 12]
dataContainer.column('c') // [1, 2, 3, 4]

rowCumsum's asInterval functionality works similar to cumsum's.

# DataContainer.pivotLonger(pivotInstructions)

Pivots 'wide' data to 'long' data- meaning that a set of selected columns and their values will be converted to two columns: one containing the selected columns' names, and one containing their values.

const dataContainer = new DataContainer({
  col1: [1, 2],
  col2: [10, 20],
  col3: ['a', 'b'],
  col4: ['aa', 'bb']
}).pivotLonger({
  columns: ['col3', 'col4'],
  namesTo: 'name',
  valuesTo: 'value'
})

dataContainer.column('col1') // [1, 1, 2, 2]
dataContainer.column('name') // ['col3', 'col4', 'col3', 'col4']
dataContainer.column('value') // ['a', 'aa', 'b', 'bb']

# DataContainer.pivotWider(pivotInstructions)

The opposite of pivotLonger: pivots 'long' to 'wide' data, meaning that two columns will be converted into a larger set of columns. Missing values in the wider data will be filled with null. This default behavior can be changed by providing a valuesFill option in de pivotInstructions object, besides the namesFrom and valuesFrom options.

const dataContainer = new DataContainer({
  idCol: ['a', 'a', 'b', 'b', 'b', 'c', 'c'],
  names: ['x', 'y', 'x', 'y', 'z', 'x', 'z'],
  values: [1, 2, 10, 20, 30, 100, 300]
}).pivotWider({
  namesFrom: 'names',
  valuesFrom: 'values'
})

dataContainer.column('idCol') // ['a', 'b', 'c']
dataContainer.column('x') // [1, 10, 100]
dataContainer.column('y') // [2, 20, null]

Adding and removing rows

All of these functions work in-place.

# DataContainer.addRow(row)

Adds a new row to the DataContainer. row must be an object with one key for every column.

const dataContainer = new DataContainer({ a: [1, 2, 3], b: ['a', 'b', 'c'] })
dataContainer.addRow({ a: 4, b: 'd' })
dataContainer.column('b') // ['a', 'b', 'c', 'd']

# DataContainer.updateRow(accessorObject, row)

Updates an existing row. accessorObject is either { index: <Number> } or { key: <key value> } (see keying).

const dataContainer = new DataContainer({ a: [1, 2, 3], b: ['a', 'b', 'c'] })
dataContainer.updateRow({ index: 2 }, { a: 100 })
dataContainer.column('a') // [1, 2, 100]

Instead of using an Object as the second argument, it is also possible to use a Function. This function will receive the existing row as first argument, and must return an Object.

const dataContainer = new DataContainer({ a: [1, 2, 3], b: ['a', 'b', 'c'] })
dataContainer.updateRow({ index: 2 }, row => ({ a: row.a + 100 }))
dataContainer.column('a') // [1, 2, 103]

# DataContainer.deleteRow(accessorObject)

Deletes an existing row.

const dataContainer = new DataContainer({ a: [1, 2, 3], b: ['a', 'b', 'c'] })
dataContainer.deleteRow({ index: 2 })
dataContainer.column('a') // [1, 2]

Adding and removing columns

All of these functions work in-place.

# DataContainer.addColumn(columnName, column)

Adds a new column. column must be an Array of the same length as the rest of the data.

const dataContainer = new DataContainer({ a: [1, 2, 3], b: ['a', 'b', 'c'] })
dataContainer.addColumn('c', [4, 5, 6])
dataContainer.column('c') // [4, 5, 6]

# DataContainer.replaceColumn(columnName, column)

Equivalent to calling .deleteColumn followed by addColumn.

# DataContainer.deleteColumn(columnName)

Deletes an existing column.

const dataContainer = new DataContainer({ a: [1, 2, 3], b: ['a', 'b', 'c'] })
dataContainer.deleteColumn('b')
dataContainer.data() // { $key: ['0', '1', '2'], a: [1, 2, 3] }

Classification

# DataContainer.bounds(binInstructions)

Returns an array containing the boundaries of the classes found by the classification/binning algorithm. See bin for the structure of binInstructions.

const dataContainer = new DataContainer({ a: [1, 2, 3, 4, 5, 6, 7] })
dataContainer.bounds(
  { column: 'a', method: 'EqualInterval', numClasses: 3 }
) // [3, 5]

# DataContainer.fullBounds(binInstructions)

Similar to .bounds, but returns the minimum and maximum value as well:

const dataContainer = new DataContainer({ a: [1, 2, 3, 4, 5, 6, 7] })
dataContainer.fullBounds(
  { column: 'a', method: 'EqualInterval', numClasses: 3 }
) // [1, 3, 5, 7]

# DataContainer.boundRanges(binInstructions)

Similar to .fullBounds, but different format:

const dataContainer = new DataContainer({ a: [1, 2, 3, 4, 5, 6, 7] })
dataContainer.boundRanges(
  { column: 'a', method: 'EqualInterval', numClasses: 3 }
) // [[1, 3], [3, 5], [5, 7]]

# DataContainer.classify(binInstructions, range)

Returns a threshold scale based on the bounds determined by classification/binning algorithm, with any type of range. range must be an array with the same length as numClasses in the binInstructions.

const dataContainer = new DataContainer({ a: [1, 2, 3, 4, 5, 6, 7] })
const scale = dataContainer.classify(
  { column: 'a', method: 'EqualInterval', numClasses: 3 },
  ['red', 'blue', 'green']
)

scale(2) // 'red'
scale(4) // 'blue'
scale(6) // 'green'