diff --git a/package-lock.json b/package-lock.json index 56e327919..ce0b9bf25 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "unovis", - "version": "1.3.1", + "version": "1.3.3", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "unovis", - "version": "1.3.1", + "version": "1.3.3", "hasInstallScript": true, "license": "Apache-2.0", "workspaces": [ @@ -24459,7 +24459,7 @@ }, "packages/angular": { "name": "@unovis/angular", - "version": "1.3.1", + "version": "1.3.3", "license": "Apache-2.0", "dependencies": { "tslib": "^2.3.1" @@ -24493,7 +24493,7 @@ "@angular/compiler": "12 - 16", "@angular/core": "12 - 16", "@unovis/shared": "*", - "@unovis/ts": "1.3.1" + "@unovis/ts": "1.3.3" } }, "packages/angular/node_modules/@types/estree": { @@ -24770,7 +24770,7 @@ }, "packages/react": { "name": "@unovis/react", - "version": "1.3.1", + "version": "1.3.3", "license": "Apache-2.0", "devDependencies": { "@rollup/plugin-node-resolve": "^13.0.4", @@ -24791,7 +24791,7 @@ "typescript": "~4.2.4" }, "peerDependencies": { - "@unovis/ts": "1.3.1", + "@unovis/ts": "1.3.3", "react": ">=16.8.0 || ^17 || ^18", "react-dom": ">=16.8.0 || ^17 || ^18" } @@ -24804,7 +24804,7 @@ }, "packages/shared": { "name": "@unovis/shared", - "version": "1.3.1", + "version": "1.3.3", "license": "Apache-2.0", "devDependencies": { "@angular/platform-browser": "^12.0.3", @@ -24813,7 +24813,7 @@ }, "packages/svelte": { "name": "@unovis/svelte", - "version": "1.3.1", + "version": "1.3.3", "license": "Apache-2.0", "devDependencies": { "@rollup/plugin-node-resolve": "^13.0.4", @@ -24834,7 +24834,7 @@ "typescript": "~4.2.4" }, "peerDependencies": { - "@unovis/ts": "1.3.1", + "@unovis/ts": "1.3.3", "svelte": "^3.48.0 || ^4.0.0" } }, @@ -24846,7 +24846,7 @@ }, "packages/ts": { "name": "@unovis/ts", - "version": "1.3.1", + "version": "1.3.3", "license": "Apache-2.0", "dependencies": { "@emotion/css": "^11.7.1", @@ -25066,7 +25066,7 @@ }, "packages/vue": { "name": "@unovis/vue", - "version": "1.3.1", + "version": "1.3.3", "license": "Apache-2.0", "devDependencies": { "@antfu/eslint-config": "^0.41.0", diff --git a/package.json b/package.json index 6dc46bcc9..6a74e662b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "unovis", - "version": "1.3.1", + "version": "1.3.3", "license": "Apache-2.0", "private": true, "workspaces": [ diff --git a/packages/angular/package.json b/packages/angular/package.json index 30408d156..c27a39def 100644 --- a/packages/angular/package.json +++ b/packages/angular/package.json @@ -1,7 +1,7 @@ { "name": "@unovis/angular", "description": "Modular data visualization framework for React, Angular, Svelte, and vanilla TypeScript or JavaScript", - "version": "1.3.1", + "version": "1.3.3", "repository": { "type": "git", "url": "https://github.com/f5/unovis.git", @@ -45,7 +45,7 @@ }, "peerDependencies": { "@unovis/shared": "*", - "@unovis/ts": "1.3.1", + "@unovis/ts": "1.3.3", "@angular/common": "12 - 16", "@angular/compiler": "12 - 16", "@angular/core": "12 - 16" diff --git a/packages/angular/src/components.ts b/packages/angular/src/components.ts index 7826da376..fdb23b5a8 100644 --- a/packages/angular/src/components.ts +++ b/packages/angular/src/components.ts @@ -2,9 +2,6 @@ export * from './core' // SVG Components -export { VisLineComponent } from './components/line/line.component' -export { VisLineModule } from './components/line/line.module' - export { VisAreaComponent } from './components/area/area.component' export { VisAreaModule } from './components/area/area.module' @@ -14,8 +11,8 @@ export { VisAxisModule } from './components/axis/axis.module' export { VisBrushComponent } from './components/brush/brush.component' export { VisBrushModule } from './components/brush/brush.module' -export { VisFreeBrushComponent } from './components/free-brush/free-brush.component' -export { VisFreeBrushModule } from './components/free-brush/free-brush.module' +export { VisChordDiagramComponent } from './components/chord-diagram/chord-diagram.component' +export { VisChordDiagramModule } from './components/chord-diagram/chord-diagram.module' export { VisCrosshairComponent } from './components/crosshair/crosshair.component' export { VisCrosshairModule } from './components/crosshair/crosshair.module' @@ -23,9 +20,24 @@ export { VisCrosshairModule } from './components/crosshair/crosshair.module' export { VisDonutComponent } from './components/donut/donut.component' export { VisDonutModule } from './components/donut/donut.module' +export { VisFreeBrushComponent } from './components/free-brush/free-brush.component' +export { VisFreeBrushModule } from './components/free-brush/free-brush.module' + +export { VisGraphComponent } from './components/graph/graph.component' +export { VisGraphModule } from './components/graph/graph.module' + export { VisGroupedBarComponent } from './components/grouped-bar/grouped-bar.component' export { VisGroupedBarModule } from './components/grouped-bar/grouped-bar.module' +export { VisLineComponent } from './components/line/line.component' +export { VisLineModule } from './components/line/line.module' + +export { VisNestedDonutComponent } from './components/nested-donut/nested-donut.component' +export { VisNestedDonutModule } from './components/nested-donut/nested-donut.module' + +export { VisSankeyComponent } from './components/sankey/sankey.component' +export { VisSankeyModule } from './components/sankey/sankey.module' + export { VisScatterComponent } from './components/scatter/scatter.component' export { VisScatterModule } from './components/scatter/scatter.module' @@ -41,15 +53,6 @@ export { VisXYLabelsModule } from './components/xy-labels/xy-labels.module' export { VisTopoJSONMapComponent } from './components/topojson-map/topojson-map.component' export { VisTopoJSONMapModule } from './components/topojson-map/topojson-map.module' -export { VisSankeyComponent } from './components/sankey/sankey.component' -export { VisSankeyModule } from './components/sankey/sankey.module' - -export { VisGraphComponent } from './components/graph/graph.component' -export { VisGraphModule } from './components/graph/graph.module' - -export { VisNestedDonutComponent } from './components/nested-donut/nested-donut.component' -export { VisNestedDonutModule } from './components/nested-donut/nested-donut.module' - // HTML Components export { VisLeafletMapComponent } from './html-components/leaflet-map/leaflet-map.component' export { VisLeafletMapModule } from './html-components/leaflet-map/leaflet-map.module' diff --git a/packages/angular/src/components/chord-diagram/chord-diagram.component.ts b/packages/angular/src/components/chord-diagram/chord-diagram.component.ts index 62ed1536e..7964415b8 100644 --- a/packages/angular/src/components/chord-diagram/chord-diagram.component.ts +++ b/packages/angular/src/components/chord-diagram/chord-diagram.component.ts @@ -105,8 +105,8 @@ export class VisChordDiagramComponent> - /** Pad angle in radians. Constant value or accessor function. Default: `0.02` */ - @Input() padAngle?: NumericAccessor> + /** Pad angle in radians. Default: `0.02` */ + @Input() padAngle?: number /** Corner radius constant value or accessor function. Default: `2` */ @Input() cornerRadius?: NumericAccessor> diff --git a/packages/angular/src/html-components/bullet-legend/bullet-legend.component.ts b/packages/angular/src/html-components/bullet-legend/bullet-legend.component.ts index 27e53f751..33c842fe1 100644 --- a/packages/angular/src/html-components/bullet-legend/bullet-legend.component.ts +++ b/packages/angular/src/html-components/bullet-legend/bullet-legend.component.ts @@ -1,5 +1,5 @@ import { Component, AfterViewInit, Input, SimpleChanges, ViewChild, ElementRef } from '@angular/core' -import { BulletLegend, BulletLegendConfigInterface, BulletLegendItemInterface, BulletShape } from '@unovis/ts' +import { BulletLegend, BulletLegendConfigInterface, BulletLegendItemInterface, BulletShape, GenericAccessor } from '@unovis/ts' import { VisGenericComponent } from '../../core' @Component({ @@ -39,8 +39,8 @@ export class VisBulletLegendComponent implements BulletLegendConfigInterface, Af /** Bullet circle size, mapped to the width and height CSS properties. Default: `null` */ @Input() bulletSize?: string | null - /** Bullet shape: `BulletShape.Circle`, `BulletShape.Line` or `BulletShape.Square`. Default: `BulletShape.Circle` */ - @Input() bulletShape?: BulletShape + /** Bullet shape enum value or accessor function. Default: `d => d.shape ?? BulletShape.Circle */ + @Input() bulletShape?: GenericAccessor component: BulletLegend | undefined diff --git a/packages/dev/src/examples/auxiliary/bullet-legend/multi-shape-legend/index.tsx b/packages/dev/src/examples/auxiliary/bullet-legend/multi-shape-legend/index.tsx new file mode 100644 index 000000000..4e4337a85 --- /dev/null +++ b/packages/dev/src/examples/auxiliary/bullet-legend/multi-shape-legend/index.tsx @@ -0,0 +1,43 @@ +import React, { useState, useCallback, useEffect } from 'react' +import { BulletLegendItemInterface, BulletShape } from '@unovis/ts' +import { VisBulletLegend, VisXYContainer, VisScatter, VisAxis } from '@unovis/react' +import { generateStackedDataRecords, StackedDataRecord } from '@src/utils/data' + +export const title = 'Shape Legend' +export const subTitle = 'with Scatter Plot' + +const STACKED = 7 +const data = generateStackedDataRecords(10, STACKED) +const items = Array(STACKED).fill(0).map((_, i) => ({ name: `Y${i}`, inactive: false })) +const shapes = Object.values(BulletShape) + +export const component = (): JSX.Element => { + const x = (d: StackedDataRecord): number => d.x + const shape = (_, i: number): string => shapes[i % shapes.length] + + const [accessors, setAccessors] = useState<(null | ((d: StackedDataRecord) => number))[]>() + const [legendItems, setLegendItems] = useState(items) + + const toggleItem = useCallback((_: BulletLegendItemInterface, index: number) => { + const newItems = legendItems.map((l, i) => i === index ? ({ ...l, inactive: !l.inactive }) : l) + setLegendItems(newItems) + }, [legendItems]) + + useEffect(() => + setAccessors(legendItems.map((l, i) => l.inactive ? null : (d: StackedDataRecord) => d.ys[i])) + , [legendItems]) + + return (<> + + + + + + + + ) +} diff --git a/packages/react/package.json b/packages/react/package.json index 0e8c4c7a1..2c4260aa1 100644 --- a/packages/react/package.json +++ b/packages/react/package.json @@ -1,7 +1,7 @@ { "name": "@unovis/react", "description": "Modular data visualization framework for React, Angular, Svelte, and vanilla TypeScript or JavaScript", - "version": "1.3.1", + "version": "1.3.3", "repository": { "type": "git", "url": "https://github.com/f5/unovis.git", @@ -35,7 +35,7 @@ "publish:dist": "rm -rf dist/.cache; cp ./{LICENSE,README.md,package.json} ./dist; cd ./dist; npm publish" }, "peerDependencies": { - "@unovis/ts": "1.3.1", + "@unovis/ts": "1.3.3", "react": ">=16.8.0 || ^17 || ^18", "react-dom": ">=16.8.0 || ^17 || ^18" }, diff --git a/packages/shared/examples/_previews/basic-scatter-plot-dark.png b/packages/shared/examples/_previews/basic-scatter-plot-dark.png new file mode 100644 index 000000000..34fbe714a Binary files /dev/null and b/packages/shared/examples/_previews/basic-scatter-plot-dark.png differ diff --git a/packages/shared/examples/_previews/basic-scatter-plot.png b/packages/shared/examples/_previews/basic-scatter-plot.png new file mode 100644 index 000000000..03321c008 Binary files /dev/null and b/packages/shared/examples/_previews/basic-scatter-plot.png differ diff --git a/packages/shared/examples/_previews/basic-scatter-chart-dark.png b/packages/shared/examples/_previews/sized-scatter-plot-dark.png similarity index 100% rename from packages/shared/examples/_previews/basic-scatter-chart-dark.png rename to packages/shared/examples/_previews/sized-scatter-plot-dark.png diff --git a/packages/shared/examples/_previews/basic-scatter-chart.png b/packages/shared/examples/_previews/sized-scatter-plot.png similarity index 100% rename from packages/shared/examples/_previews/basic-scatter-chart.png rename to packages/shared/examples/_previews/sized-scatter-plot.png diff --git a/packages/shared/examples/basic-scatter-plot/basic-scatter-plot.component.html b/packages/shared/examples/basic-scatter-plot/basic-scatter-plot.component.html new file mode 100644 index 000000000..280b11c8c --- /dev/null +++ b/packages/shared/examples/basic-scatter-plot/basic-scatter-plot.component.html @@ -0,0 +1,6 @@ + + + + + + diff --git a/packages/shared/examples/basic-scatter-plot/basic-scatter-plot.component.ts b/packages/shared/examples/basic-scatter-plot/basic-scatter-plot.component.ts new file mode 100644 index 000000000..05f4d441c --- /dev/null +++ b/packages/shared/examples/basic-scatter-plot/basic-scatter-plot.component.ts @@ -0,0 +1,21 @@ +import { Component } from '@angular/core' +import type { BulletLegendItemInterface, NumericAccessor, StringAccessor } from '@unovis/ts' +import { data, DataRecord } from './data' + +@Component({ + selector: 'basic-scatter-plot', + templateUrl: './basic-scatter-plot.component.html', +}) +export class BasicScatterPlotComponent { + data = data + + legendItems: BulletLegendItemInterface[] = [ + { name: 'Male', color: '#1fc3aa' }, + { name: 'Female', color: '#8624F5' }, + { name: 'No Data', color: '#aaa' }, + ] + + x: NumericAccessor = d => d.beakLength + y: NumericAccessor = d => d.flipperLength + color: StringAccessor = d => this.legendItems.find(i => i.name === (d.sex ?? 'No Data'))?.color +} diff --git a/packages/shared/examples/basic-scatter-plot/basic-scatter-plot.module.ts b/packages/shared/examples/basic-scatter-plot/basic-scatter-plot.module.ts new file mode 100644 index 000000000..abebd890e --- /dev/null +++ b/packages/shared/examples/basic-scatter-plot/basic-scatter-plot.module.ts @@ -0,0 +1,11 @@ +import { NgModule } from '@angular/core' +import { VisXYContainerModule, VisScatterModule, VisAxisModule, VisBulletLegendModule } from '@unovis/angular' + +import { BasicScatterPlotComponent } from './basic-scatter-plot.component' + +@NgModule({ + imports: [VisXYContainerModule, VisScatterModule, VisAxisModule, VisBulletLegendModule], + declarations: [BasicScatterPlotComponent], + exports: [BasicScatterPlotComponent], +}) +export class BasicScatterPlotModule { } diff --git a/packages/shared/examples/basic-scatter-plot/basic-scatter-plot.svelte b/packages/shared/examples/basic-scatter-plot/basic-scatter-plot.svelte new file mode 100644 index 000000000..b47fc7236 --- /dev/null +++ b/packages/shared/examples/basic-scatter-plot/basic-scatter-plot.svelte @@ -0,0 +1,23 @@ + + + + + + + + diff --git a/packages/shared/examples/basic-scatter-plot/basic-scatter-plot.ts b/packages/shared/examples/basic-scatter-plot/basic-scatter-plot.ts new file mode 100644 index 000000000..c3cd268c9 --- /dev/null +++ b/packages/shared/examples/basic-scatter-plot/basic-scatter-plot.ts @@ -0,0 +1,28 @@ +import { Axis, BulletLegend, Scatter, XYContainer } from '@unovis/ts' +import { data, DataRecord } from './data' + +const container = document.getElementById('vis-container') + +// Legend +const legend = new BulletLegend(container, { + items: [ + { name: 'Male', color: '#1fc3aa' }, + { name: 'Female', color: '#8624F5' }, + { name: 'No Data', color: '#aaa' }, + ], +}) + +// Chart +const chart = new XYContainer(container, { + height: 600, + components: [ + new Scatter({ + x: (d: DataRecord) => d.beakLength, + y: (d: DataRecord) => d.flipperLength, + color: (d: DataRecord) => legend.config.items.find(i => i.name === (d.sex ?? 'No Data'))?.color, + size: 8, + }), + ], + xAxis: new Axis({ label: 'Beak Length (mm)' }), + yAxis: new Axis({ label: 'Flipper Length (mm)' }), +}, data) diff --git a/packages/shared/examples/basic-scatter-plot/basic-scatter-plot.tsx b/packages/shared/examples/basic-scatter-plot/basic-scatter-plot.tsx new file mode 100644 index 000000000..7a498b196 --- /dev/null +++ b/packages/shared/examples/basic-scatter-plot/basic-scatter-plot.tsx @@ -0,0 +1,27 @@ +import React from 'react' +import { NumericAccessor, StringAccessor } from '@unovis/ts' +import { VisAxis, VisBulletLegend, VisScatter, VisXYContainer } from '@unovis/react' +import { data, DataRecord } from './data' + +export default function BasicScatterPlot (): JSX.Element { + const legendItems = [ + { name: 'Male', color: '#1fc3aa' }, + { name: 'Female', color: '#8624F5' }, + { name: 'No Data', color: '#aaa' }, + ] + + const x: NumericAccessor = d => d.beakLength + const y: NumericAccessor = d => d.flipperLength + const color: StringAccessor = d => legendItems.find(i => i.name === (d.sex ?? 'No Data'))?.color + + return ( + <> + + + + + + + + ) +} diff --git a/packages/shared/examples/basic-scatter-plot/basic-scatter-plot.vue b/packages/shared/examples/basic-scatter-plot/basic-scatter-plot.vue new file mode 100644 index 000000000..c97878d47 --- /dev/null +++ b/packages/shared/examples/basic-scatter-plot/basic-scatter-plot.vue @@ -0,0 +1,24 @@ + + + \ No newline at end of file diff --git a/packages/shared/examples/basic-scatter-plot/data.ts b/packages/shared/examples/basic-scatter-plot/data.ts new file mode 100644 index 000000000..e2d07455c --- /dev/null +++ b/packages/shared/examples/basic-scatter-plot/data.ts @@ -0,0 +1,3090 @@ +export type DataRecord = { + species: string; + island: string; + beakLength: number | undefined; + beakDepth: number | undefined; + flipperLength: number | undefined; + bodyMass: number | undefined; + sex: string; +} + +export const data = [ + { + species: 'Adelie', + island: 'Torgersen', + beakLength: 39.1, + beakDepth: 18.7, + flipperLength: 181, + bodyMass: 3750, + sex: 'Male', + }, + { + species: 'Adelie', + island: 'Torgersen', + beakLength: 39.5, + beakDepth: 17.4, + flipperLength: 186, + bodyMass: 3800, + sex: 'Female', + }, + { + species: 'Adelie', + island: 'Torgersen', + beakLength: 40.3, + beakDepth: 18, + flipperLength: 195, + bodyMass: 3250, + sex: 'Female', + }, + { + species: 'Adelie', + island: 'Torgersen', + }, + { + species: 'Adelie', + island: 'Torgersen', + beakLength: 36.7, + beakDepth: 19.3, + flipperLength: 193, + bodyMass: 3450, + sex: 'Female', + }, + { + species: 'Adelie', + island: 'Torgersen', + beakLength: 39.3, + beakDepth: 20.6, + flipperLength: 190, + bodyMass: 3650, + sex: 'Male', + }, + { + species: 'Adelie', + island: 'Torgersen', + beakLength: 38.9, + beakDepth: 17.8, + flipperLength: 181, + bodyMass: 3625, + sex: 'Female', + }, + { + species: 'Adelie', + island: 'Torgersen', + beakLength: 39.2, + beakDepth: 19.6, + flipperLength: 195, + bodyMass: 4675, + sex: 'Male', + }, + { + species: 'Adelie', + island: 'Torgersen', + beakLength: 34.1, + beakDepth: 18.1, + flipperLength: 193, + bodyMass: 3475, + }, + { + species: 'Adelie', + island: 'Torgersen', + beakLength: 42, + beakDepth: 20.2, + flipperLength: 190, + bodyMass: 4250, + }, + { + species: 'Adelie', + island: 'Torgersen', + beakLength: 37.8, + beakDepth: 17.1, + flipperLength: 186, + bodyMass: 3300, + }, + { + species: 'Adelie', + island: 'Torgersen', + beakLength: 37.8, + beakDepth: 17.3, + flipperLength: 180, + bodyMass: 3700, + }, + { + species: 'Adelie', + island: 'Torgersen', + beakLength: 41.1, + beakDepth: 17.6, + flipperLength: 182, + bodyMass: 3200, + sex: 'Female', + }, + { + species: 'Adelie', + island: 'Torgersen', + beakLength: 38.6, + beakDepth: 21.2, + flipperLength: 191, + bodyMass: 3800, + sex: 'Male', + }, + { + species: 'Adelie', + island: 'Torgersen', + beakLength: 34.6, + beakDepth: 21.1, + flipperLength: 198, + bodyMass: 4400, + sex: 'Male', + }, + { + species: 'Adelie', + island: 'Torgersen', + beakLength: 36.6, + beakDepth: 17.8, + flipperLength: 185, + bodyMass: 3700, + sex: 'Female', + }, + { + species: 'Adelie', + island: 'Torgersen', + beakLength: 38.7, + beakDepth: 19, + flipperLength: 195, + bodyMass: 3450, + sex: 'Female', + }, + { + species: 'Adelie', + island: 'Torgersen', + beakLength: 42.5, + beakDepth: 20.7, + flipperLength: 197, + bodyMass: 4500, + sex: 'Male', + }, + { + species: 'Adelie', + island: 'Torgersen', + beakLength: 34.4, + beakDepth: 18.4, + flipperLength: 184, + bodyMass: 3325, + sex: 'Female', + }, + { + species: 'Adelie', + island: 'Torgersen', + beakLength: 46, + beakDepth: 21.5, + flipperLength: 194, + bodyMass: 4200, + sex: 'Male', + }, + { + species: 'Adelie', + island: 'Biscoe', + beakLength: 37.8, + beakDepth: 18.3, + flipperLength: 174, + bodyMass: 3400, + sex: 'Female', + }, + { + species: 'Adelie', + island: 'Biscoe', + beakLength: 37.7, + beakDepth: 18.7, + flipperLength: 180, + bodyMass: 3600, + sex: 'Male', + }, + { + species: 'Adelie', + island: 'Biscoe', + beakLength: 35.9, + beakDepth: 19.2, + flipperLength: 189, + bodyMass: 3800, + sex: 'Female', + }, + { + species: 'Adelie', + island: 'Biscoe', + beakLength: 38.2, + beakDepth: 18.1, + flipperLength: 185, + bodyMass: 3950, + sex: 'Male', + }, + { + species: 'Adelie', + island: 'Biscoe', + beakLength: 38.8, + beakDepth: 17.2, + flipperLength: 180, + bodyMass: 3800, + sex: 'Male', + }, + { + species: 'Adelie', + island: 'Biscoe', + beakLength: 35.3, + beakDepth: 18.9, + flipperLength: 187, + bodyMass: 3800, + sex: 'Female', + }, + { + species: 'Adelie', + island: 'Biscoe', + beakLength: 40.6, + beakDepth: 18.6, + flipperLength: 183, + bodyMass: 3550, + sex: 'Male', + }, + { + species: 'Adelie', + island: 'Biscoe', + beakLength: 40.5, + beakDepth: 17.9, + flipperLength: 187, + bodyMass: 3200, + sex: 'Female', + }, + { + species: 'Adelie', + island: 'Biscoe', + beakLength: 37.9, + beakDepth: 18.6, + flipperLength: 172, + bodyMass: 3150, + sex: 'Female', + }, + { + species: 'Adelie', + island: 'Biscoe', + beakLength: 40.5, + beakDepth: 18.9, + flipperLength: 180, + bodyMass: 3950, + sex: 'Male', + }, + { + species: 'Adelie', + island: 'Dream', + beakLength: 39.5, + beakDepth: 16.7, + flipperLength: 178, + bodyMass: 3250, + sex: 'Female', + }, + { + species: 'Adelie', + island: 'Dream', + beakLength: 37.2, + beakDepth: 18.1, + flipperLength: 178, + bodyMass: 3900, + sex: 'Male', + }, + { + species: 'Adelie', + island: 'Dream', + beakLength: 39.5, + beakDepth: 17.8, + flipperLength: 188, + bodyMass: 3300, + sex: 'Female', + }, + { + species: 'Adelie', + island: 'Dream', + beakLength: 40.9, + beakDepth: 18.9, + flipperLength: 184, + bodyMass: 3900, + sex: 'Male', + }, + { + species: 'Adelie', + island: 'Dream', + beakLength: 36.4, + beakDepth: 17, + flipperLength: 195, + bodyMass: 3325, + sex: 'Female', + }, + { + species: 'Adelie', + island: 'Dream', + beakLength: 39.2, + beakDepth: 21.1, + flipperLength: 196, + bodyMass: 4150, + sex: 'Male', + }, + { + species: 'Adelie', + island: 'Dream', + beakLength: 38.8, + beakDepth: 20, + flipperLength: 190, + bodyMass: 3950, + sex: 'Male', + }, + { + species: 'Adelie', + island: 'Dream', + beakLength: 42.2, + beakDepth: 18.5, + flipperLength: 180, + bodyMass: 3550, + sex: 'Female', + }, + { + species: 'Adelie', + island: 'Dream', + beakLength: 37.6, + beakDepth: 19.3, + flipperLength: 181, + bodyMass: 3300, + sex: 'Female', + }, + { + species: 'Adelie', + island: 'Dream', + beakLength: 39.8, + beakDepth: 19.1, + flipperLength: 184, + bodyMass: 4650, + sex: 'Male', + }, + { + species: 'Adelie', + island: 'Dream', + beakLength: 36.5, + beakDepth: 18, + flipperLength: 182, + bodyMass: 3150, + sex: 'Female', + }, + { + species: 'Adelie', + island: 'Dream', + beakLength: 40.8, + beakDepth: 18.4, + flipperLength: 195, + bodyMass: 3900, + sex: 'Male', + }, + { + species: 'Adelie', + island: 'Dream', + beakLength: 36, + beakDepth: 18.5, + flipperLength: 186, + bodyMass: 3100, + sex: 'Female', + }, + { + species: 'Adelie', + island: 'Dream', + beakLength: 44.1, + beakDepth: 19.7, + flipperLength: 196, + bodyMass: 4400, + sex: 'Male', + }, + { + species: 'Adelie', + island: 'Dream', + beakLength: 37, + beakDepth: 16.9, + flipperLength: 185, + bodyMass: 3000, + sex: 'Female', + }, + { + species: 'Adelie', + island: 'Dream', + beakLength: 39.6, + beakDepth: 18.8, + flipperLength: 190, + bodyMass: 4600, + sex: 'Male', + }, + { + species: 'Adelie', + island: 'Dream', + beakLength: 41.1, + beakDepth: 19, + flipperLength: 182, + bodyMass: 3425, + sex: 'Male', + }, + { + species: 'Adelie', + island: 'Dream', + beakLength: 37.5, + beakDepth: 18.9, + flipperLength: 179, + bodyMass: 2975, + }, + { + species: 'Adelie', + island: 'Dream', + beakLength: 36, + beakDepth: 17.9, + flipperLength: 190, + bodyMass: 3450, + sex: 'Female', + }, + { + species: 'Adelie', + island: 'Dream', + beakLength: 42.3, + beakDepth: 21.2, + flipperLength: 191, + bodyMass: 4150, + sex: 'Male', + }, + { + species: 'Adelie', + island: 'Biscoe', + beakLength: 39.6, + beakDepth: 17.7, + flipperLength: 186, + bodyMass: 3500, + sex: 'Female', + }, + { + species: 'Adelie', + island: 'Biscoe', + beakLength: 40.1, + beakDepth: 18.9, + flipperLength: 188, + bodyMass: 4300, + sex: 'Male', + }, + { + species: 'Adelie', + island: 'Biscoe', + beakLength: 35, + beakDepth: 17.9, + flipperLength: 190, + bodyMass: 3450, + sex: 'Female', + }, + { + species: 'Adelie', + island: 'Biscoe', + beakLength: 42, + beakDepth: 19.5, + flipperLength: 200, + bodyMass: 4050, + sex: 'Male', + }, + { + species: 'Adelie', + island: 'Biscoe', + beakLength: 34.5, + beakDepth: 18.1, + flipperLength: 187, + bodyMass: 2900, + sex: 'Female', + }, + { + species: 'Adelie', + island: 'Biscoe', + beakLength: 41.4, + beakDepth: 18.6, + flipperLength: 191, + bodyMass: 3700, + sex: 'Male', + }, + { + species: 'Adelie', + island: 'Biscoe', + beakLength: 39, + beakDepth: 17.5, + flipperLength: 186, + bodyMass: 3550, + sex: 'Female', + }, + { + species: 'Adelie', + island: 'Biscoe', + beakLength: 40.6, + beakDepth: 18.8, + flipperLength: 193, + bodyMass: 3800, + sex: 'Male', + }, + { + species: 'Adelie', + island: 'Biscoe', + beakLength: 36.5, + beakDepth: 16.6, + flipperLength: 181, + bodyMass: 2850, + sex: 'Female', + }, + { + species: 'Adelie', + island: 'Biscoe', + beakLength: 37.6, + beakDepth: 19.1, + flipperLength: 194, + bodyMass: 3750, + sex: 'Male', + }, + { + species: 'Adelie', + island: 'Biscoe', + beakLength: 35.7, + beakDepth: 16.9, + flipperLength: 185, + bodyMass: 3150, + sex: 'Female', + }, + { + species: 'Adelie', + island: 'Biscoe', + beakLength: 41.3, + beakDepth: 21.1, + flipperLength: 195, + bodyMass: 4400, + sex: 'Male', + }, + { + species: 'Adelie', + island: 'Biscoe', + beakLength: 37.6, + beakDepth: 17, + flipperLength: 185, + bodyMass: 3600, + sex: 'Female', + }, + { + species: 'Adelie', + island: 'Biscoe', + beakLength: 41.1, + beakDepth: 18.2, + flipperLength: 192, + bodyMass: 4050, + sex: 'Male', + }, + { + species: 'Adelie', + island: 'Biscoe', + beakLength: 36.4, + beakDepth: 17.1, + flipperLength: 184, + bodyMass: 2850, + sex: 'Female', + }, + { + species: 'Adelie', + island: 'Biscoe', + beakLength: 41.6, + beakDepth: 18, + flipperLength: 192, + bodyMass: 3950, + sex: 'Male', + }, + { + species: 'Adelie', + island: 'Biscoe', + beakLength: 35.5, + beakDepth: 16.2, + flipperLength: 195, + bodyMass: 3350, + sex: 'Female', + }, + { + species: 'Adelie', + island: 'Biscoe', + beakLength: 41.1, + beakDepth: 19.1, + flipperLength: 188, + bodyMass: 4100, + sex: 'Male', + }, + { + species: 'Adelie', + island: 'Torgersen', + beakLength: 35.9, + beakDepth: 16.6, + flipperLength: 190, + bodyMass: 3050, + sex: 'Female', + }, + { + species: 'Adelie', + island: 'Torgersen', + beakLength: 41.8, + beakDepth: 19.4, + flipperLength: 198, + bodyMass: 4450, + sex: 'Male', + }, + { + species: 'Adelie', + island: 'Torgersen', + beakLength: 33.5, + beakDepth: 19, + flipperLength: 190, + bodyMass: 3600, + sex: 'Female', + }, + { + species: 'Adelie', + island: 'Torgersen', + beakLength: 39.7, + beakDepth: 18.4, + flipperLength: 190, + bodyMass: 3900, + sex: 'Male', + }, + { + species: 'Adelie', + island: 'Torgersen', + beakLength: 39.6, + beakDepth: 17.2, + flipperLength: 196, + bodyMass: 3550, + sex: 'Female', + }, + { + species: 'Adelie', + island: 'Torgersen', + beakLength: 45.8, + beakDepth: 18.9, + flipperLength: 197, + bodyMass: 4150, + sex: 'Male', + }, + { + species: 'Adelie', + island: 'Torgersen', + beakLength: 35.5, + beakDepth: 17.5, + flipperLength: 190, + bodyMass: 3700, + sex: 'Female', + }, + { + species: 'Adelie', + island: 'Torgersen', + beakLength: 42.8, + beakDepth: 18.5, + flipperLength: 195, + bodyMass: 4250, + sex: 'Male', + }, + { + species: 'Adelie', + island: 'Torgersen', + beakLength: 40.9, + beakDepth: 16.8, + flipperLength: 191, + bodyMass: 3700, + sex: 'Female', + }, + { + species: 'Adelie', + island: 'Torgersen', + beakLength: 37.2, + beakDepth: 19.4, + flipperLength: 184, + bodyMass: 3900, + sex: 'Male', + }, + { + species: 'Adelie', + island: 'Torgersen', + beakLength: 36.2, + beakDepth: 16.1, + flipperLength: 187, + bodyMass: 3550, + sex: 'Female', + }, + { + species: 'Adelie', + island: 'Torgersen', + beakLength: 42.1, + beakDepth: 19.1, + flipperLength: 195, + bodyMass: 4000, + sex: 'Male', + }, + { + species: 'Adelie', + island: 'Torgersen', + beakLength: 34.6, + beakDepth: 17.2, + flipperLength: 189, + bodyMass: 3200, + sex: 'Female', + }, + { + species: 'Adelie', + island: 'Torgersen', + beakLength: 42.9, + beakDepth: 17.6, + flipperLength: 196, + bodyMass: 4700, + sex: 'Male', + }, + { + species: 'Adelie', + island: 'Torgersen', + beakLength: 36.7, + beakDepth: 18.8, + flipperLength: 187, + bodyMass: 3800, + sex: 'Female', + }, + { + species: 'Adelie', + island: 'Torgersen', + beakLength: 35.1, + beakDepth: 19.4, + flipperLength: 193, + bodyMass: 4200, + sex: 'Male', + }, + { + species: 'Adelie', + island: 'Dream', + beakLength: 37.3, + beakDepth: 17.8, + flipperLength: 191, + bodyMass: 3350, + sex: 'Female', + }, + { + species: 'Adelie', + island: 'Dream', + beakLength: 41.3, + beakDepth: 20.3, + flipperLength: 194, + bodyMass: 3550, + sex: 'Male', + }, + { + species: 'Adelie', + island: 'Dream', + beakLength: 36.3, + beakDepth: 19.5, + flipperLength: 190, + bodyMass: 3800, + sex: 'Male', + }, + { + species: 'Adelie', + island: 'Dream', + beakLength: 36.9, + beakDepth: 18.6, + flipperLength: 189, + bodyMass: 3500, + sex: 'Female', + }, + { + species: 'Adelie', + island: 'Dream', + beakLength: 38.3, + beakDepth: 19.2, + flipperLength: 189, + bodyMass: 3950, + sex: 'Male', + }, + { + species: 'Adelie', + island: 'Dream', + beakLength: 38.9, + beakDepth: 18.8, + flipperLength: 190, + bodyMass: 3600, + sex: 'Female', + }, + { + species: 'Adelie', + island: 'Dream', + beakLength: 35.7, + beakDepth: 18, + flipperLength: 202, + bodyMass: 3550, + sex: 'Female', + }, + { + species: 'Adelie', + island: 'Dream', + beakLength: 41.1, + beakDepth: 18.1, + flipperLength: 205, + bodyMass: 4300, + sex: 'Male', + }, + { + species: 'Adelie', + island: 'Dream', + beakLength: 34, + beakDepth: 17.1, + flipperLength: 185, + bodyMass: 3400, + sex: 'Female', + }, + { + species: 'Adelie', + island: 'Dream', + beakLength: 39.6, + beakDepth: 18.1, + flipperLength: 186, + bodyMass: 4450, + sex: 'Male', + }, + { + species: 'Adelie', + island: 'Dream', + beakLength: 36.2, + beakDepth: 17.3, + flipperLength: 187, + bodyMass: 3300, + sex: 'Female', + }, + { + species: 'Adelie', + island: 'Dream', + beakLength: 40.8, + beakDepth: 18.9, + flipperLength: 208, + bodyMass: 4300, + sex: 'Male', + }, + { + species: 'Adelie', + island: 'Dream', + beakLength: 38.1, + beakDepth: 18.6, + flipperLength: 190, + bodyMass: 3700, + sex: 'Female', + }, + { + species: 'Adelie', + island: 'Dream', + beakLength: 40.3, + beakDepth: 18.5, + flipperLength: 196, + bodyMass: 4350, + sex: 'Male', + }, + { + species: 'Adelie', + island: 'Dream', + beakLength: 33.1, + beakDepth: 16.1, + flipperLength: 178, + bodyMass: 2900, + sex: 'Female', + }, + { + species: 'Adelie', + island: 'Dream', + beakLength: 43.2, + beakDepth: 18.5, + flipperLength: 192, + bodyMass: 4100, + sex: 'Male', + }, + { + species: 'Adelie', + island: 'Biscoe', + beakLength: 35, + beakDepth: 17.9, + flipperLength: 192, + bodyMass: 3725, + sex: 'Female', + }, + { + species: 'Adelie', + island: 'Biscoe', + beakLength: 41, + beakDepth: 20, + flipperLength: 203, + bodyMass: 4725, + sex: 'Male', + }, + { + species: 'Adelie', + island: 'Biscoe', + beakLength: 37.7, + beakDepth: 16, + flipperLength: 183, + bodyMass: 3075, + sex: 'Female', + }, + { + species: 'Adelie', + island: 'Biscoe', + beakLength: 37.8, + beakDepth: 20, + flipperLength: 190, + bodyMass: 4250, + sex: 'Male', + }, + { + species: 'Adelie', + island: 'Biscoe', + beakLength: 37.9, + beakDepth: 18.6, + flipperLength: 193, + bodyMass: 2925, + sex: 'Female', + }, + { + species: 'Adelie', + island: 'Biscoe', + beakLength: 39.7, + beakDepth: 18.9, + flipperLength: 184, + bodyMass: 3550, + sex: 'Male', + }, + { + species: 'Adelie', + island: 'Biscoe', + beakLength: 38.6, + beakDepth: 17.2, + flipperLength: 199, + bodyMass: 3750, + sex: 'Female', + }, + { + species: 'Adelie', + island: 'Biscoe', + beakLength: 38.2, + beakDepth: 20, + flipperLength: 190, + bodyMass: 3900, + sex: 'Male', + }, + { + species: 'Adelie', + island: 'Biscoe', + beakLength: 38.1, + beakDepth: 17, + flipperLength: 181, + bodyMass: 3175, + sex: 'Female', + }, + { + species: 'Adelie', + island: 'Biscoe', + beakLength: 43.2, + beakDepth: 19, + flipperLength: 197, + bodyMass: 4775, + sex: 'Male', + }, + { + species: 'Adelie', + island: 'Biscoe', + beakLength: 38.1, + beakDepth: 16.5, + flipperLength: 198, + bodyMass: 3825, + sex: 'Female', + }, + { + species: 'Adelie', + island: 'Biscoe', + beakLength: 45.6, + beakDepth: 20.3, + flipperLength: 191, + bodyMass: 4600, + sex: 'Male', + }, + { + species: 'Adelie', + island: 'Biscoe', + beakLength: 39.7, + beakDepth: 17.7, + flipperLength: 193, + bodyMass: 3200, + sex: 'Female', + }, + { + species: 'Adelie', + island: 'Biscoe', + beakLength: 42.2, + beakDepth: 19.5, + flipperLength: 197, + bodyMass: 4275, + sex: 'Male', + }, + { + species: 'Adelie', + island: 'Biscoe', + beakLength: 39.6, + beakDepth: 20.7, + flipperLength: 191, + bodyMass: 3900, + sex: 'Female', + }, + { + species: 'Adelie', + island: 'Biscoe', + beakLength: 42.7, + beakDepth: 18.3, + flipperLength: 196, + bodyMass: 4075, + sex: 'Male', + }, + { + species: 'Adelie', + island: 'Torgersen', + beakLength: 38.6, + beakDepth: 17, + flipperLength: 188, + bodyMass: 2900, + sex: 'Female', + }, + { + species: 'Adelie', + island: 'Torgersen', + beakLength: 37.3, + beakDepth: 20.5, + flipperLength: 199, + bodyMass: 3775, + sex: 'Male', + }, + { + species: 'Adelie', + island: 'Torgersen', + beakLength: 35.7, + beakDepth: 17, + flipperLength: 189, + bodyMass: 3350, + sex: 'Female', + }, + { + species: 'Adelie', + island: 'Torgersen', + beakLength: 41.1, + beakDepth: 18.6, + flipperLength: 189, + bodyMass: 3325, + sex: 'Male', + }, + { + species: 'Adelie', + island: 'Torgersen', + beakLength: 36.2, + beakDepth: 17.2, + flipperLength: 187, + bodyMass: 3150, + sex: 'Female', + }, + { + species: 'Adelie', + island: 'Torgersen', + beakLength: 37.7, + beakDepth: 19.8, + flipperLength: 198, + bodyMass: 3500, + sex: 'Male', + }, + { + species: 'Adelie', + island: 'Torgersen', + beakLength: 40.2, + beakDepth: 17, + flipperLength: 176, + bodyMass: 3450, + sex: 'Female', + }, + { + species: 'Adelie', + island: 'Torgersen', + beakLength: 41.4, + beakDepth: 18.5, + flipperLength: 202, + bodyMass: 3875, + sex: 'Male', + }, + { + species: 'Adelie', + island: 'Torgersen', + beakLength: 35.2, + beakDepth: 15.9, + flipperLength: 186, + bodyMass: 3050, + sex: 'Female', + }, + { + species: 'Adelie', + island: 'Torgersen', + beakLength: 40.6, + beakDepth: 19, + flipperLength: 199, + bodyMass: 4000, + sex: 'Male', + }, + { + species: 'Adelie', + island: 'Torgersen', + beakLength: 38.8, + beakDepth: 17.6, + flipperLength: 191, + bodyMass: 3275, + sex: 'Female', + }, + { + species: 'Adelie', + island: 'Torgersen', + beakLength: 41.5, + beakDepth: 18.3, + flipperLength: 195, + bodyMass: 4300, + sex: 'Male', + }, + { + species: 'Adelie', + island: 'Torgersen', + beakLength: 39, + beakDepth: 17.1, + flipperLength: 191, + bodyMass: 3050, + sex: 'Female', + }, + { + species: 'Adelie', + island: 'Torgersen', + beakLength: 44.1, + beakDepth: 18, + flipperLength: 210, + bodyMass: 4000, + sex: 'Male', + }, + { + species: 'Adelie', + island: 'Torgersen', + beakLength: 38.5, + beakDepth: 17.9, + flipperLength: 190, + bodyMass: 3325, + sex: 'Female', + }, + { + species: 'Adelie', + island: 'Torgersen', + beakLength: 43.1, + beakDepth: 19.2, + flipperLength: 197, + bodyMass: 3500, + sex: 'Male', + }, + { + species: 'Adelie', + island: 'Dream', + beakLength: 36.8, + beakDepth: 18.5, + flipperLength: 193, + bodyMass: 3500, + sex: 'Female', + }, + { + species: 'Adelie', + island: 'Dream', + beakLength: 37.5, + beakDepth: 18.5, + flipperLength: 199, + bodyMass: 4475, + sex: 'Male', + }, + { + species: 'Adelie', + island: 'Dream', + beakLength: 38.1, + beakDepth: 17.6, + flipperLength: 187, + bodyMass: 3425, + sex: 'Female', + }, + { + species: 'Adelie', + island: 'Dream', + beakLength: 41.1, + beakDepth: 17.5, + flipperLength: 190, + bodyMass: 3900, + sex: 'Male', + }, + { + species: 'Adelie', + island: 'Dream', + beakLength: 35.6, + beakDepth: 17.5, + flipperLength: 191, + bodyMass: 3175, + sex: 'Female', + }, + { + species: 'Adelie', + island: 'Dream', + beakLength: 40.2, + beakDepth: 20.1, + flipperLength: 200, + bodyMass: 3975, + sex: 'Male', + }, + { + species: 'Adelie', + island: 'Dream', + beakLength: 37, + beakDepth: 16.5, + flipperLength: 185, + bodyMass: 3400, + sex: 'Female', + }, + { + species: 'Adelie', + island: 'Dream', + beakLength: 39.7, + beakDepth: 17.9, + flipperLength: 193, + bodyMass: 4250, + sex: 'Male', + }, + { + species: 'Adelie', + island: 'Dream', + beakLength: 40.2, + beakDepth: 17.1, + flipperLength: 193, + bodyMass: 3400, + sex: 'Female', + }, + { + species: 'Adelie', + island: 'Dream', + beakLength: 40.6, + beakDepth: 17.2, + flipperLength: 187, + bodyMass: 3475, + sex: 'Male', + }, + { + species: 'Adelie', + island: 'Dream', + beakLength: 32.1, + beakDepth: 15.5, + flipperLength: 188, + bodyMass: 3050, + sex: 'Female', + }, + { + species: 'Adelie', + island: 'Dream', + beakLength: 40.7, + beakDepth: 17, + flipperLength: 190, + bodyMass: 3725, + sex: 'Male', + }, + { + species: 'Adelie', + island: 'Dream', + beakLength: 37.3, + beakDepth: 16.8, + flipperLength: 192, + bodyMass: 3000, + sex: 'Female', + }, + { + species: 'Adelie', + island: 'Dream', + beakLength: 39, + beakDepth: 18.7, + flipperLength: 185, + bodyMass: 3650, + sex: 'Male', + }, + { + species: 'Adelie', + island: 'Dream', + beakLength: 39.2, + beakDepth: 18.6, + flipperLength: 190, + bodyMass: 4250, + sex: 'Male', + }, + { + species: 'Adelie', + island: 'Dream', + beakLength: 36.6, + beakDepth: 18.4, + flipperLength: 184, + bodyMass: 3475, + sex: 'Female', + }, + { + species: 'Adelie', + island: 'Dream', + beakLength: 36, + beakDepth: 17.8, + flipperLength: 195, + bodyMass: 3450, + sex: 'Female', + }, + { + species: 'Adelie', + island: 'Dream', + beakLength: 37.8, + beakDepth: 18.1, + flipperLength: 193, + bodyMass: 3750, + sex: 'Male', + }, + { + species: 'Adelie', + island: 'Dream', + beakLength: 36, + beakDepth: 17.1, + flipperLength: 187, + bodyMass: 3700, + sex: 'Female', + }, + { + species: 'Adelie', + island: 'Dream', + beakLength: 41.5, + beakDepth: 18.5, + flipperLength: 201, + bodyMass: 4000, + sex: 'Male', + }, + { + species: 'Chinstrap', + island: 'Dream', + beakLength: 46.5, + beakDepth: 17.9, + flipperLength: 192, + bodyMass: 3500, + sex: 'Female', + }, + { + species: 'Chinstrap', + island: 'Dream', + beakLength: 50, + beakDepth: 19.5, + flipperLength: 196, + bodyMass: 3900, + sex: 'Male', + }, + { + species: 'Chinstrap', + island: 'Dream', + beakLength: 51.3, + beakDepth: 19.2, + flipperLength: 193, + bodyMass: 3650, + sex: 'Male', + }, + { + species: 'Chinstrap', + island: 'Dream', + beakLength: 45.4, + beakDepth: 18.7, + flipperLength: 188, + bodyMass: 3525, + sex: 'Female', + }, + { + species: 'Chinstrap', + island: 'Dream', + beakLength: 52.7, + beakDepth: 19.8, + flipperLength: 197, + bodyMass: 3725, + sex: 'Male', + }, + { + species: 'Chinstrap', + island: 'Dream', + beakLength: 45.2, + beakDepth: 17.8, + flipperLength: 198, + bodyMass: 3950, + sex: 'Female', + }, + { + species: 'Chinstrap', + island: 'Dream', + beakLength: 46.1, + beakDepth: 18.2, + flipperLength: 178, + bodyMass: 3250, + sex: 'Female', + }, + { + species: 'Chinstrap', + island: 'Dream', + beakLength: 51.3, + beakDepth: 18.2, + flipperLength: 197, + bodyMass: 3750, + sex: 'Male', + }, + { + species: 'Chinstrap', + island: 'Dream', + beakLength: 46, + beakDepth: 18.9, + flipperLength: 195, + bodyMass: 4150, + sex: 'Female', + }, + { + species: 'Chinstrap', + island: 'Dream', + beakLength: 51.3, + beakDepth: 19.9, + flipperLength: 198, + bodyMass: 3700, + sex: 'Male', + }, + { + species: 'Chinstrap', + island: 'Dream', + beakLength: 46.6, + beakDepth: 17.8, + flipperLength: 193, + bodyMass: 3800, + sex: 'Female', + }, + { + species: 'Chinstrap', + island: 'Dream', + beakLength: 51.7, + beakDepth: 20.3, + flipperLength: 194, + bodyMass: 3775, + sex: 'Male', + }, + { + species: 'Chinstrap', + island: 'Dream', + beakLength: 47, + beakDepth: 17.3, + flipperLength: 185, + bodyMass: 3700, + sex: 'Female', + }, + { + species: 'Chinstrap', + island: 'Dream', + beakLength: 52, + beakDepth: 18.1, + flipperLength: 201, + bodyMass: 4050, + sex: 'Male', + }, + { + species: 'Chinstrap', + island: 'Dream', + beakLength: 45.9, + beakDepth: 17.1, + flipperLength: 190, + bodyMass: 3575, + sex: 'Female', + }, + { + species: 'Chinstrap', + island: 'Dream', + beakLength: 50.5, + beakDepth: 19.6, + flipperLength: 201, + bodyMass: 4050, + sex: 'Male', + }, + { + species: 'Chinstrap', + island: 'Dream', + beakLength: 50.3, + beakDepth: 20, + flipperLength: 197, + bodyMass: 3300, + sex: 'Male', + }, + { + species: 'Chinstrap', + island: 'Dream', + beakLength: 58, + beakDepth: 17.8, + flipperLength: 181, + bodyMass: 3700, + sex: 'Female', + }, + { + species: 'Chinstrap', + island: 'Dream', + beakLength: 46.4, + beakDepth: 18.6, + flipperLength: 190, + bodyMass: 3450, + sex: 'Female', + }, + { + species: 'Chinstrap', + island: 'Dream', + beakLength: 49.2, + beakDepth: 18.2, + flipperLength: 195, + bodyMass: 4400, + sex: 'Male', + }, + { + species: 'Chinstrap', + island: 'Dream', + beakLength: 42.4, + beakDepth: 17.3, + flipperLength: 181, + bodyMass: 3600, + sex: 'Female', + }, + { + species: 'Chinstrap', + island: 'Dream', + beakLength: 48.5, + beakDepth: 17.5, + flipperLength: 191, + bodyMass: 3400, + sex: 'Male', + }, + { + species: 'Chinstrap', + island: 'Dream', + beakLength: 43.2, + beakDepth: 16.6, + flipperLength: 187, + bodyMass: 2900, + sex: 'Female', + }, + { + species: 'Chinstrap', + island: 'Dream', + beakLength: 50.6, + beakDepth: 19.4, + flipperLength: 193, + bodyMass: 3800, + sex: 'Male', + }, + { + species: 'Chinstrap', + island: 'Dream', + beakLength: 46.7, + beakDepth: 17.9, + flipperLength: 195, + bodyMass: 3300, + sex: 'Female', + }, + { + species: 'Chinstrap', + island: 'Dream', + beakLength: 52, + beakDepth: 19, + flipperLength: 197, + bodyMass: 4150, + sex: 'Male', + }, + { + species: 'Chinstrap', + island: 'Dream', + beakLength: 50.5, + beakDepth: 18.4, + flipperLength: 200, + bodyMass: 3400, + sex: 'Female', + }, + { + species: 'Chinstrap', + island: 'Dream', + beakLength: 49.5, + beakDepth: 19, + flipperLength: 200, + bodyMass: 3800, + sex: 'Male', + }, + { + species: 'Chinstrap', + island: 'Dream', + beakLength: 46.4, + beakDepth: 17.8, + flipperLength: 191, + bodyMass: 3700, + sex: 'Female', + }, + { + species: 'Chinstrap', + island: 'Dream', + beakLength: 52.8, + beakDepth: 20, + flipperLength: 205, + bodyMass: 4550, + sex: 'Male', + }, + { + species: 'Chinstrap', + island: 'Dream', + beakLength: 40.9, + beakDepth: 16.6, + flipperLength: 187, + bodyMass: 3200, + sex: 'Female', + }, + { + species: 'Chinstrap', + island: 'Dream', + beakLength: 54.2, + beakDepth: 20.8, + flipperLength: 201, + bodyMass: 4300, + sex: 'Male', + }, + { + species: 'Chinstrap', + island: 'Dream', + beakLength: 42.5, + beakDepth: 16.7, + flipperLength: 187, + bodyMass: 3350, + sex: 'Female', + }, + { + species: 'Chinstrap', + island: 'Dream', + beakLength: 51, + beakDepth: 18.8, + flipperLength: 203, + bodyMass: 4100, + sex: 'Male', + }, + { + species: 'Chinstrap', + island: 'Dream', + beakLength: 49.7, + beakDepth: 18.6, + flipperLength: 195, + bodyMass: 3600, + sex: 'Male', + }, + { + species: 'Chinstrap', + island: 'Dream', + beakLength: 47.5, + beakDepth: 16.8, + flipperLength: 199, + bodyMass: 3900, + sex: 'Female', + }, + { + species: 'Chinstrap', + island: 'Dream', + beakLength: 47.6, + beakDepth: 18.3, + flipperLength: 195, + bodyMass: 3850, + sex: 'Female', + }, + { + species: 'Chinstrap', + island: 'Dream', + beakLength: 52, + beakDepth: 20.7, + flipperLength: 210, + bodyMass: 4800, + sex: 'Male', + }, + { + species: 'Chinstrap', + island: 'Dream', + beakLength: 46.9, + beakDepth: 16.6, + flipperLength: 192, + bodyMass: 2700, + sex: 'Female', + }, + { + species: 'Chinstrap', + island: 'Dream', + beakLength: 53.5, + beakDepth: 19.9, + flipperLength: 205, + bodyMass: 4500, + sex: 'Male', + }, + { + species: 'Chinstrap', + island: 'Dream', + beakLength: 49, + beakDepth: 19.5, + flipperLength: 210, + bodyMass: 3950, + sex: 'Male', + }, + { + species: 'Chinstrap', + island: 'Dream', + beakLength: 46.2, + beakDepth: 17.5, + flipperLength: 187, + bodyMass: 3650, + sex: 'Female', + }, + { + species: 'Chinstrap', + island: 'Dream', + beakLength: 50.9, + beakDepth: 19.1, + flipperLength: 196, + bodyMass: 3550, + sex: 'Male', + }, + { + species: 'Chinstrap', + island: 'Dream', + beakLength: 45.5, + beakDepth: 17, + flipperLength: 196, + bodyMass: 3500, + sex: 'Female', + }, + { + species: 'Chinstrap', + island: 'Dream', + beakLength: 50.9, + beakDepth: 17.9, + flipperLength: 196, + bodyMass: 3675, + sex: 'Female', + }, + { + species: 'Chinstrap', + island: 'Dream', + beakLength: 50.8, + beakDepth: 18.5, + flipperLength: 201, + bodyMass: 4450, + sex: 'Male', + }, + { + species: 'Chinstrap', + island: 'Dream', + beakLength: 50.1, + beakDepth: 17.9, + flipperLength: 190, + bodyMass: 3400, + sex: 'Female', + }, + { + species: 'Chinstrap', + island: 'Dream', + beakLength: 49, + beakDepth: 19.6, + flipperLength: 212, + bodyMass: 4300, + sex: 'Male', + }, + { + species: 'Chinstrap', + island: 'Dream', + beakLength: 51.5, + beakDepth: 18.7, + flipperLength: 187, + bodyMass: 3250, + sex: 'Male', + }, + { + species: 'Chinstrap', + island: 'Dream', + beakLength: 49.8, + beakDepth: 17.3, + flipperLength: 198, + bodyMass: 3675, + sex: 'Female', + }, + { + species: 'Chinstrap', + island: 'Dream', + beakLength: 48.1, + beakDepth: 16.4, + flipperLength: 199, + bodyMass: 3325, + sex: 'Female', + }, + { + species: 'Chinstrap', + island: 'Dream', + beakLength: 51.4, + beakDepth: 19, + flipperLength: 201, + bodyMass: 3950, + sex: 'Male', + }, + { + species: 'Chinstrap', + island: 'Dream', + beakLength: 45.7, + beakDepth: 17.3, + flipperLength: 193, + bodyMass: 3600, + sex: 'Female', + }, + { + species: 'Chinstrap', + island: 'Dream', + beakLength: 50.7, + beakDepth: 19.7, + flipperLength: 203, + bodyMass: 4050, + sex: 'Male', + }, + { + species: 'Chinstrap', + island: 'Dream', + beakLength: 42.5, + beakDepth: 17.3, + flipperLength: 187, + bodyMass: 3350, + sex: 'Female', + }, + { + species: 'Chinstrap', + island: 'Dream', + beakLength: 52.2, + beakDepth: 18.8, + flipperLength: 197, + bodyMass: 3450, + sex: 'Male', + }, + { + species: 'Chinstrap', + island: 'Dream', + beakLength: 45.2, + beakDepth: 16.6, + flipperLength: 191, + bodyMass: 3250, + sex: 'Female', + }, + { + species: 'Chinstrap', + island: 'Dream', + beakLength: 49.3, + beakDepth: 19.9, + flipperLength: 203, + bodyMass: 4050, + sex: 'Male', + }, + { + species: 'Chinstrap', + island: 'Dream', + beakLength: 50.2, + beakDepth: 18.8, + flipperLength: 202, + bodyMass: 3800, + sex: 'Male', + }, + { + species: 'Chinstrap', + island: 'Dream', + beakLength: 45.6, + beakDepth: 19.4, + flipperLength: 194, + bodyMass: 3525, + sex: 'Female', + }, + { + species: 'Chinstrap', + island: 'Dream', + beakLength: 51.9, + beakDepth: 19.5, + flipperLength: 206, + bodyMass: 3950, + sex: 'Male', + }, + { + species: 'Chinstrap', + island: 'Dream', + beakLength: 46.8, + beakDepth: 16.5, + flipperLength: 189, + bodyMass: 3650, + sex: 'Female', + }, + { + species: 'Chinstrap', + island: 'Dream', + beakLength: 45.7, + beakDepth: 17, + flipperLength: 195, + bodyMass: 3650, + sex: 'Female', + }, + { + species: 'Chinstrap', + island: 'Dream', + beakLength: 55.8, + beakDepth: 19.8, + flipperLength: 207, + bodyMass: 4000, + sex: 'Male', + }, + { + species: 'Chinstrap', + island: 'Dream', + beakLength: 43.5, + beakDepth: 18.1, + flipperLength: 202, + bodyMass: 3400, + sex: 'Female', + }, + { + species: 'Chinstrap', + island: 'Dream', + beakLength: 49.6, + beakDepth: 18.2, + flipperLength: 193, + bodyMass: 3775, + sex: 'Male', + }, + { + species: 'Chinstrap', + island: 'Dream', + beakLength: 50.8, + beakDepth: 19, + flipperLength: 210, + bodyMass: 4100, + sex: 'Male', + }, + { + species: 'Chinstrap', + island: 'Dream', + beakLength: 50.2, + beakDepth: 18.7, + flipperLength: 198, + bodyMass: 3775, + sex: 'Female', + }, + { + species: 'Gentoo', + island: 'Biscoe', + beakLength: 46.1, + beakDepth: 13.2, + flipperLength: 211, + bodyMass: 4500, + sex: 'Female', + }, + { + species: 'Gentoo', + island: 'Biscoe', + beakLength: 50, + beakDepth: 16.3, + flipperLength: 230, + bodyMass: 5700, + sex: 'Male', + }, + { + species: 'Gentoo', + island: 'Biscoe', + beakLength: 48.7, + beakDepth: 14.1, + flipperLength: 210, + bodyMass: 4450, + sex: 'Female', + }, + { + species: 'Gentoo', + island: 'Biscoe', + beakLength: 50, + beakDepth: 15.2, + flipperLength: 218, + bodyMass: 5700, + sex: 'Male', + }, + { + species: 'Gentoo', + island: 'Biscoe', + beakLength: 47.6, + beakDepth: 14.5, + flipperLength: 215, + bodyMass: 5400, + sex: 'Male', + }, + { + species: 'Gentoo', + island: 'Biscoe', + beakLength: 46.5, + beakDepth: 13.5, + flipperLength: 210, + bodyMass: 4550, + sex: 'Female', + }, + { + species: 'Gentoo', + island: 'Biscoe', + beakLength: 45.4, + beakDepth: 14.6, + flipperLength: 211, + bodyMass: 4800, + sex: 'Female', + }, + { + species: 'Gentoo', + island: 'Biscoe', + beakLength: 46.7, + beakDepth: 15.3, + flipperLength: 219, + bodyMass: 5200, + sex: 'Male', + }, + { + species: 'Gentoo', + island: 'Biscoe', + beakLength: 43.3, + beakDepth: 13.4, + flipperLength: 209, + bodyMass: 4400, + sex: 'Female', + }, + { + species: 'Gentoo', + island: 'Biscoe', + beakLength: 46.8, + beakDepth: 15.4, + flipperLength: 215, + bodyMass: 5150, + sex: 'Male', + }, + { + species: 'Gentoo', + island: 'Biscoe', + beakLength: 40.9, + beakDepth: 13.7, + flipperLength: 214, + bodyMass: 4650, + sex: 'Female', + }, + { + species: 'Gentoo', + island: 'Biscoe', + beakLength: 49, + beakDepth: 16.1, + flipperLength: 216, + bodyMass: 5550, + sex: 'Male', + }, + { + species: 'Gentoo', + island: 'Biscoe', + beakLength: 45.5, + beakDepth: 13.7, + flipperLength: 214, + bodyMass: 4650, + sex: 'Female', + }, + { + species: 'Gentoo', + island: 'Biscoe', + beakLength: 48.4, + beakDepth: 14.6, + flipperLength: 213, + bodyMass: 5850, + sex: 'Male', + }, + { + species: 'Gentoo', + island: 'Biscoe', + beakLength: 45.8, + beakDepth: 14.6, + flipperLength: 210, + bodyMass: 4200, + sex: 'Female', + }, + { + species: 'Gentoo', + island: 'Biscoe', + beakLength: 49.3, + beakDepth: 15.7, + flipperLength: 217, + bodyMass: 5850, + sex: 'Male', + }, + { + species: 'Gentoo', + island: 'Biscoe', + beakLength: 42, + beakDepth: 13.5, + flipperLength: 210, + bodyMass: 4150, + sex: 'Female', + }, + { + species: 'Gentoo', + island: 'Biscoe', + beakLength: 49.2, + beakDepth: 15.2, + flipperLength: 221, + bodyMass: 6300, + sex: 'Male', + }, + { + species: 'Gentoo', + island: 'Biscoe', + beakLength: 46.2, + beakDepth: 14.5, + flipperLength: 209, + bodyMass: 4800, + sex: 'Female', + }, + { + species: 'Gentoo', + island: 'Biscoe', + beakLength: 48.7, + beakDepth: 15.1, + flipperLength: 222, + bodyMass: 5350, + sex: 'Male', + }, + { + species: 'Gentoo', + island: 'Biscoe', + beakLength: 50.2, + beakDepth: 14.3, + flipperLength: 218, + bodyMass: 5700, + sex: 'Male', + }, + { + species: 'Gentoo', + island: 'Biscoe', + beakLength: 45.1, + beakDepth: 14.5, + flipperLength: 215, + bodyMass: 5000, + sex: 'Female', + }, + { + species: 'Gentoo', + island: 'Biscoe', + beakLength: 46.5, + beakDepth: 14.5, + flipperLength: 213, + bodyMass: 4400, + sex: 'Female', + }, + { + species: 'Gentoo', + island: 'Biscoe', + beakLength: 46.3, + beakDepth: 15.8, + flipperLength: 215, + bodyMass: 5050, + sex: 'Male', + }, + { + species: 'Gentoo', + island: 'Biscoe', + beakLength: 42.9, + beakDepth: 13.1, + flipperLength: 215, + bodyMass: 5000, + sex: 'Female', + }, + { + species: 'Gentoo', + island: 'Biscoe', + beakLength: 46.1, + beakDepth: 15.1, + flipperLength: 215, + bodyMass: 5100, + sex: 'Male', + }, + { + species: 'Gentoo', + island: 'Biscoe', + beakLength: 44.5, + beakDepth: 14.3, + flipperLength: 216, + bodyMass: 4100, + }, + { + species: 'Gentoo', + island: 'Biscoe', + beakLength: 47.8, + beakDepth: 15, + flipperLength: 215, + bodyMass: 5650, + sex: 'Male', + }, + { + species: 'Gentoo', + island: 'Biscoe', + beakLength: 48.2, + beakDepth: 14.3, + flipperLength: 210, + bodyMass: 4600, + sex: 'Female', + }, + { + species: 'Gentoo', + island: 'Biscoe', + beakLength: 50, + beakDepth: 15.3, + flipperLength: 220, + bodyMass: 5550, + sex: 'Male', + }, + { + species: 'Gentoo', + island: 'Biscoe', + beakLength: 47.3, + beakDepth: 15.3, + flipperLength: 222, + bodyMass: 5250, + sex: 'Male', + }, + { + species: 'Gentoo', + island: 'Biscoe', + beakLength: 42.8, + beakDepth: 14.2, + flipperLength: 209, + bodyMass: 4700, + sex: 'Female', + }, + { + species: 'Gentoo', + island: 'Biscoe', + beakLength: 45.1, + beakDepth: 14.5, + flipperLength: 207, + bodyMass: 5050, + sex: 'Female', + }, + { + species: 'Gentoo', + island: 'Biscoe', + beakLength: 59.6, + beakDepth: 17, + flipperLength: 230, + bodyMass: 6050, + sex: 'Male', + }, + { + species: 'Gentoo', + island: 'Biscoe', + beakLength: 49.1, + beakDepth: 14.8, + flipperLength: 220, + bodyMass: 5150, + sex: 'Female', + }, + { + species: 'Gentoo', + island: 'Biscoe', + beakLength: 48.4, + beakDepth: 16.3, + flipperLength: 220, + bodyMass: 5400, + sex: 'Male', + }, + { + species: 'Gentoo', + island: 'Biscoe', + beakLength: 42.6, + beakDepth: 13.7, + flipperLength: 213, + bodyMass: 4950, + sex: 'Female', + }, + { + species: 'Gentoo', + island: 'Biscoe', + beakLength: 44.4, + beakDepth: 17.3, + flipperLength: 219, + bodyMass: 5250, + sex: 'Male', + }, + { + species: 'Gentoo', + island: 'Biscoe', + beakLength: 44, + beakDepth: 13.6, + flipperLength: 208, + bodyMass: 4350, + sex: 'Female', + }, + { + species: 'Gentoo', + island: 'Biscoe', + beakLength: 48.7, + beakDepth: 15.7, + flipperLength: 208, + bodyMass: 5350, + sex: 'Male', + }, + { + species: 'Gentoo', + island: 'Biscoe', + beakLength: 42.7, + beakDepth: 13.7, + flipperLength: 208, + bodyMass: 3950, + sex: 'Female', + }, + { + species: 'Gentoo', + island: 'Biscoe', + beakLength: 49.6, + beakDepth: 16, + flipperLength: 225, + bodyMass: 5700, + sex: 'Male', + }, + { + species: 'Gentoo', + island: 'Biscoe', + beakLength: 45.3, + beakDepth: 13.7, + flipperLength: 210, + bodyMass: 4300, + sex: 'Female', + }, + { + species: 'Gentoo', + island: 'Biscoe', + beakLength: 49.6, + beakDepth: 15, + flipperLength: 216, + bodyMass: 4750, + sex: 'Male', + }, + { + species: 'Gentoo', + island: 'Biscoe', + beakLength: 50.5, + beakDepth: 15.9, + flipperLength: 222, + bodyMass: 5550, + sex: 'Male', + }, + { + species: 'Gentoo', + island: 'Biscoe', + beakLength: 43.6, + beakDepth: 13.9, + flipperLength: 217, + bodyMass: 4900, + sex: 'Female', + }, + { + species: 'Gentoo', + island: 'Biscoe', + beakLength: 45.5, + beakDepth: 13.9, + flipperLength: 210, + bodyMass: 4200, + sex: 'Female', + }, + { + species: 'Gentoo', + island: 'Biscoe', + beakLength: 50.5, + beakDepth: 15.9, + flipperLength: 225, + bodyMass: 5400, + sex: 'Male', + }, + { + species: 'Gentoo', + island: 'Biscoe', + beakLength: 44.9, + beakDepth: 13.3, + flipperLength: 213, + bodyMass: 5100, + sex: 'Female', + }, + { + species: 'Gentoo', + island: 'Biscoe', + beakLength: 45.2, + beakDepth: 15.8, + flipperLength: 215, + bodyMass: 5300, + sex: 'Male', + }, + { + species: 'Gentoo', + island: 'Biscoe', + beakLength: 46.6, + beakDepth: 14.2, + flipperLength: 210, + bodyMass: 4850, + sex: 'Female', + }, + { + species: 'Gentoo', + island: 'Biscoe', + beakLength: 48.5, + beakDepth: 14.1, + flipperLength: 220, + bodyMass: 5300, + sex: 'Male', + }, + { + species: 'Gentoo', + island: 'Biscoe', + beakLength: 45.1, + beakDepth: 14.4, + flipperLength: 210, + bodyMass: 4400, + sex: 'Female', + }, + { + species: 'Gentoo', + island: 'Biscoe', + beakLength: 50.1, + beakDepth: 15, + flipperLength: 225, + bodyMass: 5000, + sex: 'Male', + }, + { + species: 'Gentoo', + island: 'Biscoe', + beakLength: 46.5, + beakDepth: 14.4, + flipperLength: 217, + bodyMass: 4900, + sex: 'Female', + }, + { + species: 'Gentoo', + island: 'Biscoe', + beakLength: 45, + beakDepth: 15.4, + flipperLength: 220, + bodyMass: 5050, + sex: 'Male', + }, + { + species: 'Gentoo', + island: 'Biscoe', + beakLength: 43.8, + beakDepth: 13.9, + flipperLength: 208, + bodyMass: 4300, + sex: 'Female', + }, + { + species: 'Gentoo', + island: 'Biscoe', + beakLength: 45.5, + beakDepth: 15, + flipperLength: 220, + bodyMass: 5000, + sex: 'Male', + }, + { + species: 'Gentoo', + island: 'Biscoe', + beakLength: 43.2, + beakDepth: 14.5, + flipperLength: 208, + bodyMass: 4450, + sex: 'Female', + }, + { + species: 'Gentoo', + island: 'Biscoe', + beakLength: 50.4, + beakDepth: 15.3, + flipperLength: 224, + bodyMass: 5550, + sex: 'Male', + }, + { + species: 'Gentoo', + island: 'Biscoe', + beakLength: 45.3, + beakDepth: 13.8, + flipperLength: 208, + bodyMass: 4200, + sex: 'Female', + }, + { + species: 'Gentoo', + island: 'Biscoe', + beakLength: 46.2, + beakDepth: 14.9, + flipperLength: 221, + bodyMass: 5300, + sex: 'Male', + }, + { + species: 'Gentoo', + island: 'Biscoe', + beakLength: 45.7, + beakDepth: 13.9, + flipperLength: 214, + bodyMass: 4400, + sex: 'Female', + }, + { + species: 'Gentoo', + island: 'Biscoe', + beakLength: 54.3, + beakDepth: 15.7, + flipperLength: 231, + bodyMass: 5650, + sex: 'Male', + }, + { + species: 'Gentoo', + island: 'Biscoe', + beakLength: 45.8, + beakDepth: 14.2, + flipperLength: 219, + bodyMass: 4700, + sex: 'Female', + }, + { + species: 'Gentoo', + island: 'Biscoe', + beakLength: 49.8, + beakDepth: 16.8, + flipperLength: 230, + bodyMass: 5700, + sex: 'Male', + }, + { + species: 'Gentoo', + island: 'Biscoe', + beakLength: 46.2, + beakDepth: 14.4, + flipperLength: 214, + bodyMass: 4650, + }, + { + species: 'Gentoo', + island: 'Biscoe', + beakLength: 49.5, + beakDepth: 16.2, + flipperLength: 229, + bodyMass: 5800, + sex: 'Male', + }, + { + species: 'Gentoo', + island: 'Biscoe', + beakLength: 43.5, + beakDepth: 14.2, + flipperLength: 220, + bodyMass: 4700, + sex: 'Female', + }, + { + species: 'Gentoo', + island: 'Biscoe', + beakLength: 50.7, + beakDepth: 15, + flipperLength: 223, + bodyMass: 5550, + sex: 'Male', + }, + { + species: 'Gentoo', + island: 'Biscoe', + beakLength: 47.7, + beakDepth: 15, + flipperLength: 216, + bodyMass: 4750, + sex: 'Female', + }, + { + species: 'Gentoo', + island: 'Biscoe', + beakLength: 46.4, + beakDepth: 15.6, + flipperLength: 221, + bodyMass: 5000, + sex: 'Male', + }, + { + species: 'Gentoo', + island: 'Biscoe', + beakLength: 48.2, + beakDepth: 15.6, + flipperLength: 221, + bodyMass: 5100, + sex: 'Male', + }, + { + species: 'Gentoo', + island: 'Biscoe', + beakLength: 46.5, + beakDepth: 14.8, + flipperLength: 217, + bodyMass: 5200, + sex: 'Female', + }, + { + species: 'Gentoo', + island: 'Biscoe', + beakLength: 46.4, + beakDepth: 15, + flipperLength: 216, + bodyMass: 4700, + sex: 'Female', + }, + { + species: 'Gentoo', + island: 'Biscoe', + beakLength: 48.6, + beakDepth: 16, + flipperLength: 230, + bodyMass: 5800, + sex: 'Male', + }, + { + species: 'Gentoo', + island: 'Biscoe', + beakLength: 47.5, + beakDepth: 14.2, + flipperLength: 209, + bodyMass: 4600, + sex: 'Female', + }, + { + species: 'Gentoo', + island: 'Biscoe', + beakLength: 51.1, + beakDepth: 16.3, + flipperLength: 220, + bodyMass: 6000, + sex: 'Male', + }, + { + species: 'Gentoo', + island: 'Biscoe', + beakLength: 45.2, + beakDepth: 13.8, + flipperLength: 215, + bodyMass: 4750, + sex: 'Female', + }, + { + species: 'Gentoo', + island: 'Biscoe', + beakLength: 45.2, + beakDepth: 16.4, + flipperLength: 223, + bodyMass: 5950, + sex: 'Male', + }, + { + species: 'Gentoo', + island: 'Biscoe', + beakLength: 49.1, + beakDepth: 14.5, + flipperLength: 212, + bodyMass: 4625, + sex: 'Female', + }, + { + species: 'Gentoo', + island: 'Biscoe', + beakLength: 52.5, + beakDepth: 15.6, + flipperLength: 221, + bodyMass: 5450, + sex: 'Male', + }, + { + species: 'Gentoo', + island: 'Biscoe', + beakLength: 47.4, + beakDepth: 14.6, + flipperLength: 212, + bodyMass: 4725, + sex: 'Female', + }, + { + species: 'Gentoo', + island: 'Biscoe', + beakLength: 50, + beakDepth: 15.9, + flipperLength: 224, + bodyMass: 5350, + sex: 'Male', + }, + { + species: 'Gentoo', + island: 'Biscoe', + beakLength: 44.9, + beakDepth: 13.8, + flipperLength: 212, + bodyMass: 4750, + sex: 'Female', + }, + { + species: 'Gentoo', + island: 'Biscoe', + beakLength: 50.8, + beakDepth: 17.3, + flipperLength: 228, + bodyMass: 5600, + sex: 'Male', + }, + { + species: 'Gentoo', + island: 'Biscoe', + beakLength: 43.4, + beakDepth: 14.4, + flipperLength: 218, + bodyMass: 4600, + sex: 'Female', + }, + { + species: 'Gentoo', + island: 'Biscoe', + beakLength: 51.3, + beakDepth: 14.2, + flipperLength: 218, + bodyMass: 5300, + sex: 'Male', + }, + { + species: 'Gentoo', + island: 'Biscoe', + beakLength: 47.5, + beakDepth: 14, + flipperLength: 212, + bodyMass: 4875, + sex: 'Female', + }, + { + species: 'Gentoo', + island: 'Biscoe', + beakLength: 52.1, + beakDepth: 17, + flipperLength: 230, + bodyMass: 5550, + sex: 'Male', + }, + { + species: 'Gentoo', + island: 'Biscoe', + beakLength: 47.5, + beakDepth: 15, + flipperLength: 218, + bodyMass: 4950, + sex: 'Female', + }, + { + species: 'Gentoo', + island: 'Biscoe', + beakLength: 52.2, + beakDepth: 17.1, + flipperLength: 228, + bodyMass: 5400, + sex: 'Male', + }, + { + species: 'Gentoo', + island: 'Biscoe', + beakLength: 45.5, + beakDepth: 14.5, + flipperLength: 212, + bodyMass: 4750, + sex: 'Female', + }, + { + species: 'Gentoo', + island: 'Biscoe', + beakLength: 49.5, + beakDepth: 16.1, + flipperLength: 224, + bodyMass: 5650, + sex: 'Male', + }, + { + species: 'Gentoo', + island: 'Biscoe', + beakLength: 44.5, + beakDepth: 14.7, + flipperLength: 214, + bodyMass: 4850, + sex: 'Female', + }, + { + species: 'Gentoo', + island: 'Biscoe', + beakLength: 50.8, + beakDepth: 15.7, + flipperLength: 226, + bodyMass: 5200, + sex: 'Male', + }, + { + species: 'Gentoo', + island: 'Biscoe', + beakLength: 49.4, + beakDepth: 15.8, + flipperLength: 216, + bodyMass: 4925, + sex: 'Male', + }, + { + species: 'Gentoo', + island: 'Biscoe', + beakLength: 46.9, + beakDepth: 14.6, + flipperLength: 222, + bodyMass: 4875, + sex: 'Female', + }, + { + species: 'Gentoo', + island: 'Biscoe', + beakLength: 48.4, + beakDepth: 14.4, + flipperLength: 203, + bodyMass: 4625, + sex: 'Female', + }, + { + species: 'Gentoo', + island: 'Biscoe', + beakLength: 51.1, + beakDepth: 16.5, + flipperLength: 225, + bodyMass: 5250, + sex: 'Male', + }, + { + species: 'Gentoo', + island: 'Biscoe', + beakLength: 48.5, + beakDepth: 15, + flipperLength: 219, + bodyMass: 4850, + sex: 'Female', + }, + { + species: 'Gentoo', + island: 'Biscoe', + beakLength: 55.9, + beakDepth: 17, + flipperLength: 228, + bodyMass: 5600, + sex: 'Male', + }, + { + species: 'Gentoo', + island: 'Biscoe', + beakLength: 47.2, + beakDepth: 15.5, + flipperLength: 215, + bodyMass: 4975, + sex: 'Female', + }, + { + species: 'Gentoo', + island: 'Biscoe', + beakLength: 49.1, + beakDepth: 15, + flipperLength: 228, + bodyMass: 5500, + sex: 'Male', + }, + { + species: 'Gentoo', + island: 'Biscoe', + beakLength: 47.3, + beakDepth: 13.8, + flipperLength: 216, + bodyMass: 4725, + }, + { + species: 'Gentoo', + island: 'Biscoe', + beakLength: 46.8, + beakDepth: 16.1, + flipperLength: 215, + bodyMass: 5500, + sex: 'Male', + }, + { + species: 'Gentoo', + island: 'Biscoe', + beakLength: 41.7, + beakDepth: 14.7, + flipperLength: 210, + bodyMass: 4700, + sex: 'Female', + }, + { + species: 'Gentoo', + island: 'Biscoe', + beakLength: 53.4, + beakDepth: 15.8, + flipperLength: 219, + bodyMass: 5500, + sex: 'Male', + }, + { + species: 'Gentoo', + island: 'Biscoe', + beakLength: 43.3, + beakDepth: 14, + flipperLength: 208, + bodyMass: 4575, + sex: 'Female', + }, + { + species: 'Gentoo', + island: 'Biscoe', + beakLength: 48.1, + beakDepth: 15.1, + flipperLength: 209, + bodyMass: 5500, + sex: 'Male', + }, + { + species: 'Gentoo', + island: 'Biscoe', + beakLength: 50.5, + beakDepth: 15.2, + flipperLength: 216, + bodyMass: 5000, + sex: 'Female', + }, + { + species: 'Gentoo', + island: 'Biscoe', + beakLength: 49.8, + beakDepth: 15.9, + flipperLength: 229, + bodyMass: 5950, + sex: 'Male', + }, + { + species: 'Gentoo', + island: 'Biscoe', + beakLength: 43.5, + beakDepth: 15.2, + flipperLength: 213, + bodyMass: 4650, + sex: 'Female', + }, + { + species: 'Gentoo', + island: 'Biscoe', + beakLength: 51.5, + beakDepth: 16.3, + flipperLength: 230, + bodyMass: 5500, + sex: 'Male', + }, + { + species: 'Gentoo', + island: 'Biscoe', + beakLength: 46.2, + beakDepth: 14.1, + flipperLength: 217, + bodyMass: 4375, + sex: 'Female', + }, + { + species: 'Gentoo', + island: 'Biscoe', + beakLength: 55.1, + beakDepth: 16, + flipperLength: 230, + bodyMass: 5850, + sex: 'Male', + }, + { + species: 'Gentoo', + island: 'Biscoe', + beakLength: 44.5, + beakDepth: 15.7, + flipperLength: 217, + bodyMass: 4875, + sex: 'No Data', + }, + { + species: 'Gentoo', + island: 'Biscoe', + beakLength: 48.8, + beakDepth: 16.2, + flipperLength: 222, + bodyMass: 6000, + sex: 'Male', + }, + { + species: 'Gentoo', + island: 'Biscoe', + beakLength: 47.2, + beakDepth: 13.7, + flipperLength: 214, + bodyMass: 4925, + sex: 'Female', + }, + { + species: 'Gentoo', + island: 'Biscoe', + }, + { + species: 'Gentoo', + island: 'Biscoe', + beakLength: 46.8, + beakDepth: 14.3, + flipperLength: 215, + bodyMass: 4850, + sex: 'Female', + }, + { + species: 'Gentoo', + island: 'Biscoe', + beakLength: 50.4, + beakDepth: 15.7, + flipperLength: 222, + bodyMass: 5750, + sex: 'Male', + }, + { + species: 'Gentoo', + island: 'Biscoe', + beakLength: 45.2, + beakDepth: 14.8, + flipperLength: 212, + bodyMass: 5200, + sex: 'Female', + }, + { + species: 'Gentoo', + island: 'Biscoe', + beakLength: 49.9, + beakDepth: 16.1, + flipperLength: 213, + bodyMass: 5400, + sex: 'Male', + }, +] diff --git a/packages/shared/examples/basic-scatter-plot/index.tsx b/packages/shared/examples/basic-scatter-plot/index.tsx new file mode 100644 index 000000000..480305fd3 --- /dev/null +++ b/packages/shared/examples/basic-scatter-plot/index.tsx @@ -0,0 +1,33 @@ +/* eslint-disable import/no-unresolved, import/no-webpack-loader-syntax, @typescript-eslint/no-var-requires */ +import React from 'react' +import type { Example } from '../types' + +const pathname = 'basic-scatter-plot' +const example: Example = { + component: () => { + // eslint-disable-next-line @typescript-eslint/naming-convention + const Component = require(`./${pathname}.tsx`).default + return + }, + pathname, + title: 'Basic Scatter Plot', + description: ( +
+ Palmer Archipelago (Antarctica) penguin data + (Source: K.Gorman, LTER Network) +
), + codeReact: require(`!!raw-loader!./${pathname}.tsx`).default, + codeTs: require(`!!raw-loader!./${pathname}.ts`).default, + codeAngular: { + html: require(`!!raw-loader!./${pathname}.component.html`).default, + component: require(`!!raw-loader!./${pathname}.component.ts`).default, + module: require(`!!raw-loader!./${pathname}.module.ts`).default, + }, + codeSvelte: require(`!!raw-loader!./${pathname}.svelte`).default, + codeVue: require(`!!raw-loader!./${pathname}.vue`).default, + data: require('!!raw-loader!./data').default, + preview: require(`../_previews/${pathname}.png`).default, + previewDark: require(`../_previews/${pathname}-dark.png`).default, +} + +export default example diff --git a/packages/shared/examples/examples-list.tsx b/packages/shared/examples/examples-list.tsx index f59dbf51e..fdd112855 100644 --- a/packages/shared/examples/examples-list.tsx +++ b/packages/shared/examples/examples-list.tsx @@ -29,7 +29,8 @@ export const examples: ExampleCollection[] = [ title: 'Scatter Plots', description: '', examples: [ - require('./basic-scatter-chart').default, + require('./basic-scatter-plot').default, + require('./sized-scatter-plot').default, ], }, { diff --git a/packages/shared/examples/free-brush-scatters/data.ts b/packages/shared/examples/free-brush-scatters/data.ts index 46096c277..a2df68bbc 100644 --- a/packages/shared/examples/free-brush-scatters/data.ts +++ b/packages/shared/examples/free-brush-scatters/data.ts @@ -1,2 +1,2 @@ -export { data, palette } from '../basic-scatter-chart/data' -export type { DataRecord } from '../basic-scatter-chart/data' +export { data, palette } from '../sized-scatter-plot/data' +export type { DataRecord } from '../sized-scatter-plot/data' diff --git a/packages/shared/examples/free-brush-scatters/index.tsx b/packages/shared/examples/free-brush-scatters/index.tsx index 05cfc09be..05d533339 100644 --- a/packages/shared/examples/free-brush-scatters/index.tsx +++ b/packages/shared/examples/free-brush-scatters/index.tsx @@ -26,7 +26,7 @@ const example: Example = { }, codeSvelte: require(`!!raw-loader!./${pathname}.svelte`).default, codeVue: require(`!!raw-loader!./${pathname}.vue`).default, - data: require('!!raw-loader!./../basic-scatter-chart/data.ts').default, + data: require('!!raw-loader!./../sized-scatter-plot/data.ts').default, preview: require(`../_previews/${pathname}.png`).default, previewDark: require(`../_previews/${pathname}-dark.png`).default, styles: require('!!raw-loader!./styles.css').default, diff --git a/packages/shared/examples/basic-scatter-chart/data.ts b/packages/shared/examples/sized-scatter-plot/data.ts similarity index 100% rename from packages/shared/examples/basic-scatter-chart/data.ts rename to packages/shared/examples/sized-scatter-plot/data.ts diff --git a/packages/shared/examples/basic-scatter-chart/index.tsx b/packages/shared/examples/sized-scatter-plot/index.tsx similarity index 94% rename from packages/shared/examples/basic-scatter-chart/index.tsx rename to packages/shared/examples/sized-scatter-plot/index.tsx index eb678ec57..f48e8734c 100644 --- a/packages/shared/examples/basic-scatter-chart/index.tsx +++ b/packages/shared/examples/sized-scatter-plot/index.tsx @@ -2,7 +2,7 @@ import React from 'react' import type { Example } from '../types' -const pathname = 'basic-scatter-chart' +const pathname = 'sized-scatter-plot' const example: Example = { component: () => { // eslint-disable-next-line @typescript-eslint/naming-convention @@ -10,7 +10,7 @@ const example: Example = { return }, pathname, - title: 'Basic Scatter Chart', + title: 'Scatter Plot with Varied Size', description:
Data from FiveThirtyEight diff --git a/packages/shared/examples/basic-scatter-chart/basic-scatter-chart.component.html b/packages/shared/examples/sized-scatter-plot/sized-scatter-plot.component.html similarity index 100% rename from packages/shared/examples/basic-scatter-chart/basic-scatter-chart.component.html rename to packages/shared/examples/sized-scatter-plot/sized-scatter-plot.component.html diff --git a/packages/shared/examples/basic-scatter-chart/basic-scatter-chart.component.ts b/packages/shared/examples/sized-scatter-plot/sized-scatter-plot.component.ts similarity index 87% rename from packages/shared/examples/basic-scatter-chart/basic-scatter-chart.component.ts rename to packages/shared/examples/sized-scatter-plot/sized-scatter-plot.component.ts index ec57407e8..3c822b86e 100644 --- a/packages/shared/examples/basic-scatter-chart/basic-scatter-chart.component.ts +++ b/packages/shared/examples/sized-scatter-plot/sized-scatter-plot.component.ts @@ -7,10 +7,10 @@ const colorScale = Scale.scaleOrdinal(palette).domain(categories) const formatNumber = Intl.NumberFormat('en').format @Component({ - selector: 'basic-scatter-chart', - templateUrl: './basic-scatter-chart.component.html', + selector: 'sized-scatter-plot', + templateUrl: './sized-scatter-plot.component.html', }) -export class BasicScatterChartComponent { +export class SizedScatterPlotComponent { data = data getX = (d: DataRecord): number => d.medianSalary diff --git a/packages/shared/examples/basic-scatter-chart/basic-scatter-chart.module.ts b/packages/shared/examples/sized-scatter-plot/sized-scatter-plot.module.ts similarity index 59% rename from packages/shared/examples/basic-scatter-chart/basic-scatter-chart.module.ts rename to packages/shared/examples/sized-scatter-plot/sized-scatter-plot.module.ts index 29c9bdfb7..1da16fdbf 100644 --- a/packages/shared/examples/basic-scatter-chart/basic-scatter-chart.module.ts +++ b/packages/shared/examples/sized-scatter-plot/sized-scatter-plot.module.ts @@ -1,11 +1,11 @@ import { NgModule } from '@angular/core' import { VisXYContainerModule, VisScatterModule, VisAxisModule, VisTooltipModule, VisBulletLegendModule } from '@unovis/angular' -import { BasicScatterChartComponent } from './basic-scatter-chart.component' +import { SizedScatterPlotComponent } from './sized-scatter-plot.component' @NgModule({ imports: [VisXYContainerModule, VisScatterModule, VisAxisModule, VisTooltipModule, VisBulletLegendModule], - declarations: [BasicScatterChartComponent], - exports: [BasicScatterChartComponent], + declarations: [SizedScatterPlotComponent], + exports: [SizedScatterPlotComponent], }) -export class BasicScatterChartModule { } +export class SizedScatterPlotModule { } diff --git a/packages/shared/examples/basic-scatter-chart/basic-scatter-chart.svelte b/packages/shared/examples/sized-scatter-plot/sized-scatter-plot.svelte similarity index 100% rename from packages/shared/examples/basic-scatter-chart/basic-scatter-chart.svelte rename to packages/shared/examples/sized-scatter-plot/sized-scatter-plot.svelte diff --git a/packages/shared/examples/basic-scatter-chart/basic-scatter-chart.ts b/packages/shared/examples/sized-scatter-plot/sized-scatter-plot.ts similarity index 100% rename from packages/shared/examples/basic-scatter-chart/basic-scatter-chart.ts rename to packages/shared/examples/sized-scatter-plot/sized-scatter-plot.ts diff --git a/packages/shared/examples/basic-scatter-chart/basic-scatter-chart.tsx b/packages/shared/examples/sized-scatter-plot/sized-scatter-plot.tsx similarity index 96% rename from packages/shared/examples/basic-scatter-chart/basic-scatter-chart.tsx rename to packages/shared/examples/sized-scatter-plot/sized-scatter-plot.tsx index aadfe5e2a..345aac241 100644 --- a/packages/shared/examples/basic-scatter-chart/basic-scatter-chart.tsx +++ b/packages/shared/examples/sized-scatter-plot/sized-scatter-plot.tsx @@ -7,7 +7,7 @@ const categories = [...new Set(data.map((d: DataRecord) => d.category))].sort() const colorScale = Scale.scaleOrdinal(palette).domain(categories) const formatNumber = (value: number): string => Intl.NumberFormat('en', { notation: 'compact' }).format(value) -export default function BasicScatterChart (): JSX.Element { +export default function SizedScatterPlot (): JSX.Element { const legendItems = categories.map(v => ({ name: v, color: colorScale(v) })) const tooltipTriggers = { [Scatter.selectors.point]: (d: DataRecord) => ` diff --git a/packages/shared/examples/basic-scatter-chart/basic-scatter-chart.vue b/packages/shared/examples/sized-scatter-plot/sized-scatter-plot.vue similarity index 100% rename from packages/shared/examples/basic-scatter-chart/basic-scatter-chart.vue rename to packages/shared/examples/sized-scatter-plot/sized-scatter-plot.vue diff --git a/packages/shared/package.json b/packages/shared/package.json index 3257720fc..4651b6f6a 100644 --- a/packages/shared/package.json +++ b/packages/shared/package.json @@ -1,7 +1,7 @@ { "name": "@unovis/shared", "description": "Modular data visualization framework for React, Angular, Svelte, and vanilla TypeScript or JavaScript", - "version": "1.3.1", + "version": "1.3.3", "repository": { "type": "git", "url": "https://github.com/f5/unovis.git", diff --git a/packages/svelte/package.json b/packages/svelte/package.json index be943f660..9488804ca 100644 --- a/packages/svelte/package.json +++ b/packages/svelte/package.json @@ -1,7 +1,7 @@ { "name": "@unovis/svelte", "description": "Modular data visualization framework for React, Angular, Svelte, and vanilla TypeScript or JavaScript", - "version": "1.3.1", + "version": "1.3.3", "repository": { "type": "git", "url": "https://github.com/f5/unovis.git", @@ -37,7 +37,7 @@ "publish:dist": "rm -rf dist/.cache; cp ./{LICENSE,README.md,package.json} ./dist; cd ./dist; npm publish" }, "peerDependencies": { - "@unovis/ts": "1.3.1", + "@unovis/ts": "1.3.3", "svelte": "^3.48.0 || ^4.0.0" }, "devDependencies": { diff --git a/packages/ts/package.json b/packages/ts/package.json index 677c863c5..5a5c86724 100644 --- a/packages/ts/package.json +++ b/packages/ts/package.json @@ -1,7 +1,7 @@ { "name": "@unovis/ts", "description": "Modular data visualization framework for React, Angular, Svelte, and vanilla TypeScript or JavaScript", - "version": "1.3.1", + "version": "1.3.3", "repository": { "type": "git", "url": "https://github.com/f5/unovis.git", diff --git a/packages/ts/src/components/bullet-legend/config.ts b/packages/ts/src/components/bullet-legend/config.ts index dd80dc4c1..0b4f7a5d8 100644 --- a/packages/ts/src/components/bullet-legend/config.ts +++ b/packages/ts/src/components/bullet-legend/config.ts @@ -1,4 +1,5 @@ // Local Types +import { GenericAccessor } from 'types/accessor' import { BulletLegendItemInterface, BulletShape } from './types' export interface BulletLegendConfigInterface { @@ -7,6 +8,7 @@ export interface BulletLegendConfigInterface { * { * name: string | number; * color?: string; + * shape?: BulletShape; * inactive?: boolean; * hidden?: boolean; * pointer?: boolean; @@ -24,8 +26,8 @@ export interface BulletLegendConfigInterface { labelMaxWidth?: string | null; /** Bullet shape size, mapped to the width and height CSS properties. Default: `null` */ bulletSize?: string | null; - /** Bullet shape: `BulletShape.Circle`, `BulletShape.Line` or `BulletShape.Square`. Default: `BulletShape.Circle` */ - bulletShape?: BulletShape; + /** Bullet shape enum value or accessor function. Default: `d => d.shape ?? BulletShape.Circle */ + bulletShape?: GenericAccessor; } export const BulletLegendDefaultConfig: BulletLegendConfigInterface = { @@ -35,5 +37,5 @@ export const BulletLegendDefaultConfig: BulletLegendConfigInterface = { labelFontSize: null, labelMaxWidth: null, bulletSize: null, - bulletShape: BulletShape.Circle, + bulletShape: d => d.shape ?? BulletShape.Circle, } diff --git a/packages/ts/src/components/bullet-legend/index.ts b/packages/ts/src/components/bullet-legend/index.ts index 4009ff408..db88679db 100644 --- a/packages/ts/src/components/bullet-legend/index.ts +++ b/packages/ts/src/components/bullet-legend/index.ts @@ -15,6 +15,7 @@ import { createBullets, updateBullets } from './modules/shape' // Styles import * as s from './style' + export class BulletLegend { static selectors = s protected _defaultConfig = BulletLegendDefaultConfig as BulletLegendConfigInterface @@ -48,34 +49,36 @@ export class BulletLegend { render (): void { const { config } = this - const legendItems = this.div.selectAll(`.${s.item}`) - .data(config.items) - const legendItemsEnter = legendItems.enter() - .append('div') - .attr('class', s.item) + const legendItems = this.div.selectAll(`.${s.item}`).data(config.items) + + const legendItemsEnter = legendItems.enter().append('div') + .attr('class', d => `${s.item} ${d.className ?? ''}`) .on('click', this._onItemClick.bind(this)) + const legendItemsMerged = legendItemsEnter.merge(legendItems) + legendItemsMerged + .classed(s.clickable, d => !!config.onLegendItemClick && this._isItemClickable(d)) + .style('display', (d: BulletLegendItemInterface) => d.hidden ? 'none' : null) + + // Bullet legendItemsEnter.append('span') .attr('class', s.bullet) - .call(createBullets, config) + .call(createBullets) + legendItemsMerged.select(`.${s.bullet}`) + .style('width', config.bulletSize) + .style('height', config.bulletSize) + .style('box-sizing', 'content-box') + .call(updateBullets, this.config, this._colorAccessor) + + // Labels legendItemsEnter.append('span') .attr('class', s.label) .classed(config.labelClassName, true) .style('max-width', config.labelMaxWidth) .style('font-size', config.labelFontSize) - const legendItemsMerged = legendItemsEnter.merge(legendItems) - legendItemsMerged - .classed(s.clickable, d => !!config.onLegendItemClick && this._isItemClickable(d)) - .style('display', (d: BulletLegendItemInterface) => d.hidden ? 'none' : null) - - legendItemsMerged.select(`.${s.bullet}`) - .style('min-width', config.bulletSize) - .style('height', config.bulletSize) - .call(updateBullets, this.config, this._colorAccessor) - legendItemsMerged.select(`.${s.label}`) .text((d: BulletLegendItemInterface) => d.name) diff --git a/packages/ts/src/components/bullet-legend/modules/shape.ts b/packages/ts/src/components/bullet-legend/modules/shape.ts index b60dc1fff..5bdb40f0f 100644 --- a/packages/ts/src/components/bullet-legend/modules/shape.ts +++ b/packages/ts/src/components/bullet-legend/modules/shape.ts @@ -1,11 +1,13 @@ -import { Selection } from 'd3-selection' +import { Selection, select } from 'd3-selection' +import { symbol } from 'd3-shape' // Types import { ColorAccessor } from 'types/accessor' +import { Symbol, SymbolType } from 'types/symbol' // Utils import { getColor } from 'utils/color' -import { circlePath } from 'utils/path' +import { getString } from 'utils/data' // Constants import { PATTERN_SIZE_PX } from 'styles/patterns' @@ -18,63 +20,79 @@ import { BulletShape, BulletLegendItemInterface } from '../types' // the configured size. const BULLET_SIZE = PATTERN_SIZE_PX * 3 -function getHeight (shape: BulletShape): number { - switch (shape) { - case BulletShape.Line: - return BULLET_SIZE / 2.5 - default: - return BULLET_SIZE - } -} - -function getPath (shape: BulletShape, width: number, height: number): string { - switch (shape) { - case BulletShape.Line: - return `M0,${height / 2} L${width / 2},${height / 2} L${width},${height / 2}` - case BulletShape.Square: - return `M0,0 L${width},0 L${width},${height} L0,${height}Z` - case BulletShape.Circle: - return circlePath(height / 2, height / 2, height / 2 - 1) - } +// Different shapes need different scaling to fit the full size +const shapeScale: Record = { + [BulletShape.Circle]: Math.PI / 4, + [BulletShape.Cross]: 5 / 9, + [BulletShape.Diamond]: Math.sqrt(3) / 6, + [BulletShape.Square]: 1, + [BulletShape.Star]: 0.3, + [BulletShape.Triangle]: Math.sqrt(3) / 4, + [BulletShape.Wye]: 5 / 11, } export function createBullets ( - container: Selection, - config: BulletLegendConfigInterface + container: Selection ): void { - container.append('svg') - .attr('width', '100%') - .attr('height', '100%') - .append('path') - .attr('d', getPath(config.bulletShape, BULLET_SIZE, getHeight(config.bulletShape))) + container.each((d, i, els) => { + select(els[i]).append('svg') + .attr('width', '100%') + .attr('height', '100%') + .append('path') + }) } export function updateBullets ( - container: Selection, + container: Selection, config: BulletLegendConfigInterface, colorAccessor: ColorAccessor ): void { - const width = BULLET_SIZE - const height = getHeight(config.bulletShape) + container.each((d, i, els) => { + const shape = getString(d, config.bulletShape, i) as BulletShape + const color = getColor(d, colorAccessor, i) + const width = BULLET_SIZE + const height = shape === BulletShape.Line ? BULLET_SIZE / 2.5 : BULLET_SIZE - const getOpacity = (d: BulletLegendItemInterface): number => d.inactive ? 0.4 : 1 + const selection = select(els[i]).select('svg') + .attr('viewBox', `0 0 ${width} ${height}`) + .select('path') + .attr('stroke', color) - const selection = container.select('svg') - .attr('viewBox', `0 0 ${width} ${height}`) - .select('path') - .attr('d', getPath(config.bulletShape, width, height)) - .attr('stroke', (d, i) => getColor(d, colorAccessor, i)) - .style('stroke-width', '1px') - .style('fill', (d, i) => getColor(d, colorAccessor, i)) - .style('fill-opacity', getOpacity) + if (shape === BulletShape.Line) { + selection + .attr('d', `M0,${height / 2} L${width / 2},${height / 2} L${width},${height / 2}`) + .attr('transform', null) + .style('opacity', d.inactive ? 0.4 : 1) + .style('stroke-width', '3px') + .style('fill', null) + .style('fill-opacity', null) + .style('marker-start', 'none') + .style('marker-end', 'none') + } else { + const symbolGen = symbol() + .type(Symbol[shape]) + .size(width * height * shapeScale[shape]) - if (config.bulletShape === BulletShape.Line) { - selection - .style('stroke-width', `${height / 5}px`) - .style('opacity', getOpacity) - .style('fill', null) - .style('fill-opacity', null) - .style('marker-start', 'none') - .style('marker-end', 'none') - } + const scale = (width - 2) / width + let dy = height / 2 + switch (shape) { + case BulletShape.Triangle: + dy += height / 8 + break + case BulletShape.Star: + dy += height / 16 + break + case BulletShape.Wye: + dy -= height / 16 + break + } + selection + .attr('d', symbolGen) + .attr('transform', `translate(${width / 2}, ${Math.round(dy)}) scale(${scale})`) + .style('stroke-width', '1px') + .style('opacity', null) + .style('fill', color) + .style('fill-opacity', d.inactive ? 0.4 : 1) + } + }) } diff --git a/packages/ts/src/components/bullet-legend/style.ts b/packages/ts/src/components/bullet-legend/style.ts index 17b7f712d..177a24d5f 100644 --- a/packages/ts/src/components/bullet-legend/style.ts +++ b/packages/ts/src/components/bullet-legend/style.ts @@ -61,10 +61,10 @@ export const label = css` export const bullet = css` label: legendItemBullet; margin-right: var(--vis-legend-bullet-label-spacing); - min-width: var(--vis-legend-bullet-size); height: var(--vis-legend-bullet-size); - - > svg { + width: var(--vis-legend-bullet-size); + + svg { display: block; } } diff --git a/packages/ts/src/components/bullet-legend/types.ts b/packages/ts/src/components/bullet-legend/types.ts index 5f29f9a2e..f71e9aed0 100644 --- a/packages/ts/src/components/bullet-legend/types.ts +++ b/packages/ts/src/components/bullet-legend/types.ts @@ -1,13 +1,19 @@ +/* eslint-disable @typescript-eslint/naming-convention */ +import { SymbolType } from 'types/symbol' + export interface BulletLegendItemInterface { name: string | number; color?: string; + className?: string; + shape?: BulletShape; inactive?: boolean; hidden?: boolean; pointer?: boolean; } -export enum BulletShape { - Circle = 'circle', - Line = 'line', - Square = 'square', -} +export const BulletShape = { + ...SymbolType, + Line: 'line', +} as const + +export type BulletShape = typeof BulletShape[keyof typeof BulletShape] diff --git a/packages/ts/src/components/chord-diagram/config.ts b/packages/ts/src/components/chord-diagram/config.ts index 0d86622a7..e7cd93c37 100644 --- a/packages/ts/src/components/chord-diagram/config.ts +++ b/packages/ts/src/components/chord-diagram/config.ts @@ -8,6 +8,10 @@ import { ColorAccessor, GenericAccessor, NumericAccessor, StringAccessor } from import { ChordInputLink, ChordInputNode, ChordLabelAlignment, ChordLinkDatum, ChordNodeDatum } from './types' export interface ChordDiagramConfigInterface extends ComponentConfigInterface { + /** Angular range of the diagram. Default: `[0, 2 * Math.PI]` */ + angleRange?: [number, number]; + /** Corner radius constant value or accessor function. Default: `2` */ + cornerRadius?: NumericAccessor>; /** Node id or index to highlight. Overrides default hover behavior if supplied. Default: `undefined` */ highlightedNodeId?: number | string; /** Link ids or index values to highlight. Overrides default hover behavior if supplied. Default: [] */ @@ -28,12 +32,8 @@ export interface ChordDiagramConfigInterface>; /** Node label alignment. Default: `ChordLabelAlignment.Along` */ nodeLabelAlignment?: GenericAccessor>; - /** Pad angle in radians. Constant value or accessor function. Default: `0.02` */ - padAngle?: NumericAccessor>; - /** Corner radius constant value or accessor function. Default: `2` */ - cornerRadius?: NumericAccessor>; - /** Angular range of the diagram. Default: `[0, 2 * Math.PI]` */ - angleRange?: [number, number]; + /** Pad angle in radians. Default: `0.02` */ + padAngle?: number; /** The exponent property of the radius scale. Default: `2` */ radiusScaleExponent?: number; } diff --git a/packages/ts/src/components/chord-diagram/index.ts b/packages/ts/src/components/chord-diagram/index.ts index 5245e5f9a..969984aad 100644 --- a/packages/ts/src/components/chord-diagram/index.ts +++ b/packages/ts/src/components/chord-diagram/index.ts @@ -1,6 +1,5 @@ import { max } from 'd3-array' -import { nest } from 'd3-collection' -import { HierarchyNode, hierarchy, partition } from 'd3-hierarchy' +import { partition } from 'd3-hierarchy' import { Selection } from 'd3-selection' import { scalePow, ScalePower } from 'd3-scale' import { arc } from 'd3-shape' @@ -10,24 +9,14 @@ import { ComponentCore } from 'core/component' import { GraphData, GraphDataModel } from 'data-models/graph' // Utils -import { getNumber, isNumber, groupBy, getString, getValue } from 'utils/data' +import { getNumber, isNumber, getString, getValue } from 'utils/data' import { estimateStringPixelLength } from 'utils/text' // Types -import { GraphNodeCore } from 'types/graph' import { Spacing } from 'types/spacing' // Local Types -import { - ChordInputNode, - ChordInputLink, - ChordDiagramData, - ChordHierarchyNode, - ChordNode, - ChordRibbon, - ChordLabelAlignment, - ChordLeafNode, -} from './types' +import { ChordInputNode, ChordInputLink, ChordDiagramData, ChordNode, ChordRibbon, ChordLabelAlignment, ChordLeafNode } from './types' // Config import { ChordDiagramDefaultConfig, ChordDiagramConfigInterface } from './config' @@ -35,6 +24,7 @@ import { ChordDiagramDefaultConfig, ChordDiagramConfigInterface } from './config // Modules import { createNode, updateNode, removeNode } from './modules/node' import { createLabel, updateLabel, removeLabel, LABEL_PADDING } from './modules/label' +import { getHierarchyNodes, getRibbons, positionChildren } from './modules/layout' import { createLink, updateLink, removeLink } from './modules/link' // Styles @@ -52,9 +42,11 @@ export class ChordDiagram< public config: ChordDiagramConfigInterface = this._defaultConfig datamodel: GraphDataModel = new GraphDataModel() + background: Selection nodeGroup: Selection linkGroup: Selection labelGroup: Selection + arcGen = arc>() radiusScale: ScalePower = scalePow() @@ -71,6 +63,10 @@ export class ChordDiagram< mouseover: this._onLinkMouseOver.bind(this), mouseout: this._onLinkMouseOut.bind(this), }, + [ChordDiagram.selectors.label]: { + mouseover: this._onNodeMouseOver.bind(this), + mouseout: this._onNodeMouseOut.bind(this), + }, } private get _forceHighlight (): boolean { @@ -80,6 +76,7 @@ export class ChordDiagram< constructor (config?: ChordDiagramConfigInterface) { super() if (config) this.setConfig(config) + this.background = this.g.append('rect').attr('class', s.background) this.linkGroup = this.g.append('g').attr('class', s.links) this.nodeGroup = this.g.append('g').attr('class', s.nodes) this.labelGroup = this.g.append('g').attr('class', s.labels) @@ -120,55 +117,65 @@ export class ChordDiagram< setData (data: GraphData): void { super.setData(data) - const hierarchyData = this._getHierarchyNodes() - - const partitionData = partition>() - .size([this.config.angleRange[1], 1])(hierarchyData) as ChordNode - - partitionData.each((node, i) => { - this._calculateRadialPosition(node, getNumber(node.data, this.config.padAngle)) - - // Add hierarchy data for non leaf nodes - if (node.children) { - node.data = Object.assign(node.data, { - depth: node.depth, - height: node.height, - value: node.value, - ancestors: node.ancestors().map(d => (d.data as ChordHierarchyNode).key), - }) - } - node.x0 = Number.isNaN(node.x0) ? 0 : node.x0 - node.x1 = Number.isNaN(node.x1) ? 0 : node.x1 - node.uid = `${this.uid}-n${i}` - node._state = {} + this._layoutData() + } + + _layoutData (): void { + const { nodes, links } = this.datamodel + const { padAngle, linkValue, nodeLevels } = this.config + nodes.forEach(n => { delete n._state.value }) + links.forEach(l => { + delete l._state.points + l._state.value = getNumber(l, linkValue) + l.source._state.value = (l.source._state.value || 0) + getNumber(l, linkValue) + l.target._state.value = (l.target._state.value || 0) + getNumber(l, linkValue) + }) + + const root = getHierarchyNodes(nodes, d => d._state?.value, nodeLevels) + + const partitionData = partition().size([this.config.angleRange[1], 1])(root) as ChordNode + partitionData.each((n, i) => { + positionChildren(n, padAngle) + n.uid = `${this.uid.substr(0, 4)}-${i}` + n.x0 = Number.isNaN(n.x0) ? 0 : n.x0 + n.x1 = Number.isNaN(n.x1) ? 0 : n.x1 + n._state = {} }) const partitionDataWithRoot = partitionData.descendants() this._rootNode = partitionDataWithRoot.find(d => d.depth === 0) this._nodes = partitionDataWithRoot.filter(d => d.depth !== 0) // Filter out the root node - this._links = this._getRibbons(partitionData) + this._links = getRibbons(partitionData, links, padAngle) } _render (customDuration?: number): void { super._render(customDuration) const { config, bleed } = this + this._layoutData() const duration = isNumber(customDuration) ? customDuration : config.duration const size = Math.min(this._width, this._height) const radius = size / 2 - max([bleed.top, bleed.bottom, bleed.left, bleed.right]) - this.radiusScale.range([0, radius]) + this.radiusScale.range([0, radius - config.nodeWidth]) this.arcGen - .startAngle(d => d.x0) - .endAngle(d => d.x1) + .startAngle(d => d.x0 + config.padAngle / 2 - (d.value ? 0 : Math.PI / 360)) + .endAngle(d => d.x1 - config.padAngle / 2 + (d.value ? 0 : Math.PI / 360)) .cornerRadius(d => getNumber(d.data, config.cornerRadius)) .innerRadius(d => this.radiusScale(d.y1) - getNumber(d, config.nodeWidth)) .outerRadius(d => this.radiusScale(d.y1)) - // Center the view - this.g.attr('transform', `translate(${this._width / 2},${this._height / 2})`) this.g.classed(s.transparent, this._forceHighlight) + this.background + .attr('width', this._width) + .attr('height', this._height) + .style('opacity', 0) + + // Center the view + this.nodeGroup.attr('transform', `translate(${this._width / 2},${this._height / 2})`) + this.labelGroup.attr('transform', `translate(${this._width / 2},${this._height / 2})`) + this.linkGroup.attr('transform', `translate(${this._width / 2},${this._height / 2})`) // Links const linksSelection = this.linkGroup @@ -208,11 +215,11 @@ export class ChordDiagram< // Labels const labelWidth = size - radius - config.nodeWidth const labels = this.labelGroup - .selectAll>(`.${s.gLabel}`) + .selectAll>(`.${s.label}`) .data(this._nodes, d => String(d.uid)) const labelEnter = labels.enter().append('g') - .attr('class', s.gLabel) + .attr('class', s.label) .call(createLabel, config, this.radiusScale) const labelsMerged = labels.merge(labelEnter) @@ -223,128 +230,6 @@ export class ChordDiagram< .call(removeLabel, duration) } - private _getHierarchyNodes (): HierarchyNode> { - const { config, datamodel: { nodes, links } } = this - nodes.forEach(n => { delete n._state.value }) - links.forEach(l => { - delete l._state.points - l.source._state.value = (l.source._state.value || 0) + getNumber(l, config.linkValue) - l.target._state.value = (l.target._state.value || 0) + getNumber(l, config.linkValue) - }) - - // TODO: Replace with d3-group - const nestGen = nest() - config.nodeLevels.forEach(levelAccessor => { - nestGen.key((d) => (d as unknown as Record)[levelAccessor]) - }) - const root = { key: 'root', values: nestGen.entries(nodes) } - const hierarchyNodes = hierarchy(root, d => d.values) - .sum((d) => (d as unknown as GraphNodeCore)._state?.value) - - return hierarchyNodes - } - - private _getRibbons (partitionData: ChordNode): ChordRibbon[] { - const { config, datamodel: { links } } = this - const findNode = ( - nodes: ChordLeafNode[], - id: string - ): ChordLeafNode => nodes.find(n => n.data._id === id) - const leafNodes = partitionData.leaves() as ChordLeafNode[] - - type LinksArrayType = typeof links - const groupedBySource: Record = groupBy(links, d => d.source._id) - const groupedByTarget: Record = groupBy(links, d => d.target._id) - - const getNodesInRibbon = ( - source: ChordLeafNode, - target: ChordLeafNode, - partitionHeight: number, - nodes: ChordLeafNode[] = [] - ): ChordNode[] => { - nodes[source.height] = source - nodes[partitionHeight * 2 - target.height] = target - if (source.parent && target.parent) getNodesInRibbon(source.parent, target.parent, partitionHeight, nodes) - return nodes - } - - const calculatePoints = ( - links: LinksArrayType, - type: 'in' | 'out', - depth: number - ): void => { - links.forEach(link => { - if (!link._state.points) link._state.points = [] - const sourceLeaf = findNode(leafNodes, link.source._id) - const targetLeaf = findNode(leafNodes, link.target._id) - const nodesInRibbon = getNodesInRibbon( - type === 'out' ? sourceLeaf : targetLeaf, - type === 'out' ? targetLeaf : sourceLeaf, - partitionData.height) - const currNode = nodesInRibbon[depth] - const len = currNode.x1 - currNode.x0 - const x0 = currNode._prevX1 ?? currNode.x0 - const x1 = x0 + len * getNumber(link, config.linkValue) / currNode.value - currNode._prevX1 = x1 - - const pointIdx = type === 'out' ? depth : partitionData.height * 2 - 1 - depth - link._state.points[pointIdx] = { - a0: Math.min(x0, x1), // - Math.PI / 2, - a1: Math.max(x0, x1), // - Math.PI / 2, - r: currNode.y1, - } - }) - } - - leafNodes.forEach(leafNode => { - const outLinks = groupedBySource[leafNode.data._id] || [] - const inLinks = groupedByTarget[leafNode.data._id] || [] - for (let depth = 0; depth < partitionData.height; depth += 1) { - calculatePoints(outLinks, 'out', depth) - calculatePoints(inLinks, 'in', depth) - } - }) - - const ribbons = links.map(l => { - const sourceNode = findNode(leafNodes, l.source._id) - const targetNode = findNode(leafNodes, l.target._id) - - return { - source: sourceNode, - target: targetNode, - data: l, - points: l._state.points, - _state: {}, - } - }) - - return ribbons - } - - private _calculateRadialPosition ( - hierarchyNode: ChordNode, - nodePadding = 0.02, - scalingCoeff = 0.95 - ): void { - if (!hierarchyNode.children) return - - // Calculate x0 and x1 - const nodeLength = (hierarchyNode.x1 - hierarchyNode.x0) - const scaledNodeLength = nodeLength * scalingCoeff - const delta = nodeLength - scaledNodeLength - let x0 = hierarchyNode.x0 + delta / 2 - for (const node of hierarchyNode.children) { - const childX0 = x0 - const childX1 = x0 + (node.value / hierarchyNode.value) * scaledNodeLength - nodePadding / 2 - const childNodeLength = childX1 - childX0 - const scaledChildNodeLength = childNodeLength * scalingCoeff - const childDelta = childNodeLength - scaledChildNodeLength - node.x0 = childX0 + childDelta / 2 - node.x1 = node.x0 + scaledChildNodeLength - x0 = childX1 + nodePadding / 2 + childDelta / 2 - } - } - private _onNodeMouseOver (d: ChordNode): void { let ribbons: ChordRibbon[] if (d.children) { @@ -356,6 +241,9 @@ export class ChordDiagram< const leaf = d as ChordLeafNode ribbons = this._links.filter(l => l.source.data.id === leaf.data.id || l.target.data.id === leaf.data.id) } + + // Nodes without links should still be highlighted + if (!ribbons.length) d._state.hovered = true this._highlightOnHover(ribbons) } diff --git a/packages/ts/src/components/chord-diagram/modules/label.ts b/packages/ts/src/components/chord-diagram/modules/label.ts index 49d40aaac..19eadb741 100644 --- a/packages/ts/src/components/chord-diagram/modules/label.ts +++ b/packages/ts/src/components/chord-diagram/modules/label.ts @@ -83,7 +83,7 @@ export function createLabel .attr('transform', d => getLabelTransform(d, config, radiusScale)) selection.append('text') - .attr('class', s.label) + .attr('class', s.labelText) .style('fill', d => getColor(d.data, config.nodeColor, d.height)) } @@ -100,7 +100,7 @@ export function updateLabel .attr('transform', d => getLabelTransform(d, config, radiusScale)) .style('opacity', 1) - const label: Selection, SVGElement, unknown> = selection.select(`.${s.label}`) + const label: Selection, SVGElement, unknown> = selection.select(`.${s.labelText}`) label.select('textPath').remove() label .text(d => getString(d.data, nodeLabel)) diff --git a/packages/ts/src/components/chord-diagram/modules/layout.ts b/packages/ts/src/components/chord-diagram/modules/layout.ts new file mode 100644 index 000000000..e726c9c31 --- /dev/null +++ b/packages/ts/src/components/chord-diagram/modules/layout.ts @@ -0,0 +1,118 @@ +import { group, index } from 'd3-array' +import { HierarchyNode, hierarchy } from 'd3-hierarchy' +import { pie } from 'd3-shape' + +// Utils +import { getNumber, groupBy } from 'utils/data' + +// Types +import { NumericAccessor } from 'types/accessor' + +// Local Types +import { ChordNode, ChordRibbon, ChordLinkDatum, ChordHierarchyNode, ChordLeafNode } from '../types' + +function transformData (node: HierarchyNode): void { + const { height, depth } = node + if (height > 0) { + const d = node.data as unknown as [string, T[]] + const n = node as unknown as HierarchyNode> + n.data = { key: d[0], values: d[1], depth, height, ancestors: n.ancestors().map(d => d.data.key) } + } +} + +export function getHierarchyNodes ( + data: N[], + value: NumericAccessor, + levels: string[] = [] +): HierarchyNode> { + const nodeLevels = levels.map(level => (d: N) => d[level as keyof N]) as unknown as [(d: N) => string] + const nestedData = levels.length ? group(data, ...nodeLevels) : { key: 'root', children: data } + + const root = hierarchy(nestedData) + .sum(d => getNumber(d as unknown as N, value)) + .each(transformData) + + return root as unknown as HierarchyNode> +} + +export function positionChildren (node: ChordNode, padding: number, scalingCoeff = 0.95): void { + if (!node.children) return + + const length = node.x1 - node.x0 + const scaledLength = length * scalingCoeff + const delta = length - scaledLength + + const positions = pie>() + .startAngle(node.x0 + delta / 2) + .endAngle(node.x1 - delta / 2) + .padAngle(padding) + .value(d => d.value) + .sort((a, b) => node.children.indexOf(a) - node.children.indexOf(b))(node.children) + + node.children.forEach((child, i) => { + const x0 = positions[i].startAngle + const x1 = positions[i].endAngle + const childDelta = (x1 - x0) * (1 - scalingCoeff) + child.x0 = x0 + childDelta / 2 + child.x1 = x1 - childDelta / 2 + }) +} + +export function getRibbons (data: ChordNode, links: ChordLinkDatum[], padding: number): ChordRibbon[] { + type LinksArrayType = typeof links + const groupedBySource: Record = groupBy(links, d => d.source._id) + const groupedByTarget: Record = groupBy(links, d => d.target._id) + + const leafNodes = data.leaves() as ChordLeafNode[] + const leafNodesById: Map> = index(leafNodes, d => d.data._id) + + const getNodesInRibbon = ( + source: ChordLeafNode, + target: ChordLeafNode, + partitionHeight: number, + nodes: ChordNode[] = [] + ): ChordNode[] => { + nodes[source.height] = source + nodes[partitionHeight * 2 - target.height] = target + if (source.parent && target.parent) getNodesInRibbon(source.parent, target.parent, partitionHeight, nodes) + return nodes + } + const calculatePoints = (links: LinksArrayType, type: 'in' | 'out', depth: number, maxDepth: number): void => { + links.forEach(link => { + if (!link._state.points) link._state.points = [] + + const sourceLeaf = leafNodesById.get(link.source._id) + const targetLeaf = leafNodesById.get(link.target._id) + const nodesInRibbon = getNodesInRibbon( + type === 'out' ? sourceLeaf : targetLeaf, + type === 'out' ? targetLeaf : sourceLeaf, + maxDepth + ) + const currNode = nodesInRibbon[depth] + const len = currNode.x1 - currNode.x0 - padding + const x0 = currNode._prevX1 ?? (currNode.x0 + padding / 2) + const x1 = x0 + len * link._state.value / currNode.value + currNode._prevX1 = x1 + + const pointIdx = type === 'out' ? depth : maxDepth * 2 - 1 - depth + link._state.points[pointIdx] = { a0: x0, a1: x1, r: currNode.y1 } + }) + } + + leafNodes.forEach(leafNode => { + const outLinks = groupedBySource[leafNode.data._id] || [] + const inLinks = groupedByTarget[leafNode.data._id] || [] + for (let depth = 0; depth < leafNode.depth; depth += 1) { + calculatePoints(outLinks, 'out', depth, leafNode.depth) + calculatePoints(inLinks, 'in', depth, leafNode.depth) + } + }) + + return links.map(l => ({ + source: leafNodesById.get(l.source._id), + target: leafNodesById.get(l.target._id), + data: l, + points: l._state.points, + _state: {}, + })) +} diff --git a/packages/ts/src/components/chord-diagram/modules/link.ts b/packages/ts/src/components/chord-diagram/modules/link.ts index 9b046fb45..c5deaed48 100644 --- a/packages/ts/src/components/chord-diagram/modules/link.ts +++ b/packages/ts/src/components/chord-diagram/modules/link.ts @@ -1,6 +1,5 @@ import { Selection, select } from 'd3-selection' import { ribbon } from 'd3-chord' -import { path } from 'd3-path' import { ScalePower } from 'd3-scale' import { areaRadial } from 'd3-shape' import { Transition } from 'd3-transition' @@ -41,18 +40,17 @@ function linkGen (points: ChordRibbonPoint[], radiusScale: ScalePower radiusScale(d.r)) - if (points.length === 2) { - return link(points) as string - } - const p = path() - const src = points[0] - const radius = Math.max(radiusScale(src.r), 0) + const linkPath = link(points) as string + + if (points.length === 2) return linkPath - link.context(p as CanvasRenderingContext2D) - link(points) - p.arc(0, 0, radius, src.a0 - Math.PI / 2, src.a1 - Math.PI / 2, src.a1 - src.a0 <= Number.EPSILON) + // Replace closePath with line to starting point + const area = linkPath.slice(0, -1) + const path = area.concat(`L${area.match(/M-?\d*\.?\d*[,\s*]-?\d*\.?\d*/)?.[0].slice(1)}`) - return convertLineToArc(p, radius) + // Convert line edges to arcs + const radius = Math.max(radiusScale(points[0].r), 0) + return convertLineToArc(path, radius) } export function createLink ( diff --git a/packages/ts/src/components/chord-diagram/style.ts b/packages/ts/src/components/chord-diagram/style.ts index bb54c39be..0e3d5ff68 100644 --- a/packages/ts/src/components/chord-diagram/style.ts +++ b/packages/ts/src/components/chord-diagram/style.ts @@ -18,6 +18,7 @@ export const variables = injectGlobal` --vis-chord-diagram-label-text-fill-color-bright: #ffffff; --vis-chord-diagram-label-text-fill-color-dark: #a5abb2; + --vis-chord-diagram-label-text-font-size: 16px; --vis-dark-chord-diagram-link-fill-color: #575c65; } @@ -27,6 +28,10 @@ export const variables = injectGlobal` } ` +export const background = css` + label: background; +` + export const nodes = css` label: nodes; ` @@ -53,17 +58,17 @@ export const highlightedNode = css` stroke-width: 1.5; ` -export const gLabel = css` - label: group-label; -` - export const label = css` label: label; +` + +export const labelText = css` + label: label-text; dominant-baseline: middle; user-select: none; - pointer-events: none; - + font-size: var(--vis-chord-diagram-label-text-font-size); + > textPath { dominant-baseline: central; } diff --git a/packages/ts/src/components/timeline/config.ts b/packages/ts/src/components/timeline/config.ts index df33578ca..868503db2 100644 --- a/packages/ts/src/components/timeline/config.ts +++ b/packages/ts/src/components/timeline/config.ts @@ -35,6 +35,7 @@ export interface TimelineConfigInterface extends WithOptional = { ...XYComponentDefaultConfig, + id: undefined, color: (d: unknown): string => (d as { color: string }).color, lineWidth: 8, lineCap: false, diff --git a/packages/ts/src/components/timeline/index.ts b/packages/ts/src/components/timeline/index.ts index d325a543b..4dc9dbbe2 100644 --- a/packages/ts/src/components/timeline/index.ts +++ b/packages/ts/src/components/timeline/index.ts @@ -166,24 +166,28 @@ export class Timeline extends XYComponentCore(`.${s.line}`) - .data(data, (d: Datum, i) => `${getString(d, config.id, i) ?? i}`) + .data(data, (d: Datum, i) => getString(d, config.id, i) ?? [ + this._getRecordType(d, i), getNumber(d, config.x, i), + ].join('-')) + const linesEnter = lines.enter().append('rect') .attr('class', s.line) .classed(s.rowOdd, config.alternatingRowColors ? (d, i) => !(recordLabelsUnique.indexOf(this._getRecordType(d, i)) % 2) : null) - .style('fill', (d, i) => getColor(d, config.color, i)) + .style('fill', (d, i) => getColor(d, config.color, ordinalScale(this._getRecordType(d, i)))) + .call(this._positionLines.bind(this), ordinalScale) .attr('transform', 'translate(0, 10)') .style('opacity', 0) - this._positionLines(linesEnter, ordinalScale) - const linesMerged = smartTransition(linesEnter.merge(lines), duration) + const linesMerged = linesEnter.merge(lines) .style('fill', (d, i) => getColor(d, config.color, ordinalScale(this._getRecordType(d, i)))) - .attr('transform', 'translate(0, 0)') .style('cursor', (d, i) => getString(d, config.cursor, i)) - .style('opacity', 1) + .call(this._positionLines.bind(this), ordinalScale) - this._positionLines(linesMerged, ordinalScale) + smartTransition(linesMerged, duration) + .attr('transform', 'translate(0, 0)') + .style('opacity', 1) smartTransition(lines.exit(), duration) .style('opacity', 0) @@ -243,7 +247,6 @@ export class Timeline extends XYComponentCore extends ContainerCore { - public component: ComponentCore - public config: SingleContainerConfigInterface protected _defaultConfig = SingleContainerDefaultConfig as SingleContainerConfigInterface + public component: ComponentCore + public config: SingleContainerConfigInterface = this._defaultConfig constructor (element: HTMLElement, config?: SingleContainerConfigInterface, data?: Data) { super(element) diff --git a/packages/ts/src/containers/xy-container/index.ts b/packages/ts/src/containers/xy-container/index.ts index aef110780..97add9ae0 100644 --- a/packages/ts/src/containers/xy-container/index.ts +++ b/packages/ts/src/containers/xy-container/index.ts @@ -43,10 +43,10 @@ export type XYConfigInterface = XYComponentConfigInterface | AreaConfigInterface export class XYContainer extends ContainerCore { - public datamodel: CoreDataModel = new CoreDataModel() - public config: XYContainerConfigInterface protected _defaultConfig = XYContainerDefaultConfig as XYContainerConfigInterface protected _svgDefs: Selection + public datamodel: CoreDataModel = new CoreDataModel() + public config: XYContainerConfigInterface = this._defaultConfig private _clipPath: Selection private _clipPathId = guid() private _axisMargin: Spacing = { top: 0, bottom: 0, left: 0, right: 0 } diff --git a/packages/vue/package-lock.json b/packages/vue/package-lock.json index 3063e4234..037cf7fd3 100644 --- a/packages/vue/package-lock.json +++ b/packages/vue/package-lock.json @@ -1,12 +1,12 @@ { "name": "@unovis/vue", - "version": "1.3.1", + "version": "1.3.3", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@unovis/vue", - "version": "1.3.1", + "version": "1.3.3", "license": "Apache-2.0", "devDependencies": { "@antfu/eslint-config": "^0.41.0", diff --git a/packages/vue/package.json b/packages/vue/package.json index 545be96ac..d209fe920 100644 --- a/packages/vue/package.json +++ b/packages/vue/package.json @@ -1,7 +1,7 @@ { "name": "@unovis/vue", "description": "Modular data visualization framework for React, Angular, Svelte, Vue, and vanilla TypeScript or JavaScript", - "version": "1.3.1", + "version": "1.3.3", "repository": { "type": "git", "url": "https://github.com/f5/unovis.git", diff --git a/packages/website/docs/auxiliary/Axis.mdx b/packages/website/docs/auxiliary/Axis.mdx index ac9fed098..bb02a9b3d 100644 --- a/packages/website/docs/auxiliary/Axis.mdx +++ b/packages/website/docs/auxiliary/Axis.mdx @@ -4,7 +4,7 @@ sidebar_position: 0 import CodeBlock from '@theme/CodeBlock'; import BrowserOnly from '@docusaurus/BrowserOnly' import { FrameworkTabs } from '../components/framework-tabs' -import { PropTable } from '../components/props-table' +import { PropsTable } from '@site/src/components/PropsTable' import { generateDataRecords, generateTimeSeries } from '../utils/data' import { XYWrapper, XYWrapperWithInput } from '../wrappers' @@ -281,4 +281,4 @@ events = { ## Component Props - + diff --git a/packages/website/docs/auxiliary/Brush.mdx b/packages/website/docs/auxiliary/Brush.mdx index ba964b69f..54be75835 100644 --- a/packages/website/docs/auxiliary/Brush.mdx +++ b/packages/website/docs/auxiliary/Brush.mdx @@ -1,6 +1,6 @@ import CodeBlock from '@theme/CodeBlock' import BrowserOnly from '@docusaurus/BrowserOnly' -import { PropTable } from '../components/props-table' +import { PropsTable } from '@site/src/components/PropsTable' import { generateDataRecords } from '../utils/data' import { XYWrapper, XYWrapperWithInput, axis } from '../wrappers/xy-wrapper' @@ -83,7 +83,7 @@ By default, setting the desired the range relies on moving both start and end _B `handleWidth` sets the width in pixels of the _Brush_'s handle: -### CSS Variables +## CSS Variables
Supported CSS variables and their default values @@ -100,4 +100,5 @@ By default, setting the desired the range relies on moving both start and end _B `}
- +## Component Props + diff --git a/packages/website/docs/auxiliary/BulletLegend.mdx b/packages/website/docs/auxiliary/BulletLegend.mdx index 986d331b3..8f9b11819 100644 --- a/packages/website/docs/auxiliary/BulletLegend.mdx +++ b/packages/website/docs/auxiliary/BulletLegend.mdx @@ -3,12 +3,11 @@ title: Bullet Legend sidebar_title: Bullet Legend --- import BrowserOnly from '@docusaurus/BrowserOnly' -import { PropTable } from '../components/props-table' +import CodeBlock from '@theme/CodeBlock' +import { PropsTable } from '@site/src/components/PropsTable' import { FrameworkTabs } from '../components/framework-tabs.tsx' import { generateDataRecords } from '../utils/data' -import { DocWrapper, InputWrapper, XYWrapper } from '../wrappers' -import { parseComponent } from '../utils/parser.ts' -import { trimMultiline } from '@site/src/utils/text' +import { DocWrapper } from '../wrappers' import '../styles.css' export const BulletLegendDoc = (props) => ( @@ -34,6 +33,7 @@ Each item has type `BulletLegendItemInterface`: interface BulletLegendItemInterface { name: string | number; color?: string; + shape?: BulletShape; inactive?: boolean; hidden?: boolean; pointer?: boolean; @@ -53,7 +53,7 @@ Here is an example of a basic configuration, where `labels` is an array of strin }} /> -### Custom Colors +### Color By default, our [color palette](/docs/guides/theming#color-palette) will be used to color each legend item, but you can provide your own colors in the legend item array. ```ts @@ -76,12 +76,48 @@ Either will produce the following result: { name: 'C', color: 'green' }, ]}/> +### Shape +You can specify the shape of individual bullets with the `shape` property or with the [`bulletShape`](#bullet-shapes) +component config property. This is useful when you want to have the legend with a line chart or shaped scatter plot. + +The supported shapes are apart of the `BulletShape` enum. + +```ts +import { BulletShape } from '@unovis/ts' + +const items = [ + { name: 'Circle', shape: BulletShape.Circle }, + { name: 'Square', shape: BulletShape.Square }, + { name: 'Triangle', shape: BulletShape.Triangle } + { name: 'Star', shape: BulletShape.Star } +] +``` + + ({ name, shape: name.toLowerCase() }))}/> + +
+All supported shapes: +
    +
  • BulletShape.Circle or "circle"
  • +
  • BulletShape.Cross or "cross"
  • +
  • BulletShape.Diamond or "diamond"
  • +
  • BulletShape.Line or "line"
  • +
  • BulletShape.Square or "square"
  • +
  • BulletShape.Star or "star"
  • +
  • BulletShape.Triangle or "triangle"
  • +
  • BulletShape.Wye or "wye"
  • +
+
+ + ### Inactive Items In some cases you may want to have some legend items look _inactive_, which reduces the opacity of the bullet. See how the initial legend looks when all of the items are inactive: + ```ts const items = labels.map(label => ({ name: label, inactive: true })) ``` + ({ name: `Item ${i}`, inactive: true }))}/> ### Pointer @@ -90,11 +126,28 @@ Note that there is no specified default value unless `onLegendItemCilck` propert cursor will be `pointer`. ## Bullet Shapes -You can specify the bullet shapes with `bulletShape` property. The supported shapes are: +You can specify the bullet shapes with `bulletShape` property. By default, the bullet shape is `circle` unless +an individual item has a configured shape (see [Shape](#shape) section). + + + +You can provide this property with a `BulletShape` enum value or string. +Or a constant value. This might be preferable if you want each shape to be the same. For example: + + + + +
+Alteratively, you can provide an accessor function of type: + +```ts +function (d: BulletLegendItemInterface, i: number): BulletShape | string {} +``` + +:::note +If `bulletShape` is supplied, it will take precedence over the `shape` property in the `items` array. +::: -`BulletShape.Circle` or `"circle"`: -`BulletShape.Line` or `"line"` : -`BulletShape.Square` or `"square"` : ## Label Configuration ### Font Size @@ -247,4 +300,4 @@ legend.update({ items, onLegendItemClick: toggleItem })`.trim()}
## Component Props - + diff --git a/packages/website/docs/auxiliary/Crosshair.mdx b/packages/website/docs/auxiliary/Crosshair.mdx index 3db407a2f..9f3f99846 100644 --- a/packages/website/docs/auxiliary/Crosshair.mdx +++ b/packages/website/docs/auxiliary/Crosshair.mdx @@ -1,5 +1,5 @@ import CodeBlock from '@theme/CodeBlock' -import { PropTable } from '../components/props-table.tsx' +import { PropsTable } from '@site/src/components/PropsTable' import { generateDataRecords } from '../utils/data' import { XYWrapperWithInput, XYWrapper } from '../wrappers' @@ -12,13 +12,13 @@ export const defaultProps = (chart="Line", components, n=5) => ({ components: [{ name: chart, props: { x: d=>d.x, y: [d=>d.y, d=>d.y1, d=>d.y2], ...components}, key: "components" }] }) -### Basic configuration +## Basic Configuration The _Crosshair_ component is a special tooltip designed to work in an _XYContainer_. When a user is interacting with the _XYContainer_ and a crosshair is provided, the _Crosshair_ will appear as a vertical line and render circles on the corresponding `y` values in the dataset. -### X and Y accessors +## X and Y accessors Like other components in you can supply `x` and `y` accessors to the _Crosshair_ component to control where it appears in your container. There's also a dedicated `yStacked` property for dealing with stacked values. @@ -34,20 +34,20 @@ const yStacked: ((d: DataRecord) => number)[] = [d => d.y, d => d.y1, d => d.y2] ``` d.x + 0.5} yStacked={[d=>d.y, d=>d.y1, d=>d.y2]}/> -### Hiding the crosshair +## Show/Hide Behavior By default, the _Crosshair_ component will render if the cursor is within a certain distance in pixels from a valid `x` value. You can disable this feature using the `hideWhenFarFromPointer` attribute. -### hideWhenFarFromPointerDistance +#### `hideWhenFarFromPointerDistance` Use the `hideWhenFarFromPointerDistance` attribute with a length (in pixels) that represents the minimum horizontal distance the cursor must be from a datapoint before hiding. d.x % 2 == 0)} property="hideWhenFarFromPointerDistance" defaultValue={50} inputType="range"/> -### Custom Color +## Custom Color Provide a `string` or color accessor function to the `color` attribute to customize the crosshair's point color: ['red', 'green', 'blue'][i]} showContext="minimal"/> -### Displaying Custom Content +## Adding a Tooltip You can render text content for your _Crosshair_ component by providing it with a `template` property and a _Tooltip_ component within the same container. -### Additional Styling: CSS Variables +## CSS Variables The _Crosshair_ component supports additional styling via CSS variables that you can define for your visualization container. For example: ```css title="styles.css" @@ -81,4 +81,5 @@ The _Crosshair_ component supports additional styling via CSS variables that you } - +## Component Props + diff --git a/packages/website/docs/auxiliary/FreeBrush.mdx b/packages/website/docs/auxiliary/FreeBrush.mdx index 633bc6b6f..acfa3858b 100644 --- a/packages/website/docs/auxiliary/FreeBrush.mdx +++ b/packages/website/docs/auxiliary/FreeBrush.mdx @@ -4,7 +4,7 @@ title: Free Brush --- import CodeBlock from '@theme/CodeBlock' import BrowserOnly from '@docusaurus/BrowserOnly' -import { PropTable } from '../components/props-table' +import { PropsTable } from '@site/src/components/PropsTable' import { generateScatterDataRecords } from '../utils/data' import { XYWrapper, XYWrapperWithInput } from '../wrappers' @@ -53,4 +53,5 @@ Disabling the `autoHide` property will keep the _Free Brush_ selection visible a - +## Component Props + diff --git a/packages/website/docs/auxiliary/Tooltip.mdx b/packages/website/docs/auxiliary/Tooltip.mdx index cb47db8f3..49295813d 100644 --- a/packages/website/docs/auxiliary/Tooltip.mdx +++ b/packages/website/docs/auxiliary/Tooltip.mdx @@ -1,7 +1,7 @@ import BrowserOnly from '@docusaurus/BrowserOnly' import CodeBlock from '@theme/CodeBlock' import { FrameworkTabs } from '../components/framework-tabs' -import { PropTable } from '../components/props-table' +import { PropsTable } from '@site/src/components/PropsTable' import { generateDataRecords } from '../utils/data' import { XYWrapper, XYWrapperWithInput } from '../wrappers' @@ -52,12 +52,12 @@ export const Tooltip = (props) => ( ) -### Basic configuration +## Basic Configuration The _Tooltip_ component has been designed to work alongside XY charts. The minimal _Tooltip_ configuration looks like: -### Triggers +## Triggers The `triggers` property allows a _Tooltip_ component to display custom content for a given CSS selector. d.x - 0.25, y: [d=>d.y, d=>d.y1, d => d.y3], barWidth: 20})} @@ -232,7 +232,7 @@ you need to refer to your tooltip by using a CSS selector. }} /> -## Additional Styling: CSS Variables +## CSS Variables The _Tooltip_ component supports additional styling via CSS variables that you can define for your visualization container. For example: ```css title="styles.css" @@ -262,4 +262,4 @@ The _Tooltip_ component supports additional styling via CSS variables that you c ## Component Props - + diff --git a/packages/website/docs/containers/Single_Container.mdx b/packages/website/docs/containers/Single_Container.mdx index c14fa5f00..713112a85 100644 --- a/packages/website/docs/containers/Single_Container.mdx +++ b/packages/website/docs/containers/Single_Container.mdx @@ -5,8 +5,8 @@ description: Learn how to use Single Container --- import BrowserOnly from '@docusaurus/BrowserOnly' -import { PropTable } from '../components/props-table' -import { sankeyData, orderedData } from '../utils/data' +import { PropsTable } from '@site/src/components/PropsTable' +import { sankeyData } from '../utils/data' import { DocWrapper } from '../wrappers' import './styles.css' @@ -27,7 +27,7 @@ export const sankeyProps = { components: [{ name: 'Sankey', props: {}, key: 'component'}], } -### Basic configuration +## Basic Configuration _Single Container_ is a basic container for a Unovis component. It is designed to hold one visualization component and an (optional) _Tooltip_. Just wrap the component of your choice in _Single Container_ to render it. @@ -64,6 +64,7 @@ For XY Components, instead use the XY Container, whic 2D data with X and Y coordinates. ::: +## Sizing ### Width and Height By default, _Single Container_ will try to fit within the bounds of its parent HTML element. If the parent height isn't defined, the default height of `300px` will be applied. You can also explicitly define the container's @@ -77,7 +78,6 @@ const height = 100;
{() => { - const { VisSingleContainer, VisTopoJSONMap } = require('@unovis/react') const { WorldMapTopoJSON } = require('@unovis/ts/maps') const topojson = { name: 'TopoJSONMap', props: { topojson: WorldMapTopoJSON, }, @@ -90,12 +90,9 @@ const height = 100;
-### Margin -You can set _Single Container_'s `margin` to control the spacing between the container and adjacent -elements. The `margin` property accepts a value of type `Sizing`, where each value represents the -corresponding margin size in pixels. +### Margin and Padding +You can supply _Single Container_'s `margin` and `padding` properties with values of the following type: -#### Sizing ```ts type Sizing = { top: number; @@ -104,11 +101,16 @@ type Sizing = { right: number; } ``` -Note that the chart's size is affected by this property. Notice how the following chart is affected -after setting the margin accordingly. -```ts -const margin = { left: 100, right: 100 } +Where each number represents the number of pixels for the corresponding property. + +:::info Note: Size Conflicts + +Setting margin will affect the chart's size. Notice how Sankey's width decreases when setting the +horizontal margin, despite having the same configured width: + +``` +{ left: 100, right: 100 } ``` #### Before: @@ -117,14 +119,16 @@ const margin = { left: 100, right: 100 } #### After: -### Sizing +::: + +### `sizing` Property The `sizing` property determines whether components should fit into the container or the container should expand to fit to the component's size. Currently, only Sankey supports the `sizing` option. By default, all components will fit to the size of _Single Container_. -### SVG Defs +## SVG Defs You can use the `svgDefs` property to define custom fill patterns for your components. See our Tips and Tricks for details. ## Component Props - + diff --git a/packages/website/docs/containers/XY_Container.mdx b/packages/website/docs/containers/XY_Container.mdx index 1bc568b27..c1fa90a17 100644 --- a/packages/website/docs/containers/XY_Container.mdx +++ b/packages/website/docs/containers/XY_Container.mdx @@ -5,7 +5,7 @@ description: Learn how to use XY Container --- import BrowserOnly from '@docusaurus/BrowserOnly' -import { PropTable } from '../components/props-table' +import { PropsTable } from '@site/src/components/PropsTable' import { generateDataRecords } from '../utils/data' import { XYWrapper, XYWrapperWithInput } from '../wrappers' import './styles.css' @@ -225,4 +225,4 @@ You can use the `svgDefs` property to define custom fill patterns for your compo See our Tips and Tricks for details. ## Component Props - + diff --git a/packages/website/docs/maps/LeafletFlowMap.mdx b/packages/website/docs/maps/LeafletFlowMap.mdx index 1c9c6cb80..77c43ac8c 100644 --- a/packages/website/docs/maps/LeafletFlowMap.mdx +++ b/packages/website/docs/maps/LeafletFlowMap.mdx @@ -7,7 +7,7 @@ description: Learn how to configure a Leaflet flow map import BrowserOnly from '@docusaurus/BrowserOnly' import { mapKey } from '@unovis/shared/examples/basic-leaflet-map/constants' -import { PropTable } from '../components/props-table.tsx' +import { PropsTable } from '@site/src/components/PropsTable' import { DocWrapper } from '../wrappers' import { ContextLevel } from '../wrappers/types' import cities from './data/us_cities.json' @@ -129,4 +129,4 @@ onSourcePointMouseLeave: (f: FlowDatum, event: MouseEvent) => void; ``` ## Component Props - + diff --git a/packages/website/docs/maps/LeafletMap.mdx b/packages/website/docs/maps/LeafletMap.mdx index 96a36a305..86fe3b949 100644 --- a/packages/website/docs/maps/LeafletMap.mdx +++ b/packages/website/docs/maps/LeafletMap.mdx @@ -8,7 +8,7 @@ import BrowserOnly from '@docusaurus/BrowserOnly' import { sum } from 'd3-array' import { mapKey } from '@unovis/shared/examples/basic-leaflet-map/constants' -import { PropTable } from '../components/props-table.tsx' +import { PropsTable } from '@site/src/components/PropsTable' import { DocWrapper } from '../wrappers' import { ContextLevel } from '../wrappers/types' import cities from './data/us_cities.json' @@ -479,4 +479,4 @@ events = { ``` ## Component Props - + diff --git a/packages/website/docs/maps/TopoJSONMap.mdx b/packages/website/docs/maps/TopoJSONMap.mdx index 1bf4451ae..cdb56326f 100644 --- a/packages/website/docs/maps/TopoJSONMap.mdx +++ b/packages/website/docs/maps/TopoJSONMap.mdx @@ -5,18 +5,18 @@ description: Learn how to configure a TopoJSON Map --- import BrowserOnly from '@docusaurus/BrowserOnly' import CodeBlock from '@theme/CodeBlock' -import { PropTable } from '../components/props-table.tsx' +import { PropsTable } from '@site/src/components/PropsTable' import { DocWrapper, InputWrapper } from '../wrappers' import { data, pointData, heatmapData } from './data' -export const defaultProps = (showData = true) => ({ +export const defaultProps = () => ({ name: "TopoJSONMap", configKey: "component", containerName: "SingleContainer", topojson: "WorldMapTopoJSON", height: 400, + data: {}, dataType: "MapPoint,MapLink,MapArea", - showContext: "minimal", hiddenProps: { disableZoom: true, } @@ -28,10 +28,11 @@ export const TopoDoc = (props) => ( {() => { const lib = require('@unovis/ts/maps') - const imports = { + const imports = props.showContext ? { "@unovis/ts/maps": [props.topojson], - } - if (props.showData) imports['@unovis/ts'] = ["MapData"] + "@unovis/ts": ["MapData"] + } : undefined + return ( ) @@ -39,15 +40,17 @@ export const TopoDoc = (props) => ( ) -## Basic Configuration -The _TopoJSONMap_ is a map that uses [TopoJSON](https://github.com/topojson/topojson) as its -underlying data. The basic configuration is: - +The _TopoJSONMap_ component is a map that is renders topological geo-data from the +[TopoJSON](https://github.com/topojson/topojson) data format. You can provide your own custom _topojson_ +or use one of the [pre-configured topojson files](#topojson-configuration) provided in `@unovis/ts/maps`. +In addition to custom topologies, _TopoJSONMap_ supports a variety of features including the +ability to add points and links, feature customization, zooming, alternate map projections and more. -:::note -The remaining examples will use `WorldMapTopoJSON` for the topojson configuration. -Check out [this section](#topojson-configuration) to learn about other available maps. -::: +## Basic Configuration + ## Map Data There are three main building blocks that can make up the data supplied to _TopoJSONMap_: @@ -60,14 +63,12 @@ blocks as keys mapped to their data arrays, like so: ```ts type MapData = { - area: MapArea[]; + areas: MapArea[]; points: MapPoint[]; links: MapLink[]; } ``` - - where `MapArea`, `MapPoint`, and `MapLink` are custom types that represent map data. Keep reading to learn more about the minimum configurations for these types. @@ -85,14 +86,14 @@ _TopoJSONMap_ directly. +/> -### mapFitToPoints +### `mapFitToPoints` -### Point Customization +### Point Styling The following _TopoJSONMap_ properties accept accessor functions to customize the appearance of your points: * `pointColor` @@ -104,12 +105,25 @@ points: You can provide labels with the `pointLabel` attribute, which accepts a `StringAccessor` function to be called on each `MapPoint` datum. +For example, consider the following type and accessor function: +```ts +type MapPoint = { + id: string; + latitude: number; + longitude: number; + city: string; +} + +const pointLabel = (d: MapPoint) => d.city +``` + d.city}/> -### Custom Point Labels +### Point Label Styling The following _TopoJSONMap_ properties can further customize your Point Label. * `pointLabelPosition` * `pointLabelTextBrighnessRatio` @@ -174,10 +188,10 @@ Or, you can simply provide these values through the `linkSource` and `linkTarget -### Link customization +#### Link customization You can further customize the map _Links_ with the following properties: * `linkColor` * `linkCursor` @@ -185,8 +199,8 @@ You can further customize the map _Links_ with the following properties: ## Areas To work with features in the _TopoJSONMap_, all you need is a unique `id` which is defined in the -chart's _topojson_ definition. For example, in our `WorldMapTopoJSON` _topojson_, every country has -a unique id that corresponds to the [ISO 3166-1 alpha-2](https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2) +chart's _topojson_ definition or an `areaId` accessor function. For example, in our `WorldMapTopoJSON` +_topojson_, every country has a unique id that corresponds to the [ISO 3166-1 alpha-2](https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2) country code. See our [topojson configuration](#topojson-configuration) section for more details. ```ts @@ -195,19 +209,78 @@ type MapArea = { } ``` +As a basic example, let's say you have an array of countries created from ISO codes: + +```ts +const countryCodes = [ + 'AU','BR','CN','EG','FR','IN','JP','MX','NO','PE','PH','RU','TZ','US' +] +const areaData = countryCodes.map(id => ({ id })) +const data = { areas: areaData } +``` + +The provided countries will be highlighted according to their color defined in the topojson file: + + d.id.substring(0, 2)} + excludeTabs +/> + +### Custom Color +You can override the default area colors by including a `color` property in `AreaDatum` or by providing +an `areaColor` accessor function. + +```ts +const data = { + areas = [ + { id: 'AU', color: 'red' }, + { id: 'BR', color: 'blue' }, + { id: 'CN', color: 'green' }, + ] +} +``` + d.id} - showContext="minimal" + data={{ areas: [ + { id: 'AU', color: 'red', name: 'Australia' }, + { id: 'BR', color: 'blue', name: 'Brazil' }, + { id: 'CN', color: 'green', name: 'China' }, +] }} + areaId={d => d.id} + areaColor={d => d.color} + excludeTabs /> -### Area customization -You can further customize the map _Area_ with the following properties: -* `areaColor` -* `areaCursor` +Has the same result as: + +```ts +const areaColor = (d: AreaDatum) => { + switch (d.id) { + case 'AU': return 'red' + case 'BR': return 'blue' + case 'CN': return 'green' + } +} +``` + d.id} + areaColor={d => d.color} + excludeGraph + hideTabLabels +/> + + -## Projection +## Projections You can provide a projection for your _TopoJSONMap_ with a `MapProjection` instance. See D3's [geo projections](https://github.com/d3/d3-geo) for more information. @@ -242,17 +315,17 @@ By default, zooming is enabled for a _TopoJSONMap_ component. You can disable it For further customization, you can configure the following zoom properties: -### zoomFactor +### `zoomFactor` To set the initial zoom factor. -### zoomExtent +### `zoomExtent` `zoomExtent` represents the range `[a,b]` which your map can zoom in and out, where `[a, b]` are the minimum and maximum zoom factors, respectively. -### zoomDuration +### `zoomDuration` `zoomDuration` is the duration of the animation on when zooming in on your _TopoJSONMap_. ## Heatmap Mode @@ -261,8 +334,7 @@ For datasets with a lot of points, you can enable `heatmapMode` You can customize the appearance of your heat map blur with the `heatmapModeBlurStdDeviation` property. -### heatmapModeZoomLevelThreshold -To lower or raise the threshold hat will disable the blur effect of your heat map, use the the +To lower or raise the threshold that will disable the blur effect of your heat map, use the the `heatmapModeZoomLevelThreshold` property. You can provide a zoom level, (i.e. `2` for _2x_ zoom), that once reached, that will no longer display the blur effect. @@ -324,7 +396,7 @@ const events = { } ``` -## Additional Styling: CSS Variables +## CSS Variables The _TopoJSONMap_ component supports additional styling via CSS variables that you can define for your visualization container. @@ -349,4 +421,4 @@ your visualization container. ## Component Props - + diff --git a/packages/website/docs/misc/Donut.mdx b/packages/website/docs/misc/Donut.mdx index 43ce3b1ff..8d9fc3dc1 100644 --- a/packages/website/docs/misc/Donut.mdx +++ b/packages/website/docs/misc/Donut.mdx @@ -3,7 +3,7 @@ description: Learn how to configure a Donut chart --- import CodeBlock from '@theme/CodeBlock' -import { PropTable } from '../components/props-table.tsx' +import { PropsTable } from '@site/src/components/PropsTable' import { DocWrapper, InputWrapper } from '../wrappers' export const defaultProps = () => ({ @@ -135,4 +135,4 @@ All supported CSS variables and their default values ``` ## Component Props - + diff --git a/packages/website/docs/misc/NestedDonut.mdx b/packages/website/docs/misc/NestedDonut.mdx index 30348f7e4..6ddcc3d74 100644 --- a/packages/website/docs/misc/NestedDonut.mdx +++ b/packages/website/docs/misc/NestedDonut.mdx @@ -1,7 +1,7 @@ --- title: Nested Donut --- -import { PropTable } from '../components/props-table.tsx' +import { PropsTable } from '@site/src/components/PropsTable' import { sample } from '../utils/data.ts' import { DocWrapper } from '../wrappers' @@ -287,4 +287,4 @@ _Nested Donut_ supports the following CSS variables: ## Component Props - + diff --git a/packages/website/docs/networks-and-flows/ChordDiagram.mdx b/packages/website/docs/networks-and-flows/ChordDiagram.mdx index ae3be5c07..fdaeea013 100644 --- a/packages/website/docs/networks-and-flows/ChordDiagram.mdx +++ b/packages/website/docs/networks-and-flows/ChordDiagram.mdx @@ -4,8 +4,7 @@ sidebar_label: Chord Diagram --- import CodeBlock from '@theme/CodeBlock' -import { PropTable } from '../components/props-table.tsx' -import { FrameworkTabs } from '../components/framework-tabs.tsx' +import { PropsTable } from '@site/src/components/PropsTable' import { generateNodeLinkData } from '../utils/data.ts' import { DocWrapper, InputWrapper } from '../wrappers' @@ -191,4 +190,5 @@ events = { - +## Component Props + diff --git a/packages/website/docs/networks-and-flows/Graph.mdx b/packages/website/docs/networks-and-flows/Graph.mdx index 9e4a62702..15ad4fe0d 100644 --- a/packages/website/docs/networks-and-flows/Graph.mdx +++ b/packages/website/docs/networks-and-flows/Graph.mdx @@ -4,11 +4,10 @@ description: Learn how to configure a Network Graph import CodeBlock from '@theme/CodeBlock' import BrowserOnly from '@docusaurus/BrowserOnly' +import { PropsTable } from '@site/src/components/PropsTable' import { scaleLinear } from 'd3-scale' -import { sample} from "../utils/data"; -import { PropTable } from '../components/props-table.tsx' import { DocWrapper, InputWrapper } from '../wrappers' -import { generateNodeLinkData } from '../utils/data' +import { generateNodeLinkData, sample } from '../utils/data' export const defaultProps = () => ({ name: "Graph", @@ -714,4 +713,4 @@ const events = { ``` ## Component Props - + diff --git a/packages/website/docs/networks-and-flows/Sankey.mdx b/packages/website/docs/networks-and-flows/Sankey.mdx index 63c1a5d3c..abb1b3578 100644 --- a/packages/website/docs/networks-and-flows/Sankey.mdx +++ b/packages/website/docs/networks-and-flows/Sankey.mdx @@ -3,7 +3,7 @@ description: Learn how to configure a Sankey Diagram --- import CodeBlock from '@theme/CodeBlock' -import { PropTable } from '../components/props-table.tsx' +import { PropsTable } from '@site/src/components/PropsTable' import { DocWrapper, InputWrapper } from '../wrappers' import { sankeyData } from '../utils/data' @@ -277,4 +277,4 @@ const events = { ## Component Props - + diff --git a/packages/website/docs/wrappers/xy-wrapper/index.tsx b/packages/website/docs/wrappers/xy-wrapper/index.tsx index 6ea41064d..da00110e9 100644 --- a/packages/website/docs/wrappers/xy-wrapper/index.tsx +++ b/packages/website/docs/wrappers/xy-wrapper/index.tsx @@ -14,13 +14,11 @@ type XYWrapperProps = DocWrapperProps & { showAxes?: boolean; } -export const axis = (t: 'x' | 'y', props = {}): DocComponent => ({ name: 'Axis', props: { type: t, ...props }, key: `${t}Axis` }) +export const axis = (t: 'x' | 'y', props = {}): DocComponent => ({ name: 'Axis', props: { type: t, ...props } }) -const getProps = ({ showAxes, ...rest }: XYWrapperProps): DocWrapperProps => { - if (showAxes) { - rest.components?.push(axis('x'), axis('y')) - } - return { ...defaultProps, ...rest } +const getProps = ({ showAxes, components = [], ...rest }: XYWrapperProps): DocWrapperProps => { + const axes = showAxes ? ['x', 'y'].map(type => axis(type)) : [] + return { ...defaultProps, components: [...components, ...axes], ...rest } } export const XYWrapper = (props: XYWrapperProps): JSX.Element => diff --git a/packages/website/docs/xy-charts/Area.mdx b/packages/website/docs/xy-charts/Area.mdx index 1e2908cba..4a3bf7425 100644 --- a/packages/website/docs/xy-charts/Area.mdx +++ b/packages/website/docs/xy-charts/Area.mdx @@ -2,9 +2,9 @@ description: Learn how to configure an Area chart --- -import CodeBlock from "@theme/CodeBlock"; -import { PropTable } from "../components/props-table"; -import { generateDataRecords } from "../utils/data"; +import CodeBlock from "@theme/CodeBlock" +import { PropsTable } from "@site/src/components/PropsTable" +import { generateDataRecords } from "../utils/data" import { XYWrapper, XYWrapperWithInput } from '../wrappers/xy-wrapper' export const defaultProps = (n=10) => ({ @@ -14,38 +14,71 @@ export const defaultProps = (n=10) => ({ y: d=>d.y }); -### Basic configuration +## Basic Configuration -### Multiple Areas -_Area_ can accept an array of `y` accessors to display multiple areas at once: - d.y, (d) => d.y1, (d) => d.y2]} showContext="full"/> - -### Curve Types +## Curve Types Using the `curveType: CurveType` property you can set various curve type options. For example: Learn more about configurable curves from D3's [documentation](https://github.com/d3/d3-shape#curves) -### Color +## Color Setting color for a single _Area_ component is simple, you can achieve that just by setting the `color` property of the component to a hex string. -#### Multiple Colors -If you want to configure multiple colors for your _Area_ component, you'll have to create a color accessor function, similar to `x` and `y` accessors -described in the base configuration: +## Stacked Areas +### Y Accessors +_Area_ can accept an array of `y` accessors to display stacked areas from your provided data. + + +**Note**: It is important that an array of accessors or provided, not a single accessor which returns +an array. For example, if you wanted to generate a chart with three areas of random data: + + - d.y, (d) => d.y1, (d) => d.y2]} color={(d, i) => ['#6A9DFF', '#1acb9a', '#f88080'][i]}/> -### Dealing with small values +```ts +/* ✅ Do this */ +const y = [ + () => Math.random(), + () => Math.random(), + () => Math.random(), +] + +/* ⛔ Not this */ +const y = d => [Math.random(), Math.random(), Math.random()] +``` + +### Multiple Colors +If you want to configure multiple colors for your _Area_ component, you'll have to supply a single +accessor. A common configuration is to utilize the data's index: + + d.y, (d) => d.y1, (d) => d.y2]} color={(d, i) => ['red', 'green', 'blue'][i]}/> + +## Dealing with small values If your data has small or zero values leading to some parts of the area to become invisible, you can force those area segments to have 1px height despite their actual value by setting `minHeight1Px` to `true`. This can be useful if you want to visually emphasize that the data behind the chart is defined but just very small. d.y < 9 ? 0 : d.y]} property="minHeight1Px" inputType="checkbox" defaultValue={true} showAxes/> -### Additional Styling: CSS Variables +## Events + +The _Area_ component supports the following events: +```ts +import { Area } from '@unovis/ts' + +events = [Area.selectors.area]: { + click: (data: DataRecord[]) => {}, + mouseover: (data: DataRecord[]) => {}, + mouseleave: (data: DataRecord[]) => {} +} +``` + + +## CSS Variables The _Area_ component supports additional styling via CSS variables that you can define for your visualization container. For example: ```css title="styles.css" @@ -54,7 +87,7 @@ The _Area_ component supports additional styling via CSS variables that you can --vis-area-hover-fill-opacity: 1; --vis-area-stroke-width: 1px; ``` - d.y, (d) => d.y1, (d) => d.y2]} className="custom-area"/> + d.y, (d) => d.y1, (d) => d.y2]} className="custom-area"/>
Supported CSS variables and their default values @@ -73,19 +106,5 @@ The _Area_ component supports additional styling via CSS variables that you can
-### Events - -The _Area_ component supports the following events: -```ts -import { Area } from '@unovis/ts' - -events = [Area.selectors.area]: { - click: (data: DataRecord[]) => {}, - mouseover: (data: DataRecord[]) => {}, - mouseleave: (data: DataRecord[]) => {} -} -``` - - ## Component Props - + diff --git a/packages/website/docs/xy-charts/GroupedBar.mdx b/packages/website/docs/xy-charts/GroupedBar.mdx index 14817aac4..a919e58e7 100644 --- a/packages/website/docs/xy-charts/GroupedBar.mdx +++ b/packages/website/docs/xy-charts/GroupedBar.mdx @@ -4,7 +4,7 @@ title: Grouped Bar description: Learn how to configure a Grouped Bar chart --- import CodeBlock from '@theme/CodeBlock'; -import { PropTable } from '../components/props-table' +import { PropsTable } from '@site/src/components/PropsTable' import { data, generateDataRecords } from '../utils/data' import { XYWrapper, XYWrapperWithInput, DynamicXYWrapper } from '../wrappers/xy-wrapper' @@ -15,67 +15,67 @@ export const defaultProps = (n=10) => ({ y: [d => d.y, d => d.y1, d => d.y2], }); -### Basic configuration +## Basic Configuration The _Grouped Bar_ component has been designed to work together with _XY Container_. The minimal _Grouped Bar_ configuration looks like: - d.y} showContext="full"/> - -### Multiple Bars -_Grouped Bar_ can accept an array of y accessors to display values as a group of bars: +## Orientation +_Grouped Bar_ supports horizontal and vertical orientation. + d.y, d => d.y1, d => d.y2]} height={250} showAxes inputType="select" property="orientation" defaultValue="horizontal" options={["vertical", "horizontal"]}/> + +## Bar Colors +Set the color of the bar by assigning the color property to a hex string and/or by assigning the color property to a function evaluated per each bar. +In this example, each bar's color is assigned based on its index: + ['#04c0c7', '#5144d3', '#da348f'][i]}/> + + +## Rounded Corners +You can apply rounded corners to the top bar in your _Grouped Bar_ component using the `roundedCorners` property, which accepts +either a `number` (in pixels) or `boolean` argument. + + +## Bar Sizing +There are multiple configuration properties that contribute to the size of you bars in _Grouped Bar_. +Some are on the group level while others on the individual. + ### Group Width By default, the width of the bars is calculated automatically based on their count. But you can also strictly set the bar's width in pixels using the barWidth property: -### Limiting Dynamic Group Bar Width +### Limiting Dynamic Group Width When you don't know the number of bars in advance, and you're relying on automatic bar width calculation, you might want to limit the maximum bar width to prevent the bars from being too wide when there are just a few of them. That can be achieved by setting the groupMaxWidth property. -### Group Padding -Another way to control width in a *BarGroup* component is by changing the `groupPadding` argument, which specifies how much of the available sector should be empty, in the range of [0,1). +### Group vs. Bar Padding +Another way to control bar width is by adding padding to bar groups (`groupPadding` property) or +individual bars (`barPadding). The value which specifies how much of the available sector should be +empty in the range of [0,1]. + -### Bar Padding -Or for individual bars within each bar group, the `barPadding` attribute is available: -### Bar Height Limit +### Minimum Bar Height When you have highly scattered data with very low and high values, the bars corresponding to the lower values can be so small, so they become invisible. If you want to prevent that you can set the minimum bar height to 1 pixel using the `barMinHeight` boolean property. ({ x: d.x, y: i % 2 === 1 ? d.y * 100 : Math.random() }))} showAxes property="barMinHeight" inputType="number" defaultValue={1}/> -### Dealing With Inconsistent data +## Preventing Overlaps with `dataStep` When your data has gaps, it's impossible to do calculate of the bar width automatically. The visualization will still try to do that, but most likely the result will be wrong, and you'll see wide overlapping bars. However, you can help the calculation by setting your data step implicitly using the dataStep property. Consider the following example, with data mainly clumped in the domain `0 < x < 1`: ({ x: i < 5 ? Math.random() : i - 6, y: d.y }))} showAxes exampleProps={{dataStep: 0.1}}/> -### Rounded Corners -You can apply rounded corners to the top bar in your _Grouped Bar_ component using the `roundedCorners` property, which accepts -either a `number` (in pixels) or `boolean` argument. - - - - -### Bar Color -Set the color of the bar by assigning the color property to a hex string and/or by assigning the color property to a function evaluated per each bar. -In this example, each bar's color is assigned based on its value: - ['#04c0c7', '#5144d3', '#da348f'][i]}/> - -### Orientation -_Grouped Bar_ supports horizontal and vertical orientation. - d.y, d => d.y1, d => d.y2]} height={250} showAxes inputType="select" property="orientation" defaultValue="horizontal" options={["vertical", "horizontal"]}/> - -### Ordinal Data +## Using Ordinal Data Read our guide about using ordinal/categorical values with _XY Components_ [here](/docs/guides/tips-and-tricks/#displaying-ordinal-values) -### Events +## Events ```ts import { GroupedBar } from '@unovis/ts` ... @@ -92,7 +92,7 @@ events = { ``` -### Additional Styling: CSS Variables +## CSS Variables The _Grouped Bar_ component supports additional styling via CSS variables that you can define for your visualization container. For example: ```css title="styles.css" @@ -117,4 +117,4 @@ The _Grouped Bar_ component supports additional styling via CSS variables that y ## Component Props - + diff --git a/packages/website/docs/xy-charts/Line.mdx b/packages/website/docs/xy-charts/Line.mdx index 9f8b5554c..b700af463 100644 --- a/packages/website/docs/xy-charts/Line.mdx +++ b/packages/website/docs/xy-charts/Line.mdx @@ -3,44 +3,56 @@ sidebar_position: 1 description: Learn how to configure a Line chart --- import CodeBlock from "@theme/CodeBlock"; -import { PropTable } from '../components/props-table' +import { PropsTable } from '@site/src/components/PropsTable' import { data, generateDataRecords } from '../utils/data' import { XYWrapper, XYWrapperWithInput } from '../wrappers' export const lineProps = (n=10) => ({ name: "Line", + height: 150, data: generateDataRecords(n), x: d=>d.x, y: d=>d.y, }); -### Basic configuration +## Basic Configuration The _Line_ component has been designed to work together with _XY Container_. The minimal _Line_ configuration looks like: -### Line Width + +## Multiple Lines +_Line_ can also accept an array of `y` accessors to display multiple lines: + d.y, d => d.y1, d => d.y2]}/> + +## Line Appearance +### Width Specify the Line's width in pixels using the `lineWidth` property: -### Curve Types +### Curve Type Using the `curveType: CurveType` property you can set various curve type options. For example: -### Colored Line +### Color #### For multiple lines: If you want to set color for multiple lines at once, you'll have to define a colors array in your component and reference colors by index in the accessor function: - d.y, d => d.y1, d => d.y2]} color={(d,i) => ['#6A9DFF', '#1acb9a', '#f88080'][i]}/> + d.y, d => d.y1, d => d.y2]} color={(d,i) => ['red', 'green', 'blue'][i]}/> -### Dashed Line + +### Dashes You can configure a dashed line with the lineDashArray property and a dash array. See [SVG stroke-dasharray](https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/stroke-dasharray) to learn more about this attribute. -### Dealing with missing data +#### For multiple lines: +Similar to [mutlti-color configuration](#for-multiple-lines), you can provide an accessor function to +customize each line. + +## Dealing with missing data In the case of missing data (when the data values are `undefined`, `NaN`, `''`, etc ...), you can assign a fallback value for _Line_ using the `fallbackValue` config property. The default value is `undefined`, which means that the line will break in the areas of no data and continue when the data appears again. If you set `fallbackValue` to `null`, the values @@ -58,12 +70,21 @@ Consider the following example, where the dataset contains `undefined` values ov defaultValue={7} showAxes/> -### Multiple Lines -_Line_ can also accept an array of `y` accessors to display multiple lines: - d.y, d => d.y1, d => d.y2]}/> - +## Events +```ts +import { Line } from '@unovis/ts` +... +const events = { + [Line.selectors.line]: { + mouseover: (data: DataRecord[]) => {}, + mouseleave: (data: DataRecord[]) => {}, + ... + } +} +``` + -### Additional Styling: CSS Variables +## CSS Variables The _Line_ component supports additional styling via CSS variables that you can define for your visualization container. For example: ```css title="styles.css" @@ -84,19 +105,6 @@ The _Line_ component supports additional styling via CSS variables that you can -### Events -```ts -import { Line } from '@unovis/ts` -... -events = { - [Line.selectors.line]: { - mouseover: (data: DataRecord[]) => {}, - mouseleave: (data: DataRecord[]) => {}, - ... - } -} -``` - ## Component Props - + diff --git a/packages/website/docs/xy-charts/Scatter.mdx b/packages/website/docs/xy-charts/Scatter.mdx index ea20794ea..62ecd0b50 100644 --- a/packages/website/docs/xy-charts/Scatter.mdx +++ b/packages/website/docs/xy-charts/Scatter.mdx @@ -4,7 +4,7 @@ description: Learn how to configure a Scatter plot import CodeBlock from '@theme/CodeBlock' import Details from "@theme/Details" -import { PropTable } from '../components/props-table' +import { PropsTable } from '@site/src/components/PropsTable' import { generateScatterDataRecords } from '../utils/data' import { XYWrapper, XYWrapperWithInput } from '../wrappers/xy-wrapper' @@ -15,28 +15,29 @@ export const defaultProps = (n = 10, colored = false) => ({ y: d => d.y, }); -### Basic configuration +## Basic Configuration The _Scatter_ component has been designed to work together with _XYContainer_. The minimal _Scatter_ configuration looks like: d.y} showContext="full"/> -### Point Shape +## Point Appearance +### Custom Shape You can change the point's symbol using the `symbol` attribute, which accepts a `SymbolType` or a corresponding `string` (`'circle'`, `'cross'`, `'diamond'`, `'square'`, `'star'`, `'triangle'`, `'wye'` ) or a function (executed per data point) that returns either of them. -### Point Color +### Custom Color You can provide _Scatter_ with a single `color` hex value or a `color` accessor function. -### Point Stroke Color +### Stroke Color You can also set a stroke color for your points by providing a `strokeColor` hex value or a `strokeColor` accessor function. -### Point Stroke Width +### Stroke Width If you want to change the stroke width, you can use the `strokeWidth` property, which accepts a constant value or an accessor function. -### Point Size and Size Range +### Size and Size Range You can use the `size` property to set the point size (i.e. point diameter if `shape` is set to `SymbolType.Circle`) in pixels by providing a constant value or an accessor function, e.g.: `d => d.size`. @@ -47,9 +48,9 @@ property will be treated as relative and all the points will be rescaled accordi In tha case if you provide a constant value to `size`, every resulting size value will be the median of `sizeRange`. Fox example, if `sizeRange` is set to `[10,50]`, that will make the default size equal to 30px (or `(min + max)/2`). - Math.random()} sizeRange={[10, 50]}/> + 10 * Math.random()} sizeRange={[10, 50]}/> -### Point Label +## Point Labels You can also place labels on _Scatter's_ points by passing a string accessor function to the `label` property. d.label}/> @@ -78,12 +79,12 @@ When there're overlapping labels, _Scatter_ will hide some of them to avoid clut If you want to disable hiding overlapping labels, you can set the `labelHideOverlapping` property to `false`. d.color} label={d => `Point ${d.label}`} property="labelHideOverlapping" inputType="checkbox" defaultValue={true}/> -### Custom Cursor +## Custom Cursor _Scatter_ also allows to set a [custom](https://developer.mozilla.org/en-US/docs/Web/CSS/cursor) `cursor` when hovering over a point by providing a `cursor` accessor function. - (['crosshair', 'grab', 'cell'])[i % 3]}/> + -### Events +## Events ```ts import { Scatter } from '@unovis/ts` ... @@ -97,7 +98,7 @@ events = { -### Additional Styling: CSS Variables +## CSS Variables The _Scatter_ component supports additional styling via CSS variables that you can define for your visualization container. For example: @@ -135,4 +136,4 @@ The _Scatter_ component supports additional styling via CSS variables that you c ## Component Props - + diff --git a/packages/website/docs/xy-charts/StackedBar.mdx b/packages/website/docs/xy-charts/StackedBar.mdx index 8f1537110..f8c45c37d 100644 --- a/packages/website/docs/xy-charts/StackedBar.mdx +++ b/packages/website/docs/xy-charts/StackedBar.mdx @@ -3,10 +3,10 @@ sidebar_label: Stacked Bar title: Stacked Bar description: Learn how to configure a Stacked Bar chart --- -import BrowserOnly from '@docusaurus/BrowserOnly' + import CodeBlock from '@theme/CodeBlock' -import { PropTable } from '../components/props-table' -import { data, generateDataRecords } from '../utils/data' +import { PropsTable } from '@site/src/components/PropsTable' +import { generateDataRecords } from '../utils/data' import { XYWrapper, XYWrapperWithInput, DynamicXYWrapper } from '../wrappers/xy-wrapper' export const defaultProps = (n=10) => ({ @@ -16,19 +16,55 @@ export const defaultProps = (n=10) => ({ y: d=>d.y, }); -### Basic configuration +## Basic Configuration The _Stacked Bar_ component has been designed to work together with _XY Container_. The minimal _Stacked Bar_ configuration looks like: -### Multiple Stacked Bars +## Multiple Stacked Bars _Stacked Bar_ can accept an array of y accessors to display values as a stack of bars: d.y, d => d.y1, d => d.y2]}/> -### Orientation +## Orientation _Stacked Bar_ supports horizontal and vertical orientation. - d.y, d => d.y1, d => d.y2]} height={250} showAxes property="orientation" inputType="select" options={["vertical", "horizontal"]}/> + d.y, d => d.y1, d => d.y2]} height={250} showAxes property="orientation" inputType="select" options={["horizontal","vertical"]}/> + +## Rounded Corners +You can apply rounded corners to the top bar in your _Stacked Bar_ component using the `roundedCorners` property, which accepts +either a `number` (in pixels) or `boolean` argument. + + +## Bar Color +Set the color of each bar by assigning the color property to a color string, a color accessor function +or an array of color strings. + +### Stacked Bars +For stacked data, use an array of values or a single callback function. + +```ts +// Either of these work +const colors = ['red', 'green', 'blue'] +const color = (d: DataRecord, i: number) => ['red', 'green', 'blue'][i] +``` + + d.y, d => d.y1, d => d.y2]} showContext='container' color={['red', 'green', 'blue']}/> +### Non-Stacked Bars +If you want non-stacked data (i.e. single bars) to display variable colors, you can use a single +color accessor. The following sets the color based on the value of y: + +```ts +const color = (d: DataRecord) => d.y > 7 ? '#FF4F4E' : '#1acb9a'} +``` + d.y > 7 ? '#FF4F4E' : '#1acb9a'}/> + + +## Bar Sizing ### Bar Width By default, the width of the bars is calculated automatically based on their count. But you can also strictly set the bar's width in pixels using the barWidth property: @@ -41,7 +77,7 @@ maximum bar width to prevent the bars from being too wide when there are just a Another way to control the bar's width is by changing the barPadding property, which specifies how much of the available sector should be empty, in the range of [0,1). -### Bar Height Limit +### Minimum Bar Height When you have highly scattered data with very low and high values, the bars corresponding to the lower values can be so small, so they become invisible. If you want to prevent that you can set the minimum bar height to 1 pixel using the `barMinHeight1Px` boolean property. ({ x: d.x, y: i % 2 === 1 ? d.y * 20 : Math.random() }))} property="barMinHeight1Px" inputType="checkbox" showAxes/> -### Dealing With Inconsisent Data +## Preventing Overlaps with `dataStep` When your data has gaps, it's impossible to do calculate of the bar width automatically. The visualization will still try to do that, but most likely the result will be wrong, and you'll see wide overlapping bars. However, you can help the calculation by setting your data step implicitly using the dataStep property. Consider the following example, with data mainly clumped in the domain `0 < x < 1`: ({ x: i < 5 ? Math.random() : i - 6, y: d.y }))} exampleProps={{dataStep: 0.1}}/> -### Rounded Corners -You can apply rounded corners to the top bar in your _Stacked Bar_ component using the `roundedCorners` property, which accepts -either a `number` (in pixels) or `boolean` argument. - - d.y} property="roundedCorners" inputType="range" inputProps={{ min: 0, max: 30}} defaultValue={5}/> - - -### Bar Color -Set the color of the bar by assigning the color property to a hex string and/or by assigning the color property to a function evaluated per each bar. -In this example, each bar's color is assigned based on it's value: - d.y > 7 ? '#FF4F4E' : '#1acb9a'}/> - -### Ordinal Data +## Ordinal Data Read our guide about using ordinal/categorical values with _XY Components_ [here](/docs/guides/tips-and-tricks/#displaying-ordinal-values) -### Events +## Events ```ts import { StackedBar } from '@unovis/ts` ... @@ -93,7 +112,7 @@ events = { ``` -### Additional Styling: CSS Variables +## CSS Variables The _Stacked Bar_ component supports additional styling via CSS variables that you can define for your visualization container. For example: ```css title="styles.css" @@ -122,4 +141,4 @@ The _Stacked Bar_ component supports additional styling via CSS variables that y ## Component Props - + diff --git a/packages/website/docs/xy-charts/Timeline.mdx b/packages/website/docs/xy-charts/Timeline.mdx index c3f4c597a..690456630 100644 --- a/packages/website/docs/xy-charts/Timeline.mdx +++ b/packages/website/docs/xy-charts/Timeline.mdx @@ -5,7 +5,7 @@ description: Learn how to configure a Timeline chart import CodeBlock from "@theme/CodeBlock" import BrowserOnly from "@docusaurus/core/lib/client/exports/BrowserOnly" import { XYWrapper, XYWrapperWithInput, axis } from '../wrappers/xy-wrapper' -import { PropTable } from '../components/props-table' +import { PropsTable } from '@site/src/components/PropsTable' import { generateTimeSeries } from '../utils/data' export const xAxis = ({ @@ -173,7 +173,7 @@ const events = { } ``` -## Additional Styling: CSS Variables +## CSS Variables The _Timeline_ component supports additional styling via CSS variables: @@ -206,4 +206,5 @@ The _Timeline_ component supports additional styling via CSS variables: - +## Component Props + diff --git a/packages/website/releases/1.2.md b/packages/website/releases/1.2.md index b7e407cdb..f729afe21 100644 --- a/packages/website/releases/1.2.md +++ b/packages/website/releases/1.2.md @@ -10,7 +10,7 @@ authors: title: Software Engineer | Data Visualization url: https://github.com/reb-dev image_url: https://avatars.githubusercontent.com/u/52078477 -image: https://i.imgur.com/mErPwqL.png +image: https://unovis.dev/img/unovis-social.png hide_table_of_contents: false date: 2023-06-27T10:00 --- diff --git a/packages/website/releases/1.3.md b/packages/website/releases/1.3.md index 0cf09f21f..9a3d49b3e 100644 --- a/packages/website/releases/1.3.md +++ b/packages/website/releases/1.3.md @@ -10,7 +10,7 @@ authors: title: Software Engineer | Data Visualization url: https://github.com/reb-dev image_url: https://avatars.githubusercontent.com/u/52078477 -image: https://i.imgur.com/mErPwqL.png +image: https://unovis.dev/img/unovis-social.png hide_table_of_contents: false date: 2023-11-08T10:00 --- diff --git a/packages/website/src/components/PropsTable/index.module.css b/packages/website/src/components/PropsTable/index.module.css new file mode 100644 index 000000000..58048874d --- /dev/null +++ b/packages/website/src/components/PropsTable/index.module.css @@ -0,0 +1,43 @@ +.table { + font-size: smaller; + width: 100%; + max-width: 100%; + overflow: hidden; +} + +.table > thead, +.table > tbody, +.table > tfoot { + display: block; + width: 100% !important; +} + +.table > tfoot > tr { + display: block; + text-align: end; +} + +.table tr { + display: grid; + width: 100%; + grid-template-columns: 25% 25% 50%; + border-top: none; + border-bottom: none; +} + +.table td { + display: block; + width: 100%; +} + +.table td > pre { + display: block; + width: 100%; +} + +.table pre { + font-size: smaller; + padding: 5px; + width: min-content; + margin: 0; +} diff --git a/packages/website/docs/components/props-table.tsx b/packages/website/src/components/PropsTable/index.tsx similarity index 83% rename from packages/website/docs/components/props-table.tsx rename to packages/website/src/components/PropsTable/index.tsx index 57a5d25b2..07aef3860 100644 --- a/packages/website/docs/components/props-table.tsx +++ b/packages/website/src/components/PropsTable/index.tsx @@ -3,12 +3,19 @@ import ReactMarkdown from 'react-markdown' import { PropItem } from 'react-docgen-typescript' import { useDynamicImport } from 'docusaurus-plugin-react-docgen-typescript/pkg/dist-src/hooks/useDynamicImport' -export const PropTable = ({ name }): JSX.Element => { +import s from './index.module.css' + +export type PropsTableProps = { + name: string; +} + +export const PropsTable = ({ name }: PropsTableProps): JSX.Element => { const props: Record = useDynamicImport(name) const propsArray = Object.entries(props || {}) .sort((a, b) => Number(b[1].required) - Number(a[1].required)) + .filter(p => p[1].name !== 'ref') return ( - +
diff --git a/packages/website/src/css/custom.css b/packages/website/src/css/custom.css index a671d968c..693a5045d 100644 --- a/packages/website/src/css/custom.css +++ b/packages/website/src/css/custom.css @@ -24,7 +24,6 @@ --ifm-font-family-base: Inter; --ifm-table-cell-padding: 0.2rem; --ifm-heading-color: var(--unovis-color-deep-blue); - --ifm-table-head-background: #f6f8fa; --ifm-table-stripe-background: #f6f8fa; --ifm-container-width-xl: 1920px; --docusaurus-highlighted-code-line-bg: #FFD651; @@ -39,16 +38,6 @@ --unovis-color-red: #E2203A; } -table thead tr { - border-top: none; - border-bottom: none; -} - -table tr { - border-top: none; - border-bottom: none; -} - html[data-theme="dark"] { --ifm-heading-color: white; @@ -97,47 +86,6 @@ html[data-theme='dark'] .docusaurus-highlight-code-line { display: none; } -.table { - font-size: smaller; - width: 100%; - max-width: 100%; - overflow: hidden; -} -.table > thead, -.table > tbody, -.table > tfoot { - display: block; - width: 100% !important; -} - -.table > tfoot > tr { - display: block; - text-align: end; -} - -.table tr { - display: grid; - width: 100%; - grid-template-columns: 25% 25% 50%; -} - -.table td { - display: block; - width: 100%; -} - -.table td > pre { - display: block; - width: 100%; -} - -.table pre { - font-size: smaller; - padding: 5px; - width: min-content; - margin: 0; -} - .main-wrapper { position: relative; }
Name