DefinitelyTyped/types/d3-selection/d3-selection-tests.ts
Nathan Shively-Sanders a94ee28b62
d3-selection: Disable tslint error on TS 3.9 (#43269)
DOM updates in 3.9 mean that Typescript now knows that
`HTMLBodyElement.ownerDocument` can't actually be null. Previously, a
non-null assertion was needed.

Because the tests have to compile on 3.9 AND older versions, I just
disabled the rule.
2020-03-20 11:53:58 -07:00

1308 lines
53 KiB
TypeScript

/**
* Typescript definition tests for d3/d3-selection module
*
* Note: These tests are intended to test the definitions only
* in the sense of typing and call signature consistency. They
* are not intended as functional tests.
*/
import * as d3Selection from 'd3-selection';
// ---------------------------------------------------------------------------------------
// Some preparatory work for definition testing below
// ---------------------------------------------------------------------------------------
// Generic DOM related variables
const xDoc: Document = document;
let xWindow: Window = window;
interface BodyDatum {
foo: string;
bar: number;
}
interface DivDatum {
padding: string;
text: string;
}
interface SVGDatum {
width: number;
height: number;
}
interface CircleDatum {
nodeId: string;
name: string;
label: string;
cx: number;
cy: number;
r: number;
}
interface CircleDatumAlternative {
nodeId: string;
name: string;
label: string;
cx: number;
cy: number;
r: number;
color: string;
}
// ---------------------------------------------------------------------------------------
// Tests of Top-Level Selection Functions
// ---------------------------------------------------------------------------------------
// test top-level .selection() -----------------------------------------------------------
const topSelection: d3Selection.Selection<HTMLElement, any, null, undefined> = d3Selection.selection();
// test top-level select() ---------------------------------------------------------------
// Using select() with string argument and no type parameters creates selection
// with Group element of type BaseType and datum of type 'any'. The parent element is of type HTMLElement with datum of type 'any'
const baseTypeEl: d3Selection.Selection<d3Selection.BaseType, any, HTMLElement, any> = d3Selection.select('body');
// Using select() with string argument and type parameters creates selection
// with Group element of type HTMLBodyElement and datum of BodyDatum type. The parent element is of type HTMLElement with datum of type 'any'
let body: d3Selection.Selection<HTMLBodyElement, BodyDatum, HTMLElement, any> = d3Selection.select<HTMLBodyElement, BodyDatum>('body');
// Using select() with node argument and no type parameters creates selection
// with Group element of type BaseType and datum of type 'any' The parent element is of type 'null' with datum of type 'undefined'
const baseTypeEl2: d3Selection.Selection<d3Selection.BaseType, any, null, undefined> = d3Selection.select(baseTypeEl.node());
// $ExpectError
const body2: d3Selection.Selection<HTMLElement, any, null, undefined> = d3Selection.select(baseTypeEl.node()); // fails as baseTypeEl.node() is of type cannot be assigned to HTMLElement
const body3: d3Selection.Selection<HTMLBodyElement, any, null, undefined> = d3Selection.select(body.node()!); // element types match, but datum is of type 'any' as it cannot be inferred from .node()
// Using select() with node argument and type parameters creates selection
// with Group element of type HTMLBodyElement and datum of BodyDatum type. The parent element is of type 'null' with datum of type 'undefined'
const body4: d3Selection.Selection<HTMLBodyElement, BodyDatum, null, undefined> = d3Selection.select<HTMLBodyElement, BodyDatum>(body.node()!);
// Explicitly cast body.node() to HTMLBodyElement to narrow selection definition.
const body5: d3Selection.Selection<HTMLBodyElement, BodyDatum, null, undefined> = d3Selection.select<HTMLBodyElement, BodyDatum>(body.node()!);
// $ExpectError
d3Selection.select<HTMLBodyElement, BodyDatum>(baseTypeEl.node()!); // fails as baseTypeEl.node() is not of type HTMLBodyElement
// test, when it is not certain, whether an element of the type to be selected exists
let maybeSVG1: d3Selection.Selection<SVGSVGElement | null, any, HTMLElement, undefined>;
maybeSVG1 = d3Selection.select<SVGSVGElement | null, any>('svg');
let maybeSVG2: d3Selection.Selection<SVGSVGElement | null, any, null, undefined>;
// $ExpectError
maybeSVG2 = d3Selection.select<SVGSVGElement, any>(maybeSVG1.node()); // fails with strict function types
maybeSVG2 = d3Selection.select<SVGSVGElement | null, any>(maybeSVG1.node());
// fails, as node type mismatches selection type
// $ExpectError
const body7: d3Selection.Selection<HTMLBodyElement | null, any, HTMLElement, undefined> = d3Selection.select<HTMLBodyElement | null, any>(maybeSVG1.node());
// test "special case DOM objects"
d3Selection.select(xDoc);
d3Selection.select(xWindow);
// test top-level selectAll() -------------------------------------------------------------
// Using selectAll(), selectAll(null) or selectAll(undefined) creates an empty selection
let emptyRootSelection: d3Selection.Selection<null, undefined, null, undefined> = d3Selection.selectAll();
emptyRootSelection = d3Selection.selectAll(null);
emptyRootSelection = d3Selection.selectAll(undefined);
// Using selectAll(...) with string argument and no type parameters creates selection
// with Group elements of type BaseType and datum of type 'any'. The parent element is of type HTMLElement with datum of type 'any'
const baseTypeElements: d3Selection.Selection<d3Selection.BaseType, any, HTMLElement, any> = d3Selection.selectAll('div');
// Using selectAll() with string argument and type parameters creates selection
// with Group element of type HTMLDivElement and datum of DivDatum type. The parent element is of type HTMLElement with datum of type 'any'
const divElements: d3Selection.Selection<HTMLDivElement, DivDatum, HTMLElement, any> = d3Selection.selectAll<HTMLDivElement, DivDatum>('div');
// Using selectAll(...) with node array argument and no type parameters creates selection
// with Group element of type BaseType and datum of type 'any' The parent element is of type 'null' with datum of type 'undefined'
const baseTypeElements2: d3Selection.Selection<d3Selection.BaseType, any, null, undefined> = d3Selection.selectAll(baseTypeElements.nodes());
// fails, group elements types not of compatible for baseTypeElements
// $ExpectError
const divElements2: d3Selection.Selection<HTMLDivElement, any, null, undefined> = d3Selection.selectAll(baseTypeElements.nodes());
// element types match, but datum is of type 'any' as it cannot be inferred from .nodes()
const divElements3: d3Selection.Selection<HTMLDivElement, any, null, undefined> = d3Selection.selectAll(divElements.nodes());
// Using selectAll(...) with node array argument and type parameters creates selection
// with Group element of type HTMLDivElement and datum of DivDatum type. The parent element is of type 'null' with datum of type 'undefined'
const divElements4: d3Selection.Selection<HTMLDivElement, DivDatum, null, undefined> = d3Selection.selectAll<HTMLDivElement, DivDatum>(divElements.nodes());
// $ExpectError
d3Selection.selectAll<HTMLDivElement, DivDatum>(baseTypeElements.nodes()); // fails as baseTypeEl.node() is not of type HTMLBodyElement
// selectAll(...) accepts NodeListOf<...> argument
const xSVGCircleElementList: NodeListOf<SVGCircleElement> = document.querySelectorAll('circle');
const circleSelection: d3Selection.Selection<SVGCircleElement, any, null, undefined> = d3Selection.selectAll(xSVGCircleElementList);
// selectAll(...) accepts HTMLCollection, HTMLCollectionOf<...> argument
const documentLinks: d3Selection.Selection<HTMLAnchorElement | HTMLAreaElement, any, null, undefined> = d3Selection.selectAll<HTMLAnchorElement | HTMLAreaElement, any>(document.links);
// ---------------------------------------------------------------------------------------
// Tests of Sub-Selection Functions
// ---------------------------------------------------------------------------------------
// select(...) sub-selection --------------------------------------------------------------
// Expected: datum propagates down from selected element to sub-selected descendant element
// Parent element and Parent Datum of sub-selected element is the same as starting selection
// Using select(...) sub-selection with a string argument.
const svgEl: d3Selection.Selection<SVGSVGElement, SVGDatum, HTMLElement, any> = d3Selection.select<SVGSVGElement, SVGDatum>('svg');
let firstG: d3Selection.Selection<SVGGElement, SVGDatum, HTMLElement, any> = svgEl.select<SVGGElement>('g');
// $ExpectError
const firstG_2: d3Selection.Selection<SVGGElement, SVGDatum, SVGSVGElement, any> = svgEl.select<SVGGElement>('g'); // fails, parent element of selection does not change with .select(...)
// firstG = svgEl.select<SVGSVGElement>('svg'); // fails, element type of SVGSVGElement provided, but SVGGElement expected on left-hand side (silly test to begin with)
// test, when it is not certain, whether an element of the type to be selected exists
let maybeG: d3Selection.Selection<SVGGElement | null, SVGDatum, HTMLElement, any>;
// maybeG = svgEl.select<SVGGElement>('g'); // fails, with strictFunctionTypes
maybeG = svgEl.select<SVGGElement | null>('g');
// Using select(...) sub-selection with a selector function argument.
function svgGroupSelector(this: SVGSVGElement, d: SVGDatum, i: number, groups: SVGSVGElement[] | ArrayLike<SVGSVGElement>): SVGGElement {
return this.querySelector('g')!; // this-type compatible with group element-type to which the selector function will be applied
}
firstG = svgEl.select(svgGroupSelector);
firstG = svgEl.select(function() {
const that: SVGSVGElement = this;
// $ExpectError
const that2: HTMLElement = this; // fails, type mismatch
console.log('Get <svg> Element width using "this": ', this.width.baseVal.value); // 'this' type is SVGSVGElement
return this.querySelector('g')!; // this of type SVGSVGElement by type inference
});
firstG = svgEl.select(function(d, i, g) {
const that: SVGSVGElement = this;
// $ExpectError
const that2: HTMLElement = this; // fails, type mismatch
const datum: SVGDatum = d;
const index: number = i;
const group: SVGSVGElement[] | d3Selection.ArrayLike<SVGSVGElement> = g;
console.log('Get <svg> Element width using "this": ', this.width.baseVal.value); // 'this' type is SVGSVGElement
console.log('Width in datum:', d.width); // d is type of originating selection element Datum: SVGDatum
if (g.length > 1) {
console.log('Get width of 2nd <svg> Element in group: ', g[1].width.baseVal.value); // type is SVGSVGElement
}
return this.querySelector('g')!; // this of type SVGSVGElement by type inference
});
// $ExpectError
firstG = svgEl.select(function() {
return this.querySelector('a'); // fails, return type HTMLAnchorElement is not compatible with SVGGElement expected by firstG
});
// selectAll(...) sub-selection --------------------------------------------------------------
// Expected: datum from selected element(s) does not propagate down to sub-selected descendant elements.
// Group elements of original selection become Parent elements in sub-selection.
// selectAll(), selectAll(null) selectAll(undefined) return empty sub-selection
let emptySubSelection: d3Selection.Selection<null, undefined, SVGSVGElement, SVGDatum> = svgEl.selectAll();
emptySubSelection = svgEl.selectAll(null);
emptySubSelection = svgEl.selectAll(undefined);
// Using selectAll(...) sub-selection with a string argument.
let elementsUnknownData: d3Selection.Selection<SVGGElement, any, SVGSVGElement, SVGDatum> = svgEl.selectAll<SVGGElement, any>('g');
// let elementsAndDataUnknown: d3Selection.Selection<d3Selection.BaseType, any, SVGSVGElement, SVGDatum> = svgEl.selectAll('g'); // fails with strictFunctionTypes
let gElementsOldData: d3Selection.Selection<SVGGElement, CircleDatum, SVGSVGElement, SVGDatum> = svgEl.selectAll<SVGGElement, CircleDatum>('g');
// Using selectAll(...) sub-selection with a selector function argument.
function svgGroupSelectorAll(this: SVGSVGElement, d: SVGDatum, i: number, groups: SVGSVGElement[] | d3Selection.ArrayLike<SVGSVGElement>): NodeListOf<SVGGElement> {
return this.querySelectorAll('g'); // this-type compatible with group element-type to which the selector function will be applied
}
gElementsOldData = svgEl.selectAll<SVGGElement, CircleDatum>(svgGroupSelectorAll);
function wrongSvgGroupSelectorAll(this: HTMLElement, d: SVGDatum, i: number, groups: HTMLElement[]): NodeListOf<SVGGElement> {
return this.querySelectorAll('g');
}
// $ExpectError
gElementsOldData = svgEl.selectAll<SVGGElement, CircleDatum>(wrongSvgGroupSelectorAll); // fails, wrong this context
gElementsOldData = svgEl.selectAll<SVGGElement, CircleDatum>(function() {
const that: SVGSVGElement = this;
// $ExpectError
const that2: HTMLElement = this; // fails, type mismatch
console.log('Get <svg> Element width using "this": ', this.width.baseVal.value); // 'this' type is SVGSVGElement
return this.querySelectorAll('g'); // this of type SVGSVGElement by type inference
});
gElementsOldData = svgEl.selectAll<SVGGElement, CircleDatum>(function(d, i, g) {
const that: SVGSVGElement = this;
// $ExpectError
const that2: HTMLElement = this; // fails, type mismatch
const datum: SVGDatum = d;
const index: number = i;
const group: SVGSVGElement[] | d3Selection.ArrayLike<SVGSVGElement> = g;
console.log('Get <svg> Element width using "this": ', this.width.baseVal.value); // 'this' type is SVGSVGElement
console.log('Width in datum:', d.width); // type of d is SVGDatum
if (g.length > 1) {
console.log('Get width of 2nd <svg> Element in group: ', g[1].width.baseVal.value); // type of group is SVGSVGElement[]
}
return this.querySelectorAll('g');
});
// $ExpectError
gElementsOldData = svgEl.selectAll<SVGGElement, CircleDatum>(function() { // fails, return type HTMLAnchorElement is not compatible with SVGGElement expected by selectAll-typing
return this.querySelectorAll('a');
});
elementsUnknownData = svgEl.selectAll(svgGroupSelectorAll);
elementsUnknownData = svgEl.selectAll(function() {
const that: SVGSVGElement = this;
// $ExpectError
const that2: HTMLElement = this; // fails, type mismatch
console.log('Get <svg> Element width using "this": ', this.width.baseVal.value); // 'this' type is SVGSVGElement
return this.querySelectorAll('g'); // this of type SVGSVGElement by type inference
});
// $ExpectError
gElementsUnknownData = svgEl.selectAll(function() { // fails, return type HTMLAnchorElement is not compatible with SVGGElement
return this.querySelectorAll('a');
});
maybeG.selectAll(function(d, i, g) {
const that: SVGGElement | null = this;
// $ExpectError
const that2: SVGGElement = this; // fails with strictNullChecks
// $ExpectError
const that3: HTMLElement | null = this; // fails, type mismatch
const datum: SVGDatum = d;
const index: number = i;
const group: Array<SVGGElement | null> | d3Selection.ArrayLike<SVGGElement | null> = g;
return that ? that.querySelectorAll('circle') : [];
});
// Selection Helper methods -------------------------------------------------------------
// selector(...) and selectorAll(...) ----------------------------------------------------
// $ExpectError
d3Selection.select<SVGGElement>(d3Selection.selector<SVGGElement>('g')); // fails, selector as argument to top-level select not supported
// supported on sub-selection
firstG = svgEl.select(d3Selection.selector<SVGGElement>('g')); // type parameter of select(...) inferred
// $ExpectError
firstG = svgEl.select<SVGGElement>(d3Selection.selector<HTMLDivElement>('div')); // fails, select and selector mismatch
gElementsOldData = svgEl.selectAll<SVGGElement, CircleDatum>(d3Selection.selectorAll<SVGGElement>('g'));
// filter() ------------------------------------------------------------------------------
// Scenario 1: Filter retaining the element type of the select group (i.e. no type narrowing during filtering)
let filteredGElements: d3Selection.Selection<SVGGElement, CircleDatum, SVGSVGElement, SVGDatum>;
filteredGElements = gElementsOldData.filter('.top-level');
filteredGElements = gElementsOldData.filter(function(d, i, g) {
const that: SVGGElement = this;
// $ExpectError
const that2: HTMLElement = this; // fails, type mismatch
const datum: CircleDatum = d;
const index: number = i;
const group: SVGGElement[] | d3Selection.ArrayLike<SVGGElement> = g;
console.log('Element Id of <g> DOM element: ', this.id); // this context SVGGElement
if (g.length > 0) {
console.log('Element Id of first <g> DOM element in group: ', g[0].id); // group: Array<SVGGElement>
}
return d.r > 10; // uses CircleDatum
});
// Scenario 2: Filtering narrows the type of selected elements in a known way
// assume the class ".any-svg-type" can only be assigned to SVGElements in the DOM
let filteredGElements2: d3Selection.Selection<SVGGElement, any, HTMLElement, any>;
filteredGElements2 = d3Selection.selectAll<SVGElement, any>('.any-svg-type').filter<SVGGElement>('g');
// $ExpectError
filteredGElements2 = d3Selection.selectAll('.any-type').filter('g'); // fails without using narrowing generic on filter method
filteredGElements2 = d3Selection.selectAll<SVGElement, any>('.any-svg-type').filter<SVGGElement>(function() {
const that: SVGElement = this;
return that.tagName === 'g' || that.tagName === 'G';
});
// $ExpectError
filteredGElements2 = d3Selection.selectAll<SVGElement, any>('.any-svg-type').filter(function() {
const that: SVGElement = this;
return that.tagName === 'g' || that.tagName === 'G';
}); // fails without using narrowing generic on filter method
// matcher() -----------------------------------------------------------------------------
filteredGElements = gElementsOldData.filter(d3Selection.matcher('.top-level'));
// ---------------------------------------------------------------------------------------
// Tests of Modification
// ---------------------------------------------------------------------------------------
// Getter return values tests -------------------------------------------------------------
let flag: boolean;
let str: string;
let dummy: any;
flag = body.classed('any-class');
str = body.attr('class');
str = body.style('background-color');
dummy = body.property('foo'); // arbitrary property
str = body.text();
str = body.html();
// Setters tests -------------------------------------------------------------------------
let circles: d3Selection.Selection<SVGCircleElement, CircleDatumAlternative, HTMLElement, any>;
let divs: d3Selection.Selection<HTMLDivElement, DivDatum, HTMLElement, any>;
// attr(...) Tests
circles = d3Selection.selectAll<SVGCircleElement, CircleDatumAlternative>('circle')
.attr('cx', 10) // number
.attr('stroke', 'blue'); // string
circles = circles // re-assignment test chaining return-type
.attr('cx', function(d, i, g) {
const that: SVGGElement = this;
// $ExpectError
const that2: HTMLElement = this; // fails, type mismatch
const datum: CircleDatum = d;
const index: number = i;
const group: SVGGElement[] | d3Selection.ArrayLike<SVGGElement> = g;
console.log('Pre-change center x-coordinate: ', this.cx.baseVal.value); // this context SVGCircleElement
if (g.length > 0) {
console.log('Owner SVG Element of first group element:', g[0].ownerSVGElement); // group : Array<SVGCircleElement>
}
return d.cx; // numeric return value
})
.attr('stroke', d => d.color); // string return value
divs = d3Selection.selectAll<HTMLDivElement, DivDatum>('div')
.attr('contenteditable', false) // boolean
.attr('contenteditable', () => false); // boolean return value
// classed(...) Tests
divs = divs
.classed('success', true);
divs = divs
.classed('zero-px-padding', function(d, i, g) {
const that: HTMLDivElement = this;
// $ExpectError
const that2: SVGElement = this; // fails, type mismatch
const datum: DivDatum = d;
const index: number = i;
const group: HTMLDivElement[] | d3Selection.ArrayLike<HTMLDivElement> = g;
console.log('Client Rectangle Top: ', this.getBoundingClientRect().top); // this context HTMLDivElement
if (g.length > 0) {
console.log('Alignment of first group element:', g[0].align); // group : Array<HTMLDivElement>
}
return d.padding === '0px'; // boolean return value
});
// style(...) Tests
divs = divs
.style('background-color', 'blue') // string
.style('hidden', false); // boolean
// $ExpectError
divs = divs.style('color', 'green', 'test') // fails, invalid priority value
.style('color', 'green', 'important');
divs = divs
.style('padding', function(d, i, g) {
const that: HTMLDivElement = this;
// $ExpectError
const that2: SVGElement = this; // fails, type mismatch
const datum: DivDatum = d;
const index: number = i;
const group: HTMLDivElement[] | d3Selection.ArrayLike<HTMLDivElement> = g;
console.log('Client Rectangle Top: ', this.getBoundingClientRect().top); // this context HTMLDivElement
if (g.length > 0) {
console.log('Alignment of first group element:', g[0].align); // group : Array<HTMLDivElement>
}
return d.padding; // string return value
})
.style('hidden', () => true, null); // boolean return + test: priority = null
// $ExpectError
divs = divs.style('color', () => 'green', 'test') // fails, test: invalid priority value
.style('color', () => 'green', 'important'); // boolean return + test: priority = 'important';
// property(...) Tests
circles = circles
.property('__hitchhikersguide__', {
value: 42,
survival: 'towel'
}); // any
circles = circles
.property('__hitchhikersguide__', function(d, i, g) {
const that: SVGCircleElement = this;
// $ExpectError
const that2: HTMLElement = this; // fails, type mismatch
const datum: CircleDatumAlternative = d;
const index: number = i;
const group: SVGCircleElement[] | d3Selection.ArrayLike<SVGCircleElement> = g;
console.log('Pre-change center x-coordinate: ', this.cx.baseVal.value); // this context SVGCircleElement
if (g.length > 0) {
console.log('Owner SVG Element of first group element:', g[0].ownerSVGElement); // group : Array<SVGCircleElement>
}
return {
value: 42,
survival: 'towel'
}; // returns not so arbitrary object, again
});
// text(...) test
body = body
.text('Not so meaningful blurp.') // string
.text(42) // number will be converted to string by D3
.text(true); // boolean will be converted to string by D3
body = body
.text(function(d, i, g) {
const that: HTMLBodyElement = this;
// $ExpectError
const that2: SVGElement = this; // fails, type mismatch
const datum: BodyDatum = d;
const index: number = i;
const group: HTMLBodyElement[] | d3Selection.ArrayLike<HTMLBodyElement> = g;
console.log('Body background color: ', this.bgColor); // this context HTMLBodyElement
return d.foo; // BodyDatum
})
.text(d => 42) // number will be converted to string by D3
.text(d => true); // boolean will be converted to string by D3
body = body
.html('<div> 42 </div>');
body = body
.html(function(d, i, g) {
const that: HTMLBodyElement = this;
// $ExpectError
const that2: SVGElement = this; // fails, type mismatch
const datum: BodyDatum = d;
const index: number = i;
const group: HTMLBodyElement[] | d3Selection.ArrayLike<HTMLBodyElement> = g;
return `<div> Body Background Color: ${this.bgColor}, Foo Datum: ${d.foo}</div>`; // this context HTMLBodyElement, datum BodyDatum
});
// ---------------------------------------------------------------------------------------
// Tests of Datum and Data Join
// ---------------------------------------------------------------------------------------
const data: CircleDatum[] = [
{ nodeId: 'c1', cx: 10, cy: 10, r: 5, name: 'foo', label: 'Foo' },
{ nodeId: 'c2', cx: 20, cy: 20, r: 5, name: 'bar', label: 'Bar' },
{ nodeId: 'c3', cx: 30, cy: 30, r: 5, name: 'fooBar', label: 'Foo Bar' }
];
const data2: CircleDatumAlternative[] = [
{ nodeId: 'c1', cx: 10, cy: 10, r: 5, name: 'foo', label: 'Foo', color: 'seagreen' },
{ nodeId: 'c2', cx: 20, cy: 20, r: 5, name: 'bar', label: 'Bar', color: 'midnightblue' },
{ nodeId: 'c4', cx: 10, cy: 15, r: 10, name: 'newbie', label: 'Newbie', color: 'red' }
];
// Tests of Datum -----------------------------------------------------------------------
// TEST GETTER
const bodyDatum: BodyDatum = body.datum();
// TEST REMOVE DATUM
body.datum(null); // removes datum, i.e. return type has group datum type 'undefined'
// TEST SETTER METHODS
let newBodyDatum: { newFoo: string };
// object-based
newBodyDatum = body.datum({ newFoo: 'new foo' }).datum(); // inferred type
// $ExpectError
body.datum<BodyDatum>({ newFoo: 'new foo' }); // fails, data type incompatible
// function-based
newBodyDatum = body.datum(function(d, i, g) {
const that: HTMLBodyElement = this;
// $ExpectError
const that2: SVGElement = this; // fails, type mismatch
const datum: BodyDatum = d;
const index: number = i;
const group: HTMLBodyElement[] | d3Selection.ArrayLike<HTMLBodyElement> = g;
console.log('HTML5 Custom Data Attributes of body: ', this.dataset); // this typings HTMLBodyElement
console.log('Old foo:', d.foo); // current data of type BodyDatum
return { newFoo: 'new foo' };
}).datum(); // inferred type
// $ExpectError
newBodyDatum = body.datum<BodyDatum>(function(d) { // fails, data type incompatible with return value type
console.log('HTML5 Custom Data Attributes of body: ', this.dataset); // this typings HTMLBodyElement
return { newFoo: 'new foo' };
}).datum(); // inferred type
// SCENARIO 1: Fully type-parameterized
// object-based
d3Selection.select<SVGSVGElement, SVGDatum>('#svg-1')
.select<SVGGElement>('g.circles-group') // first matching element only
.datum<CircleDatumAlternative[]>(data2)
.classed('has-transform-property', function(d) {
console.log('Color of first data element array', d.length > 0 ? d[0].color : 'Data array empty');
return this.transform !== undefined;
});
// SCENARIO 2: Partially type-parameterized (To have DOM object type -> 'this' and datum-type in 'classed' method call)
d3Selection.select('#svg-1') // irrelevant typing to get contextual typing in last step of chain
.select<SVGGElement>('g.circles-group')
.datum(data2) // new data type inferred
.classed('has-transform-property', function(d, i, g) {
const that: SVGGElement = this;
// $ExpectError
const that2: HTMLElement = this; // fails, type mismatch
const datum: CircleDatumAlternative[] = d;
const index: number = i;
const group: SVGGElement[] | d3Selection.ArrayLike<SVGGElement> = g;
console.log('Color of first data element array', d.length > 0 ? d[0].color : 'Data array empty'); // CircleDatumAlternative type
return this.transform !== undefined;
});
// below fails, as 'this' in .classed(...) will default to BaseType, which may be null and does not have 'transform' property
d3Selection.select('#svg-1') // irrelevant typing to get contextual typing in last step of chain
.select('g.circles-group') // missing typing of selected DOM element for use in .classed(...)
.datum(data2) // new data type inferred
.classed('has-transform-property', function(d) {
console.log('Color of first data element array', d.length > 0 ? d[0].color : 'Data array empty'); // CircleDatumAlternative type
// $ExpectError
return this!.transform !== undefined;
});
// SCENARIO 3: Only inferred typing (To have datum-type in 'classed' method call, no need for DOM object access)
d3Selection.select('#svg-1') // irrelevant typing to get contextual typing in last step of chain
.select('g.circles-group') // irrelevant typing to get contextual typing in last step of chain
.datum(data2) // new data type inferred
.classed('has-green-first-data-element', function(d, i, g) {
const that: d3Selection.BaseType = this;
// $ExpectError
const that2: Element = this; // fails, type mismatch
const datum: CircleDatumAlternative[] = d;
const index: number = i;
const group: d3Selection.BaseType[] | d3Selection.ArrayLike<d3Selection.BaseType> = g;
return d.length > 0 && d[0].color === 'green';
});
// Tests of Data Join --------------------------------------------------------------------
const dimensions: SVGDatum = {
width: 500,
height: 300
};
const startCircleData: CircleDatumAlternative[] = [
{
nodeId: 'n1',
name: 'node_1',
label: 'Test Node 1',
cx: 10,
cy: 10,
r: 5,
color: 'slateblue'
},
{
nodeId: 'n2',
name: 'node_2',
label: 'Test Node 2',
cx: 30,
cy: 30,
r: 10,
color: 'slateblue'
}
];
const endCircleData: CircleDatumAlternative[] = [
{
nodeId: 'n1',
name: 'node_1',
label: 'Test Node 1',
cx: 15,
cy: 15,
r: 5,
color: 'slateblue'
},
{
nodeId: 'n3',
name: 'node_3',
label: 'Test Node 3',
cx: 40,
cy: 40,
r: 20,
color: 'red'
}
];
let circles2: d3Selection.Selection<SVGCircleElement, CircleDatumAlternative, SVGSVGElement, SVGDatum>;
// Test creating initial data-driven circle selection
// and append materialized SVGCircleElement per enter() selection element
// - use data(...) with array-signature and infer data type from array type passed into data(...)
// - use enter() to obtain enter selection
// - materialize svg circles using append(...) with type-parameter and string argument
circles2 = d3Selection.select<SVGSVGElement, any>('#svg2')
.datum(dimensions)
.attr('width', d => d.width)
.attr('height', d => d.height)
.selectAll() // create empty Selection
.data(startCircleData) // assign data for circles to be added (no previous circles)
.enter() // obtain enter selection
.append('circle');
// UPDATE-selection with continuation in data type ---------------------------------
// Assign new data and use key(...) function for mapping
circles2 = circles2 // returned update selection has the same type parameters as original selection, if data type is unchanged
.data<CircleDatumAlternative>(endCircleData, d => d.nodeId);
// $ExpectError
circles2.data<DivDatum>(endCircleData, (d) => d.nodeId); // fails, forced data type parameter and data argument mismatch
// ENTER-selection -----------------------------------------------------------------
// TODO: Related to BaseType Choice issue
let enterElements: d3Selection.Selection<d3Selection.EnterElement, CircleDatumAlternative, SVGSVGElement, SVGDatum>;
enterElements = circles2.enter(); // enter selection
const enterCircles = enterElements
.append('circle') // enter selection with materialized DOM elements (svg circles)
.attr('cx', d => d.cx)
.attr('cy', d => d.cy)
.attr('r', d => d.r)
.style('stroke', d => d.color)
.style('fill', d => d.color);
// EXIT-selection ----------------------------------------------------------------------
// tests exit(...) and remove()
const exitCircles = circles2.exit<CircleDatumAlternative>(); // Note: need to re-type datum type, as the exit selection elements have the 'old data'
exitCircles
.style('opacity', function(d, i, g) {
const that: SVGCircleElement = this;
// $ExpectError
const that2: HTMLElement = this; // fails, type mismatch
const datum: CircleDatumAlternative = d;
const index: number = i;
const group: SVGCircleElement[] | d3Selection.ArrayLike<SVGCircleElement> = g;
console.log('Circle Radius exit node: ', this.r.baseVal.value); // this type is SVGCircleElement
return d.color === 'green' ? 1 : 0; // data type as per .exit<...>() parameterization
})
.remove();
// Note: the alternative using only .exit() without typing, will fail, if access to datum properties is attempted.
// If access to d is not required, the short-hand is acceptable e.g. circles2.exit().remove();
const exitCircles2 = circles2.exit(); // Note: Without explicit re-typing to the old data type, the data type default to '{}'
exitCircles2
.style('opacity', function(d) {
console.log('Circle Radius exit node: ', this.r.baseVal.value);
// $ExpectError
return d.color === 'green' ? 1 : 0; // fails, as data type is defaulted to {}. If datum access is required, this should trigger the thought to type .exit<...>
});
// MERGE ENTER + UPDATE ------------------------------------------------------------------
circles2 = enterCircles.merge(circles2); // merge enter and update selections
// FURTHER DATA-JOIN TESTs (function argument, changes in data type between old and new data)
const matrix = [
[11975, 5871, 8916, 2868],
[1951, 10048, 2060, 6171],
[8010, 16145, 8090, 8045],
[1013, 990, 940, 6907]
];
// SCENARIO 1 - Fully type parameterized, when there is a need for `this` typings in callbacks
// and enforcement of type of data used in data join as input to data(...)
let nMatrix: number[][];
let nRow: number[];
let tr: d3Selection.Selection<HTMLTableRowElement, number[], HTMLTableElement, any>;
tr = d3Selection.select('body')
.append('table')
.selectAll()
.data(matrix)
// $ExpectError
.data<number[]>([{test: 1}, {test: 2}]) // fails, using this data statement instead, would fail because of its type parameter not being met by input
.enter().append('tr');
nMatrix = tr.data(); // i.e. matrix
let td: d3Selection.Selection<HTMLTableDataCellElement, number, HTMLTableRowElement, number[]>;
td = tr.selectAll()
.data(d => d) // d : Array<number> inferred (Array[4] of number per parent <tr>)
.enter().append('td')
.text(function(d, i, g) {
const that: HTMLTableDataCellElement = this;
const datum: number = d;
const index: number = i;
const group: HTMLTableDataCellElement[] | d3Selection.ArrayLike<HTMLTableDataCellElement> = g;
console.log('Abbreviated text for object', this.abbr); // this-type HTMLTableDataCellElement (demonstration only)
return d;
}); // d:number inferred
nRow = td.data(); // flattened matrix (Array[16] of number)
// SCENARIO 2 - Completely inferred types, when there is no need for `this` typings
const tr2 = d3Selection.select('body')
.append('table')
.selectAll('tr')
.data(matrix)
.enter().append('tr');
nMatrix = tr2.data(); // i.e. matrix
const td2 = tr2.selectAll('td')
.data(d => d) // d : Array<number> inferred (Array[4] of number per parent <tr>)
.enter().append('td')
.text(d => d); // d:number inferred
nRow = td2.data(); // flattened matrix (Array[16] of number)
// ---------------------------------------------------------------------------------------
// Tests of Alternative DOM Manipulation
// ---------------------------------------------------------------------------------------
// append(...), creator(...) and create(...) ---------------------------------------------
let newCircle: d3Selection.Selection<SVGCircleElement, BodyDatum, HTMLElement, any>;
newCircle = body.append('circle');
let newDiv: d3Selection.Selection<HTMLDivElement, BodyDatum, HTMLElement, any>;
newDiv = body.append('div');
newDiv = body.append<HTMLDivElement>('div');
// using creator
newCircle = body.append(d3Selection.creator('circle'));
newDiv = body.append(d3Selection.creator('div'));
newDiv = body.append(d3Selection.creator<HTMLDivElement>('custom_div_elem'));
// $ExpectError
newDiv = body.append(d3Selection.creator('a'));
// $ExpectError
newDiv = body.append(d3Selection.creator<HTMLAnchorElement>('a'));
newDiv = body.append(function(d, i, g) {
const that: HTMLBodyElement = this;
// $ExpectError
const that2: SVGElement = this; // fails, type mismatch
const datum: BodyDatum = d;
const index: number = i;
const group: HTMLBodyElement[] | d3Selection.ArrayLike<HTMLBodyElement> = g;
console.log('Body element foo property: ', d.foo); // data of type BodyDatum
// tslint:disable-next-line:no-unnecessary-type-assertion
return this.ownerDocument!.createElement('div'); // this-type HTMLBodyElement
});
// $ExpectError
newDiv = body.append<HTMLDivElement>(function(d) {
// tslint:disable-next-line:no-unnecessary-type-assertion
return this.ownerDocument!.createElement('a'); // fails, HTMLDivElement expected by type parameter, HTMLAnchorElement returned
});
// $ExpectError
newDiv = body.append(function(d) {
// tslint:disable-next-line:no-unnecessary-type-assertion
return this.ownerDocument!.createElement('a'); // fails, HTMLDivElement expected by inference, HTMLAnchorElement returned
});
// create a detached element
let detachedCircle: d3Selection.Selection<SVGCircleElement, undefined, null, undefined>;
detachedCircle = d3Selection.create('circle');
let detachedDiv: d3Selection.Selection<HTMLDivElement, undefined, null, undefined>;
detachedDiv = d3Selection.create('div');
detachedDiv = d3Selection.create<HTMLDivElement>('custom_div_elem');
// insert(...) ---------------------------------------------------------------------------
// Two arguments; the first can be string, selection, or a
const typeValueFunction = function(
this: HTMLBodyElement,
d: BodyDatum,
i: number,
g: HTMLBodyElement[] | d3Selection.ArrayLike<HTMLBodyElement>
) {
// tslint:disable-next-line:no-unnecessary-type-assertion
return this.ownerDocument!.createElement('p'); // this-type HTMLParagraphElement
};
const beforeValueFunction = function(
this: HTMLBodyElement,
d: BodyDatum,
i: number,
g: HTMLBodyElement[] | d3Selection.ArrayLike<HTMLBodyElement>
) {
return this.children[0];
};
let newParagraph: d3Selection.Selection<HTMLParagraphElement, BodyDatum, HTMLElement, any>;
// 1 args, with 3 possibilities each, makes 3 possible combinations:
newParagraph = body.insert('p', 'p.second-paragraph');
newParagraph = body.insert('p', beforeValueFunction);
newParagraph = body.insert('p');
// 2 args, with 3 possibilities each, makes 9 possible combinations:
newParagraph = body.insert<HTMLParagraphElement>('p', 'p.second-paragraph');
newParagraph = body.insert<HTMLParagraphElement>('p', beforeValueFunction);
newParagraph = body.insert<HTMLParagraphElement>('p');
newParagraph = body.insert(d3Selection.creator<HTMLParagraphElement>('p'), 'p.second-paragraph');
newParagraph = body.insert(d3Selection.creator<HTMLParagraphElement>('p'), beforeValueFunction);
newParagraph = body.insert(d3Selection.creator<HTMLParagraphElement>('p'));
newParagraph = body.insert(typeValueFunction, 'p.second-paragraph');
newParagraph = body.insert(typeValueFunction, beforeValueFunction);
newParagraph = body.insert(typeValueFunction);
// clone(...) ----------------------------------------------------------------------------
let clonedParagraph: d3Selection.Selection<HTMLParagraphElement, BodyDatum, HTMLElement, any>;
// shallow clone
clonedParagraph = newParagraph.clone();
// deep clone
newParagraph.append('span');
clonedParagraph = newParagraph.clone(true);
// sort(...) -----------------------------------------------------------------------------
// NB: Return new selection of same type
circles2 = circles2.sort((a, b) => b.r - a.r);
// order(...) ----------------------------------------------------------------------------
// returns 'this' selection
circles2 = circles2.order();
// raise() -------------------------------------------------------------------------------
// returns 'this' selection
circles2 = circles2.raise();
// lower() -------------------------------------------------------------------------------
// returns 'this' selection
circles2 = circles2.lower();
// ---------------------------------------------------------------------------------------
// Control FLow
// ---------------------------------------------------------------------------------------
// empty() -------------------------------------------------------------------------------
const emptyFlag = gElementsOldData.empty();
// node() and nodes() --------------------------------------------------------------------
const bodyNode: HTMLBodyElement | null = body.node();
const gElementsNodes: SVGGElement[] = gElementsOldData.nodes();
// size() --------------------------------------------------------------------------------
const size: number = gElementsOldData.size();
// each() -------------------------------------------------------------------------------
// returns 'this' selection
circles = circles.each(function(d, i, g) { // check chaining return type by re-assigning
const that: SVGCircleElement = this;
// $ExpectError
const that2: HTMLElement = this; // fails, type mismatch
const datum: CircleDatum = d;
const index: number = i;
const group: SVGCircleElement[] | d3Selection.ArrayLike<SVGCircleElement> = g;
if (this.r.baseVal.value < d.r) { // this of type SVGCircleElement, datum of type CircleDatumAlternative
d3Selection.select(this).attr('r', d.r);
}
console.log(g[i].cx.baseVal.value); // group : Array<SVGCircleElement>
});
// call() -------------------------------------------------------------------------------
function enforceMinRadius(selection: d3Selection.Selection<SVGCircleElement, CircleDatumAlternative, any, any>, minRadius: number): void {
selection.attr('r', function(d) {
const r: number = +d3Selection.select(this).attr('r');
return Math.max(r, minRadius);
});
}
// returns 'this' selection
circles = circles.call(enforceMinRadius, 40); // check chaining return type by re-assigning
// $ExpectError
circles.call((selection: d3Selection.Selection<HTMLDivElement, CircleDatum, any, any>) => {
// fails, group element types of selection not compatible: SVGCircleElement v HTMLDivElement
});
// $ExpectError
circles.call((selection: d3Selection.Selection<SVGCircleElement, DivDatum, any, any>) => {
// fails, group datum types of selection not compatible: CircleDatumAlternative v DivDatum
});
// ---------------------------------------------------------------------------------------
// Tests of Event Handling
// ---------------------------------------------------------------------------------------
// on(...) -------------------------------------------------------------------------------
let listener: undefined | ((this: HTMLBodyElement, datum: BodyDatum, index: number, group: HTMLBodyElement[] | d3Selection.ArrayLike<HTMLBodyElement>) => void);
body = body.on('click', function(d, i, g) {
const that: HTMLBodyElement = this;
// $ExpectError
const that2: SVGElement = this; // fails, type mismatch
const datum: BodyDatum = d;
const index: number = i;
const group: HTMLBodyElement[] | d3Selection.ArrayLike<HTMLBodyElement> = g;
console.log('onclick print body background color: ', this.bgColor); // HTMLBodyElement
console.log('onclick print "foo" datum property: ', d.foo); // BodyDatum type
});
// get current listener
listener = body.on('click');
if (listener) {
// returns 'this' selection
body = body.on('click', listener); // check chaining return type by re-assigning
}
// remove listener
body = body.on('click', null); // check chaining return type by re-assigning
// dispatch(...) -------------------------------------------------------------------------
const fooEventParam: d3Selection.CustomEventParameters = {
cancelable: true,
bubbles: true,
detail: [1, 2, 3, 4]
};
// returns 'this' selection
body = body.dispatch('fooEvent', fooEventParam); // re-assign for chaining test;
body = body.dispatch('fooEvent', function(d, i, g) { // re-assign for chaining test;
const that: HTMLBodyElement = this;
// $ExpectError
const that2: SVGElement = this; // fails, type mismatch
const datum: BodyDatum = d;
const index: number = i;
const group: HTMLBodyElement[] | d3Selection.ArrayLike<HTMLBodyElement> = g;
let eParam: d3Selection.CustomEventParameters;
console.log('fooEvent dispatch body background color', this.bgColor);
eParam = {
cancelable: true,
bubbles: true,
detail: d.foo // d is of type BodyDatum
};
return eParam;
});
// event and customEvent() ----------------------------------------------------------------
// TODO: Tests of event are related to issue #3 (https://github.com/tomwanzek/d3-v4-definitelytyped/issues/3)
// No tests for event, as it now is of type any
interface SuccessEvent {
type: string;
team: string;
sourceEvent?: any;
}
const successEvent = { type: 'wonEuro2016', team: 'Island' };
function customListener(this: HTMLBodyElement | null, finalOpponent: string): string {
const e = d3Selection.event as SuccessEvent;
return `${e.team} defeated ${finalOpponent} in the EURO 2016 Cup. Who would have thought!!!`;
}
const resultText: string = d3Selection.customEvent(successEvent, customListener, body.node(), 'Wales');
// $ExpectError
const result = d3Selection.customEvent(successEvent, customListener, circles.nodes()[0], 'Wales'); // fails, incompatible 'this' context in call
// $ExpectError
const resultValue: number = d3Selection.customEvent(successEvent, customListener, body.node(), 'Wales'); // fails, incompatible return types
// $ExpectError
d3Selection.customEvent<SVGCircleElement, any>(successEvent, customListener, circles.nodes()[0], 'Wales'); // fails, incompatible 'this' context in type parameter and call
// $ExpectError
d3Selection.customEvent<HTMLBodyElement, any>(successEvent, customListener, circles.nodes()[0], 'Wales'); // fails, incompatible 'this' context in type parameter and call
// $ExpectError
d3Selection.customEvent<HTMLBodyElement, number>(successEvent, customListener, body.node(), 'Wales'); // fails, incompatible return types
// mouse() ---------------------------------------------------------------------------------
let position: [number, number] | null;
const svg: SVGSVGElement = d3Selection.select<SVGSVGElement, any>('svg').node()!;
const g: SVGGElement = d3Selection.select<SVGGElement, any>('g').node()!;
const h: HTMLElement = d3Selection.select<HTMLElement, any>('div').node()!;
const changedTouches: TouchList = new TouchList(); // dummy
position = d3Selection.mouse(svg);
position = d3Selection.mouse(g);
position = d3Selection.mouse(h);
// touch() and touches() ---------------------------------------------------------------------
position = d3Selection.touch(svg, 0);
position = d3Selection.touch(g, 0);
position = d3Selection.touch(h, 0);
position = d3Selection.touch(svg, changedTouches, 0);
position = d3Selection.touch(g, changedTouches, 0);
position = d3Selection.touch(h, changedTouches, 0);
let positions: Array<[number, number]>;
positions = d3Selection.touches(svg, changedTouches);
positions = d3Selection.touches(g, changedTouches);
positions = d3Selection.touches(h, changedTouches);
positions = d3Selection.touches(svg, changedTouches);
positions = d3Selection.touches(g, changedTouches);
positions = d3Selection.touches(h, changedTouches);
// clientPoint() ---------------------------------------------------------------------
let clientPoint: [number, number];
declare let mEvt: MouseEvent;
declare let tEvt: Touch;
declare let msgEvt: MSGestureEvent;
declare let customEvt: {clientX: number, clientY: number}; // minimally conforming object
clientPoint = d3Selection.clientPoint(svg, mEvt);
clientPoint = d3Selection.clientPoint(g, tEvt);
clientPoint = d3Selection.clientPoint(h, msgEvt);
clientPoint = d3Selection.clientPoint(h, customEvt);
// ---------------------------------------------------------------------------------------
// Tests of style
// ---------------------------------------------------------------------------------------
declare let n: Element;
str = d3Selection.style(n, 'opacity');
// ---------------------------------------------------------------------------------------
// Tests of Local
// ---------------------------------------------------------------------------------------
let xElement: Element = d3Selection.select<Element, any>('foo').node()!;
const foo: d3Selection.Local<number[]> = d3Selection.local<number[]>();
const propName: string = foo.toString();
// direct set & get on Local<T> object
xElement = foo.set(xElement, [1, 2, 3]);
let array: number[] | undefined = foo.get(xElement);
// test read & write of .property() access to locals
array = d3Selection.select(xElement)
.property(foo, [3, 2, 1])
.property(foo, () => [999])
.property(foo);
foo.remove(xElement);
// ---------------------------------------------------------------------------------------
// Tests of Namespace
// ---------------------------------------------------------------------------------------
const predefinedNamespaces: d3Selection.NamespaceMap = d3Selection.namespaces;
const svgNamespace: string = predefinedNamespaces['svg'];
const svgTextObject: d3Selection.NamespaceLocalObject | string = d3Selection.namespace('svg:text');
predefinedNamespaces['dummy'] = 'http://www.w3.org/2020/dummynamespace';
// ---------------------------------------------------------------------------------------
// Tests of Window
// ---------------------------------------------------------------------------------------
xWindow = d3Selection.window(xElement);
xWindow = d3Selection.window(xDoc);
xWindow = d3Selection.window(xWindow);
// ---------------------------------------------------------------------------------------
// JOIN - Convenient alternative to explicit enter update and exit methods
// ---------------------------------------------------------------------------------------
interface OldDatum {
oldData: number;
}
interface Datum {
data: string;
}
let selText: d3Selection.Selection<SVGTextElement, Datum, SVGSVGElement, SVGDatum>;
let selTextAndCircle: d3Selection.Selection<SVGTextElement | SVGCircleElement, Datum, SVGSVGElement, SVGDatum>;
const text = svgEl.selectAll<SVGTextElement, OldDatum>('text').data<Datum>([{data: 'a'}]);
declare const r: () => boolean;
// with only enter param
selText = text.join('text');
selText = text.join<SVGTextElement>('custom');
selText = text.join(enter => enter.append('text').text(d => d.data));
selText = text.join('circle'); // $ExpectError
selText = text.join<SVGCircleElement>('custom'); // $ExpectError
selText = text.join(enter => enter.append('circle')); // $ExpectError
selTextAndCircle = text.join('circle');
selTextAndCircle = text.join<SVGCircleElement>('custom');
selTextAndCircle = text.join(enter => enter.append('circle').text(d => d.data));
// with all param
selText = text.join(
'text',
update => r() ? undefined : update.text(d => d.data).attr('fill', 'gray'),
exit => exit.text(d => d.data).remove(),
);
selText = text.join<SVGTextElement>(
'custom',
update => r() ? undefined : update.text(d => d.data).attr('fill', 'gray'),
exit => exit.text(d => d.data).remove(),
);
selText = text.join(
enter => enter.append('text').text(d => d.data),
update => r() ? undefined : update.text(d => d.data).attr('fill', 'gray'),
exit => exit.text(d => d.data).remove(),
);
selTextAndCircle = text.join(
'circle',
update => r() ? undefined : update.text(d => d.data).attr('fill', 'gray'),
exit => exit.text(d => d.data).remove(),
);
selTextAndCircle = text.join<SVGCircleElement>(
'custom',
update => r() ? undefined : update.text(d => d.data).attr('fill', 'gray'),
exit => exit.text(d => d.data).remove(),
);
selTextAndCircle = text.join(
enter => enter.append('circle').text(d => d.data),
update => r() ? undefined : update.text(d => d.data).attr('fill', 'gray'),
exit => exit.text(d => d.data).remove(),
);
// with all param and old datum
selText = text.join<'text', OldDatum>(
'text',
update => r() ? undefined : update.text(d => d.data).attr('fill', 'gray'),
exit => exit.text(d => `Bye ${d.oldData}`).remove(),
);
selText = text.join<SVGTextElement, OldDatum>(
'custom',
update => r() ? undefined : update.text(d => d.data).attr('fill', 'gray'),
exit => exit.text(d => `Bye ${d.oldData}`).remove(),
);
selText = text.join<SVGTextElement, OldDatum>(
enter => enter.append('text').text(d => d.data),
update => r() ? undefined : update.text(d => d.data).attr('fill', 'gray'),
exit => exit.text(d => d.oldData).remove(),
);
selTextAndCircle = text.join<'circle', OldDatum>(
'circle',
update => r() ? undefined : update.text(d => d.data).attr('fill', 'gray'),
exit => exit.text(d => d.oldData).remove(),
);
selTextAndCircle = text.join<SVGCircleElement, OldDatum>(
'circle',
update => r() ? undefined : update.text(d => d.data).attr('fill', 'gray'),
exit => exit.text(d => d.oldData).remove(),
);
selTextAndCircle = text.join<SVGCircleElement, OldDatum>(
enter => enter.append('circle').text(d => d.data),
update => r() ? undefined : update.text(d => d.data).attr('fill', 'gray'),
exit => exit.text(d => d.oldData).remove(),
);
// Example from: https://github.com/d3/d3-selection/issues/194#issuecomment-427577484
const groups = svgEl.selectAll<SVGGElement, {}>('g')
.data([{r: 10, text: 'hi'}])
.join(
(enter) => {
const g = enter.append('g').attr('class', 'tick');
g.append('circle');
g.append('text');
return g;
},
() => undefined,
(exit) => exit.remove()
)
.attr('transform', (_, i) => `translate(0, ${i})`);
groups.select('circle')
.attr('r', d => d.r);
groups.select('text')
.text(d => d.text)
.attr('dy', '0.32em');