Commit 58c2751e authored by S Anand's avatar S Anand

ENH: scales with reverse, discrete scale. Fixes #65, #171, #172

@tejesh.p
parent f78adf7e
Pipeline #93550 passed with stage
in 2 minutes and 58 seconds
**g1.scale**
Scales is an abstraction over d3-scales and d3-scale-chromatic libraries using configurations.
Background: Scales can be thought of functions that maps input (`domain`) of numbers/categoricals to output(`range`) of numbers/categoricals. The output range can be colors, height/width pixel values of a chart.
```html
<script src="https://cdn.jsdelivr.net/combine/npm/d3-scale,npm/d3-scale-chromatic,npm/g1"></script>
```
External dependpencies: [d3-scale](https://github.com/d3/d3-scale/) and [d3-scale-chromatic](https://github.com/d3/d3-scale-chromatic)
[Read the documentation](https://learn.gramener.com/guide/g1/scales) for usage.
([source](docs/scales.md))
Scales are dictionaries with the following keys:
- `scale:` d3 scale to use. Defaults to `'linear'`.
- `domain`: a list that contains the scale's domain or `{metric: 'col_name'}`
- `range`: a list that contains the scale's range or `{scheme: 'schemeName', count: k}`. `k` can be between 3-11 (Refer `k` value for schemes in https://github.com/d3/d3-scale-chromatic).
`scale` can take any of the values from below scale types.
**Scale Types**:
- **Quantitative Scales**
- `Linear`
- `Log`
- `Pow`
- `Sqrt`
- `Sequential`
- **Discrete Scales**
- `Ordinal`
- `Band`
- `Point`
- **Discretizing Scales**
- `Quantile`
- `Quantize`
- `Threshold`
Linear scale mapping numericals to colors (Red white Green)
```js
var numericals_to_color = g1.scale(data, {
scale: 'linear', // default
domain: [0, 50, 100], // can be {metric: 'age'} to auto-calculate domain
range: ['red', 'white', 'green']
})
numericals_to_color('0') // "#efedf5"
numericals_to_color('50') // "#bcbddc"
numericals_to_color('100') // "#756bb1"
```
To map qualitative data to color scheme
```js
var categorical_to_colorscheme = g1.scale(data, {
domain: ['hyd', 'chennai', 'bnglr', 'delhi'], // can be {metric: 'city'} to auto-calculate domain
scale: 'Ordinal',
range: {scheme: 'purples', count: 3}
})
categorical_to_colorscheme('hyd') // "#756bb1"
categorical_to_colorscheme('chennai') // "#bcbddc"
categorical_to_colorscheme('bnglr') // "#efedf5"
categorical_to_colorscheme('delhi') // "#756bb1" (rotates to first color because count is 3)
```
To map qualitative data to *reverse* color scheme
```js
var categorical_to_colorscheme = g1.scale(data, {
domain: [], // if left empty, first categorical value will be mapped to first color from range
scale: 'Ordinal',
range: {
scheme: 'schemePurples',
count: 3,
reverse: true
}
})
categorical_to_colorscheme('apple') // "#756bb1"
categorical_to_colorscheme('orange') // "#bcbddc"
categorical_to_colorscheme('grapes') // "#efedf5"
```
To map continuous data (numericals) to color schemes:
**Notes**:
- Quantile scale divides *total data points* to bins, with each bin containing equal number of data points.
- Quantile scale takes domain as entire dataset. Metric is mandatory and should be continuous data.
```js
var quantile_scale = g1.scale(data, {
domain: {metric: 'age'},
scale: 'Quantile',
range: {scheme: 'BrBG', count: 3}
})
```
**Notes**:
- Quantize scale divides *the domain* into bins of equi value (not count).
```js
var quantize_scale = g1.scale([], {
domain: [0, 100],
scale: 'Quantize',
range: {
scheme: 'Purples',
count: 3,
reverse: true
}
})
quantize_scale(15) // "#efedf5" [0 - 33.3]
quantize_scale(40) // "#bcbddc" [33.3 - 66.6]
quantize_scale(70) // "#756bb1" [66.6 - 100]
var quantize_scale = g1.scale(data, {
domain: {metric: 'age'},
scale: 'Quantize',
range: {
scheme: 'schemePurples',
count: 3
}
})
quantize_scale(15) // "#756bb1"
quantize_scale(40) // "#bcbddc"
quantize_scale(70) // "#efedf5"
```
**Notes**:
- Threshold scales should have `n+1` values in range, when `n` is the number of values in domain.
```js
var quantize_scale = g1.scale([], {
scale: 'Threshold',
domain: [35],
range: ['red', 'green'] // <35 maps to Red, >35 maps to Green
})
quantize_scale(25) // 'red'
quantize_scale(75) // 'green'
var quantize_scale = g1.scale([], {
scale: 'Threshold',
domain: [35],
range: ['red', 'green'], // <35 maps to Red, >35 maps to Green
reverse: true
})
quantize_scale(25) // 'green'
quantize_scale(75) // 'red'
```
import uglify from 'rollup-plugin-uglify'
import { uglify } from 'rollup-plugin-uglify'
import htmlparts from './rollup-plugin-htmlparts.js'
import resolve from 'rollup-plugin-node-resolve'
import commonjs from 'rollup-plugin-commonjs'
......@@ -6,7 +6,7 @@ import babel from 'rollup-plugin-babel'
import babelrc from 'babelrc-rollup'
const babelConfig = {
'presets': ['env']
'presets': ['es2015']
}
export default [
......@@ -22,7 +22,8 @@ export default [
resolve(),
commonjs(),
htmlparts('src/formhandler.template.html'),
htmlparts('src/dropdown.template.html')
htmlparts('src/dropdown.template.html'),
babel(babelrc({ config: babelConfig, exclude: 'node_modules/**' }))
]
},
{
......@@ -39,88 +40,85 @@ export default [
commonjs(),
htmlparts('src/formhandler.template.html'),
htmlparts('src/dropdown.template.html'),
uglify({ sourceMap: true })
babel(babelrc({ config: babelConfig, exclude: 'node_modules/**' })),
uglify({ sourcemap: true })
]
},
{
input: "index-datafilter",
output: { file: "dist/datafilter.min.js", format: "umd", name: "g1", extend: true, sourcemap: true },
plugins: [uglify({ sourceMap: true })]
plugins: [uglify({ sourcemap: true })]
},
{
input: "index-urlfilter",
output: { file: "dist/urlfilter.min.js", format: "umd", name: "g1", extend: true, sourcemap: true },
plugins: [uglify({ sourceMap: true })]
plugins: [uglify({ sourcemap: true })]
},
{
input: "index-formhandler",
output: { file: "dist/formhandler.min.js", format: "umd", name: "g1", extend: true, sourcemap: true },
plugins: [
htmlparts('src/formhandler.template.html'),
process.env.npm_lifecycle_event == 'dev' ? '' : uglify({ sourceMap: true })
process.env.npm_lifecycle_event == 'dev' ? '' : uglify({ sourcemap: true })
],
},
{
input: "index-highlight",
output: { file: "dist/highlight.min.js", format: "umd", name: "g1", extend: true, sourcemap: true },
plugins: [uglify({ sourceMap: true })]
plugins: [uglify({ sourcemap: true })]
},
{
input: "index-template",
output: { file: "dist/template.min.js", format: "umd", name: "g1", extend: true, sourcemap: true },
plugins: [uglify({ sourceMap: true })]
plugins: [uglify({ sourcemap: true })]
},
{
input: "index-translate",
output: { file: "dist/translate.min.js", format: "umd", name: "g1", extend: true, sourcemap: true },
plugins: [uglify({ sourceMap: true })]
plugins: [uglify({ sourcemap: true })]
},
{
input: "index-event",
output: { file: "dist/event.min.js", format: "umd", name: "g1", extend: true, sourcemap: true },
plugins: [uglify({ sourceMap: true })]
plugins: [uglify({ sourcemap: true })]
},
{
input: "index-leaflet",
output: { file: "dist/leaflet.min.js", format: "umd", name: "g1", extend: true, sourcemap: true },
plugins: [uglify({ sourceMap: true })]
plugins: [uglify({ sourcemap: true })]
},
{
input: "index-mapviewer",
input: "index-scale",
output: {
file: "dist/mapviewer.min.js", format: "umd", name: "g1", extend: true, sourcemap: true, globals: {
leaflet: 'L',
d3: 'd3'
}
file: "dist/scale.min.js", format: "umd", name: "g1", extend: true, sourcemap: true, globals: { d3: 'd3' }
},
plugins: [
resolve(),
commonjs(),
babel(babelrc({ config: babelConfig, exclude: 'node_modules/**' })),
process.env.npm_lifecycle_event == 'dev' ? '' : uglify({ sourceMap: true })
babel(babelrc({ config: babelConfig, exclude: 'node_modules/**' })), // for d3.extent
process.env.npm_lifecycle_event == 'dev' ? '' : uglify({ sourcemap: true })
],
// indicate which modules should be treated as external
external: ['leaflet', 'd3']
external: ['d3']
},
{
input: "index-sanddance",
output: { file: "dist/sanddance.min.js", format: "umd", name: "g1", extend: true, sourcemap: true },
plugins: [uglify({ sourceMap: true })]
},
{
input: "index-scale",
output: { file: "dist/scale.min.js", format: "umd", name: "g1", extend: true, sourcemap: true },
plugins: [uglify({ sourceMap: true })],
plugins: [
resolve(),
commonjs(),
babel(babelrc({ config: babelConfig, exclude: 'node_modules/**' })),
uglify({ sourcemap: true })
]
},
{
input: "index-urlchange",
output: { file: "dist/urlchange.min.js", format: "umd", name: "g1", extend: true, sourcemap: true },
plugins: [uglify({ sourceMap: true })]
plugins: [uglify({ sourcemap: true })]
},
{
input: "index-ajax",
output: { file: "dist/ajax.min.js", format: "umd", name: "g1", extend: true, sourcemap: true },
plugins: [uglify({ sourceMap: true })]
plugins: [uglify({ sourcemap: true })]
},
{
input: "index-dropdown",
......@@ -129,7 +127,7 @@ export default [
resolve(),
commonjs(),
htmlparts('src/dropdown.template.html'),
uglify({ sourceMap: true })
uglify({ sourcemap: true })
]
},
{
......@@ -138,17 +136,36 @@ export default [
plugins: [
resolve(),
commonjs(),
uglify({ sourceMap: true })
uglify({ sourcemap: true })
],
},
{
input: "index-fuzzysearch",
output: { file: "dist/fuzzysearch.min.js", format: "umd", name: "g1", extend: true, sourcemap: true },
plugins: [uglify({ sourceMap: true })]
plugins: [uglify({ sourcemap: true })]
},
{
input: "index-types",
output: { file: "dist/types.min.js", format: "umd", name: "g1", extend: true, sourcemap: true },
plugins: [uglify({ sourceMap: true })]
plugins: [uglify({ sourcemap: true })]
},
{
input: "index-mapviewer",
output: {
file: "dist/mapviewer.min.js", format: "umd", name: "g1", extend: true, sourcemap: true, globals: {
leaflet: 'L',
d3: 'd3'
}
},
plugins: [
resolve(),
commonjs(),
babel(babelrc({
config: {
'presets': ['env']
}, exclude: 'node_modules/**' })),
process.env.npm_lifecycle_event == 'dev' ? '' : uglify({ sourcemap: true })
],
external: ['leaflet', 'd3']
}
]
......@@ -21,45 +21,93 @@ var color_scale = g1.scale({
- scale: d3 scale to use. Defaults to linear
- range: set the range of the scale
- domain: override the domain (which defaults to the extent of the data metric)
*/
export {scale}
NEW SPEC:
- scale: 'linear' or 'quantile'
- domain: {metric: 'col_name'}
- range: for array or {scheme: 'Blues', count: 6}
function scale (data, config) {
return function(val) {
var result, scale, color
var metricFormula = typeof config.metric == 'function' ? config.metric
: function(d) { return d[config.metric]}
TODO:
- Support multi-metric domains
- Support order_by on aggregate of metrics
- Support clamp, round for quantitative scales
- Support nice
*/
if (config.scheme) {
color = config.scheme
if (color.lastIndexOf('scheme', 0) !== 0) {
color = 'interpolate' + color
}
}
import uniq from 'lodash-es/uniq'
import upperFirst from 'lodash-es/upperFirst'
import iteratee from 'lodash-es/iteratee'
import { extent } from 'd3-array'
if (config.scale) {
scale = config.scale.replace(/\w+/g,
function(w){
return w[0].toUpperCase() + w.slice(1).toLowerCase()
})
} else if (color) {
scale = 'Sequential'
} else {
scale = 'Linear'
}
var domain = config.domain || d3.extent(data, metricFormula)
if (color) {
result = d3['scale' + scale](d3[color])
.domain(domain)
} else if (config.range) {
result = d3['scale' + scale]()
.domain(domain)
.range(config.range)
var scale_types = {
'Ordinal': 'discrete',
'Band': 'discrete',
'Point': 'discrete',
'Linear': 'continuous',
'Log': 'continuous',
'Pow': 'continuous',
'Sqrt': 'continuous',
'Sequential': 'continuous',
'Quantile': 'quantile', // Exception because domain is entire dataset
'Quantize': 'discretizing',
'Threshold': 'discretizing'
},
renames = { Sequential: 'Linear' },
domain_function = {
discrete: uniq,
quantile: d => d,
discretizing: extent,
continuous: extent
}
return result(metricFormula(val))
function backward_compat(config, _scale) {
if (config.scheme && !Array.isArray(config.range)) {
config.range = { scheme: config.scheme }
if (scale_types[_scale] != 'continuous' && config.count) config.range.count = config.count
}
if (!config.domain) config.domain = { metric: config.metric }
config.metric = config.domain.metric || config.metric
return config
}
function get_domain(data, config_domain, metric, scale) {
return Array.isArray(config_domain) && scale != 'Quantile' ? config_domain
: domain_function[scale_types[scale]](data.map(iteratee(metric)))
}
function get_range(config_range, _scale) {
return Array.isArray(config_range) ? config_range
: scale_types[_scale] == 'continuous' ? [0, 1]
: get_colors_from_scheme(get_scheme(config_range.scheme), config_range.count)
}
function flip(array, reverse) {
return reverse ? array.slice().reverse() : array
}
function get_scheme(scheme) {
return d3[scheme.startsWith('scheme') ? scheme : 'scheme' + upperFirst(scheme)]
}
function get_colors_from_scheme(scheme, count) {
// For discrete colors ranges, like schemeBlues, k value is [3, 9]. So, the first 3 values are empty
return Object.values(scheme).length !== scheme.length ? scheme[count ? count : scheme.length - 1] : scheme
}
function scale(data, config) {
var _scale = upperFirst(config.scale || 'Linear')
config = backward_compat(config, _scale)
var result = d3['scale' + (renames[_scale] || _scale)]()
.domain(get_domain(data, config.domain, config.metric, _scale))
.range(flip(get_range(config.range, _scale), config.range.reverse === true || config.reverse))
return (scale_types[_scale] == 'continuous' && !Array.isArray(config.range)) ?
(val) => d3['interpolate' + config.range.scheme](result(typeof val === 'object' ? iteratee(config.metric)(val) : val))
: (val) => result(typeof val === 'object' ? iteratee(config.metric)(val) : val)
}
export { scale }
This diff is collapsed.
......@@ -20,6 +20,9 @@
<body>
<h1>Lodash templates</h1>
<template id="t1">Your platform is <%= navigator.userAgent %></template>
<script>
tape.onFinish(function () { window.renderComplete = true })
</script>
<script>
function strip(text) {
return text.replace(/^\s+/, '').replace(/\s+$/, '')
......
This diff is collapsed.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment