如何使用LightningChartJS防止光标重叠?

时间:2020-09-22 22:09:56

标签: javascript charts lightningchart

我正在使用LightningChartJS 1.3.1版,并具有包含多个系列的图表。

我希望所有行上都有光标,并同时在光标旁边显示一个标签。

enter image description here

据我所知,可以使用以下方法将光标移到最接近的序列:

chart.setAutoCursorMode(AutoCursorModes.snapToClosest)

我没有找到默认情况下为所有行启用光标的方法。

因此,我使用 mousemove 事件监听器捕获事件并将光标置于所有行上。 不幸的是,如果直线靠近或彼此交叉并且光标在直线上不精确,则标签会重叠,因为我必须通过在源数据中搜索最近的索引来找到y值。

如果有人可以帮助我回答以下问题,我将不胜感激:

1。如何防止标签重叠和控制标签位置?

2。有没有更优雅的方式来显示所有系列上的光标?

如果无法在y轴上控制标签,
也许将左右位置交替设置就足够了。

3。怎么做?

enter image description here

请参见以下示例:

const {
    AutoCursorModes, AxisTickStrategies, ChartMarkerXY, ChartXY, ColorHEX, ColorPalettes, ColorRGBA, DataPatterns, emptyFill, emptyLine, FontSettings, lightningChart, MarkerBuilders, PointShape, SolidFill, SolidLine, translatePoint, transparentFill, UIBackgrounds, UIDraggingModes, UIElement, UIElementBuilders, UILayoutBuilders, UIOrigins, UIVisibilityModes, VisibleTicks
} = lcjs

const setData = (count) => {
    const data = [];
    
    for (var i = 0; i < count; i++) {
        data.push({x: i, y: Math.floor(Math.random() * 100) + 50});
    }
    return data;
}

const getIndexTimeStamp = (arr, x) => {

        const goal = x;


        let closestValue = Infinity;
        let closestIndex = -1;

        for (let i = 0; i < arr.length; ++i) {
            const diff = Math.abs(arr[i].x - goal);
            
            if (diff < closestValue) {
                closestValue = diff;
                closestIndex = i;
            }
        }

        return closestIndex;
    }

const setChartMarkerPosition = (marker, colorHex, locationX, yValue, content) => {
        marker.setPosition({ x: locationX, y:  yValue });

        marker
            .setResultTableVisibility(UIVisibilityModes.always)
            .setResultTable((table) => table
                .setContent([[content]])
                .setTextFillStyle(new SolidFill({color: ColorHEX(colorHex)}))
                .setBackground(background => background)
             )
            .setGridStrokeXVisibility(UIVisibilityModes.whenDragged)
            .setGridStrokeYVisibility(UIVisibilityModes.whenDragged)
            .setTickMarkerXVisibility(UIVisibilityModes.whenDragged)
            .setTickMarkerYVisibility(UIVisibilityModes.whenDragged);

};

const chart = lightningChart().ChartXY({
    containerId: "chart",
    defaultAxisXTickStrategy: Object.assign({}, AxisTickStrategies.Numeric)
});

const axisY = chart.getDefaultAxisY();

axisY.setInterval(0, 200, false, true);

const series1 = chart.addLineSeries({ dataPattern: DataPatterns.horizontalProgressive}).setStrokeStyle(new SolidLine({thickness: 1.2, fillStyle: new SolidFill({color: ColorHEX('#FF0000')} )} ))
.setResultTableFormatter((tableBuilder, series, x, y) => tableBuilder
                // is empty to skip marker text
);

const series2 = chart.addLineSeries({ dataPattern: DataPatterns.horizontalProgressive}).setStrokeStyle(new SolidLine({thickness: 1.2, fillStyle: new SolidFill({color: ColorHEX('#FFFF00')})}))
.setResultTableFormatter((tableBuilder, series, x, y) => tableBuilder
                // is empty to skip marker text);
);

const series3 = chart.addLineSeries({ dataPattern: DataPatterns.horizontalProgressive}).setStrokeStyle(new SolidLine({thickness: 1.2, fillStyle: new SolidFill({color: ColorHEX('#FFFFFF')})}))
.setResultTableFormatter((tableBuilder, series, x, y) => tableBuilder
                // is empty to skip marker text
);

const data1 = setData(100);
const data2 = setData(100);
const data3 = setData(100);

series1.add( data1 );
series2.add( data2 );
series3.add( data3 );

const elem = document.getElementById('chart');
const elemLeftSpace = elem.getBoundingClientRect().left;
const elemTopSpace = elem.getBoundingClientRect().top;

let marker1;
let marker2;
let marker3;

elem.addEventListener( 'mousemove', ( event ) => {

     const cursorPoint = chart.solveNearest({x: event.clientX - elemLeftSpace, y: event.clientY - elemTopSpace});
    
    if (cursorPoint) {
        const locationOnAxes = translatePoint(
            chart.engine.clientLocation2Engine(event.clientX, event.clientY),
            chart.engine.scale,
            {
               x: chart.getDefaultAxisX().scale,
               y: chart.getDefaultAxisY().scale
            });
            
           
            const foundSeries_1 = getIndexTimeStamp(data1, Math.ceil(cursorPoint.location.x));
            const foundSeries_2 = getIndexTimeStamp(data2, Math.ceil(cursorPoint.location.x));
            const foundSeries_3 = getIndexTimeStamp(data3, Math.ceil(cursorPoint.location.x));


            if (foundSeries_1 > -1) {
                
                if (!marker1) { marker1 = chart.addChartMarkerXY(); }
           
                setChartMarkerPosition( 
                    marker1, 
                    '#FF0000', 
                    cursorPoint.location.x, 
                    data1[foundSeries_1].y, 
                    'Marker 1: ' + (cursorPoint.location.y).toFixed(1)
                );
            }
            
            if (foundSeries_2 > -1) {
                
                if (!marker2) { marker2 = chart.addChartMarkerXY(); }
           
                setChartMarkerPosition( 
                    marker2, 
                    '#FFFF00', 
                    cursorPoint.location.x, 
                    data2[foundSeries_2].y, 
                    'Marker 2 ' + (cursorPoint.location.y).toFixed(3)
                );
            }
            
            if (foundSeries_3 > -1) {
                
                if (!marker3) { marker3 = chart.addChartMarkerXY(); }
           
                setChartMarkerPosition( 
                    marker3, 
                    '#FFFFFF', 
                    cursorPoint.location.x, 
                    data3[foundSeries_3].y, 
                    'Marker 3: ' + (cursorPoint.location.y).toFixed(1)
                );
            }
            
    }
});
<div class="wrapper">
   <div id="chart" style="height: 200px;"></div>
</div>

<script src="https://unpkg.com/@arction/lcjs@1.3.1/dist/lcjs.iife.js"></script>

1 个答案:

答案 0 :(得分:1)

还没有内置的多系列游标。

实现您要执行的操作的最简单方法是分别创建一个图表标记和该标记的标签。这样,您可以完全控制标签的位置。

可以通过检查标签是否会与其他标签碰撞以及是否会碰撞来防止重叠,然后将标签移动足够的距离以免碰撞。

const positionLabels = (labels, markers) => {
    const info = []
    labels.forEach((label, i) => {
        const mPos = markers[i].getPosition()
        info[i] = {
            mPlacement: mPos,
            size: label.getSize(),
            screenPos: translatePoint(mPos, { x: chart.getDefaultAxisX().scale, y: chart.getDefaultAxisY().scale }, chart.pixelScale),
            label
        }
    })
    info.sort((a, b) => a.mPlacement.y - b.mPlacement.y)
    const midIndex = Math.floor((info.length - 1) / 2)
    // Ensure labels don't overlap
    // The middle most label is kept in place other labels are moved up or down, if needed, based on available space
    for (let i = midIndex + 1; i < info.length; i += 1) {
        const currLabel = info[i]
        const compareTarget = info[i - 1]
        if (currLabel.screenPos.y - currLabel.size.y / 2 < compareTarget.screenPos.y + compareTarget.size.y / 2) {
            currLabel.screenPos.y = compareTarget.screenPos.y + compareTarget.size.y / 2 + currLabel.size.y / 2
        }
    }
    for (let i = midIndex - 1; i >= 0; i -= 1) {
        const currLabel = info[i]
        const compareTarget = info[i + 1]
        if (currLabel.screenPos.y + compareTarget.size.y / 2 > compareTarget.screenPos.y - compareTarget.size.y / 2) {
            currLabel.screenPos.y = compareTarget.screenPos.y - (compareTarget.size.y / 2 + currLabel.size.y / 2)
        }
    }
    // apply new positions
    info.forEach(inf => inf.label.setPosition(inf.screenPos))
}

在该代码段中,我会遍历每个标记/标签,并确保标签不会碰撞。标签会被移动,以便最中间的标签将始终位于标记的旁边,但标签上方或下方的标签将被移动,从而不会出现任何重叠。

有关如何执行此操作的详细信息,请参见下面的示例。

const {
  UIVisibilityModes,
  SolidFill,
  ColorHEX,
  lightningChart,
  AxisTickStrategies,
  DataPatterns,
  SolidLine,
  translatePoint,
  UIElementBuilders,
  UIOrigins,
  UIBackgrounds,
  Themes
} = lcjs
const setData = (count) => {
  const data = [];

  for (var i = 0; i < count; i++) {
    data.push({
      x: i,
      y: Math.floor(Math.random() * 100) + 50
    });
  }
  return data;
}

const setChartMarkerPosition = (cm, locationX, yValue, content) => {
  cm.marker.restore()
  cm.label.restore()
  const pos = {
    x: locationX,
    y: yValue
  }
  cm.marker.setPosition(pos)
  cm.label.setText(content)
};

const positionLabels = (labels, markers) => {
  const info = []
  labels.forEach((label, i) => {
    const mPos = markers[i].getPosition()
    info[i] = {
      mPlacement: mPos,
      size: label.getSize(),
      screenPos: translatePoint(mPos, {
        x: chart.getDefaultAxisX().scale,
        y: chart.getDefaultAxisY().scale
      }, chart.pixelScale),
      label
    }
  })
  info.sort((a, b) => a.mPlacement.y - b.mPlacement.y)
  const midIndex = Math.floor((info.length - 1) / 2)
  // Ensure labels don't overlap
  // The middle most label is kept in place other labels are moved up or down, if needed, based on available space
  for (let i = midIndex + 1; i < info.length; i += 1) {
    const currLabel = info[i]
    const compareTarget = info[i - 1]
    if (currLabel.screenPos.y - currLabel.size.y / 2 < compareTarget.screenPos.y + compareTarget.size.y / 2) {
      currLabel.screenPos.y = compareTarget.screenPos.y + compareTarget.size.y / 2 + currLabel.size.y / 2
    }
  }
  for (let i = midIndex - 1; i >= 0; i -= 1) {
    const currLabel = info[i]
    const compareTarget = info[i + 1]
    if (currLabel.screenPos.y + compareTarget.size.y / 2 > compareTarget.screenPos.y - compareTarget.size.y / 2) {
      currLabel.screenPos.y = compareTarget.screenPos.y - (compareTarget.size.y / 2 + currLabel.size.y / 2)
    }
  }
  // apply new positions
  info.forEach(inf => inf.label.setPosition(inf.screenPos))
}

const chart = lightningChart().ChartXY({
  containerId: "chart",
  defaultAxisXTickStrategy: Object.assign({}, AxisTickStrategies.Numeric)
});

const axisY = chart.getDefaultAxisY();

axisY.setInterval(0, 200, false, true);
const emptyTableBuilder = (tableBuilder, series, x, y) => tableBuilder
const series1 = chart.addLineSeries({
    dataPattern: DataPatterns.horizontalProgressive
  })
  .setStrokeStyle(new SolidLine({
    thickness: 1.2,
    fillStyle: new SolidFill({
      color: ColorHEX('#FF0000')
    })
  }))
  .setResultTableFormatter(emptyTableBuilder
    // is empty to skip marker text
  );

const series2 = chart.addLineSeries({
    dataPattern: DataPatterns.horizontalProgressive
  })
  .setStrokeStyle(new SolidLine({
    thickness: 1.2,
    fillStyle: new SolidFill({
      color: ColorHEX('#FFFF00')
    })
  }))
  .setResultTableFormatter(emptyTableBuilder
    // is empty to skip marker text);
  );

const series3 = chart.addLineSeries({
    dataPattern: DataPatterns.horizontalProgressive
  })
  .setStrokeStyle(new SolidLine({
    thickness: 1.2,
    fillStyle: new SolidFill({
      color: ColorHEX('#FFFFFF')
    })
  }))
  .setResultTableFormatter(emptyTableBuilder
    // is empty to skip marker text
  );

const data1 = setData(100);
const data2 = setData(100);
const data3 = setData(100);

series1.add(data1);
series2.add(data2);
series3.add(data3);

const createCustomMarker = (colorHex) => {
  const marker = chart.addChartMarkerXY()
    .setResultTableVisibility(UIVisibilityModes.never)
    .setGridStrokeXVisibility(UIVisibilityModes.never)
    .setGridStrokeYVisibility(UIVisibilityModes.never)
    .setTickMarkerXVisibility(UIVisibilityModes.never)
    .setTickMarkerYVisibility(UIVisibilityModes.never)
  const fill = new SolidFill({
    color: ColorHEX(colorHex)
  })
  return {
    marker: marker
      .setPointMarker(m => m.setFillStyle(fill)),
    label: chart.addUIElement(UIElementBuilders.TextBox
        .setBackground(UIBackgrounds.Rectangle)
        .addStyler(styler => styler
          .setBackground(bg => bg
            .setStrokeStyle(Themes.dark.uiBackgroundStrokeStyle)
            .setFillStyle(Themes.dark.uiBackgroundFillStyle)
          )
        ), chart.pixelScale)
      .setOrigin(UIOrigins.LeftCenter)
      .setTextFillStyle(fill)
  }
}

const elem = document.getElementById('chart');

let marker1 = createCustomMarker('#FF0000')
let marker2 = createCustomMarker('#FFFF00')
let marker3 = createCustomMarker('#FFFFFF')
marker1.marker.dispose()
marker1.label.dispose()
marker2.marker.dispose()
marker2.label.dispose()
marker3.marker.dispose()
marker3.label.dispose()

elem.addEventListener('mousemove', (event) => {
  const mousePos = chart.engine.clientLocation2Engine(event.clientX, event.clientY)

  const p1 = series1.solveNearestFromScreen(mousePos, true)
  if (p1) {
    setChartMarkerPosition(
      marker1,
      p1.location.x,
      p1.location.y,
      'Marker 1: ' + (p1.location.y).toFixed(1)
    );
  } else {
    // hide marker if no point is resolved
    marker1.marker.dispose()
    marker1.label.dispose()
  }

  const p2 = series2.solveNearestFromScreen(mousePos, true)
  if (p2) {
    setChartMarkerPosition(
      marker2,
      p2.location.x,
      p2.location.y,
      'Marker 2 ' + (p2.location.y).toFixed(3)
    );
  } else {
    // hide marker if no point is resolved
    marker2.marker.dispose()
    marker2.label.dispose()
  }

  const p3 = series3.solveNearestFromScreen(mousePos, true)
  if (p3) {
    setChartMarkerPosition(
      marker3,
      p3.location.x,
      p3.location.y,
      'Marker 3: ' + (p3.location.y).toFixed(1)
    );
  } else {
    // hide marker if no point is resolved
    marker3.marker.dispose()
    marker3.label.dispose()
  }

  positionLabels([marker1.label, marker2.label, marker3.label], [marker1.marker, marker2.marker, marker3.marker])
});
// hide the markers when mouse is not over the area
elem.addEventListener('mouseleave', () => {
  marker1.marker.dispose()
  marker1.label.dispose()
  marker2.marker.dispose()
  marker2.label.dispose()
  marker3.marker.dispose()
  marker3.label.dispose()
})
<div class="wrapper">
  <div id="chart" style="height: 200px;"></div>
</div>

<script src="https://unpkg.com/@arction/lcjs@1.3.1/dist/lcjs.iife.js"></script>