klinecharts / KLineChart

📈Lightweight k-line chart that can be highly customized. Zero dependencies. Support mobile.(可高度自定义的轻量级k线图,无第三方依赖,支持移动端)
https://klinecharts.com/
Apache License 2.0
2.39k stars 602 forks source link

[Enhancement] Measure tool #86

Closed aduryagin closed 3 years ago

aduryagin commented 3 years ago

Please, integrate measure tool, it may be very useful.

image image

liihuu commented 3 years ago

You can implement one with custom graphic mark.

aduryagin commented 3 years ago

Yes, i can. It would be cool if this tool is integrated into the library like priceLine or priceChannelLine

aduryagin commented 3 years ago

If someone needs this tool too. (May have bugs)

import { checkPointOnSegment } from 'klinecharts/lib/mark/graphicHelper';

export const MEASURE_GRAPHIC_MARK = {
  name: 'measure',
  totalStep: 3,
  checkMousePointOn: (key, type, points, mousePoint) => {
    return checkPointOnSegment(points[0], points[1], mousePoint);
  },
  createGraphicDataSource: (step, tpPoint, xyPoints) => {
    if (xyPoints.length === 2) {
      const startPrice = tpPoint[0].price;
      const endPrice = tpPoint[1].price;
      const priceDiff = endPrice - startPrice;
      const percent = priceDiff / (startPrice / 100);
      const numberFormatter = new Intl.NumberFormat('en-US', {
        maximumSignificantDigits: 2,
      });

      return [
        {
          type: 'arrows',
          dataSource: [
            [
              {
                x: xyPoints[0].x + (xyPoints[1].x - xyPoints[0].x) / 2,
                y: xyPoints[0].y,
              },
              {
                x: xyPoints[0].x + (xyPoints[1].x - xyPoints[0].x) / 2,
                y: xyPoints[1].y,
              },
            ],
            [
              {
                x: xyPoints[0].x,
                y: xyPoints[0].y + (xyPoints[1].y - xyPoints[0].y) / 2,
              },
              {
                x: xyPoints[1].x,
                y: xyPoints[0].y + (xyPoints[1].y - xyPoints[0].y) / 2,
              },
            ],
          ],
        },
        {
          type: 'area',
          dataSource: [
            [
              { ...xyPoints[0] },
              { x: xyPoints[1].x, y: xyPoints[0].y },
              { ...xyPoints[1] },
              { x: xyPoints[0].x, y: xyPoints[1].y },
            ],
          ],
        },
        {
          type: 'measure',
          dataSource: [
            {
              x: xyPoints[0].x + (xyPoints[1].x - xyPoints[0].x) / 2,
              y: xyPoints[1].y,
              text: `${numberFormatter.format(
                priceDiff,
              )} (${numberFormatter.format(percent)}%)`,
            },
          ],
        },
      ];
    }
    return [];
  },
  drawExtend: (ctx, graphicDataSources) => {
    const arrowsDataSource = graphicDataSources?.[0]?.dataSource;
    const polygonDataSource = graphicDataSources?.[1]?.dataSource;
    const measureDataSource = graphicDataSources?.[2]?.dataSource;

    if (!arrowsDataSource) return;

    // fill rectangle
    const rectangleWidth =
      polygonDataSource[0][1].x - polygonDataSource[0][0].x;
    const rectangleHeight =
      polygonDataSource[0][2].y - polygonDataSource[0][0].y;
    const isShort = rectangleHeight > 0;
    ctx.fillStyle = isShort ? 'rgba(255,82,82,0.1)' : 'rgba(33,150,243,0.1)';
    ctx.fillRect(
      polygonDataSource[0][0].x,
      polygonDataSource[0][0].y,
      rectangleWidth,
      rectangleHeight,
    );

    // arrows
    ctx.beginPath();
    const minRectangleSize = 10;
    const strokeStyle = isShort ? 'rgba(255,82,82,1)' : 'rgba(33,150,243,1)';

    // vertical arrow
    ctx.strokeStyle = strokeStyle;
    ctx.moveTo(arrowsDataSource[0][0].x, arrowsDataSource[0][0].y);
    ctx.lineTo(arrowsDataSource[0][1].x, arrowsDataSource[0][1].y);

    // draw arrows if rectangle is not small
    if (Math.abs(rectangleHeight) > minRectangleSize) {
      ctx.lineTo(
        arrowsDataSource[0][1].x + (isShort ? -5 : 5),
        arrowsDataSource[0][1].y + (isShort ? -5 : 5),
      );
      ctx.moveTo(arrowsDataSource[0][1].x, arrowsDataSource[0][1].y);
      ctx.lineTo(
        arrowsDataSource[0][1].x + (isShort ? 5 : -5),
        arrowsDataSource[0][1].y + (isShort ? -5 : 5),
      );
    }
    ctx.stroke();

    // horizontal
    ctx.strokeStyle = strokeStyle;
    ctx.moveTo(arrowsDataSource[1][0].x, arrowsDataSource[1][0].y);
    ctx.lineTo(arrowsDataSource[1][1].x, arrowsDataSource[1][1].y);

    // draw arrows if rectangle is not small
    if (Math.abs(rectangleWidth) > minRectangleSize) {
      const isLeft = rectangleWidth < 0;
      ctx.lineTo(
        arrowsDataSource[1][1].x + (isLeft ? 5 : -5),
        arrowsDataSource[1][1].y + 5,
      );
      ctx.moveTo(arrowsDataSource[1][1].x, arrowsDataSource[1][1].y);
      ctx.lineTo(
        arrowsDataSource[1][1].x + (isLeft ? 5 : -5),
        arrowsDataSource[1][1].y - 5,
      );
    }
    ctx.stroke();

    // measure box
    ctx.fillStyle = isShort ? 'rgba(255,82,82,1)' : 'rgba(33,150,243,1)';
    const textSize = ctx.measureText(measureDataSource[0].text);
    const measureBoxX = measureDataSource[0].x - textSize.width / 2 - 10;
    const measureBoxY = measureDataSource[0].y + (isShort ? 10 : -40);
    const measureBoxWidth = textSize.width + 20;
    const measureBoxHeight = 30;
    ctx.fillRect(measureBoxX, measureBoxY, measureBoxWidth, measureBoxHeight);
    ctx.textAlign = 'center';
    ctx.fillStyle = '#fff';
    ctx.textBaseline = 'middle';
    ctx.font = ctx.font.replace(/\d+px/, '12px');
    ctx.fillText(
      measureDataSource[0].text,
      measureDataSource[0].x,
      measureBoxY + measureBoxHeight / 2,
    );
  },
};

https://user-images.githubusercontent.com/6934028/109313000-c4d9df00-7858-11eb-835c-0c8e366d33e4.mov

liihuu commented 3 years ago

@aduryagin Cool !