【问题标题】:How to prevent overlap cursor labels with LightningChartJS?如何防止与 LightningChartJS 重叠光标标签?
【发布时间】:2020-10-05 16:59:17
【问题描述】:

我正在使用 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>

【问题讨论】:

    标签: javascript charts lightningchart


    【解决方案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))
    }
    

    在那个 sn-p 中,我遍历每个标记/标签并确保标签没有碰撞。标签被移动,因此最中间的标签将始终位于它的标记旁边,但它上方或下方的标签被移动,因此永远不会有任何重叠。

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

    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>

    【讨论】:

    • 感谢详细的解释和解决方法。 :o)
    猜你喜欢
    • 2013-01-15
    • 2012-11-11
    • 2017-02-01
    • 1970-01-01
    • 2015-09-21
    • 1970-01-01
    • 1970-01-01
    • 2017-06-03
    • 2017-02-04
    相关资源
    最近更新 更多