Skip to content

Commit

Permalink
Merge pull request #176 from mhkeller/sort-domain-bool
Browse files Browse the repository at this point in the history
Add `x|y|z|rDomainSort` props. Update `calcUniques`.
  • Loading branch information
mhkeller authored Mar 12, 2024
2 parents 01dc1bb + c49e479 commit 628ad2b
Show file tree
Hide file tree
Showing 12 changed files with 415 additions and 142 deletions.
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,16 @@
Changelog
===

# 8.1.0

> 2024-03-11
This adds `xDomainSort`, `yDomainSort`, `zDomainSort` and `rDomainSort` props that will kick in when the scale is ordinal. By default, unique value calculations return a sorted list. You can now use these props to set that to `false` and return the uniques in the order they appeared in the data.

Also updates the API to `calcUniques` helper function to also accept field-specific sort instructions.

* [PR#176](https://github.com/mhkeller/layercake/pull/176)

# 8.0.3

> 2024-03-07
Expand Down
106 changes: 61 additions & 45 deletions src/content/guide/03-layercake-props.md
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ If your `domain` or `range` includes values that are colors, the debug interface

### padding `Object`

An object that can specify `top`, `right`, `bottom`, or `left` padding in pixels. Any unspecified values are filled in as `0`. Padding operates like CSS `box-sizing: border-box;` where values are subtracted from the parent container's width and height, the same as [a D3 margin convention](https://bl.ocks.org/mbostock/3019563).
An object that can specify `top`, `right`, `bottom`, or `left` padding in pixels. Any unspecified values are filled in as `0`. Padding operates like CSS `box-sizing: border-box;` where values are subtracted from the parent container's width and height, the same as [a D3 margin convention](https://bl.ocks.org/mbostock/3019563). (It's not called "margin" here because the behavior is more like CSS padding which goes from the outer edge inward, whereas margin goes from the outer edge outward.)

```svelte
<LayerCake
Expand All @@ -141,8 +141,6 @@ An object that can specify `top`, `right`, `bottom`, or `left` padding in pixels
>
```

> Another way to set padding is to add it via normal CSS on your target div. The target element is assigned CSS of `box-sizing: border-box;` so padding settings won't affect the width or height. If you set any padding via CSS, the padding object will be ignored.
### xScale `d3.scaleLinear()`

The D3 scale that should be used for the x-dimension. Pass in an instantiated D3 scale if you want to override the default [`d3.scaleLinear()`](https://github.com/d3/d3-scale#scalelinear) or you want to add extra options.
Expand Down Expand Up @@ -205,29 +203,63 @@ Same as [xDomain](/guide#xdomain) but for the z scale.

Same as [xDomain](/guide#xdomain) but for the r scale.

### xReverse `Boolean=false`
### xDomainSort `Boolean=true`

Reverse the default x range. By default this is `false` and the range is `[0, width]`.
Taken into account only when the x-scale is ordinal. It sets whether the calculated unique items come back sorted. It uses [d3.ascending](https://d3js.org/d3-array/sort#ascending) to do the sort calculation.

This is ignored if you set [xRange](/guide#xrange).
Set this to `false` if you want the unique items to appear in the order they were found in the data.

### yReverse `Boolean=true`
### yDomainSort `Boolean=true`

Reverse the default y range. By default this is `true` and the range is `[height, 0]` unless using `scaleBand` for yScale in which case this is `false`.
Same as [xDomainSort](/guide#xdomainsort) but for the y domain.

This is ignored if you set [yRange](/guide#yrange).
### zDomainSort `Boolean=true`

### zReverse `Boolean=false`
Same as [xDomainSort](/guide#xdomainsort) but for the z domain.

Reverse the default z range. By default this is `false` and the range is `[0, width]`.
### rDomainSort `Boolean=true`

This is ignored if you set [zRange](/guide#zrange).
Same as [xDomainSort](/guide#xdomainsort) but for the r domain.

### rReverse `Boolean=false`
### xPadding `Array:[leftPixels: Number, rightPixels: Number]`

Reverse the default r range. By default this is `false` and the range is `[1, 25]`.
Assign a pixel value to add to the min or max of the x scale. This will increase the scales domain by the scale unit equivalent of the provided pixels. This is useful for adding extra space to a scatter plot so that your circles don't interfere with your y-axis. It's better than fussing with the range since you don't need to add a magic number to other components, like axes.

This is ignored if you set [rRange](/guide#rrange).
It will log out a warning if you try to use it on a scale that has a domain or range that isn't two items, such as with ordinal scales.

```svelte
<LayerCake
xPadding= { [10, 10] } // Add ten pixels of data units to both sides of the scale's domain
>
```

### yPadding `Array:[leftPixels: Number, rightPixels: Number]`

Same as [xPadding](/guide#xpadding) but for the y domain.

### zPadding `Array:[leftPixels: Number, rightPixels: Number]`

Same as [xPadding](/guide#xpadding) but for the z domain.

### rPadding `Array:[leftPixels: Number, rightPixels: Number]`

Same as [xPadding](/guide#xpadding) but for the r domain.

### xNice `Boolean=false|Number`

Applies D3's [scale.nice()](https://github.com/d3/d3-scale#continuous_nice) to the x domain. This is a separate option instead of being one you can apply to a passed in scale because D3's "nice" transformation only works on existing domains and does not use a state to be able to tell if your existing scale wants to be nice. Can also pass `count` number as argument for greater control.

### yNice `Boolean=false|Number`

Same as [xNice](/guide#xnice) but for the y domain.

### zNice `Boolean=false|Number`

Same as [xNice](/guide#xnice) but for the z domain.

### rNice `Boolean=false|Number`

Same as [xNice](/guide#xnice) but for the r domain.

### xRange `Function|Array:[min: Number, max: Number]|String[]|Number[]`

Expand Down Expand Up @@ -266,53 +298,37 @@ Same as [xRange](/guide#xrange) but for the r scale. Override the default y rang

This overrides setting [rReverse](/guide#rreverse) to `true`.

### xPadding `Array:[leftPixels: Number, rightPixels: Number]`

Assign a pixel value to add to the min or max of the x scale. This will increase the scales domain by the scale unit equivalent of the provided pixels. This is useful for adding extra space to a scatter plot so that your circles don't interfere with your y-axis. It's better than fussing with the range since you don't need to add a magic number to other components, like axes.

It will log out a warning if you try to use it on a scale that has a domain or range that isn't two items, such as with ordinal scales.

```svelte
<LayerCake
xPadding= { [10, 10] } // Add ten pixels of data units to both sides of the scale's domain
>
```

### yPadding `Array:[leftPixels: Number, rightPixels: Number]`

Same as [xPadding](/guide#xpadding) but for the y domain.

### zPadding `Array:[leftPixels: Number, rightPixels: Number]`
### xReverse `Boolean=false`

Same as [xPadding](/guide#xpadding) but for the z domain.
Reverse the default x range. By default this is `false` and the range is `[0, width]`.

### rPadding `Array:[leftPixels: Number, rightPixels: Number]`
This is ignored if you set [xRange](/guide#xrange).

Same as [xPadding](/guide#xpadding) but for the r domain.
### yReverse `Boolean=true`

### xNice `Boolean=false|Number`
Reverse the default y range. By default this is `true` and the range is `[height, 0]` unless using `scaleBand` for yScale in which case this is `false`.

Applies D3's [scale.nice()](https://github.com/d3/d3-scale#continuous_nice) to the x domain. This is a separate option instead of being one you can apply to a passed in scale because D3's "nice" transformation only works on existing domains and does not use a state to be able to tell if your existing scale wants to be nice. Can also pass `count` number as argument for greater control.
This is ignored if you set [yRange](/guide#yrange).

### yNice `Boolean=false|Number`
### zReverse `Boolean=false`

Same as [xNice](/guide#xnice) but for the y domain.
Reverse the default z range. By default this is `false` and the range is `[0, width]`.

### zNice `Boolean=false|Number`
This is ignored if you set [zRange](/guide#zrange).

Same as [xNice](/guide#xnice) but for the z domain.
### rReverse `Boolean=false`

### rNice `Boolean=false|Number`
Reverse the default r range. By default this is `false` and the range is `[1, 25]`.

Same as [xNice](/guide#xnice) but for the r domain.
This is ignored if you set [rRange](/guide#rrange).

### extents `Object`

Manually set the extents of the x, y or r scale as a two-dimensional array of the min and max you want. Setting values here will skip any dynamic extent calculation of the data for that dimension.
Manually set the extents of the x, y or r scale. Setting values here will skip any dynamic extent calculation of the data for that dimension. This is similar to setting a fixed domain using `xDomain`, `yDomain`, `rDomain` or `zDomain` with the exception that this prop has the performance improvement of skipping the domain calculation. It may be removed in future versions, however. See [Issue #179](https://github.com/mhkeller/layercake/issues/179).

```svelte
<LayerCake
extents={{ x: [0, 100], y: [50, 100] }}
extents={{ x: [0, 100], y: [50, 100], z: ['apple', 'carrot', 'ginger'] }}
>
```

Expand Down
42 changes: 40 additions & 2 deletions src/content/guide/99-helper-functions.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,9 +93,13 @@ console.log(extents);
*/
```

### calcUniques(flatData: `Array`, fields: `{x?: Function, y?: Function, z?: Function, r?: Function}`[, { sort: `Boolean=false`] }: Object)
### calcUniques(flatData: `Array`, fields: `{x?: Function, y?: Function, z?: Function, r?: Function}`[, sortOptions: { sort: `Boolean`, x: `Boolean`, y: `Boolean`, z: `Boolean`, r: `Boolean` }])

The same API and behavior as `calcExtents` but instead of a two-value array of `[min, max]` values, it returns an array of unique items. It returns the values in the order they appear in the data. Optionally pass in `true` in the third options argument to do a simple `.sort()`.
The same API and behavior as `calcExtents` but instead of a two-value array of `[min, max]` values, it returns an array of unique items.

By default, it returns the values in the order they appear in the data. Optionally pass in `sort: true` in the third options argument to do a `.sort(d3.ascending)`. (See [d3-array sort ascending](https://d3js.org/d3-array/sort#ascending) for details on the sort.)

You can also specify sorts on a per-field basis by passing in booleans for specific keys that appear in your `fields` object such as `{ x: true }`.

```js
const uniques = calcUniques(flatData, {
Expand All @@ -107,11 +111,45 @@ console.log(uniques);
/*
{
x: [0, 85, 123, 43, 10],
y: ['group-3', 'group-2', 'group-1']
}
*/
```

Sort all fields:

```js
const uniques = calcUniques(flatData, {
x: d => d.myX,
y: d => d.myY
}, { sort: true });

console.log(uniques);
/*
{
x: [0, 10, 43, 85, 123],
y: ['group-1', 'group-2', 'group-3']
}
*/
```

Sort only the x field:

```js
const uniques = calcUniques(flatData, {
x: d => d.myX,
y: d => d.myY
}, { x: true });

console.log(uniques);
/*
{
x: [0, 10, 43, 85, 123],
y: ['group-3', 'group-2', 'group-1']
}
*/
```

The accessor functions can also return an array. This is useful if you want to scan multiple keys per object:

```js
Expand Down
31 changes: 28 additions & 3 deletions src/lib/LayerCake.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,14 @@
export let zReverse = false;
/** @type {Boolean} [rReverse=false] Reverse the default r range. By default this is `false` and the range is `[1, 25]`. Ignored if you set the rRange prop. */
export let rReverse = false;
/** @type {Boolean} [xDomainSort=true] Only used when scale is ordinal. Set whether the calculated unique items come back sorted. */
export let xDomainSort = true;
/** @type {Boolean} [yDomainSort=true] Only used when scale is ordinal. Set whether the calculated unique items come back sorted. */
export let yDomainSort = true
/** @type {Boolean} [zDomainSort=true] Only used when scale is ordinal. Set whether the calculated unique items come back sorted. */
export let zDomainSort = true;
/** @type {Boolean} [rDomainSort=true] Only used when scale is ordinal. Set whether the calculated unique items come back sorted. */
export let rDomainSort = true;
/** @type {{top?: Number, right?: Number, bottom?: Number, left?: Number}} [padding={}] The amount of padding to put around your chart. It operates like CSS box-sizing: border-box; where values are subtracted from the parent container's width and height, the same as a [D3 margin convention](https://bl.ocks.org/mbostock/3019563). */
export let padding = {};
/** @type {{ x?: [min: Number, max: Number], y?: [min: Number, max: Number], r?: [min: Number, max: Number], z?: [min: Number, max: Number] }} [extents] Manually set the extents of the x, y or r scale as a two-dimensional array of the min and max you want. Setting values here will skip any dynamic extent calculation of the data for that dimension. */
Expand Down Expand Up @@ -200,6 +208,10 @@
const _yScale = writable(yScale);
const _zScale = writable(zScale);
const _rScale = writable(rScale);
const _xDomainSort = writable(xDomainSort);
const _yDomainSort = writable(yDomainSort);
const _zDomainSort = writable(zDomainSort);
const _rDomainSort = writable(rDomainSort);
const _config = writable(config);
const _custom = writable(custom);
Expand Down Expand Up @@ -306,9 +318,14 @@
* in as a domain, which can be a partial domain
*/
const extents_d = derived(
[_flatData, activeGetters_d, _extents, _xScale, _yScale, _rScale, _zScale],
([$flatData, $activeGetters, $extents, $_xScale, $_yScale, $_rScale, $_zScale]) => {
const scaleLookup = { x: $_xScale, y: $_yScale, r: $_rScale, z: $_zScale };
[_flatData, activeGetters_d, _extents, _xScale, _yScale, _rScale, _zScale, _xDomainSort, _yDomainSort, _zDomainSort, _rDomainSort],
([$flatData, $activeGetters, $extents, $_xScale, $_yScale, $_rScale, $_zScale, $_xDomainSort, $_yDomainSort, $_zDomainSort, $_rDomainSort]) => {
const scaleLookup = {
x: { scale: $_xScale, sort: $_xDomainSort },
y: { scale: $_yScale, sort: $_yDomainSort },
r: { scale: $_rScale, sort: $_rDomainSort },
z: { scale: $_zScale, sort: $_zDomainSort }
};
const getters = filterObject($activeGetters, $extents);
const activeScales = Object.fromEntries(Object.keys(getters).map((k) => [k, scaleLookup[k]]));
Expand Down Expand Up @@ -421,6 +438,10 @@
yNice: _yNice,
zNice: _zNice,
rNice: _rNice,
xDomainSort: _xDomainSort,
yDomainSort: _yDomainSort,
zDomainSort: _zDomainSort,
rDomainSort: _rDomainSort,
xReverse: _xReverse,
yReverse: _yReverse,
zReverse: _zReverse,
Expand Down Expand Up @@ -503,6 +524,10 @@
yNice={$_yNice}
zNice={$_zNice}
rNice={$_rNice}
xDomainSort={$_xDomainSort}
yDomainSort={$_yDomainSort}
zDomainSort={$_zDomainSort}
rDomainSort={$_rDomainSort}
xReverse={$_xReverse}
yReverse={$_yReverse}
zReverse={$_zReverse}
Expand Down
10 changes: 6 additions & 4 deletions src/lib/helpers/calcScaleExtents.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ import isOrdinalDomain from './isOrdinalDomain.js';
* for the others, calculate an extent
*/
export default function calcScaleExtents (flatData, getters, activeScales) {
const scaleGroups = Object.keys(activeScales).reduce((groups, k) => {
const domainType = isOrdinalDomain(activeScales[k]) === true ? 'ordinal' : 'other';
const scaleGroups = Object.entries(activeScales).reduce((groups, [k, scaleInfo]) => {
const domainType = isOrdinalDomain(scaleInfo.scale) === true ? 'ordinal' : 'other';
// @ts-ignore
if (!groups[domainType]) groups[domainType] = {};
groups[domainType][k] = getters[k];
Expand All @@ -19,8 +19,10 @@ export default function calcScaleExtents (flatData, getters, activeScales) {

let extents = {};
if (scaleGroups.ordinal) {
// @ts-ignore
extents = calcUniques(flatData, scaleGroups.ordinal, { sort: true });
const sortOptions = Object.fromEntries(Object.entries(activeScales).map(([k, scaleInfo]) => {
return [k, scaleInfo.sort];
}));
extents = calcUniques(flatData, scaleGroups.ordinal, sortOptions);
}
if (scaleGroups.other) {
// @ts-ignore
Expand Down
31 changes: 16 additions & 15 deletions src/lib/lib/calcUniques.js
Original file line number Diff line number Diff line change
@@ -1,29 +1,30 @@
import { ascending, InternSet } from 'd3-array';

/**
Calculate the unique values of desired fields
For example, data like this:
[{ x: 0, y: -10 }, { x: 10, y: 0 }, { x: 5, y: 10 }]
and a fields object like this:
`{'x': d => d.x, 'y': d => d.y}`
returns an object like this:
returns an object like this:e
`{ x: [0, 10, 5], y: [-10, 0, 10] }`
@param {Array} data A flat array of objects.
@param {{x?: Function, y?: Function, z?: Function, r?: Function}} fields An object containing `x`, `y`, `r` or `z` keys that equal an accessor function. If an accessor function returns an array of values, each value will also be evaluated..
@param {Array} data A flat array of.
@param {{x?: Function, y?: Function, z?: Function, r?: Function}} fields An object containing `x`, `y`, `r` or `z` keys that equal an accessor function. If an accessor function returns an array of values, each value will also be evaluated.
@param {{ sort?: Boolean, x?: Boolean , y?: Boolean , z?: Boolean , r?: Boolean }} sortOptions An object containing `sort`, `x`, `y`, `r` or `z` keys with Boolean values that designate how results should be sorted. Default is un-sorted. Pass in `sort: true` to sort all fields or specify fields individually.
@returns {{x?: [min: Number, max: Number]|[min: String, max: String], y?: [min: Number, max: Number]|[min: String, max: String], z?: [min: Number, max: Number]|[min: String, max: String], r?: [min: Number, max: Number]|[min: String, max: String]}} An object with the same structure as `fields` but instead of an accessor, each key contains an array of unique items.
*/
import { ascending, InternSet } from 'd3-array';

export default function calcUniques (data, fields, { sort = false } = {}) {
export default function calcUniques(data, fields, sortOptions = {}) {
if (!Array.isArray(data)) {
throw new TypeError(`The first argument of calcUniques() must be an array. You passed in a ${typeof data}. If you got this error using the <LayerCake> component, consider passing a flat array to the \`flatData\` prop. More info: https://layercake.graphics/guide/#flatdata`);
throw new TypeError(
`The first argument of calcUniques() must be an array. You passed in a ${typeof data}. If you got this error using the <LayerCake> component, consider passing a flat array to the \`flatData\` prop. More info: https://layercake.graphics/guide/#flatdata`
);
}

if (
Array.isArray(fields)
|| fields === undefined
|| fields === null
) {
throw new TypeError('The second argument of calcUniques() must be an '
+ 'object with field names as keys as accessor functions as values.');
if (Array.isArray(fields) || fields === undefined || fields === null) {
throw new TypeError(
'The second argument of calcUniques() must be an ' +
'object with field names as keys as accessor functions as values.'
);
}

const uniques = {};
Expand Down Expand Up @@ -55,7 +56,7 @@ export default function calcUniques (data, fields, { sort = false } = {}) {
}
}
const results = Array.from(set);
if (sort === true) {
if (sortOptions.sort === true || sortOptions[s] === true) {
results.sort(ascending);
}
uniques[s] = results;
Expand Down
2 changes: 1 addition & 1 deletion src/routes/_examples/Brush.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@

<div class="brushed-chart-container">
<LayerCake
padding={{ right: 10, bottom: 20, left: 25 }}
padding={{ bottom: 20, left: 25 }}
x={xKey}
y={yKey}
yDomain={[0, null]}
Expand Down
Loading

0 comments on commit 628ad2b

Please sign in to comment.