D3原始碼解讀系列之Shape

arlendp2012發表於2019-11-01

Shape模組提供各種形狀的生成器,這些形狀的產生是資料驅動的,通過控制輸入資料來形成一種視覺的表現。

Pies

餅圖生成器不直接產生圖形,而是計算出需要的角度資訊,然後傳入d3.arc中進行繪製。

  // 繪製餅圖
function pie() {
    var value = identity$1,
        sortValues = descending$1,
        sort = null,
        startAngle = constant$1(0),
        endAngle = constant$1(tau$2),
        padAngle = constant$1(0);

    function pie(data) {
      var i,
          n = data.length,
          j,
          k,
          //統計data陣列中的資料和
          sum = 0,
          index = new Array(n),
          arcs = new Array(n),
          a0 = +startAngle.apply(this, arguments),
          //將|endAngle - startAngle|限定在 2 * PI之間
          da = Math.min(tau$2, Math.max(-tau$2, endAngle.apply(this, arguments) - a0)),
          a1,
          //限定padAngle的範圍
          p = Math.min(Math.abs(da) / n, padAngle.apply(this, arguments)),
          // da<0表示弧線為逆時針方向,pa的值也應進行相應處理
          pa = p * (da < 0 ? -1 : 1),
          v;

      for (i = 0; i < n; ++i) {
        if ((v = arcs[index[i] = i] = +value(data[i], i, data)) > 0) {
          sum += v;
        }
      }

      // 按照處理後的arcs資料大小對index進行排序,或者直接對data進行排序
      if (sortValues != null) index.sort(function(i, j) { return sortValues(arcs[i], arcs[j]); });
      else if (sort != null) index.sort(function(i, j) { return sort(data[i], data[j]); });

      // 計算arcs,按照排序後的index來逐個計算
      for (i = 0, k = sum ? (da - n * pa) / sum : 0; i < n; ++i, a0 = a1) {
        j = index[i], v = arcs[j], a1 = a0 + (v > 0 ? v * k : 0) + pa, arcs[j] = {
          data: data[j],
          index: i,
          value: v,
          startAngle: a0,
          endAngle: a1,
          padAngle: p
        };
      }

      return arcs;
    }
    //設定value函式或數值,value函式會被依次傳入data[i]、i和data。
    pie.value = function(_) {
      return arguments.length ? (value = typeof _ === "function" ? _ : constant$1(+_), pie) : value;
    };
    //設定數值的排序方式
    pie.sortValues = function(_) {
      return arguments.length ? (sortValues = _, sort = null, pie) : sortValues;
    };

    pie.sort = function(_) {
      return arguments.length ? (sort = _, sortValues = null, pie) : sort;
    };

    pie.startAngle = function(_) {
      return arguments.length ? (startAngle = typeof _ === "function" ? _ : constant$1(+_), pie) : startAngle;
    };

    pie.endAngle = function(_) {
      return arguments.length ? (endAngle = typeof _ === "function" ? _ : constant$1(+_), pie) : endAngle;
    };

    pie.padAngle = function(_) {
      return arguments.length ? (padAngle = typeof _ === "function" ? _ : constant$1(+_), pie) : padAngle;
    };

    return pie;
}
複製程式碼

Lines

可以產生樣條曲線或者多段線。

d3.line

預設的設定是構造多條直線段。

//d3.line(),繪製多條直線段,中間可能斷開
  function line() {
    var x$$ = x,
        y$$ = y,
        defined = constant$1(true),
        context = null,
        curve = curveLinear,
        output = null;

    function line(data) {
      var i,
          n = data.length,
          d,
          defined0 = false,
          buffer;

      if (context == null) output = curve(buffer = path());

      for (i = 0; i <= n; ++i) {
        if (!(i < n && defined(d = data[i], i, data)) === defined0) {
          if (defined0 = !defined0) output.lineStart();
          else output.lineEnd();
        }
        if (defined0) output.point(+x$$(d, i, data), +y$$(d, i, data));
      }
    // 返回path的計算結果
      if (buffer) return output = null, buffer + "" || null;
    }
    // 設定獲取x的函式
    line.x = function(_) {
      return arguments.length ? (x$$ = typeof _ === "function" ? _ : constant$1(+_), line) : x$$;
    };
    // 設定獲取y的函式
    line.y = function(_) {
      return arguments.length ? (y$$ = typeof _ === "function" ? _ : constant$1(+_), line) : y$$;
    };
    // defined函式用於判斷當前點是否已被定義,若為true,則會計算x、y座標和繪製直線;否則會跳過當前點,結束當前直線的繪製。
    line.defined = function(_) {
      return arguments.length ? (defined = typeof _ === "function" ? _ : constant$1(!!_), line) : defined;
    };
    // 設定curve函式
    line.curve = function(_) {
      return arguments.length ? (curve = _, context != null && (output = curve(context)), line) : curve;
    };
    // 設定context
    line.context = function(_) {
      return arguments.length ? (_ == null ? context = output = null : output = curve(context = _), line) : context;
    };

    return line;
}
複製程式碼

d3.radialLine

構造放射線,與上述d3.line類似,只是將x、y函式替換成角度和半徑函式,並且改放射線總是相對於(0, 0)進行繪製。

// d3.radialLine,在d3.line的基礎上進行修改,主要的差別是curve函式和座標系。
function radialLine$1() {
    return radialLine(line().curve(curveRadialLinear));
}

// 構造放射線,分別構造線性和放射狀曲線
var curveRadialLinear = curveRadial(curveLinear);

//線性的curve函式
function curveLinear(context) {
    return new Linear(context);
}

//將當前curve函式包裝成放射狀curve
function curveRadial(curve) {
    function radial(context) {
      return new Radial(curve(context));
    }
    radial._curve = curve;
    return radial;
}

// 將line中的(x, y)座標替換成(angle, radius)
function radialLine(l) {
    var c = l.curve;

    l.angle = l.x, delete l.x;
    l.radius = l.y, delete l.y;

    l.curve = function(_) {
      return arguments.length ? c(curveRadial(_)) : c()._curve;
    };
    return l;
}
複製程式碼

Areas

用於產生一塊區域。

d3.area

/* d3.area
 * 首先根據x1, y1函式進行繪製,完成後根據x0, y0函式來反方向繪製,整個圖形的繪製過程是順時針方向。
 * 預設繪製的是x0 = x1,y0 = 0的一塊區域。
 */
 function area$1() {
    var x0 = x,
        x1 = null,
        y0 = constant$1(0),
        y1 = y,
        defined = constant$1(true),
        context = null,
        curve = curveLinear,
        output = null;

    function area(data) {
      var i,
          j,
          k,
          n = data.length,
          d,
          defined0 = false,
          buffer,
          x0z = new Array(n),
          y0z = new Array(n);

      if (context == null) output = curve(buffer = path());

      for (i = 0; i <= n; ++i) {
        if (!(i < n && defined(d = data[i], i, data)) === defined0) {
          //defined0由false變為true時,表示繪製開始,相反表示繪製結束
          if (defined0 = !defined0) {
            j = i;
            output.areaStart();
            output.lineStart();
          } else {
            output.lineEnd();
            output.lineStart();
            //反向繪製x0z, y0z,繪製方向為順時針方向
            for (k = i - 1; k >= j; --k) {
              output.point(x0z[k], y0z[k]);
            }
            output.lineEnd();
            // 關閉繪製區域
            output.areaEnd();
          }
        }
        //defined0為true時可以繪製
        if (defined0) {
          x0z[i] = +x0(d, i, data), y0z[i] = +y0(d, i, data);
          //優先使用x1和y1函式進行計算
          output.point(x1 ? +x1(d, i, data) : x0z[i], y1 ? +y1(d, i, data) : y0z[i]);
        }
      }

      if (buffer) return output = null, buffer + "" || null;
    }
    // 返回與當前area有相同defined、curve和context的line構造器
    function arealine() {
      return line().defined(defined).curve(curve).context(context);
    }
    // 設定x函式,將該函式賦值給x0,null賦值給x1
    area.x = function(_) {
      return arguments.length ? (x0 = typeof _ === "function" ? _ : constant$1(+_), x1 = null, area) : x0;
    };

    area.x0 = function(_) {
      return arguments.length ? (x0 = typeof _ === "function" ? _ : constant$1(+_), area) : x0;
    };

    area.x1 = function(_) {
      return arguments.length ? (x1 = _ == null ? null : typeof _ === "function" ? _ : constant$1(+_), area) : x1;
    };
    // 設定y函式,將該函式賦值給y0,null賦值給y1
    area.y = function(_) {
      return arguments.length ? (y0 = typeof _ === "function" ? _ : constant$1(+_), y1 = null, area) : y0;
    };

    area.y0 = function(_) {
      return arguments.length ? (y0 = typeof _ === "function" ? _ : constant$1(+_), area) : y0;
    };

    area.y1 = function(_) {
      return arguments.length ? (y1 = _ == null ? null : typeof _ === "function" ? _ : constant$1(+_), area) : y1;
    };
    // 分別對line構造器設定x和y函式
    area.lineX0 =
    area.lineY0 = function() {
      return arealine().x(x0).y(y0);
    };

    area.lineY1 = function() {
      return arealine().x(x0).y(y1);
    };

    area.lineX1 = function() {
      return arealine().x(x1).y(y0);
    };
    // defined函式用來判斷是否繪製當前點。這樣可以生成離散的圖形。
    area.defined = function(_) {
      return arguments.length ? (defined = typeof _ === "function" ? _ : constant$1(!!_), area) : defined;
    };

    area.curve = function(_) {
      return arguments.length ? (curve = _, context != null && (output = curve(context)), area) : curve;
    };

    area.context = function(_) {
      return arguments.length ? (_ == null ? context = output = null : output = curve(context = _), area) : context;
    };

    return area;
  }
複製程式碼

d3.radialArea

繪製放射狀區域。

/*
 * d3.radialArea
 * 在area的基礎上修改curve函式,將x,y座標轉換為angle,radius座標,其餘繪製方式不變
 */
function radialArea() {
    var a = area$1().curve(curveRadialLinear),
        c = a.curve,
        x0 = a.lineX0,
        x1 = a.lineX1,
        y0 = a.lineY0,
        y1 = a.lineY1;
    // 將d3.area中的(x0, y0)和(x1, y1)轉化成(startAngle, innerRadius)和(endAngle, outerRadius)
    a.angle = a.x, delete a.x;
    a.startAngle = a.x0, delete a.x0;
    a.endAngle = a.x1, delete a.x1;
    a.radius = a.y, delete a.y;
    a.innerRadius = a.y0, delete a.y0;
    a.outerRadius = a.y1, delete a.y1;
    a.lineStartAngle = function() { return radialLine(x0()); }, delete a.lineX0;
    a.lineEndAngle = function() { return radialLine(x1()); }, delete a.lineX1;
    a.lineInnerRadius = function() { return radialLine(y0()); }, delete a.lineY0;
    a.lineOuterRadius = function() { return radialLine(y1()); }, delete a.lineY1;
    // 對自定義的curve函式進行包裝,防止計算時方法不能使用
    a.curve = function(_) {
      return arguments.length ? c(curveRadial(_)) : c()._curve;
    };
    return a;
  }
複製程式碼

Curve

curve的功能就是將離散的點進行連線,形成一個連續的圖形,它並不是直接使用,而是傳入上述如d3.lined3.area等函式的curve函式中來控制這些離散的點的連線方式。

d3.curveBasis

通過特定控制點的貝塞爾曲線將離散的點進行連線。

function point(that, x, y) {
    that._context.bezierCurveTo(
      (2 * that._x0 + that._x1) / 3,
      (2 * that._y0 + that._y1) / 3,
      (that._x0 + 2 * that._x1) / 3,
      (that._y0 + 2 * that._y1) / 3,
      (that._x0 + 4 * that._x1 + x) / 6,
      (that._y0 + 4 * that._y1 + y) / 6
    );
}
function Basis(context) {
    this._context = context;
}

Basis.prototype = {
    areaStart: function() {
      this._line = 0;
    },
    areaEnd: function() {
      this._line = NaN;
    },
    lineStart: function() {
      this._x0 = this._x1 =
      this._y0 = this._y1 = NaN;
      this._point = 0;
    },
    lineEnd: function() {
      switch (this._point) {
        case 3: point(this, this._x1, this._y1); // proceed
        case 2: this._context.lineTo(this._x1, this._y1); break;
      }
      if (this._line || (this._line !== 0 && this._point === 1)) this._context.closePath();
      this._line = 1 - this._line;
    },
    //首先移動至起點即第一個點,記錄下第一個點和第二個點座標,連線當前點(第一個點)和((5 * x0 + x1) / 6, (5 * y0 + y1) / 6),並繪製改點到((x0 + 4 * x1 + x) / 6, (y0 + 4 * y1 + y) / 6)點的三次貝塞爾曲線
    point: function(x, y) {
      x = +x, y = +y;
      switch (this._point) {
        case 0: this._point = 1; this._line ? this._context.lineTo(x, y) : this._context.moveTo(x, y); break;
        case 1: this._point = 2; break;
        case 2: this._point = 3; this._context.lineTo((5 * this._x0 + this._x1) / 6, (5 * this._y0 + this._y1) / 6); // proceed
        default: point(this, x, y); break;
      }
      this._x0 = this._x1, this._x1 = x;
      this._y0 = this._y1, this._y1 = y;
    }
};
// d3.curveBasis
function basis(context) {
    return new Basis(context);
}
複製程式碼

d3.curveBasisClosed

通過特定控制點的貝塞爾曲線連線離散的點,並形成一個閉合圖形。

function BasisClosed(context) {
    this._context = context;
}

BasisClosed.prototype = {
    areaStart: noop,
    areaEnd: noop,
    //(x1, y1)和(x2, y2)用於記錄第一個和第二個點的座標
    lineStart: function() {
      this._x0 = this._x1 = this._x2 = this._x3 = this._x4 =
      this._y0 = this._y1 = this._y2 = this._y3 = this._y4 = NaN;
      this._point = 0;
    },
    lineEnd: function() {
      switch (this._point) {
        //如果只有一個點,則移動到第一個點即該點處並關閉圖形
        case 1: {
          this._context.moveTo(this._x2, this._y2);
          this._context.closePath();
          break;
        }
        //如果有兩個點,則按如下方式處理
        case 2: {
          this._context.moveTo((this._x2 + 2 * this._x3) / 3, (this._y2 + 2 * this._y3) / 3);
          this._context.lineTo((this._x3 + 2 * this._x2) / 3, (this._y3 + 2 * this._y2) / 3);
          this._context.closePath();
          break;
        }
        //從最後一個點向前繪製,x2,x3,x4分別記錄的是前三個點座標
        case 3: {
          this.point(this._x2, this._y2);
          this.point(this._x3, this._y3);
          this.point(this._x4, this._y4);
          break;
        }
      }
    },
    point: function(x, y) {
      x = +x, y = +y;
      switch (this._point) {
        //記錄最初的三個點的座標
        case 0: this._point = 1; this._x2 = x, this._y2 = y; break;
        case 1: this._point = 2; this._x3 = x, this._y3 = y; break;
        //到最後會繪製一個閉合圖形,與初始點連線
        case 2: this._point = 3; this._x4 = x, this._y4 = y; this._context.moveTo((this._x0 + 4 * this._x1 + x) / 6, (this._y0 + 4 * this._y1 + y) / 6); break;
        default: point(this, x, y); break;
      }
      this._x0 = this._x1, this._x1 = x;
      this._y0 = this._y1, this._y1 = y;
    }
  };
//d3.curveBasisClosed
function basisClosed(context) {
    return new BasisClosed(context);
}
複製程式碼

d3.curveBasisOpen

function BasisOpen(context) {
    this._context = context;
}

BasisOpen.prototype = {
    areaStart: function() {
      this._line = 0;
    },
    areaEnd: function() {
      this._line = NaN;
    },
    lineStart: function() {
      this._x0 = this._x1 =
      this._y0 = this._y1 = NaN;
      this._point = 0;
    },
    lineEnd: function() {
      if (this._line || (this._line !== 0 && this._point === 3)) this._context.closePath();
      this._line = 1 - this._line;
    },
    //記錄第一個和第二個點的座標,從第三個點處開始操作,移動至((x0 + 4 * x1 + x) / 6, (y0 + y1 * 4 + y) / 6) 點處
    point: function(x, y) {
      x = +x, y = +y;
      switch (this._point) {
        case 0: this._point = 1; break;
        case 1: this._point = 2; break;
        case 2: this._point = 3; var x0 = (this._x0 + 4 * this._x1 + x) / 6, y0 = (this._y0 + 4 * this._y1 + y) / 6; this._line ? this._context.lineTo(x0, y0) : this._context.moveTo(x0, y0); break;
        case 3: this._point = 4; // proceed
        default: point(this, x, y); break;
      }
      this._x0 = this._x1, this._x1 = x;
      this._y0 = this._y1, this._y1 = y;
    }
};
//d3.curveBasisOpen
function basisOpen(context) {
    return new BasisOpen(context);
}
複製程式碼

d3.curveBundle

根據制定的控制點連線離散的點,用於分層級的關係圖中,與d3.line一起使用,而不能與d3.area使用。

Bundle.prototype = {
    lineStart: function() {
      this._x = [];
      this._y = [];
      this._basis.lineStart();
    },
    //結束時開始處理資料並繪製圖形
    lineEnd: function() {
      var x = this._x,
          y = this._y,
          j = x.length - 1;

      if (j > 0) {
        var x0 = x[0],
            y0 = y[0],
            //計算起始點到結束點之間x和y的差值
            dx = x[j] - x0,
            dy = y[j] - y0,
            i = -1,
            t;

        while (++i <= j) {
          t = i / j;
          //根據比例確定繪製點的位置,繪製範圍在(x0, y0)和(x[j], y[j])之間
          //當x接近0時,結果近似為一條從起始點到結束點的直線;當x接近1時,結果接近d3.curveBasis
          this._basis.point(
            this._beta * x[i] + (1 - this._beta) * (x0 + t * dx),
            this._beta * y[i] + (1 - this._beta) * (y0 + t * dy)
          );
        }
      }

      this._x = this._y = null;
      this._basis.lineEnd();
    },
    //該方法不會繪製圖形,只是將資料存入陣列在結束繪製時開始處理資料
    point: function(x, y) {
      this._x.push(+x);
      this._y.push(+y);
    }
  };
  //d3.curveBundle
  var bundle = (function custom(beta) {

    function bundle(context) {
      return beta === 1 ? new Basis(context) : new Bundle(context, beta);
    }

    bundle.beta = function(beta) {
      return custom(+beta);
    };

    return bundle;
})(0.85);
複製程式碼

Custom Curves

自定義curve函式,需要自定義幾個指定的方法。

  • curve.areaStart() 表示一個新的區域的開始,每個區域包含兩條線段,topline是資料的順序繪製,baseline則反向繪製。
  • curve.areaEnd() 表示當前區域的結束。
  • curve.lineStart() 表示一條新的線段的開始,接下來會繪製多個點。
  • curve.lineEnd() 表示當前線段的結束。
  • curve.point(x, y) 在當前線段上根據給定的(x, y)座標繪製一個新的點。

Symbols

//d3.symbol,預設繪製面積為64的圓形
function symbol() {
    var type = constant$1(circle),
        size = constant$1(64),
        context = null;

    function symbol() {
      var buffer;
      if (!context) context = buffer = path();
      type.apply(this, arguments).draw(context, +size.apply(this, arguments));
      if (buffer) return context = null, buffer + "" || null;
    }
    //設定圖形型別
    symbol.type = function(_) {
      return arguments.length ? (type = typeof _ === "function" ? _ : constant$1(_), symbol) : type;
    };
    //設定圖形的面積
    symbol.size = function(_) {
      return arguments.length ? (size = typeof _ === "function" ? _ : constant$1(+_), symbol) : size;
    };
    //設定繪製上下文
    symbol.context = function(_) {
      return arguments.length ? (context = _ == null ? null : _, symbol) : context;
    };

    return symbol;
}
複製程式碼

以內建的circle型別為例

var circle = {
    //size是該圓形的面積
    draw: function(context, size) {
      var r = Math.sqrt(size / pi$2);
      context.moveTo(r, 0);
      context.arc(0, 0, r, 0, tau$2);
    }
};
複製程式碼

若自定義type,則應該實現draw方法。

相關文章