Example #1
0
    shapeData.forEach(d => {

      const s = new shapes[d.key]().config(shapeConfig).data(d.values);

      if (d.key === "Bar") {

        let space;
        const scale = this._discrete === "x" ? x : y;
        const vals = (this._discrete === "x" ? xDomain : yDomain).filter(d => typeof d !== "string" || d.indexOf("d3plus-buffer-") < 0);
        const range = this._discrete === "x" ? xRange : yRange;
        if (vals.length > 1) space = scale(vals[1]) - scale(vals[0]);
        else space = range[range.length - 1] - range[0];
        space -= this._groupPadding;

        let barSize = space;

        const groups = nest()
          .key(d => d[this._discrete])
          .key(d => d.group)
          .entries(d.values);

        const ids = merge(groups.map(d => d.values.map(v => v.key)));
        const uniqueIds = Array.from(new Set(ids));

        if (max(groups.map(d => d.values.length)) === 1) {
          s[this._discrete]((d, i) => shapeConfig[this._discrete](d, i));
        }
        else {

          barSize = (barSize - this._barPadding * uniqueIds.length - 1) / uniqueIds.length;

          const offset = space / 2 - barSize / 2;

          const xMod = scales.scaleLinear()
            .domain([0, uniqueIds.length - 1])
            .range([-offset, offset]);

          s[this._discrete]((d, i) => shapeConfig[this._discrete](d, i) + xMod(uniqueIds.indexOf(d.group)));

        }

        s.width(barSize);
        s.height(barSize);

      }
      else if (d.key === "Line" && this._confidence) {

        const areaConfig = Object.assign({}, shapeConfig);
        const key = this._discrete === "x" ? "y" : "x";
        const scaleFunction = this._discrete === "x" ? y : x;
        areaConfig[`${key}0`] = d => scaleFunction(this._confidence[0] ? d.lci : d[key]);
        areaConfig[`${key}1`] = d => scaleFunction(this._confidence[1] ? d.hci : d[key]);

        const area = new shapes.Area().config(areaConfig).data(d.values);
        const confidenceConfig = Object.assign(this._shapeConfig, this._confidenceConfig);
        area.config(configPrep.bind(this)(confidenceConfig, "shape", "Area")).render();
        this._shapes.push(area);
      }

      const classEvents = events.filter(e => e.includes(`.${d.key}`)),
            globalEvents = events.filter(e => !e.includes(".")),
            shapeEvents = events.filter(e => e.includes(".shape"));
      for (let e = 0; e < globalEvents.length; e++) s.on(globalEvents[e], d => this._on[globalEvents[e]](d.data, d.i));
      for (let e = 0; e < shapeEvents.length; e++) s.on(shapeEvents[e], d => this._on[shapeEvents[e]](d.data, d.i));
      for (let e = 0; e < classEvents.length; e++) s.on(classEvents[e], d => this._on[classEvents[e]](d.data, d.i));

      s.config(configPrep.bind(this)(this._shapeConfig, "shape", d.key)).render();
      this._shapes.push(s);

    });
Example #2
0
  /**
      @memberof Legend
      @desc Renders the current Legend to the page. If a *callback* is specified, it will be called once the legend is done drawing.
      @param {Function} [*callback* = undefined]
      @chainable
  */
  render(callback) {

    if (this._select === void 0) this.select(select("body").append("svg").attr("width", `${this._width}px`).attr("height", `${this._height}px`).node());

    // Shape <g> Group
    this._group = elem("g.d3plus-Legend", {parent: this._select});

    let availableHeight = this._height;
    this._titleHeight = 0;
    this._titleWidth = 0;
    if (this._title) {

      const f = this._titleConfig.fontFamily || this._titleClass.fontFamily()(),
            s = this._titleConfig.fontSize || this._titleClass.fontSize()();
      let lH = lH = this._titleConfig.lineHeight || this._titleClass.lineHeight();
      lH = lH ? lH() : s * 1.4;

      const res = textWrap()
        .fontFamily(f)
        .fontSize(s)
        .lineHeight(lH)
        .width(this._width)
        .height(this._height)
        (this._title);
      this._titleHeight = lH + res.lines.length + this._padding;
      this._titleWidth = max(res.widths);
      availableHeight -= this._titleHeight;
    }

    // Calculate Text Sizes
    this._lineData = this._data.map((d, i) => {

      const label = this._label(d, i);

      let res = {
        data: d,
        i,
        id: this._id(d, i),
        shapeWidth: this._fetchConfig("width", d, i),
        shapeHeight: this._fetchConfig("height", d, i),
        y: 0
      };

      if (!label) {
        res.sentence = false;
        res.words = [];
        res.height = 0;
        res.width = 0;
        return res;
      }

      const f = this._fetchConfig("fontFamily", d, i),
            lh = this._fetchConfig("lineHeight", d, i),
            s = this._fetchConfig("fontSize", d, i);

      const h = availableHeight - (this._data.length + 1) * this._padding,
            w = this._width;

      res = Object.assign(res, textWrap()
        .fontFamily(f)
        .fontSize(s)
        .lineHeight(lh)
        .width(w)
        .height(h)
        (label));

      res.width = Math.ceil(max(res.lines.map(t => textWidth(t, {"font-family": f, "font-size": s})))) + s * 0.75;
      res.height = Math.ceil(res.lines.length * (lh + 1));
      res.og = {height: res.height, width: res.width};
      res.f = f;
      res.s = s;
      res.lh = lh;

      return res;

    });

    let spaceNeeded;
    const availableWidth = this._width - this._padding * 2;
    spaceNeeded = this._rowWidth(this._lineData);

    if (this._direction === "column" || spaceNeeded > availableWidth) {
      let lines = 1, newRows = [];

      const maxLines = max(this._lineData.map(d => d.words.length));
      this._wrapLines = function() {

        lines++;

        if (lines > maxLines) return;

        const wrappable = lines === 1 ? this._lineData.slice()
          : this._lineData.filter(d => d.width + d.shapeWidth + this._padding * (d.width ? 2 : 1) > availableWidth && d.words.length >= lines)
              .sort((a, b) => b.sentence.length - a.sentence.length);

        if (wrappable.length && availableHeight > wrappable[0].height * lines) {

          let truncated = false;
          for (let x = 0; x < wrappable.length; x++) {
            const label = wrappable[x];
            const h = label.og.height * lines, w = label.og.width * (1.5 * (1 / lines));
            const res = textWrap().fontFamily(label.f).fontSize(label.s).lineHeight(label.lh).width(w).height(h)(label.sentence);
            if (!res.truncated) {
              label.width = Math.ceil(max(res.lines.map(t => textWidth(t, {"font-family": label.f, "font-size": label.s})))) + label.s;
              label.height = res.lines.length * (label.lh + 1);
            }
            else {
              truncated = true;
              break;
            }
          }
          if (!truncated) this._wrapRows();
        }
        else {
          newRows = [];
          return;
        }

      };

      this._wrapRows = function() {
        newRows = [];
        let row = 1, rowWidth = 0;
        for (let i = 0; i < this._lineData.length; i++) {
          const d = this._lineData[i],
                w = d.width + this._padding * (d.width ? 2 : 1) + d.shapeWidth;
          if (sum(newRows.map(row => max(row, d => max([d.height, d.shapeHeight])))) > availableHeight) {
            newRows = [];
            break;
          }
          if (w > availableWidth) {
            newRows = [];
            this._wrapLines();
            break;
          }
          else if (rowWidth + w < availableWidth) {
            rowWidth += w;
          }
          else if (this._direction !== "column") {
            rowWidth = w;
            row++;
          }
          if (!newRows[row - 1]) newRows[row - 1] = [];
          newRows[row - 1].push(d);
          if (this._direction === "column") {
            rowWidth = 0;
            row++;
          }
        }
      };

      this._wrapRows();

      if (!newRows.length || sum(newRows, this._rowHeight.bind(this)) + this._padding > availableHeight) {
        spaceNeeded = sum(this._lineData.map(d => d.shapeWidth + this._padding)) - this._padding;
        for (let i = 0; i < this._lineData.length; i++) {
          this._lineData[i].width = 0;
          this._lineData[i].height = 0;
        }
        this._wrapRows();
      }

      if (newRows.length && sum(newRows, this._rowHeight.bind(this)) + this._padding < availableHeight) {
        newRows.forEach((row, i) => {
          row.forEach(d => {
            if (i) {
              d.y = sum(newRows.slice(0, i), this._rowHeight.bind(this));
            }
          });
        });
        spaceNeeded = max(newRows, this._rowWidth.bind(this));
      }
    }

    const innerHeight = max(this._lineData, (d, i) => max([d.height, this._fetchConfig("height", d.data, i)]) + d.y) + this._titleHeight,
          innerWidth = max([spaceNeeded, this._titleWidth]);

    this._outerBounds.width = innerWidth;
    this._outerBounds.height = innerHeight;

    let xOffset = this._padding,
        yOffset = this._padding;
    if (this._align === "center") xOffset = (this._width - innerWidth) / 2;
    else if (this._align === "right") xOffset = this._width - this._padding - innerWidth;
    if (this._verticalAlign === "middle") yOffset = (this._height - innerHeight) / 2;
    else if (this._verticalAlign === "bottom") yOffset = this._height - this._padding - innerHeight;
    this._outerBounds.x = xOffset;
    this._outerBounds.y = yOffset;

    this._titleClass
      .data(this._title ? [{text: this._title}] : [])
      .duration(this._duration)
      .select(this._group.node())
      .textAnchor({left: "start", center: "middle", right: "end"}[this._align])
      .width(this._width - this._padding * 2)
      .x(this._padding)
      .y(this._outerBounds.y)
      .config(this._titleConfig)
      .render();

    this._shapes = [];
    const baseConfig = configPrep.bind(this)(this._shapeConfig, "legend"),
          config = {
            id: d => d.id,
            label: d => d.label,
            lineHeight: d => d.lH
          };

    const data = this._data.map((d, i) => {

      const obj = {
        __d3plus__: true,
        data: d, i,
        id: this._id(d, i),
        label: this._lineData[i].width ? this._label(d, i) : false,
        lH: this._fetchConfig("lineHeight", d, i),
        shape: this._shape(d, i)
      };

      return obj;

    });

    // Legend Shapes
    this._shapes = [];
    ["Circle", "Rect"].forEach(Shape => {

      this._shapes.push(new shapes[Shape]()
        .data(data.filter(d => d.shape === Shape))
        .duration(this._duration)
        .labelConfig({padding: 0})
        .select(this._group.node())
        .verticalAlign("top")
        .config(assign({}, baseConfig, config))
        .render());

    });

    if (callback) setTimeout(callback, this._duration + 100);

    return this;

  }
Example #3
0
  /**
      Extends the draw behavior of the abstract Viz class.
      @private
  */
  _draw(callback) {
    super._draw(callback);
    const height = this._height - this._margin.top - this._margin.bottom,
          width = this._width - this._margin.left - this._margin.right;

    const radius = min([height, width]) / 2 - this._outerPadding,
          transform = `translate(${width / 2}, ${height / 2})`;

    const nestedAxisData = nest()
        .key(this._metric)
        .entries(this._filteredData),
          nestedGroupData = nest()
        .key(this._id)
        .key(this._metric)
        .entries(this._filteredData);

    const maxValue = max(nestedGroupData.map(h => h.values.map(d => sum(d.values, (x, i) => this._value(x, i)))).flat());

    const circularAxis = Array.from(Array(this._levels).keys()).map(d => ({
      id: d,
      r: radius * ((d + 1) / this._levels)
    }));

    new Circle()
      .data(circularAxis)
      .select(
        elem("g.d3plus-Radar-radial-circles", {
          parent: this._select,
          enter: {transform},
          update: {transform}
        }).node()
      )
      .config(configPrep.bind(this)(this._axisConfig.shapeConfig, "shape", "Circle"))
      .render();

    const totalAxis = nestedAxisData.length;
    const polarAxis = nestedAxisData
      .map((d, i) => {
        const width = this._outerPadding;
        const fontSize =
          this._shapeConfig.labelConfig.fontSize &&
            this._shapeConfig.labelConfig.fontSize(d, i) ||
          11;

        const lineHeight = fontSize * 1.4;
        const height = lineHeight * 2;
        const padding = 10,
              quadrant = parseInt(360 - 360 / totalAxis * i / 90, 10) % 4 + 1,
              radians = tau / totalAxis * i;

        let angle = 360 / totalAxis * i;

        let textAnchor = "start";
        let x = padding;

        if (quadrant === 2 || quadrant === 3) {
          x = -width - padding;
          textAnchor = "end";
          angle += 180;
        }

        const labelBounds = {
          x,
          y: -height / 2,
          width,
          height
        };

        return {
          id: d.key,
          angle,
          textAnchor,
          labelBounds,
          rotateAnchor: [-x, height / 2],
          x: radius * Math.cos(radians),
          y: radius * Math.sin(radians)
        };
      })
      .sort((a, b) => a.key - b.key);

    new Rect()
      .data(polarAxis)
      .rotate(d => d.angle)
      .width(0)
      .height(0)
      .x(d => d.x)
      .y(d => d.y)
      .label(d => d.id)
      .labelBounds(d => d.labelBounds)
      .labelConfig(this._axisConfig.shapeConfig.labelConfig)
      .select(
        elem("g.d3plus-Radar-text", {
          parent: this._select,
          enter: {transform},
          update: {transform}
        }).node()
      )
      .render();

    new Path()
      .data(polarAxis)
      .d(d => `M${0},${0} ${-d.x},${-d.y}`)
      .select(
        elem("g.d3plus-Radar-axis", {
          parent: this._select,
          enter: {transform},
          update: {transform}
        }).node()
      )
      .config(configPrep.bind(this)(this._axisConfig.shapeConfig, "shape", "Path"))
      .render();

    const groupData = nestedGroupData.map(h => {

      const q = h.values.map((d, i) => {
        const value = sum(d.values, (x, i) => this._value(x, i));
        const r = value / maxValue * radius,
              radians = tau / totalAxis * i;
        return {
          x: r * Math.cos(radians),
          y: r * Math.sin(radians)
        };
      });

      const d = `M ${q[0].x} ${q[0].y} ${q
        .map(l => `L ${l.x} ${l.y}`)
        .join(" ")} L ${q[0].x} ${q[0].y}`;

      return {
        arr: h.values.map(d => merge(d.values)),
        id: h.key,
        points: q,
        d,
        __d3plus__: true,
        data: merge(h.values.map(d => merge(d.values)))
      };

    });

    const pathConfig = configPrep.bind(this)(this._shapeConfig, "shape", "Path");
    const events = Object.keys(pathConfig.on);
    pathConfig.on = {};
    for (let e = 0; e < events.length; e++) {
      const event = events[e];
      pathConfig.on[event] = (d, i) => {
        const x = d.points.map(p => p.x + width / 2);
        const y = d.points.map(p => p.y + height / 2);
        const cursor = mouse(this._select.node());
        const xDist = x.map(p => Math.abs(p - cursor[0]));
        const yDist = y.map(p => Math.abs(p - cursor[1]));
        const dists = xDist.map((d, i) => d + yDist[i]);
        this._on[event].bind(this)(d.arr[dists.indexOf(min(dists))], i);
      };
    }

    this._shapes.push(
      new Path()
        .data(groupData)
        .d(d => d.d)
        .select(
          elem("g.d3plus-Radar-items", {
            parent: this._select,
            enter: {transform},
            update: {transform}
          }).node()
        )
        .config(pathConfig)
        .render()
    );

    return this;
  }