In Web development, we will spend a lot of time on DOM operation. In general, we will choose a third-party library such as jQuery to replace the native method, because the native method will make the code redundant and difficult to operate. D3 also provides its own set of methods to easily manipulate the DOM, such as changing styles, registering events, and so on.

D3 Selection is mainly used to manipulate the DOM directly, such as setting properties and changing styles, and it can be combined with the powerful data Join feature to manipulate elements and the data bound to them. The method in Selection evaluates and returns the current selection, which allows chaining calls to the method. Because calling a method this way makes each line of code long, the convention is to indent it with four Spaces if the method returns the current selection; If the method returns a new Selection, indent it with two Spaces.

Select elements

Element selection is achieved through two methods, select, which returns only the first matched element, and selectAll, which returns all matched elements. Since all subsequent operations are performed on selection, the object is derived from the Selection constructor, and the source code is as follows:

function Selection(groups, parents) {
    this._groups = groups;
    this._parents = parents;
}
Copy the code

As you can see, the Selection object contains two basic properties, _groups, which stores the node group, and _parents, which stores the node’s parent information.

selection.select(selector)

var p = d3.selectAll('div')
          .select('p');
Copy the code

This method looks for each element in selection and selects the first child element that matches a selector.

/* * Selection select method * If the select method does not have an element, the location is set aside in the array (assign null) for later insertion. * /function selection_select(select) {
    if(typeof select ! = ="function") select = selector(select); // When select is a function, we call the function directly, passing in data, the current index, and the current node group, and setting this to the current DOM objectfor (var groups = this._groups, m = groups.length, subgroups = new Array(m), j = 0; j < m; ++j) {
        for (var group = groups[j], n = group.length, subgroup = subgroups[j] = new Array(n), node, subnode, i = 0; i < n; ++i) {
            if((node = group[I]) && (subNode = select.call(node, node.__data__, I, group))) {// When a node has data information, the child element of node also adds that data informationif ("__data__" innode) subnode.__data__ = node.__data__; subgroup[i] = subnode; }}} // Use the _parents attribute of the parent as the _parents attribute of the childreturn new Selection(subgroups, this._parents);
}
Copy the code

If a selector is a string, the selector method is first called to convert it to a function.

function selector(selector) {
    return selector == null ? none$2 : function() {// Only the first selected element is returnedreturn this.querySelector(selector);
    };
}
Copy the code

As you can see, internally it calls js’s native querySelector method. After the select argument is processed, it is converted to a function, which is called later in the loop through select.call, passing in the __data__ attribute value of the node, the index of the node in the node group, and the node group. The following points are worth noting:

  • If the node contains__data__Property is set on matched child elements as well.
  • The new selection obtained by the select method_parentsThe value doesn’t change.

selection.selectAll(selector)

var p = d3.selectAll('div')
          .selectAll('p');
Copy the code

The method looks for each element in selection, selects the child element matching the selector, and returns elements in selection that are grouped according to their parent. The source code is as follows:

/* * selectAll of selection changes the selection structure, and parents also change */function selection_selectAll(select) {
    if(typeof select ! = ="function") select = selectorAll(select);
    for (var groups = this._groups, m = groups.length, subgroups = [], parents = [], j = 0; j < m; ++j) {
        for (var group = groups[j], n = group.length, node, i = 0; i < n; ++i) {
            if(node = group[i]) { subgroups.push(select.call(node, node.__data__, i, group)); parents.push(node); }}}return new Selection(subgroups, parents);
}
Copy the code

As can be seen from the above code, after calling the select method on node, the search result is stored in the subgroups, and node is stored in the parents array as the parent node, so that the node corresponds to the parent node one by one, and finally returns the new selection.

selection.filter(filter)

var red = d3.selectAll('p')
            .filter('.red')
Copy the code

Construct a new selection for elements whose filter is true and return it:

// The filter method filters the current selection to retain the elements that meet the criteriafunction selection_filter(match) {
    if(typeof match ! = ="function") match = matcherThe $1(match);
    for (var groups = this._groups, m = groups.length, subgroups = new Array(m), j = 0; j < m; ++j) {
            for (var group = groups[j], n = group.length, subgroup = subgroups[j] = [], node, i = 0; i < n; ++i) {
                if((node = group[i]) && match.call(node, node.__data__, i, group)) { subgroup.push(node); }}}return new Selection(subgroups, this._parents);
}
Copy the code

If match is not a function, it is processed by the matcher$1 function.

var matcher = function(selector) {
    return function() {// element.matches (s), returns if the Element can be selected using the s selectortrue; Otherwise returnsfalse
        return this.matches(selector);
    };
};
Copy the code

You can see that inside the matcher function is a call to the native Element. Matches implementation.

selection.merge(other_selection)

var circle = svg.selectAll("circle").data(data) // UPDATE
    .style("fill"."blue");

circle.exit().remove(); // EXIT

circle.enter().append("circle") // ENTER
    .style("fill"."green")
  .merge(circle) // ENTER + UPDATE
    .style("stroke"."black");
Copy the code

This method merges two selections into a new selection and returns the following source code:

functionSelection_merge (selection) {// New selection has the same _.groups length as groups0, and only counts in mfor (var groups0 = this._groups, groups1 = selection._groups, m0 = groups0.length, m1 = groups1.length, m = Math.min(m0, m1), merges = new Array(m0), j = 0; j < m; ++j) {
        for(var group0 = groups0[j], group1 = groups1[j], n = group0.length, merge = merges[j] = new Array(n), node, i = 0; i < n; ++ I) {// select group1[I] where group1[I] does not exist and group1[I] does existif(node = group0[i] || group1[i]) { merge[i] = node; }} // if m1 < m0, copy the rest of groups0 overfor (; j < m0; ++j) {
      merges[j] = groups0[j];
    }
    return new Selection(merges, this._parents);
}
Copy the code

This method essentially populates the empty element in this Selection.

Modify the element

After selecting an element, you can use the Selection method to modify the element, such as style, attributes, and so on.

selection.attr(name[, value])

var p = d3.selectAll('p')
            .attr('class'.'red');
Copy the code

Sets the name and value of the element in Selection and returns the current selection.

function selection_attr(name, value) {
    var fullname = namespace(name);

    ifVar node = this.node(); (arguments.length < 2) {var node = this.node();return fullname.local
            ? node.getAttributeNS(fullname.space, fullname.local)
            : node.getAttribute(fullname);
    }

    return this.each((value == null
        ? (fullname.local ? attrRemoveNS : attrRemove) : (typeof value === "function"
        ? (fullname.local ? attrFunctionNS : attrFunction)
        : (fullname.local ? attrConstantNS : attrConstant)))(fullname, value));
}
Copy the code

If there is only a name argument, the name attribute value of the first existing Element is returned, using the native element.getAttribute method. When there are two arguments, the selection.each method is called to operate on each element in selection.

function selection_each(callback) {
    for (var groups = this._groups, j = 0, m = groups.length; j < m; ++j) {
        for (var group = groups[j], i = 0, n = group.length, node; i < n; ++i) {
            if(node = group[i]) callback.call(node, node.__data__, i, group); }}return this;
}
Copy the code

If value is a function, name and value are passed to the attrFunction function for processing.

function attrFunction(name, value) {
    return function() {
        var v = value.apply(this, arguments);
        if (v == null) this.removeAttribute(name);
        else this.setAttribute(name, v);
    };
}
Copy the code

In the selection.each method, you pass parameters into the value function, and select Settings and delete properties based on the result returned by value.

selection.classed(names[, value])

var p = d3.selectAll('p')
            .classed('red warn'.true);
Copy the code

Set the class name of the element in Selection to the following source:

// Add name to all element class names when value is true; Otherwise delete namefunction selection_classed(name, value) {
    var names = classArray(name + ""); // If only name exists, the first node in _groups of the selection object contains all class names of nametrue; Otherwise, returnfalse
    if (arguments.length < 2) {
        var list = classList(this.node()), i = -1, n = names.length;
        while (++i < n) if(! list.contains(names[i]))return false;
        return true;
    }
    return this.each((typeof value === "function"
        ? classedFunction : value
        ? classedTrue
        : classedFalse)(names, value));
}
Copy the code

The classArray method splits the class name string into arrays:

// Split the class name into arrays, as in'button button-warn'= > ['button'.'button-warn']
function classArray(string) {
    return string.trim().split(/^|\s+/);
}
Copy the code

selection.style(name[, value[, priority]])

var p = d3.selectAll('p')
            .style('color'.'red');
Copy the code

This method styles elements in Selection. The source code is as follows:

// Set the selection style, noting the unit of the stylefunction selection_style(name, value, priority) {
    var node;
    return arguments.length > 1
        ? this.each((value == null
              ? styleRemove : typeof value === "function"
              ? styleFunction
              : styleConstant)(name, value, priority == null ? "" : priority))
        : window(node = this.node())
            .getComputedStyle(node, null)
            .getPropertyValue(name);
}
Copy the code

GetComputedStyle (element).getPropertyValue(name). (The value is read-only.) The delete style is by element. Style. RemoveProperty (name), set properties through the element. The style.css. SetProperty (name, value).

selection.property(name[, value])

var checkbox = d3.selectAll('input[type=checkbox]')
                    .property('checked'.'checked');
Copy the code

This method sets some special properties.

function selection_property(name, value) {
    return arguments.length > 1
        ? this.each((value == null
            ? propertyRemove : typeof value === "function"
            ? propertyFunction
            : propertyConstant)(name, value))
        : this.node()[name];
}

function propertyRemove(name) {
    return function() {
      delete this[name];
    };
  }

function propertyConstant(name, value) {
    return function() {
      this[name] = value;
    };
}

function propertyFunction(name, value) {
    return function() {
      var v = value.apply(this, arguments);
      if (v == null) delete this[name];
      else this[name] = v;
    };
}
Copy the code

Internally, this is done by directly modifying the attributes of the element.

selection.text([value])

This method sets the text content of all elements in selection, and replaces the child elements of the element.

function textRemove() {
    this.textContent = "";
}

function textConstant(value) {
    return function() {
      this.textContent = value;
    };
}

function textFunction(value) {
    return function() {
      var v = value.apply(this, arguments);
      this.textContent = v == null ? "": v; }; } // Set the textContent property of the element, which returns the plain text inside the element, excluding the node tag (but containing the text inside the tag).function selection_text(value) {
    return arguments.length
        ? this.each(value == null
            ? textRemove : (typeof value === "function"
            ? textFunction
            : textConstant)(value))
        : this.node().textContent;
}
Copy the code

The above code uses the element.textcontent method to get and modify the textContent.

selection.html([value])

Set innerHTML for all elements in Selection. The method is similar to the selection.text method above, except that all the contents of an element are modified with element.innerhtml.

selection.append(type)

Adds a new element to the element in Selection.

/* * Selection's append method * Returns a new Selection object, */function selection_append(name) {
    var create = typeof name === "function" ? name : creator(name);
    return this.select(function() {//arguments are arguments passed to the current anonymous functionreturn this.appendChild(create.apply(this, arguments));
    });
}

function creatorInherit(name) {
    return functionVar document = this.ownerDocument, uri = this.namespaceuri;return&& uri = = = XHTML document. The documentElement.? NamespaceURI = = = XHTML document. The createElement method (name) / / create a dom object: document.createElementNS(uri, name); }; }Copy the code

The selection.select method is called, and since the element.appendChild method returns that child, the new selection returned contains all the added children.

selection.insert(type, before)

Insert a new element into an element in selection, similar to the selection.append method above, but internally using the element.insertbefore method.

selection.sort(compare)

Sort the elements in selection according to the compare function. After sorting, sort the DOM according to the sorting result, and return the newly created selection object.

function selection_sort(compare) {
    if(! compare) compare = ascending$2;

    functionCompareNode (a, b) {// Compare the data size of the nodereturna && b ? compare(a.__data__, b.__data__) : ! a - ! b; } // copy a copy of the nodes contained in _groups selectionfor (var groups = this._groups, m = groups.length, sortgroups = new Array(m), j = 0; j < m; ++j) {
        for (var group = groups[j], n = group.length, sortgroup = sortgroups[j] = new Array(n), node, i = 0; i < n; ++i) {
            if(node = group[i]) { sortgroup[i] = node; } // Call the array's sort method sortgroup.sort(compareNode); }returnnew Selection(sortgroups, this._parents).order(); } / / incrementfunction ascending$2(a, b) {
    return a < b ? -1 : a > b ? 1 : a >= b ? 0 : NaN;
}
Copy the code

If there is no compare argument, the order is sorted incrementally by default. Simultaneous sorting first compares the size of the __data__ attribute value in the element, and then calls the order method after sorting selection.

selection.sort(compare)

Sort the DOM by the order of the elements in each group in Selection

// Sort the DOM nodesfunction selection_order() {
    for (var groups = this._groups, j = -1, m = groups.length; ++j < m;) {
        for (var group = groups[j], i = group.length - 1, next = group[i], node; --i >= 0;) {
            if(node = group[I]) {// Move node before next and assign node to nextif(next && next ! == node.nextSibling) next.parentNode.insertBefore(node, next); next = node; }}}return this;
}
Copy the code

Connection data

Connecting data binds data to selection objects, essentially storing the data in the __data__ property so that later selection operations can use the bound data directly. To understand update, Enter, and exit, see Thinking With Joins.

selection.data([data[, key]])

This method binds the specified data array to the selected element, and returns a selection of elements containing successfully bound data, also called updata Selection.

functionSelection_data (value, key) {// When value is false, the __data__ attribute of all selection elements is returned as an arrayif(! value) { data = new Array(this.size()), j = -1; this.each(function(d) { data[++j] = d; });
      return data;
    }

    var bind = key ? bindKey : bindIndex,
        parents = this._parents,
        groups = this._groups;

    if(typeof value ! = ="function") value = constant$4(value);

    for (var m = groups.length, update = new Array(m), enter = new Array(m), exit = new Array(m), j = 0; j < m; ++j) {
      var parent = parents[j],
          group = groups[j],
          groupLength = group.length,
          data = value.call(parent, parent && parent.__data__, j, parents),
          dataLength = data.length,
          enterGroup = enter[j] = new Array(dataLength),
          updateGroup = update[j] = new Array(dataLength),
          exitGroup = exit[j] = new Array(groupLength);

      bind(parent, group, enterGroup, updateGroup, exitGroup, data, key); // Set the _next attribute on enter to store the first update node after its indexfor (var i0 = 0, i1 = 0, previous, next; i0 < dataLength; ++i0) {
        if (previous = enterGroup[i0]) {
          if (i0 >= i1) i1 = i0 + 1;
          while(! (next = updateGroup[i1]) && ++i1 < dataLength); previous._next = next || null; }}} // Enter andexitUpdate = new Selection(update, parents); update._enter = enter; update._exit =exit;
    return update;
}
Copy the code

As you can see from the code above, the same data data is bound to each group. When there is no key parameter, the bindIndex method is used for binding data, which is bound by index once.

function bindIndex(parent, group, enter, update, exit, data) { var i = 0, node, groupLength = group.length, dataLength = data.length; /* * Bind data to node and store the node in an Update array * store the remaining data in an Enter array */for (; i < dataLength; ++i) {
      if (node = group[i]) {
        node.__data__ = data[i];
        update[i] = node;
      } else{ enter[i] = new EnterNode(parent, data[i]); }} // Store the remaining nodesexitIn the arrayfor (; i < groupLength; ++i) {
      if (node = group[i]) {
        exit[i] = node; }}}Copy the code

If there is a key argument, the bindKey method is called to bind the data.

function bindKey(parent, group, enter, update, exit, data, key) { var i, node, nodeByKeyValue = {}, groupLength = group.length, dataLength = data.length, keyValues = new Array(groupLength), keyValue; // Calculate the keyValue for each node in the group, and store it if the next node has the same keyValue as the previous nodeexitIn the arrayfor (i = 0; i < groupLength; ++i) {
      if (node = group[i]) {
        keyValues[i] = keyValue = keyPrefix + key.call(node, node.__data__, i, group);
        if (keyValue in nodeByKeyValue) {
          exit[i] = node;
        } else{ nodeByKeyValue[keyValue] = node; }} // Calculate the keyValue of each data. If the keyValue already exists in the nodeByKeyValue array, store the node corresponding to it in the update array and bind data; Otherwise, store data into Enterfor (i = 0; i < dataLength; ++i) {
      keyValue = keyPrefix + key.call(parent, data[i], i, data);
      if (node = nodeByKeyValue[keyValue]) {
        update[i] = node;
        node.__data__ = data[i];
        nodeByKeyValue[keyValue] = null;
      } else{ enter[i] = new EnterNode(parent, data[i]); }} // Store the remaining nodes without binding dataexitAn array offor (i = 0; i < groupLength; ++i) {
      if ((node = group[i]) && (nodeByKeyValue[keyValues[i]] === node)) {
        exit[i] = node; }}}Copy the code

selection.enter()

Return the result of Enter Selection, which is selections. _Enter.

function sparse(update) {
    returnnew Array(update.length); } // Selection's Enter methodfunction selection_enter() {
    return new Selection(this._enter || this._groups.map(sparse), this._parents);
}
Copy the code

If selection has no _enter property, that is, no data operation has been performed, an empty array is created.

selection.exit()

Returns the result of exit selection in Selection, selection._exit.

function selection_exit() {
    return new Selection(this._exit || this._groups.map(sparse), this._parents);
}
Copy the code

selection.datum([value])

Setting binding data for each element in Selection does not affect the values of Enter and exit.

function selection_datum(value) {
    return arguments.length
        ? this.property("__data__", value)
        : this.node().__data__;
}
Copy the code

You can see that it calls the selection. Property method to set the __data__ property. This method is often used to access HTML5 data attributes, such as

selection.datum(function() {return this.dataset});
Copy the code

The element.dataset is a native method that returns all the data attributes bound to the element.

Handle events

selection.on(typenames[, listener[, capture]])

This method is used to add or remove events to elements in Selection.

function selection_on(typename, value, capture) {
    var typenames = parseTypenamesThe $1(typename + ""), i, n = typenames.length, t; // If only the typename parameter is available, according totypeAnd the name value to find the corresponding value in the __on attribute of the first element in selection.if (arguments.length < 2) {
      var on = this.node().__on;
      if (on) for (var j = 0, m = on.length, o; j < m; ++j) {
        for (i = 0, o = on[j]; i < n; ++i) {
          if ((t = typenames[i]).type === o.type && t.name === o.name) {
            returno.value; }}}return; } // If value is true, add events; Otherwise remove the event. on = value ? onAdd : onRemove;if (capture == null) capture = false;
    for (i = 0; i < n; ++i) this.each(on(typenames[i], value, capture));
    return this;
  }
Copy the code

The typename parameter is processed first, using the parseTypenames function.

// The typenames are divided into arrays based on Spaces and the separated strings'. 'To split the string intotypeAnd the name part, as in:'click.foo click.bar'= > [{type: 'click', name: 'foo'}, {type: 'click', name: 'bar'}]
function parseTypenamesThe $1(typenames) {
    return typenames.trim().split(/^|\s+/).map(function(t) {
      var name = "", i = t.indexOf(".");
      if (i >= 0) name = t.slice(i + 1), t = t.slice(0, i);
      return {type: t, name: name};
    });
}
Copy the code

Choose to add or remove events based on whether value is true.

// Add an event functionfunction onAdd(typename, value, capture) {
    var wrap = filterEvents.hasOwnProperty(typename.type) ? filterContextListener : contextListener;
    return function(d, i, group) {
        var on = this.__on, o, listener = wrap(value, i, group);
        if (on) for(var j = 0, m = on.length; j < m; ++j) {// If the new eventtypeIf name is the same as the previously bound event, remove the previous event and bind the new eventif ((o = on[j]).type === typename.type && o.name === typename.name) {
                this.removeEventListener(o.type, o.listener, o.capture);
                this.addEventListener(o.type, o.listener = listener, o.capture = capture);
                o.value = value;
                return; This. AddEventListener (typename. Type, listener, capture); o = {type: typename.type, name: typename.name, value: value, listener: listener, capture: capture};
        if(! on) this.__on = [o];else on.push(o);
    };
}
Copy the code
// Remove the event functionfunction onRemove(typename) {
    return function() {
        var on = this.__on;
        if(! on)return;
        for (var j = 0, i = -1, m = on.length, o; j < m; ++j) {
            if(o = on[j], (! Typename. Type | | o.t ype. = = = typename type) && o.n ame = = = typename. Name) {/ / the node removal enclosing removeEventListener (o.t ype, o.listener, o.capture); }else{// Change the node's __ON attribute value on[++ I] = o; }}if(++i) on.length = i; // If on is empty, the __ON attribute is removedelse delete this.__on;
    };
}
Copy the code

selection.dispatch(type[, parameters])

Assigns a custom event of the specified type to an element in Selection, where parameters may contain the following:

  • Bubbles: Set to true, events can bubble
  • Cancelable: Set to true to indicate that the event can be cancelled
  • The source code for the custom data bound to the event is as follows:
// Dispatch eventsfunction selection_dispatch(type, params) {
    return this.each((typeof params === "function"
        ? dispatchFunction
        : dispatchConstant)(type, params));
}

function dispatchConstant(type, params) {
    return function() {
      return dispatchEvent(this, type, params);
    };
}

function dispatchFunction(type, params) {
    return function() {
      return dispatchEvent(this, type, params.apply(this, arguments));
    };
}
Copy the code

The dispatchEvent function is as follows:

// Create a custom event and assign it to the specified elementfunction dispatchEvent(node, type, params) { var window? = window(node), event = window? .CustomEvent;if (event) {
      event = new event(type, params);
    } else{// This method is deprecated event = window? .document.createEvent("Event");
        if (params) event.initEvent(type, params.bubbles, params.cancelable), event.detail = params.detail;
        else event.initEvent(type.false.false);
    }

    node.dispatchEvent(event);
}
Copy the code

d3.event

Store the current event, set when the event listener is called, reset after the execution of the handler function, you can get the event information contained in it, such as event.pagex.

d3.customEvent(event, listener[, that[, arguments]])

This method invokes the specified listener.

// Invoke the specified listenerfunctionCustomEvent (event1, listener, that, args) {var event0 = exports.event; event1.sourceEvent = exports.event; exports.event = event1; try {returnlistener.apply(that, args); } finally {// exports.event = event0; }}Copy the code

d3.mouse(container)

Returns the x and y coordinates of the current current event relative to the specified container.

function mouse(node) {
    var event = sourceEvent(); // If it is a Touch event, return the Touch objectif (event.changedTouches) event = event.changedTouches[0];
    return pointA $5(node, event);
}
Copy the code

The point$5 method is used to calculate coordinates.

function pointA $5(the node, the event) {/ / if the node is SVG element, for SVG var SVG container = node. OwnerSVGElement | | node;if(svg.createsvgPoint) {var point = svg.createsvgPoint (); // Assign the x and Y coordinates of the event relative to the client to the point object point.x = event.clientX, point.y = event.clienty; Point = point.matrixTransform(node.getScreenctm ().inverse());return[point.x, point.y]; } var rect = node.getBoundingClientRect(); // Returns the coordinates of the event event relative to the containerreturn [event.clientX - rect.left - node.clientLeft, event.clientY - rect.top - node.clientTop];
}
Copy the code

The control flow

Some advanced operations for Selection.

selection.each(function)

Calls the specified function for each selected element.

// Selection's each methodfunction selection_each(callback) {

    for (var groups = this._groups, j = 0, m = groups.length; j < m; ++j) {
      for(var group = groups[j], i = 0, n = group.length, node; i < n; ++ I) {// Call the callback functionif(node = group[i]) callback.call(node, node.__data__, i, group); }}return this;
}
Copy the code

Selection. The call (function /, the arguments… )

Passing selection and other arguments to the specified function and returning the current selection is the same as the direct chaining call function(Selection).

function selection_call() {
    var callback = arguments[0];
    arguments[0] = this;
    callback.apply(null, arguments);
    return this;
}
Copy the code

A local variable

Local variables in D3 can define local state independent of data and are scoped to DOM elements. Its constructor and prototype methods are as follows:

function Local() {
    this._ = "@" + (++nextId).toString(36);
}

Local.prototype = local.prototype = {
    constructor: Local,
    get: function(node) {
      var id = this._;
      while(! (idin node)) if(! (node = node.parentNode))return;
      return node[id];
    },
    set: function(node, value) {
      return node[this._] = value;
    },
    remove: function(node) {
      return this._ in node && delete node[this._];
    },
    toString: function() {
      returnthis._; }};Copy the code