问题描述
我正在使用LightningChartJS 1.3.1版,并具有包含多个系列的图表。
据我所知,可以使用以下方法将光标移到最接近的序列:
chart.setAutoCursorMode(AutoCursorModes.snapToClosest)
因此,我使用 mousemove 事件监听器捕获事件并将光标置于所有行上。 不幸的是,如果直线靠近或彼此交叉并且光标在直线上不精确,则标签会重叠,因为我必须通过在源数据中搜索最近的索引来找到y值。
如果有人可以帮助我回答以下问题,我将不胜感激:
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,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,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',data2[foundSeries_2].y,'Marker 2 ' + (cursorPoint.location.y).toFixed(3)
);
}
if (foundSeries_3 > -1) {
if (!marker3) { marker3 = chart.addChartMarkerXY(); }
setChartMarkerPosition(
marker3,'#FFFFFF',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>
解决方法
还没有内置的多系列游标。
实现您要执行的操作的最简单方法是分别创建一个图表标记和该标记的标签。这样,您可以完全控制标签的位置。
可以通过检查标签是否会与其他标签碰撞以及是否会碰撞来防止重叠,然后将标签移动足够的距离以免碰撞。
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,{
x: chart.getDefaultAxisX().scale,y: chart.getDefaultAxisY().scale
},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,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>