如何防止光标标签与 LightningChartJS 重叠?

How to prevent overlap cursor labels with LightningChartJS?

我正在使用 LightningChartJS v. 1.3.1 并且有一个包含多个系列的图表。

我想让光标在所有行上,同时在光标旁边显示一个标签。

据我所知,有一种方法可以将光标定位到最近的系列:

chart.setAutoCursorMode(AutoCursorModes.snapToClosest)

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

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

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

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

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

如果无法控制y轴上的标签,
可能左右交替设置位置就够了

3。怎么做到的?

请看下面的例子:

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>

还没有内置 multi-series 游标。

实现您想要做的事情的最简单方法是分别创建图表标记和标记的标签。这样您就可以完全控制标签的位置。

可以通过检查标签是否会与其他标签发生碰撞来防止重叠,如果会发生碰撞,则应将标签移动到足以避免发生碰撞的程度。

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))
}

在该片段中,我遍历每个 marker/label 并确保标签不会发生冲突。标签已移动,因此最中间的标签将始终位于它的标记旁边,但其上方或下方的标签已移动,因此永远不会有任何重叠。

请参阅下面的完整示例,了解如何实施。

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>