D3原始碼解讀系列之Selections

arlendp2012發表於2019-11-01

在web開發中我們會花大量的時間用於dom的操作上,一般情況下我們會選擇第三方庫如jQuery來替代原生的方法,因為原生的方法在操作上會使程式碼大量重複冗餘而不易操作。d3同樣提供了一套自己的方法來很方便的對dom進行操作,像修改樣式、註冊事件等等都可以通過它來完成。

d3的selection主要用於直接對DOM進行操作,如設定屬性、修改樣式等等,同時它可以和data join(資料連線)這一強大的功能結合起來對元素和元素上繫結的資料進行操作。 selection中的方法計算後返回當前selection,這樣可以進行方法的鏈式呼叫。由於通過這種方式呼叫方法會使得每行的程式碼很長,因此按照約定:若該方法返回的是當前的selection,則使用四個空格進行縮排;若方法返回的是新的selection,則使用兩個空格進行縮排。

選擇元素

元素的選擇通過兩種方法selectselectAll來實現,前者只返回第一個匹配的元素,而後者返回所有匹配元素。 由於之後所有的操作都是在selection上進行,而該物件則是通過Selection建構函式得到的,原始碼如下:

function Selection(groups, parents) {
    this._groups = groups;
    this._parents = parents;
}
複製程式碼

可以看出,selection物件包含兩個基本屬性_groups_parents,前者用於儲存結點組,而後者則儲存結點的父節點資訊。

selection.select(selector)

var p = d3.selectAll('div')
          .select('p');
複製程式碼

該方法對selection中的每個元素進行查詢,選擇其中第一個匹配selector的子元素,原始碼如下:

/*
 * Selection的select方法
 * 通過select方法選擇時,若不存在元素,則會在陣列中將該位置留出(賦值為null)用於之後插入時使用。
 */

function selection_select(select) {
    if (typeof select !== "function") select = selector(select);
    //當select是函式時,直接呼叫該函式,並依次對該函式傳入data資訊、當前的索引和當前的結點group,同時將函式的this設定為當前dom物件
    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] = new Array(n), node, subnode, i = 0; i < n; ++i) {
            if ((node = group[i]) && (subnode = select.call(node, node.__data__, i, group))) {
          //當node中有data資訊時,node的子元素也新增該data資訊
            if ("__data__" in node) subnode.__data__ = node.__data__;
                subgroup[i] = subnode;
            }
        }
    }
    //將父級的_parents屬性作為子元素的_parents屬性
    return new Selection(subgroups, this._parents);
}
複製程式碼

若selector為選擇器字串時,則會先呼叫selector方法將其轉化為函式,原始碼如下:

function selector(selector) {
    return selector == null ? none$2 : function() {
        //只返回第一個選中元素
        return this.querySelector(selector);
    };
}
複製程式碼

可以看出,其內部呼叫的是js的原生方法querySelector。 上述程式碼對select引數進行處理後,使得其轉化為函式,在後來的迴圈中呼叫時,通過select.call進行呼叫,傳入的引數依次為結點的__data__屬性值,結點在該結點組中的索引,該結點組。 有以下幾點值得注意:

  • 若結點中包含__data__屬性則會對匹配的子元素也設定該屬性。
  • 通過select方法得到的新的selection的_parents值並不會改變。

selection.selectAll(selector)

var p = d3.selectAll('div')
          .selectAll('p');
複製程式碼

該方法對selection中的每個元素進行查詢,選擇其中匹配selector的子元素,返回的selection中的元素根據其父結點進行對應的分組,原始碼如下:

/*
 * 對selection進行selectAll計算會改變selection結構,parents也會改變
 */
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);
}
複製程式碼

上述程式碼可以看出,對node呼叫select方法後,查詢到的結果存入subgroups中,同時將node作為父結點存入parents陣列中,使得結點與父結點一一對應,最終返回新的selection。

selection.filter(filter)

var red = d3.selectAll('p')
            .filter('.red')
複製程式碼

將使得filter為true的元素構造成新的selection並返回,原始碼如下:

//filter方法對當前selection進行過濾,保留滿足條件的元素
function selection_filter(match) {
    if (typeof match !== "function") match = matcher$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);
}
複製程式碼

match不為函式,則通過matcher$1函式對其進行處理,其原始碼如下:

var matcher = function(selector) {
    return function() {
        //Element.matches(s),如果元素能通過s選擇器選擇到則返回true;否則返回false
        return this.matches(selector);
    };
};
複製程式碼

可看出matcher函式內部是呼叫原生的Element.matches方法實現。

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");
複製程式碼

該方法將兩個selection進行合併成一個新的selection並返回,原始碼如下:

function selection_merge(selection) {
    //新的selection的_.groups長度和groups0相同,合併時只在m範圍內計算
    for (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) {
            //groups0陣列大小不變,只有在group0[i]不存在,group1[i]存在時才選擇group1[i]
            if (node = group0[i] || group1[i]) {
                merge[i] = node;
            }
        }
    }
    // 若m1 < m0,則將groups0剩餘的複製過來
    for (; j < m0; ++j) {
      merges[j] = groups0[j];
    }
    return new Selection(merges, this._parents);
}
複製程式碼

該方法實際上相當於對this selection中的空元素進行填充。

修改元素

在選擇元素之後可以使用selection的方法來修改元素,如樣式、屬性等。

selection.attr(name[, value])

var p = d3.selectAll('p')
            .attr('class', 'red');
複製程式碼

對selection中的元素以指定的name和value設定屬性,並返回當前selection,原始碼如下:

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

    if (arguments.length < 2) {
        //得到selection中第一個存在的元素
        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));
}
複製程式碼

若只有name引數時,則返回第一個存在的元素的name屬性值,呼叫的是原生的Element.getAttribute方法。 當有兩個引數時,呼叫selection.each方法對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;
}
複製程式碼

若value值為函式,則將name和value傳入attrFunction函式中進行處理。

function attrFunction(name, value) {
    return function() {
        var v = value.apply(this, arguments);
        if (v == null) this.removeAttribute(name);
        else this.setAttribute(name, v);
    };
}
複製程式碼

selection.each方法中,將引數傳入value函式中,根據value返回結果選擇設定和刪除屬性操作。

selection.classed(names[, value])

var p = d3.selectAll('p')
            .classed('red warn', true);
複製程式碼

對selection中的元素設定類名,原始碼如下:

// 當value為真值時,在所有元素類名中新增name;否則刪除name
function selection_classed(name, value) {
    var names = classArray(name + "");
    // 當只有name引數時,判斷該selection物件的_groups裡第一個存在的結點是否包含所有的name的類名,如果是則返回true;否則,返回false
    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));
}
複製程式碼

其中classArray方法是將類名字串拆分成陣列:

// 將類名拆分成陣列,如'button button-warn' => ['button', 'button-warn']
function classArray(string) {
    return string.trim().split(/^|\s+/);
}
複製程式碼

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

var p = d3.selectAll('p')
            .style('color', 'red');
複製程式碼

該方法對selection中的元素設定樣式,原始碼如下:

// 設定selection的樣式,注意樣式的單位問題
function 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);
}
複製程式碼

從上述程式碼可以看出,獲取樣式是通過window.getComputedStyle(element).getPropertyValue(name)來得到(該方法得到的值是隻讀的),而刪除樣式則是通過element.style.removeProperty(name)來實現,設定屬性通過element.style.setProperty(name, value)來實現。

selection.property(name[, value])

var checkbox = d3.selectAll('input[type=checkbox]')
                    .property('checked', 'checked');
複製程式碼

該方法設定一些特殊的屬性。

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;
    };
}
複製程式碼

內部通過直接修改元素的屬性來實現。

selection.text([value])

該方法對selection中的所有元素設定文字內容,同時會替換掉元素中的子元素,原始碼如下:

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;
    };
}
// 設定元素的textContent屬性,該屬性返回的是元素內的純文字內容,不包含結點標籤(但包含標籤內的文字)
function selection_text(value) {
    return arguments.length
        ? this.each(value == null
            ? textRemove : (typeof value === "function"
            ? textFunction
            : textConstant)(value))
        : this.node().textContent;
}
複製程式碼

上述程式碼是通過element.textContent方法來獲取和修改文字內容。

selection.html([value])

對selection中的所有元素設定innerHTML。方法同上述selection.text類似,只是通過element.innerHTML來修改元素內的所有內容。

selection.append(type)

對selection中的元素新增新的元素。

/*
 * selection的append方法
 * 該方法返回新的Selection物件,
 */
function selection_append(name) {
    var create = typeof name === "function" ? name : creator(name);
    return this.select(function() {
        //arguments是傳入當前匿名函式的引數
        return this.appendChild(create.apply(this, arguments));
    });
}

function creatorInherit(name) {
    return function() {
        //ownerDocument返回當前document物件
        var document = this.ownerDocument,
            uri = this.namespaceURI;
        return uri === xhtml && document.documentElement.namespaceURI === xhtml
            ? document.createElement(name)//建立dom物件
            : document.createElementNS(uri, name);
    };
}
複製程式碼

該方法中呼叫到了selection.select方法,由於element.appendChild方法返回的是該子結點,因此返回的新的selection包含的是所有新增的子結點。

selection.insert(type, before)

對selection中的元素插入新的元素,同上述selection.append方法類似,只是內部使用element.insertBefore方法實現。

selection.sort(compare)

根據compare函式對selection中的元素進行排序,排好序後按照排序結果對dom進行排序,返回排序後新建立的selection物件。

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

    function compareNode(a, b) {
      // 比較結點的data大小
      return a && b ? compare(a.__data__, b.__data__) : !a - !b;
    }
    // copy一份selection中的_groups包含的結點
    for (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;
            }
        }
        // 呼叫array的sort方法
        sortgroup.sort(compareNode);
    }
    return new Selection(sortgroups, this._parents).order();
}

// 遞增
function ascending$2(a, b) {
    return a < b ? -1 : a > b ? 1 : a >= b ? 0 : NaN;
}
複製程式碼

若沒有compare引數,則預設以遞增的方式排序。同時排序是首先比較元素中的__data__屬性值的大小,對selection排好序後呼叫order方法。

selection.sort(compare)

按照selection中每組內元素的順序對dom進行排序

// 對dom結點進行排序
function 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]) {
                // 將node移至next的前面,並將node賦值給next
                if (next && next !== node.nextSibling) next.parentNode.insertBefore(node, next);
                next = node;
            }
        }
    }
    return this;
}
複製程式碼

連線資料

連線資料是將資料繫結到selection物件上,實際上是將資料儲存到__data__屬性中,這樣之後對selection的操作過程中便可以直接使用繫結好的資料。主要要理解updateenterexit,可參考文章Thinking With Joins

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

該方法將指定的data陣列繫結到選中的元素上,返回的selection包含成功繫結資料的元素,也叫做updata selection,原始碼如下:

function selection_data(value, key) {
    //當value為假值時,將selection所有元素的__data__屬性以陣列形式返回
    if (!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);

      // 對enter結點設定_next屬性,儲存其索引之後的第一個update結點
      for (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和exit存入update selection的屬性中
    update = new Selection(update, parents);
    update._enter = enter;
    update._exit = exit;
    return update;
}
複製程式碼

通過上述程式碼可以看到,對每組group繫結的是相同的data資料。 當沒有key引數時,繫結資料使用的是bindIndex方法,按照索引一次繫結。

function bindIndex(parent, group, enter, update, exit, data) {
    var i = 0,
        node,
        groupLength = group.length,
        dataLength = data.length;

    /*
     * 將data資料繫結到node,並將該node存入update陣列中
     * 將剩餘的data資料存入enter陣列中
     */
    for (; i < dataLength; ++i) {
      if (node = group[i]) {
        node.__data__ = data[i];
        update[i] = node;
      } else {
        enter[i] = new EnterNode(parent, data[i]);
      }
    }

    // 將剩餘的node存入exit陣列中
    for (; i < groupLength; ++i) {
      if (node = group[i]) {
        exit[i] = node;
      }
    }
  }
複製程式碼

若含有key引數,則呼叫bindKey方法來繫結資料。

function bindKey(parent, group, enter, update, exit, data, key) {
    var i,
        node,
        nodeByKeyValue = {},
        groupLength = group.length,
        dataLength = data.length,
        keyValues = new Array(groupLength),
        keyValue;

    // 對group中每個結點計算keyValue,如果之後的結點含有與前面結點相同的keyValue則將該結點存入exit陣列中
    for (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;
        }
      }
    }

    // 對每一個data計算keyValue,如果該keyValue已存在nodeByKeyValue陣列中,則將其對應的node存入update陣列且繫結data資料;否則將data存入enter中
    for (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]);
      }
    }

    // 將剩餘的沒有繫結資料的結點存入exit陣列
    for (i = 0; i < groupLength; ++i) {
      if ((node = group[i]) && (nodeByKeyValue[keyValues[i]] === node)) {
        exit[i] = node;
      }
    }
}
複製程式碼

selection.enter()

返回selections中的enter selection,即selection._enter的結果。

function sparse(update) {
    return new Array(update.length);
  }
// selection的enter方法
function selection_enter() {
    return new Selection(this._enter || this._groups.map(sparse), this._parents);
}
複製程式碼

若selection沒有_enter屬性,即沒有進行過data操作,則建立空的陣列。

selection.exit()

返回selection中的exit selection,即selection._exit的結果。

function selection_exit() {
    return new Selection(this._exit || this._groups.map(sparse), this._parents);
}
複製程式碼

selection.datum([value])

對selection中的每個元素設定繫結資料,該方法並不會影響到enterexit的值。

function selection_datum(value) {
    return arguments.length
        ? this.property("__data__", value)
        : this.node().__data__;
}
複製程式碼

可見其呼叫selection.property方法來設定__data__屬性。 經常會使用該方法來進行HTML5 data屬性的訪問,如

selection.datum(function() {return this.dataset});
複製程式碼

element.dataset是原生方法,返回的是元素繫結的所有data屬性。

處理事件

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

該方法用於對selection中的元素新增或者移除事件。

function selection_on(typename, value, capture) {
    var typenames = parseTypenames$1(typename + ""), i, n = typenames.length, t;
    // 如果只有typename引數,根據type和name值來找到selection中第一個存在的元素的__on屬性中對應的value值。
    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) {
            return o.value;
          }
        }
      }
      return;
    }
    // 如果value為真值,則新增事件;否則移除事件。
    on = value ? onAdd : onRemove;
    if (capture == null) capture = false;
    for (i = 0; i < n; ++i) this.each(on(typenames[i], value, capture));
    return this;
  }
複製程式碼

首先是對typename引數進行處理,使用的是parseTypenames函式。

// 對typenames根據空格來劃分成陣列,並根據分離後的字串中的'.'來將該字串分割為type和name部分,如:'click.foo click.bar' => [{type: 'click', name: 'foo'}, {type: 'click', name: 'bar'}]
function parseTypenames$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};
    });
}
複製程式碼

根據value是否為真值來選擇新增或者移除事件。

// 新增事件函式
function 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) {
            // 如果新事件的type和name和之前已繫結的事件相同,則移除之前的事件並繫結新的事件
            if ((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;
            }
        }
        // 新增事件,並將事件資訊存入selection.__on屬性中
        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);
    };
}
複製程式碼
// 移除事件函式
function 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.type === typename.type) && o.name === typename.name) {
            //對結點移除事件
            this.removeEventListener(o.type, o.listener, o.capture);
            } else {
                //修改結點的__on屬性值
                on[++i] = o;
            }
        }
        if (++i) on.length = i;
        //on為空則刪除__on屬性
        else delete this.__on;
    };
}
複製程式碼

selection.dispatch(type[, parameters])

對selection中的元素分派指定型別的自定義事件,其中parameters可能包含以下內容:

  • bubbles:設定為true表示事件可以冒泡
  • cancelable:設定為true表示事件可以被取消
  • detail:繫結到事件上的自定義資料 原始碼如下:
//分派事件
function 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));
    };
}
複製程式碼

dispatchEvent函式如下:

//建立自定義事件並分派給指定元素
function dispatchEvent(node, type, params) {
    var window$$ = window(node),
        event = window$$.CustomEvent;

    if (event) {
      event = new event(type, params);
    } else {
        //該方法已被廢棄
        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);
}
複製程式碼

d3.event

儲存當前事件,在呼叫事件監聽器的時候設定,處理函式執行完畢後重置,可以獲取其中包含的事件資訊如:event.pageX

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

該方法呼叫指定的監聽器。

//呼叫指定的監聽器
function customEvent(event1, listener, that, args) {
    //記錄當前事件
    var event0 = exports.event;
    event1.sourceEvent = exports.event;
    exports.event = event1;
    try {
        return listener.apply(that, args);
    } finally {
        //監聽器執行完後恢復事件
        exports.event = event0;
    }
}
複製程式碼

d3.mouse(container)

返回當前當前事件相對於指定容器的x和y座標。

function mouse(node) {
    var event = sourceEvent();
    //如果是觸控事件,返回Touch物件
    if (event.changedTouches) event = event.changedTouches[0];
    return point$5(node, event);
}
複製程式碼

point$5方法用於計算座標。

function point$5(node, event) {
    //若node是svg元素,則獲取svg容器
    var svg = node.ownerSVGElement || node;

    if (svg.createSVGPoint) {
      //建立SVGPoint物件
      var point = svg.createSVGPoint();
      //將事件相對客戶端的x和y座標賦值給point物件
      point.x = event.clientX, point.y = event.clientY;
      //進行座標轉換
      point = point.matrixTransform(node.getScreenCTM().inverse());
      return [point.x, point.y];
    }

    var rect = node.getBoundingClientRect();
    //返回event事件相對於容器的座標
    return [event.clientX - rect.left - node.clientLeft, event.clientY - rect.top - node.clientTop];
}
複製程式碼

控制流

用於selection的一些高階操作。

selection.each(function)

為每一個選中的元素呼叫指定的函式。

//selection的each方法
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) {
        //通過call方式呼叫回撥函式
        if (node = group[i]) callback.call(node, node.__data__, i, group);
        }
    }
    return this;
}
複製程式碼

selection.call(function[, arguments…])

將selection和其他引數傳入指定函式中執行,並返回當前selection,這和直接鏈式呼叫function(selection)相同。

function selection_call() {
    var callback = arguments[0];
    arguments[0] = this;
    callback.apply(null, arguments);
    return this;
}
複製程式碼

區域性變數

d3的區域性變數可以定義與data獨立開來的區域性狀態,它的作用域是dom元素。 其建構函式和原型方法如下:

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

Local.prototype = local.prototype = {
    constructor: Local,
    get: function(node) {
      var id = this._;
      while (!(id in 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() {
      return this._;
    }
};
複製程式碼

相關文章