Select 线系列中的点

Select points in line series

我想用鼠标左键使用修饰键,这样 select 矩形内的数据,而不是缩放到该数据。这可能吗?我找不到合适的API。如果有办法 select 落在多边形内的数据(如套索工具)。

这是完全自定义的 ChartXY 交互的一个示例。 要点:

  • 禁用默认矩形适合和缩放交互。

  • 线系列数据缓存到变量中,可用于自定义统计。

  • RectangleSeries 用于可视化图表上的拖动区域。

  • UI元素用于显示所选区域的统计数据。

  • ChartXY.onSeriesBackgroundMouseDrag 事件用于将自定义操作挂接到用户交互。

下面是一个代码片段,其中用鼠标左键拖动会创建一个矩形区域,其中显示突出显示的 X 区域和已解决的 Y 数据范围。 释放鼠标按钮会导致求解完整的选定数据点数组(长度记录到控制台)。

const {
  Point,
  ColorRGBA,
  SolidFill,
  RadialGradientFill,
  SolidLine,
  translatePoint,
  lightningChart,
  UIOrigins,
  UIElementBuilders,
  UILayoutBuilders,
  emptyFill
} = lcjs;

const { createProgressiveTraceGenerator } = xydata;

const chart = lightningChart()
    .ChartXY()
    // Disable default chart interactions with left mouse button.
    .setMouseInteractionRectangleFit(false)
    .setMouseInteractionRectangleZoom(false)
    .setTitleFillStyle(emptyFill)

const axisX = chart.getDefaultAxisX()
const axisY = chart.getDefaultAxisY()

const lineSeries = chart.addLineSeries({
    dataPattern: {
        pattern: 'ProgressiveX',
    },
})

// Generate test data set.
let dataSet
createProgressiveTraceGenerator()
    .setNumberOfPoints(10 * 1000)
    .generate()
    .toPromise()
    .then((data) => {
        // Cache data set for analytics logic + add static data to series.
        dataSet = data
        lineSeries.add(data)
    })

// Rectangle Series is used to display data selection area.
const rectangleSeries = chart.addRectangleSeries()
const rectangle = rectangleSeries
    .add({ x1: 0, y1: 0, x2: 0, y2: 0 })
    .setFillStyle(
        new RadialGradientFill({
            stops: [
                { offset: 0, color: ColorRGBA(255, 255, 255, 30) },
                { offset: 1, color: ColorRGBA(255, 255, 255, 60) },
            ],
        }),
    )
    .setStrokeStyle(
        new SolidLine({
            thickness: 2,
            fillStyle: new SolidFill({ color: ColorRGBA(255, 255, 255, 255) }),
        }),
    )
    .dispose()

// UI elements are used to display information about the selected data points.
const uiInformationLayout = chart.addUIElement(UILayoutBuilders.Column, { x: axisX, y: axisY }).dispose()
const uiLabel0 = uiInformationLayout.addElement(UIElementBuilders.TextBox)
const uiLabel1 = uiInformationLayout.addElement(UIElementBuilders.TextBox)

// Add events for custom interactions.
chart.onSeriesBackgroundMouseDrag((_, event, button, startLocation) => {
    // If not left mouse button, don't do anything.
    if (button !== 0) return

    // Translate start location and current location to axis coordinates.
    const startLocationAxis = translatePoint(
        chart.engine.clientLocation2Engine(startLocation.x, startLocation.y),
        chart.engine.scale,
        lineSeries.scale,
    )
    const curLocationAxis = translatePoint(
        chart.engine.clientLocation2Engine(event.clientX, event.clientY),
        chart.engine.scale,
        lineSeries.scale,
    )

    // Place Rectangle figure between start location and current location.
    rectangle.restore().setDimensions({
        x1: startLocationAxis.x,
        y1: startLocationAxis.y,
        x2: curLocationAxis.x,
        y2: curLocationAxis.y,
    })

    // * Gather analytics from actively selected data *
    const xStart = Math.min(startLocationAxis.x, curLocationAxis.x)
    const xEnd = Math.max(startLocationAxis.x, curLocationAxis.x)
    // Selected Y range has to be solved from data set.
    // NOTE: For top solve performance, results should be cached and only changes from previous selection area should be checked.
    const { yMin, yMax } = solveDataRangeY(xStart, xEnd)

    // Set UI labels text.
    uiLabel0.setText(`X: [${xStart.toFixed(0)}, ${xEnd.toFixed(0)}]`)
    uiLabel1.setText(`Y: [${yMin.toFixed(1)}, ${yMax.toFixed(1)}]`)

    // Place UI layout above Rectangle.
    uiInformationLayout
        .restore()
        .setOrigin(UIOrigins.LeftBottom)
        .setPosition({ x: xStart, y: Math.max(startLocationAxis.y, curLocationAxis.y) })
})

chart.onSeriesBackgroundMouseDragStop((_, event, button, startLocation) => {
    // If not left mouse button, don't do anything.
    if (button !== 0) return

    // Translate start location and current location to axis coordinates.
    const startLocationAxis = translatePoint(
        chart.engine.clientLocation2Engine(startLocation.x, startLocation.y),
        chart.engine.scale,
        lineSeries.scale,
    )
    const curLocationAxis = translatePoint(
        chart.engine.clientLocation2Engine(event.clientX, event.clientY),
        chart.engine.scale,
        lineSeries.scale,
    )

    // Print selected data points to console.
    const xStart = Math.max(0, Math.floor(Math.min(startLocationAxis.x, curLocationAxis.x)))
    const xEnd = Math.min(dataSet.length - 1, Math.ceil(Math.max(startLocationAxis.x, curLocationAxis.x)))
    const selectedDataPoints = dataSet.slice(xStart, xEnd)
    console.log(`Selected ${selectedDataPoints.length} data points.`)

    // Hide visuals.
    rectangle.dispose()
    uiInformationLayout.dispose()
})

// Logic for solving Y data range between supplied X range from active data set.
const solveDataRangeY = (xStart, xEnd) => {
    // Reduce Y data min and max values within specified X range from data set.
    // Note, this can be very heavy for large data sets - repeative calls should be avoided as much as possible for best performance.
    let yMin = Number.MAX_SAFE_INTEGER
    let yMax = -Number.MAX_SAFE_INTEGER
    xStart = Math.max(0, Math.floor(xStart))
    xEnd = Math.min(dataSet.length - 1, Math.ceil(xEnd))
    for (let iX = xStart; iX < xEnd; iX += 1) {
        const y = dataSet[iX].y
        yMin = y < yMin ? y : yMin
        yMax = y > yMax ? y : yMax
    }
    return { yMin, yMax }
}
<script src="https://unpkg.com/@arction/xydata@1.4.0/dist/xydata.iife.js"></script>
<script src="https://unpkg.com/@arction/lcjs@3.0.0/dist/lcjs.iife.js"></script>

这种自定义交互有很多不同的方向,虽然我们不能用一个例子来涵盖每一个方向,但大部分逻辑应该保持不变。