Cesium has various polyline materials (PolylineArrow, PolylineDash, PolylineGlow etc.) but
I want to draw a polyline consisting of lines that has an arrow in their head and an arrow in their center.
I think it can be done by adding GLSL code to PolylineArrowMaterial.
Only one idea that I have is adding section of polyline with arrow header instead on single polyline like this :
points.forEach((x, i) => {
if (i < points.length - 1) {
polylines.add({
positions: [points[i], points[i + 1]],
width: 12.0,
material: Cesium.Material.fromType('PolylineArrow', {
color: new Cesium.Color.fromBytes(255, 255, 0, 185)
})
});
}
});
Related
I'd like to customize a legend for line data so that the legend graphic is a line (styled like the actually data line) rather than a box.
As far as I can tell from the source, the graphic can be a point or a box, and the height of the box is fixed to the font size. The 'generateLabels' option does not seem to allow for extending around these contraints.
Version 2.2.1.
Thanks for any help.
NOTE: This solution only works if you have a local version of Chart.js since it needs to edit a function in the source code of the library, which can't be done if you import it form a CDN.
To achieve what you want, you will need to edit the drawLegendBox function (link to source here).
First, as if you wanted to do a pointStyle legend, add the useLineStyle and set it to true like this :
options: {
legend: {
labels : {
useLineStyle: true
}
}
}
Then you need to go to your local version of Chart.js (obvisouly, you cannot edit it if you import it from a CDN) and search for the function drawLegendBox (on Chart.js v2.2.1, it is roughly line 6460; in Chart.js v2.9.4 search for labelOpts && labelOpts.usePointStyle).
Scroll down a little bit to see something like this :
if (opts.labels && opts.labels.usePointStyle) {
// Recalulate x and y for drawPoint() because its expecting
// x and y to be center of figure (instead of top left)
var radius = fontSize * Math.SQRT2 / 2;
var offSet = radius / Math.SQRT2;
var centerX = x + offSet;
var centerY = y + offSet;
// Draw pointStyle as legend symbol
Chart.canvasHelpers.drawPoint(ctx, legendItem.pointStyle, radius, centerX, centerY);
}
// --- NEW CONDITION GOES HERE ---
else {
// Draw box as legend symbol
ctx.strokeRect(x, y, boxWidth, fontSize);
ctx.fillRect(x, y, boxWidth, fontSize);
}
And add this between the two conditions :
else if (opts.labels && opts.labels.useLineStyle) {
ctx.beginPath();
ctx.moveTo(x, y + fontSize * 0.45);
ctx.lineTo(x + boxWidth, y + fontSize * 0.45);
ctx.stroke();
}
With this edit, everytime you will set useLineStyle to true, legend boxes will be drawn as lines, as the following screenshot :
I was able to use pointStyle: line, in the dataset and then under options use labels: {usePointStyle: true,},
Just to improve on this solution from tektiv.
If you want to show a dashed line too use this code in the same spot.
(chartJs 2.7.2 around Line 16289):
if (opts.labels && opts.labels.usePointStyle) {
// CHARTJS CODE
} else if (opts.labels && opts.labels.useLineStyle) {
if (legendItem.borderDash) {
ctx.setLineDash(legendItem.borderDash);
}
ctx.beginPath();
ctx.moveTo(x, y + fontSize / 2);
ctx.lineTo(x + boxWidth, y + fontSize / 2);
ctx.stroke();
} else {
// CHARTJS CODE
}
chart.js v3
For this version, none of the previously mentioned built-in configurations work. You can set boxHeight: 0 on the legend labels in order to get a line instead of a box:
{
legend: {
labels: {
boxHeight: 0
}
}
}
You can make line legend by changing width of legend box (for example 2px), it will be vertical line but it's looks nice too
plugins: {
legend: {
display: true,
labels: {
boxWidth: 2
}
}
}
Is it possible to get some more space between the chart and the x-axis?
Is it possible to get some more space between the right side of the chart and the end of the canvas area? I want to add some more elements to the canvas right beside the chart but this is not possible because the chart takes the whole canvas width so it would overlap.
Shifting x axis Labels Vertically
The easiest way to do 1. is by adding spaces to your x labels. You can extend your chart type and override your initialize function to do this (increase 30 to something larger if your labels are long to start with anyway)
initialize: function(data){
data.labels.forEach(function(item, index) {
data.labels[index] += Array(Math.max(30 - item.length, 0)).join(" ");
})
Chart.types.Bar.prototype.initialize.apply(this, arguments);
},
Edit : As pointed out in the comments, this causes a horizontal shift as well and the label ends no longer align with the x axis markers.
Since both the x axis and the x labels are drawn in a single function and you have no other variables you can mess around with (safely) this means you'll have to change the actual scale draw function.
Look for a ctx.translate towards the end of the draw function and change it to
ctx.translate(xPos, (isRotated) ? this.endPoint + 22 : this.endPoint + 18);
You'll also have to adjust the endpoint (which drives the y limits) a bit so that the additional y offset doesn't cause the labels to overflow the chart (look for the line adjusting this in the draw override for 2.).
Leaving a gap on the Right Side
To do 2, you override your draw function (in your extended chart) and change xScalePaddingRight. However since this doesn't affect your horizontal grid lines you have to overlay a filled rectangle once your draw is complete. Your complete draw function would look like this
draw: function(){
// this line is for 1.
if (!this.scale.done) {
this.scale.endPoint -= 20
// we should do this only once
this.scale.done = true;
}
var xScalePaddingRight = 120
this.scale.xScalePaddingRight = xScalePaddingRight
Chart.types.Bar.prototype.draw.apply(this, arguments);
this.chart.ctx.fillStyle="#FFF";
this.chart.ctx.fillRect(this.chart.canvas.width - xScalePaddingRight, 0, xScalePaddingRight, this.chart.canvas.height);
}
Original fiddle - https://jsfiddle.net/gvdmxc5t/
Fiddle with modified Scale draw function - https://jsfiddle.net/xgc6a77a/ (I turned off animation in this one so that the endpoint is shifted only once, but you could just hard code it, or add some extra code so that it's done only once)
The 'tickMarkLength' option extends the grid lines outside the chart and pushes the ticks down.
xAxes: [
{
gridLines: {
tickMarkLength: 15
},
}
]
use this for chartjs 2.0
scales: {
xAxes: [{
barPercentage: 0.9,
categoryPercentage: 0.55
}]
Reference
In chartjs v3, there is an "offset" flag that you can set to true. This will create padding.
scales: {
x: {
offset: true,
}
}
If true, extra space is added to the both edges and the axis is scaled to fit into the chart area. This is set to true for a bar chart by default.
Documentation
I'm trying to understand how sets work with transforms. Basically, I would like to have a 'container' set with all children in it, that I can move around the canvas.
I created a fiddle to show what I mean, this is a simplification of a larger drawing. http://jsfiddle.net/thibs/Hsvpf/
I've created 3 squares, red, black, blue. Each are added to a set and then they are added to a main container (set). I've added outlines to show canvas and set.
Red and black sets do not have transforms on them, but blue does. Blue remains in the 'container' set... until the container gets a transform.
Why is that? I thought that transforms were applied to all the chidlren of the set...?
Thanks in advance
Here is the fiddle code:
var paper = Raphael('holder');
var container = paper.set();
paper.rect(0, 0, '100%', '100%').attr({
stroke : 'red'
});
var rectRedSet = paper.set();
var rectRed = paper.rect(100, 10, 20, 20).attr({
'fill' : 'red',
'stroke-opacity' : 0
});
rectRedSet.push(rectRed);
container.push(rectRedSet);
var rectBlackSet = paper.set();
var rectBlack = paper.rect(150, 10, 20, 20).attr({
'fill' : 'black',
'stroke-opacity' : 0
});
rectBlackSet.push(rectBlack);
container.push(rectBlackSet);
var rectBlueSet = paper.set();
rectBlue = paper.rect(0, 0, 20, 20).attr({
'fill' : 'blue',
'stroke-opacity' : 0
});
rectBlueSet.push(rectBlue);
rectBlueSet.transform('t50,150');
container.push(rectBlueSet);
var containerBBox = container.getBBox();
paper.rect(containerBBox.x, containerBBox.y, containerBBox.width, containerBBox.height).attr({
stroke : 'black'
});
//trying to get the entire container and its children to move to 0,0
//commenting out the transform below will keep rectBlue in the container...?
container.transform('t0,0');
A "set" in Raphael is not like a "group" in SVG. A set in Raphael is just a collection of elements that you can manipulate at the same time. So when you set the transform on the container set, it is really just setting the transform on every element inside the set, overwriting any previous transform settings.
You can append or prepend to existing transformations in Raphael using "..." notation.You need to change your last line to:
container.transform("...t0,0")
But "t0,0" doesn't actually move anything anywhere. If you want to move the container so the top left corner is at 0,0, then you need to write:
container.transform('...t-' + containerBBox.x + ',-' + containerBBox.y);
I have a problem in my app. I draw lines and points with the help of mouse and I need to detect wether I intersect the line or get the point. And with points it works well, but with lines it doesn't work. I add lines to the AnchorPane. Then I spotted, when cursor is situated inside of 3 lines (Triange) - it's always intersect. Here is my example:
ArrayList<Line> lines = new ArrayList<Line>();
Line l1 = new Line(10, 150, 50, 10);
Line l2 = new Line(10, 150, 100, 150);
Line l3 = new Line(100, 150, 50, 10);
lines.add(l1);
lines.add(l2);
lines.add(l3);
for (int i=0; i<lines.size();++i) {
if (lines.get(i).intersects(48, 48, 4, 4)) {
System.out.println("found a bug!" + " line #"+i);
}
}
If someone knew the answer - it would be great!
I'll imagine that your question is: how to pick a line by clicking anywhere within two pixels of the line?
In the sample output below, the user has just clicked very close to the right line of the triangle, causing that line to be highlighted.
The code below works by testing each shape in the pane and seeing if the shape intersects a rectangular box 2 pixels on either side of a mouse press location. The solution is very similar to that used in solving: Checking Collision of Shapes with JavaFX.
import javafx.application.Application;
import javafx.event.EventHandler;
import javafx.scene.*;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.Pane;
import javafx.scene.shape.*;
import javafx.stage.Stage;
public class LinePicker extends Application {
public static void main(String[] args) { Application.launch(LinePicker.class); }
#Override public void start(final Stage stage) throws Exception {
final Pane pane = new Pane();
pane.getChildren().setAll(
new Line( 10, 150, 50, 10),
new Line( 10, 150, 100, 150),
new Line(100, 150, 50, 10)
);
pane.setPrefSize(
200, 200
);
Scene scene = new Scene(addPickHandler(pane));
stage.setScene(scene);
stage.show();
}
private Pane addPickHandler(final Pane pane) {
pane.setOnMousePressed(new EventHandler<MouseEvent>() {
#Override public void handle(MouseEvent event) {
final Rectangle hotspot = new Rectangle(
event.getX() - 2,
event.getY() - 2,
4,
4
);
for (Node child : pane.getChildren()) {
if (child instanceof Shape) {
final Shape shape = (Shape) child;
Shape intersect = Shape.intersect(shape, hotspot);
if (intersect.getBoundsInLocal().getWidth() != -1) {
shape.setStyle("-fx-stroke: red;");
} else {
shape.setStyle("-fx-stroke: black;");
}
}
}
}
});
return pane;
}
}
Comment
It is a little confusing to me why shved90 was unable to get the above solution to function for him in his application.
Shev comments that "In Line, as I understand, it checks all the bounds and if the line would be with some angle its bound would be like rectangle and as a result I check whether I spotted on it (not only on line and its nearest points). But I needed geometry operation."
However the solution presented here is a geometry based solution in that the click hotspot must intersect the actual line and not the rectangular bounds encompassing the line. This is because the Shape.intersect method used in the solution works on the intersection of the actual shapes involved and not the bounds of the shapes involved.
Note however that in the shved90's original question, he uses the Node intersects method rather than the Shape.intersect method. The documentation of Node intersects states "The default behavior of this function is simply to check if the given coordinates intersect with the local bounds.", which, from his comment, is obviously not what shved90 wanted.
Shev does note that "I've found Line2d class in javaFx, as like in Java2D, I convert to this line and use it's intersection". I would not recommend using the Line2D class bundled with JavaFX as it is a private com.sun api that is not guaranteed to be present or have a binary backward compatible API in future JavaFX releases.
I started to play a little bit with raphaeljs, however I'm having a small problem when dragging and applying a transformation to a Paper.set()
Here is my example: http://jsfiddle.net/PQZmp/2/
1) Why is the drag event added only to the marker and not the slider?
2) The transformation is supposed to be relative(i.e. translate by and not translate to), however if I drag the marker twice, the second dragging starts from the beginning and not from the end of the first.
EDIT:
After the response of Zero, I created a new JSFiddle example: http://jsfiddle.net/9b9W3/1/
1) It would be cool if this referenced the set instead of the first element of the set. Can't this be done with dragger.apply(slider)? I tried it, but only works on the first execution of the method (perhaps inside Raphael it is already being done but to the first element inside the set instead of the set)
2) According to Raphael docs the transformation should be relative to the object position (i.e. translate by and not translate to). But it is not what is happening according to the jsfiddle above (check both markers drag events).
3) So 2) above creates a third question. If a transform("t30,0") is a translation by 30px horizontally, how is the origin calculated? Based on attr("x") or getBBox().x?
The drag event is actually being added to both the marker and the slider -- but your slider has a stroke-width of 1 and no fill, so unless you catch the exact border, the click "falls through" to the canvas.
Behind that is another issue: the drag is being applied to both elements, but this in your drag handler references a specific element, not the set -- so both elements will drag independently from each other.
Lastly: the reason that each drag is starting from the initial position is because the dx, dy parameters in dragger are relative to the coordinates of the initial drag event, and your transform does not take previous transforms into account. Consider an alternative like this:
var r = new Raphael(0, 0, 400, 200);
var marker = r.path("M10,0L10,100").attr({"stroke-width": 5});
var button = r.rect(0, 0, 20, 20, 1).attr( { 'stroke-width': 2, fill: 'white' } );
var slider = r.set( marker, button );
var startx, starty;
var startDrag = function(){
var bbox = slider.getBBox();
startx = bbox.x;
starty = bbox.y;
console.log(this);
}, dragger = function(dx, dy){
slider.transform("t" + ( startx + dx ) + "," + starty );
}, endDrag = function(){
};
slider.drag(dragger, startDrag, endDrag);
To address your updates:
I believe you can specify the context in which the drag function will be executed as optional fourth, fifth, and six parameters to element.drag. I haven't tried this myself, but it looks like this should work great:
slider.drag( dragger, startDrag, endDrag, slider, slider, slider );
The transformation is relative to the object position. This works great for the first slider because its starting position is 0, but not so great for the second slider because...
...the transformation for min/max sliders should actually be relative to the scale, not the individual markers. Thus you will notice that your max slider (the red one) returns to its initial position just as you drag the mouse cursor back over the zero position. Make sense?
var position;
var rect = paper.rect(20, 20, 40, 40).attr({
cursor: "move",
fill: "#f00",
stroke: "#000"
});
t = paper.text(70,70, 'test').attr({
"font-size":16,
"font-family":
"Arial, Helvetica, sans-serif"
});
var st = paper.set();
st.push(rect, t);
rect.mySet = st;
rect.drag(onMove, onStart, onEnd);
onStart = function () {
positions = new Array();
this.mySet.forEach(function(e) {
var ox = e.attr("x");
var oy = e.attr("y");
positions.push([e, ox, oy]);
});
}
onMove = function (dx, dy) {
for (var i = 0; i < positions.length; i++) {//you can use foreach but I want to
// show that is a simple array
positions[i][0].attr({x: positions[i][1] + dx, y: positions[i][2] + dy});
}
}
onEnd = function() {}