I would like to display breaks in the x axis of the line chart and display them with a fixed width, like at the image below:
Found this nice article that describes the functionality:
https://www.arction.com/news/using-scale-breaks-data-visualization/
If I understand correctly, there is the possibility in the SDK to display these with scaleAreas or even better with ClipAreas.
But I could not find this possibility in the LightningChart JS framework.
As starting point I was able to generate breaks in data with a NaN value and added a Textbox.
So my questions are:
1. How to add breaks and fit to predefined width in xaxis
2. How to rotate the Textbox 90 degrees?
Help would be greatly appreciated.
Thanks in advance. :o)
// Extract required parts from LightningChartJS.
const {
lightningChart,
AxisTickStrategies,
DataPatterns,
emptyFill,
ColorHEX,
emptyLine,
UIElementBuilders,
UIBackgrounds,
UIOrigins,
UIDraggingModes,
SolidLine,
SolidFill
} = lcjs
// Create a XY Chart.
const dateOrigin = new Date(2020, 0, 1)
const chart = lightningChart().ChartXY({
defaultAxisXTickStrategy: AxisTickStrategies.DateTime(
dateOrigin
)
})
.setTitle('Demo')
chart.setPadding({ right: '1' })
// Add a progressive line series.
// Using the DataPatterns object to select the horizontalProgressive pattern for the line series.
const lineSeries = chart.addLineSeries({ dataPattern: DataPatterns.horizontalProgressive })
.setName('Demo')
// Generate some points using for each month
const dataFrequency = 10;
// Setup view nicely.
chart.getDefaultAxisY()
.setScrollStrategy(undefined)
.setInterval(-20, 120)
.setTitle('Demo y')
// Data for the plotting
const data = [];
for (let i = 0; i < 1500; i++) {
let index = i;
if (i === 500) {
index = NaN;
}
if (i > 500) {
index = i + 1000;
}
data.push({
x: index,
y: Math.floor(Math.random() * 100)
});
}
chart.addUIElement(
UIElementBuilders.TextBox
.setBackground(UIBackgrounds.Rectangle),
chart.uiScale
)
.setText('Break')
.setPosition({ x: 45, y: 50 })
.setOrigin(UIOrigins.Center)
.setDraggingMode(UIDraggingModes.notDraggable)
.setFont((font) => font
.setSize(40)
)
.setBackground(style => style
.setFillStyle(new SolidFill({ color: ColorHEX('#f00') }))
)
.setTextFillStyle((style) => style
.setColor(ColorHEX('#0f0'))
)
// Adding points to the series
lineSeries.add(data.map((point) => ({ x: point.x * dataFrequency, y: point.y })))
<script src="https://unpkg.com/#arction/lcjs#1.3.1/dist/lcjs.iife.js"></script>
LightningChart JS doesn't have support for scale breaks yet. It's something that will most likely be developed at some point but there is no timeline for it yet.
TextBox rotation is not yet possible for UITextBox, it's coming in a future release but not the next release.
Related
I'm working recently to draw some shapes (svg images) along a curved linestring, i managed to draw the shapes using geometry by adding the styles for each shape. And now, i need to modify this linestring feature it means i want that the shapes move also when modifying the line and later the shapes will be changed with other images (circles..).
i have created my own function modify that recreate the feature and called drawShape function but it's heavy and not working correctly.
so is there any other solution to facilitate the draw of the shapes to facilate modify function also the other tasks (edit segment: change shapes), add segment...
Thanks in advance.
Some parts of code are passed here
// that function map the poignees passes the id( number of segment ) to drawShapes function
const drawShapesAll = function(feature){
poingees.forEach((elm, id) => {
if(id === poingees.length -1) return;
drawShapes(feature, id+1, elm.src); // id+1 : represent the number of segment
});
}
//drawShapes funtion: draw shapes for one segment
const drawShapes = function(feature, coordsSeg, src){
//src : svg image that i want to add it (triangle)
const styles = feature.getStyle();
const index0 = getClosestPointToCoords(coords, poignees[numSeg-1]);
const index1 = getClosestPointToCoords(coords, poignees[numSeg]);
const coordsSeg = []; //coordsSeg: represent coordinates (geometry) for segment i (the curved line from poigne i to poigne i+1)
for(var j=index0; j<= index1; j++) {
coordsSeg.push(coords[j]);
}
const coordsTrans = coordsSeg.map((coord) => (transform(coord, "EPSG:3857", "EPSG:4326"))); // get coordinates tranformed for each segment
const num = turf.distance(coordsTrans[0], coordsTrans[coordsTrans.length -1]) / 200; // calculate the distance for segment i then devide it by 200 to get the number of shapes for that segment (200 represent the distance between each shape)
let line = {
"type": "Feature",
"properties": {
},
"geometry": {
"type": "LineString",
"coordinates": coordsTrans
}
};
while(i < number ){ // a loop function that draw shapes with number equal = num
let {point,rotation:rotation2} = getPointInCurve(line, coordsSeg, 200 * i); //
let pointGeo = new Point(point);
styles.push(new Style({
geometry: pointGeo,
image: new Icon (({
rotation: - rotation2,
offset: [1, 1],
anchor : [0.9, 0.9],
src : src,
scale: 0.3
}))
}));
feature.setStyle(styles);
i++;
}
}
// getPointInCurve : get point geometry(coordinates) and rotation along the segment
function getPointInCurve(line, coords, distance){
//that function call bezier function , get the curved coordinates ,
// i used along to get the position of each shape which has distance = 200*i
const curved = turf.bezier(line);
const along = turf.along(curved, distance);
const alongCoordTrans = convertCoordinates(along.geometry.coordinates[0], along.geometry.coordinates[1]);
const prev = coords[closestPoint(coords, alongCoordTrans).index];
const dx = alongCoordTrans[0] - coords[coords.indexOf(prev) - 5][0] ; // calculate dx and dy
const dy = alongCoordTrans[1] - coords[coords.indexOf(prev) - 5][1];
const rotation2 = Math.atan2(dy, dx); // calculate rotation of that shape
const min = getClosestPointToCoords(coords, alongCoordTrans);
const closetPoint = coords[min];
return {point:closetPoint, rotation:rotation2}; // return the point coordinates and rotation of shape i,
}
I have an axis, as defined by 2 vectors, for example one that points upwards at x = 10:
const axisStart = new Vector3(10, 0, 0)
const axisEnd = new Vector3(10, 0, 1)
I'm getting the normalized axis direction like so:
const axisDirection = new Vector3().subVectors(axisEnd, axisStart).normalize()
How can I rotate a vector (e.g. Vector3(50, 0, 0)) around my original axis?
I've tried using Vector3.applyAxisAngle(axisDirection , radians), but because the axis has been normalized, the rotation happens around the world center (0, 0) and not around the axis' original position.
I've solved this by finding the exact point on the axis around which the point rotates, using this answer and translating the pseudocode from it into typescript:
getPivotPoint(pointToRotate: Vector3, axisStart: Vector3, axisEnd: Vector3) {
const d = new Vector3().subVectors(axisEnd, axisStart).normalize()
const v = new Vector3().subVectors(pointToRotate, axisStart)
const t = v.dot(d)
const pivotPoint = axisStart.add(d.multiplyScalar(t))
return pivotPoint
}
Then, as #Ouroborus pointed out, I can then translate the point, apply the rotation, and translate it back:
rotatePointAroundAxis(pointToRotate: Vector3, axisStart: Vector3, axisEnd, radians: number) {
const axisDirection = new Vector3().subVectors(axisEnd, axisStart).normalize()
const pivotPoint = getPivotPoint(pointToRotate, axisStart, axisEnd)
const translationToWorldCenter = new Vector3().subVectors(pointToRotate, pivotPoint)
const translatedRotated = translationToWorldCenter.clone().applyAxisAngle(axisDirection, radians)
const destination = pointToRotate.clone().add(translatedRotated).sub(translationToWorldCenter)
return destination
}
The above code is working nicely, leaving it here for my future self and for other who might find this useful.
I have a Functiongraph line defined like this:
const f1 = function(x) {
const slope = me.options.gLine1Slope;
return (x - 2.5) * slope + 2.5;
};
this.l1 = this.board.create('functiongraph', [f1, -30, 30], {
recursionDepthLow: 8,
recursionDepthHigh: 15
});
this.l1.setPosition(window.JXG.COORDS_BY_USER, [
forceFloat(this.options.gLine1OffsetX),
forceFloat(this.options.gLine1OffsetY)
]);
I'm aware that Functiongraph is just a wrapper for Curve, so I've been reading through both API docs.
I'm manually positioning this line based on these offset values because it can be dragged around the plane by the user.
I can get a value close to the Y-intercept, like this:
f1(0) + (this.options.gLine1OffsetY - this.options.gLine1OffsetX)
But it's not quite right, after this line is dragged around a bit. Can anyone give some guidance on how to get the Y-intercept for this curve? I suppose I can just iterate through the data array of points along this curve, and pick the one where Y is closest to 0. I was just hoping there is a more straightforward way as well, though.
You are right. Getting the y-intercept of the function graph after dragging it around freely is not easy. The reason is that dragging objects is mostly implemented using projective transformations. This makes it complicated to get the y-intercept of the curve which is visible at the moment. The easiest approach I can think of for the moment is to intersect the curve with the vertical axis and get the position of that point. Here is a slighlty modified version of your example:
const board = JXG.JSXGraph.initBoard('jxgbox', {
boundingbox: [-5, 5, 5, -5], axis:true
});
var me = {
gLine1Slope:2,
gLine1OffsetX: 1,
gLine1OffsetY: -1
};
const f1 = function(x) {
const slope = me.gLine1Slope;
return x * slope;
};
var l1 = board.create('functiongraph', [f1, -30, 30], {fixed: false});
var p = board.create('intersection', [l1, board.defaultAxes.y], {
withLabel: false, visible: false});
// Now, we can move the curve and get the y-intercept
// in p.Y()
l1.setPosition(window.JXG.COORDS_BY_USER, [
me.gLine1OffsetX,
me.gLine1OffsetY
]);
board.update();
board.on('up', function(evt) {
document.getElementById('jxg_debug').value = p.Y();
});
document.getElementById('jxg_debug').value = p.Y();
See it live at https://jsfiddle.net/3s90qx57/2/
I'm trying to implement semantic zooming while using Mike Bostock's Towards Reusable Charts pattern (where a chart is represented as a function). In my zoom handler, I'd like to use transform.rescaleX to update my scale and then simply call the function again.
It almost works but the rescaling seems to accumulate zoom transforms getting faster and faster. Here's my fiddle:
function chart() {
let aspectRatio = 10.33;
let margin = { top: 0, right: 0, bottom: 5, left: 0 };
let current = new Date();
let scaleBand = d3.scaleBand().padding(.2);
let scaleTime = d3.scaleTime().domain([d3.timeDay(current), d3.timeDay.ceil(current)]);
let axis = d3.axisBottom(scaleTime);
let daysThisMonth = d3.timeDay.count(d3.timeMonth(current), d3.timeMonth.ceil(current));
let clipTypes = [ClipType.Scheduled, ClipType.Alarm, ClipType.Motion];
let zoom = d3.zoom().scaleExtent([1 / daysThisMonth, 1440]);
let result = function(selection) {
selection.each(function(data) {
let selection = d3.select(this);
let outerWidth = this.getBoundingClientRect().width;
let outerHeight = outerWidth / aspectRatio;
let width = outerWidth - margin.left - margin.right;
let height = outerHeight - margin.top - margin.bottom;
scaleBand.domain(d3.range(data.length)).range([0, height * .8]);
scaleTime.range([0, width]);
zoom.on('zoom', _ => {
scaleTime = d3.event.transform.rescaleX(scaleTime);
selection.call(result);
});
let svg = selection.selectAll('svg').data([data]);
let svgEnter = svg.enter().append('svg').attr('viewBox', '0 0 ' + outerWidth + ' ' + outerHeight);//.attr('preserveAspectRatio', 'xMidYMin slice');
svg = svg.merge(svgEnter);
let defsEnter = svgEnter.append('defs');
let defs = svg.select('defs');
let gMainEnter = svgEnter.append('g').attr('id', 'main');
let gMain = svg.select('g#main').attr('transform', 'translate(' + margin.left + ' ' + margin.top + ')');
let gAxisEnter = gMainEnter.append('g').attr('id', 'axis');
let gAxis = gMain.select('g#axis').call(axis.scale(scaleTime));
let gCameraContainerEnter = gMainEnter.append('g').attr('id', 'camera-container');
let gCameraContainer = gMain.select('g#camera-container').attr('transform', 'translate(' + 0 + ' ' + height * .2 + ')').call(zoom);
let gCameraRowsEnter = gCameraContainerEnter.append('g').attr('id', 'camera-rows');
let gCameraRows = gCameraContainer.select('g#camera-rows');
let gCameras = gCameraRows.selectAll('g.camera').data(d => {
return d;
});
let gCamerasEnter = gCameras.enter().append('g').attr('class', 'camera');
gCameras = gCameras.merge(gCamerasEnter);
gCameras.exit().remove();
let rectClips = gCameras.selectAll('rect.clip').data(d => {
return d.clips.filter(clip => {
return clipTypes.indexOf(clip.type) !== -1;
});
});
let rectClipsEnter = rectClips.enter().append('rect').attr('class', 'clip').attr('height', _ => {
return scaleBand.bandwidth();
}).attr('y', (d, i, g) => {
return scaleBand(Array.prototype.indexOf.call(g[i].parentNode.parentNode.childNodes, g[i].parentNode)); //TODO: sloppy
}).style('fill', d => {
switch(d.type) {
case ClipType.Scheduled:
return '#0F0';
case ClipType.Alarm:
return '#FF0';
case ClipType.Motion:
return '#F00';
};
});
rectClips = rectClips.merge(rectClipsEnter).attr('width', d => {
return scaleTime(d.endTime) - scaleTime(d.startTime);
}).attr('x', d => {
return scaleTime(d.startTime);
});
rectClips.exit().remove();
let rectBehaviorEnter = gCameraContainerEnter.append('rect').attr('id', 'behavior').style('fill', '#000').style('opacity', 0);
let rectBehavior = gCameraContainer.select('rect#behavior').attr('width', width).attr('height', height * .8);//.call(zoom);
});
};
return result;
}
// data model
let ClipType = {
Scheduled: 0,
Alarm: 1,
Motion: 2
};
let data = [{
id: 1,
src: "assets/1.jpg",
name: "Camera 1",
server: 1
}, {
id: 2,
src: "assets/2.jpg",
name: "Camera 2",
server: 1
}, {
id: 3,
src: "assets/1.jpg",
name: "Camera 3",
server: 2
}, {
id: 4,
src: "assets/1.jpg",
name: "Camera 4",
server: 2
}].map((_ => {
let current = new Date();
let randomClips = d3.randomUniform(24);
let randomTimeSkew = d3.randomUniform(-30, 30);
let randomType = d3.randomUniform(3);
return camera => {
camera.clips = d3.timeHour.every(Math.ceil(24 / randomClips())).range(d3.timeDay.offset(current, -30), d3.timeDay(d3.timeDay.offset(current, 1))).map((d, indexEndTime, g) => {
return {
startTime: indexEndTime === 0 ? d : d3.timeMinute.offset(d, randomTimeSkew()),
endTime: indexEndTime === g.length - 1 ? d3.timeDay(d3.timeDay.offset(current, 1)) : null,
type: Math.floor(randomType())
};
}).map((d, indexStartTime, g) => {
if(d.endTime === null)
d.endTime = g[indexStartTime + 1].startTime;
return d;
});
return camera;
};
})());
let myChart = chart();
let selection = d3.select('div#container');
selection.datum(data).call(myChart);
<div id="container"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.13.0/d3.min.js"></script>
Edit: The zoom handler below works fine, but I'd like a more general solution:
let newScaleTime = d3.event.transform.rescaleX(scaleTime);
d3.select('g#axis').call(axis.scale(newScaleTime));
d3.selectAll('rect.clip').attr('width', d => {
return newScaleTime(d.endTime) - newScaleTime(d.startTime);
}).attr('x', d => {
return newScaleTime(d.startTime);
});
The short answer is you need to implement a reference scale to indicate what the scale's base state is when unmanipulated by the zoom. Otherwise you will run into the problem you describe: "It almost works but the rescaling seems to accumulate zoom transforms getting faster and faster. "
To see why a reference scale is needed, zoom in on the graph and out (once each) without moving the mouse. When you zoom in, the axis changes. When you zoom out the axis does not. Note the scale factor on the intial zoom in and the first time you zoom out: 1.6471820345351462 on the zoom in, 1 on the zoom out. The number represents how much the to magnify/minify whatever it is we are zooming in on. On the initial zoom in we magnify by a factor of ~1.65. On the preceding zoom out we minify by a factor of 1, ie: not at all. If on the other hand you zoom out first, you minify by a factor of about 0.6 and then if you were to zoom in you magnify by a factor of 1. I've built a stripped down of your example to show this:
function chart() {
let zoom = d3.zoom().scaleExtent([0.25,20]);
let scale = d3.scaleLinear().domain([0,1000]).range([0,550]);
let axis = d3.axisBottom;
let result = function(selection) {
selection.each(function() {
let selection = d3.select(this);
selection.call(axis(scale));
selection.call(zoom);
zoom.on('zoom', function() {
scale = d3.event.transform.rescaleX(scale);
console.log(d3.event.transform.k);
selection.call(result);
});
})
}
return result;
}
d3.select("svg").call(chart());
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.10.0/d3.min.js"></script>
<svg width="550" height="200"></svg>
The scale should be relative to the initial zoom factor, usually 1. In otherwords, the zoom is cumulative, it records magnification/minification as a factor of the initial scale, not the last step (otherwise transform k values would only be one of three values: one value for zooming out, another for zooming in and one for remaining the same and all relative to the current scale). This is why rescaling the initial scale doesn't work - you lose the reference point to the initial scale that the zoom is referencing.
From the docs, if you redefine a scale with d3.event.transform.rescaleX, we get a scale that reflects the zoom's (cumulative) transformation:
[the rescaleX] method does not modify the input scale x; x thus
represents the untransformed scale, while the returned scale
represents its transformed view. (docs)
Building on this, if we zoom in twice in a row, the first time we zoom in we see the transform.k value is ~1.6x on the first time, the second time it is ~2.7x. But, since we rescale the scale, we apply a zoom of 2.7x on a scale that has already been zoomed in 1.6x, giving us a scale factor of ~4.5x rather than 2.7x. To make matters worse, if we zoom in twice and then out once, the zoom (out) event gives us a scale value that is still greater than 1 (~1.6 on first zoom in, ~2.7 on second, ~1.6 on zoom out), hence we are still zooming in despite scrolling out:
function chart() {
let zoom = d3.zoom().scaleExtent([0.25,20]);
let scale = d3.scaleLinear().domain([0,1000]).range([0,550]);
let axis = d3.axisBottom;
let result = function(selection) {
selection.each(function() {
let selection = d3.select(this);
selection.call(axis(scale));
selection.call(zoom);
zoom.on('zoom', function() {
scale = d3.event.transform.rescaleX(scale);
var magnification = 1000/(scale.domain()[1] - scale.domain()[0]);
console.log("Actual magnification: "+magnification+"x");
console.log("Intended magnification: "+d3.event.transform.k+"x")
console.log("---");
selection.call(result);
});
})
}
return result;
}
d3.select("svg").call(chart());
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.10.0/d3.min.js"></script>
<svg width="550" height="200"></svg>
I haven't discussed the x offset portion of the zoom, but you can imagine that a similar problem occurs - the zoom is cumulative but you lose the initial reference point that those cumulative changes are in reference to.
The idiomatic solution is to use a reference scale and the zoom to create a working scale used for plotting rectangles/axes/etc. The working scale is initially the same as the reference scale (generally) and is set as so: workingScale = d3.event.transform.rescaleX(referenceScale) on each zoom.
function chart() {
let zoom = d3.zoom().scaleExtent([0.25,20]);
let workingScale = d3.scaleLinear().domain([0,1000]).range([0,550]);
let referenceScale = d3.scaleLinear().domain([0,1000]).range([0,550]);
let axis = d3.axisBottom;
let result = function(selection) {
selection.each(function() {
let selection = d3.select(this);
selection.call(axis(workingScale));
selection.call(zoom);
zoom.on('zoom', function() {
workingScale = d3.event.transform.rescaleX(referenceScale);
var magnification = 1000/(workingScale.domain()[1] - workingScale.domain()[0]);
console.log("Actual magnification: "+magnification+"x");
console.log("Intended magnification: "+d3.event.transform.k+"x")
console.log("---");
selection.call(result);
});
})
}
return result;
}
d3.select("svg").call(chart());
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.10.0/d3.min.js"></script>
<svg width="550" height="200"></svg>
I have a thermometer of svg type. I want to create some animation using Snap. I could animate the text with the given value but could not animate the dashed line that you can see in the screenshot to move as per the given value. How should i do it?
Here is my code
const svg = Snap(this.svg);
const { color } = this.state;
svg.line(55, 366, 90, 366).attr({
id: 'marker-line',
stroke: color,
strokeDasharray: '2 4',
strokeWidth: '1'
});
const animateMarker = (value, svg, marker, lastValue) => {
// Snap.animate(value);
const markerLine = svg.select('#marker-line');
if (markerLine) {
Snap.animate(
lastValue || 0,
value,
val => {
// markerLine.attr({ y1: 366-val });
marker.textContent = roundOffDecimalDigit(val, 2); // eslint-disable-line no-param-reassign
},
400
);
}
};
Without the rest of the code, it's difficult to tell. Your code looks ok though, just it only sets one of the y co-ordinates, and I assume you want to set both. Eg
Snap.animate(lastValue,value, function( val ) { markerLine.attr({ y1: val, y2: val }); }, 400 )
If it's still not working, try putting it on a jsfiddle and it will be easier to help.