@goshawk22/leaflet-elevation
Version:
A Leaflet plugin that allows to add elevation profiles using d3js
650 lines (542 loc) • 15.3 kB
JavaScript
export const Area = ({
width,
height,
xAttr,
yAttr,
scaleX,
scaleY,
interpolation = "curveLinear"
}) => {
return d3.area()
.curve(typeof interpolation === 'string' ? d3[interpolation] : interpolation)
.x(d => (d.xDiagCoord = scaleX(d[xAttr])))
.y0(height)
.y1(d => scaleY(d[yAttr]));
};
export const Path = ({
name,
color,
strokeColor,
strokeOpacity,
fillOpacity,
className = ''
}) => {
let path = d3.create('svg:path')
if (name) path.classed(name + ' ' + className, true);
path.style("pointer-events", "none");
path
.attr("fill", color || '#3366CC')
.attr("stroke", strokeColor || '#000')
.attr("stroke-opacity", strokeOpacity || '1')
.attr("fill-opacity", fillOpacity || '0.8');
return path;
};
export const Axis = ({
type = "axis",
tickSize = 6,
tickPadding = 3,
position,
height,
width,
axis,
scale,
ticks,
tickFormat,
label,
labelX,
labelY,
name = "",
onAxisMount,
}) => {
return g => {
let [w, h] = [0, 0];
if (position == "bottom") h = height;
if (position == "right") w = width;
if (axis == "x" && type == "grid") {
tickSize = -height;
} else if (axis == "y" && type == "grid") {
tickSize = -width;
}
let axisScale = d3["axis" + position.replace(/\b\w/g, l => l.toUpperCase())]()
.scale(scale)
.ticks(typeof ticks === 'function' ? ticks() : ticks)
.tickPadding(tickPadding)
.tickSize(tickSize)
.tickFormat(tickFormat);
let axisGroup = g.append("g")
.attr("class", [axis, type, position, name].join(" "))
.attr("transform", "translate(" + w + "," + h + ")")
.call(axisScale);
if (label) {
axisGroup.append("svg:text")
.attr("x", labelX)
.attr("y", labelY)
.text(label);
}
if (onAxisMount) {
axisGroup.call(onAxisMount);
}
return axisGroup;
};
};
export const Grid = (props) => {
props.type = "grid";
return Axis(props);
};
export const PositionMarker = ({
theme,
xCoord = 0,
yCoord = 0,
labels = {},
item = {},
length = 0,
}) => {
return g => {
g.attr("class", "height-focus-group");
let line = g.select('.height-focus.line');
let circle = g.select('.height-focus.circle-lower');
let text = g.select('.height-focus-label');
if (!line.node()) line = g.append('svg:line');
if (!circle.node()) circle = g.append('svg:circle');
if (!text.node()) text = g.append('svg:text');
if (isNaN(xCoord) || isNaN(yCoord)) return g;
circle
.attr("class", theme + " height-focus circle-lower")
.attr("transform", "translate(" + xCoord + "," + yCoord + ")")
.attr("r", 6)
.attr("cx", 0)
.attr("cy", 0);
line
.attr("class", theme + " height-focus line")
.attr("x1", xCoord)
.attr("x2", xCoord)
.attr("y1", yCoord)
.attr("y2", length);
text
.attr("class", theme + " height-focus-label")
.style("pointer-events", "none")
.attr("x", xCoord + 5)
.attr("y", length);
let label;
Object
.keys(labels)
.sort((a, b) => labels[a].order - labels[b].order) // TODO: any performance issues?
.forEach((i)=> {
label = text.select(".height-focus-" + labels[i].name);
if (!label.size()) {
label = text.append("svg:tspan")
.attr("class", "height-focus-" + labels[i].name /*+ " " + "order-" + labels[i].order*/)
.attr("dy", "1.5em");
}
label.text(typeof labels[i].value !== "function" ? labels[i].value : labels[i].value(item));
});
text.select('tspan').attr("dy", text.selectAll('tspan').size() > 1 ? "-1.5em" : "0em" );
text.selectAll('tspan').attr("x", xCoord + 5);
return g;
}
};
export const LegendItem = ({
name,
label,
width,
height,
margins = {},
color,
path,
className = ''
}) => {
return g => {
g
.attr("class", "legend-item legend-" + name.toLowerCase())
.attr("data-name", name);
g.on('click.legend', () => d3.select(g.node().ownerSVGElement || g)
.dispatch("legend_clicked", {
detail: {
path: path.node(),
name: name,
legend: g.node(),
enabled: !path.classed('leaflet-hidden'),
}
})
);
g.append("svg:rect")
.attr("x", (width / 2) - 50)
.attr("y", height + margins.bottom / 2)
.attr("width", 50)
.attr("height", 10)
.attr("fill", color)
.attr("stroke", "#000")
.attr("stroke-opacity", "0.5")
.attr("fill-opacity", "0.25")
.attr("class", className);
g.append('svg:text')
.text(L._(label || name))
.attr("x", (width / 2) + 5)
.attr("font-size", 10)
.style("font-weight", "700")
.attr('y', height + margins.bottom / 2)
.attr('dy', "0.75em");
return g;
}
};
export const LegendSmall = ({
width,
height,
items,
onClick
}) => {
return g => {
let idx = 0;
g.data([{
x: width,
y: height + 40
}]).attr("transform", d => "translate(" + d.x + "," + d.y + ")").classed('legend-switcher');
// let label = g.selectAll(".legend-switcher-label") .data([{ idx: 0 }]);
let label = g.append('svg:text')
.attr("class", "legend-switcher-label")
.attr("text-anchor", "end")
.attr("x", -25)
.attr("y", 5)
.on("mousedown", (e, d) => setIdx(L.Util.wrapNum((idx + 1), [0, items.length])));
let symbol = g.selectAll(".legend-switcher-symbol").data([
{ type: d3.symbolTriangle, x: 0, y: 3, angle: 0, size: 50, id: "down" },
{ type: d3.symbolTriangle, x: -13, y: 0, angle: 180, size: 50, id: "up" }
]);
symbol.exit().remove();
label.exit().remove();
symbol.enter()
.append("svg:path")
.attr("class", "legend-switcher-symbol")
.attr("cursor", 'pointer')
.attr("fill", "#000")
.merge(symbol)
.attr("d",
d3.symbol()
.type(d => d.type)
.size(d => d.size)
)
.attr("transform", d => "translate(" + d.x + "," + d.y + ") rotate(" + d.angle + ")")
.on("mousedown", (e, d) => setIdx(L.Util.wrapNum((d.id === "up" ? idx + 1 : idx - 1), [0, items.length])));
const setIdx = (id) => {
idx = id;
// label.enter()
// .append('text')
// .attr("class", "legend-switcher-label")
// .attr("text-anchor", "end")
// .attr("x", -25)
// .attr("y", 5)
// .merge(label.data([{ idx: id }]))
// .text(d => items.length ? (items[idx][0].toUpperCase() + items[idx].slice(1)) : '')
// .on("mousedown", (e, d) => setIdx(L.Util.wrapNum((idx + 1), [0, items.length])));
label.text(items.length ? L._((items[idx][0].toUpperCase() + items[idx].slice(1))) : '');
onClick(items[idx]);
};
setIdx(0);
return g;
};
};
export const Tooltip = ({
xCoord,
yCoord,
width,
height,
labels = {},
item = {},
}) => {
return g => {
let line = g.select('.mouse-focus-line');
let box = g.select('.mouse-focus-label');
if (!line.node()) line = g.append('svg:line');
if (!box.node()) box = g.append("g");
let rect = box.select(".mouse-focus-label-rect");
let text = box.select(".mouse-focus-label-text");
if (!rect.node()) rect = box.append("svg:rect");
if (!text.node()) text = box.append("svg:text");
// Sets focus-label-text position to the left / right of the mouse-focus-line
let xAlign = 0;
let yAlign = 0;
let bbox = { width: 0, height: 0 };
try { bbox = text.node().getBBox(); } catch (e) { return g; }
if (xCoord) xAlign = xCoord + (xCoord < width / 2 ? 10 : -bbox.width - 10);
if (yCoord) yAlign = Math.max(yCoord - bbox.height, L.Browser.webkit ? 0 : -Infinity);
line
.attr('class', 'mouse-focus-line')
.attr('x2', xCoord)
.attr('y2', 0)
.attr('x1', xCoord)
.attr('y1', height);
box
.attr('class', 'mouse-focus-label');
rect
.attr("class", "mouse-focus-label-rect")
.attr("x", xAlign - 5)
.attr("y", yAlign - 5)
.attr("width", bbox.width + 10)
.attr("height", bbox.height + 10)
.attr("rx", 3)
.attr("ry", 3);
text
.attr("class", "mouse-focus-label-text")
.style("font-weight", "700")
.attr("y", yAlign);
let label;
Object
.keys(labels)
.sort((a, b) => labels[a].order - labels[b].order) // TODO: any performance issues?
.forEach((i)=> {
label = text.select(".mouse-focus-label-" + labels[i].name);
if (!label.size()) {
label = text.append("svg:tspan", ".mouse-focus-label-x")
.attr("class", "mouse-focus-label-" + labels[i].name /*+ " " + "order-" + labels[i].order*/)
.attr("dy", "1.5em");
}
label.text(typeof labels[i].value !== "function" ? labels[i].value : labels[i].value(item));
});
text.select('tspan').attr("dy", "1em");
text.selectAll('tspan').attr("x", xAlign);
return g;
};
};
export const Ruler = ({
height,
width,
}) => {
return g => {
g.data([{
x: 0,
y: height,
}])
.attr("transform", d => "translate(" + d.x + "," + d.y + ")");
let rect = g.selectAll('.horizontal-drag-rect').data([{ w: width }]);
let line = g.selectAll('.horizontal-drag-line').data([{ w: width }]);
let label = g.selectAll('.horizontal-drag-label').data([{ w: width - 8 }]);
let symbol = g.selectAll('.horizontal-drag-symbol')
.data([{
type: d3.symbolTriangle,
x: width + 7,
y: 0,
angle: -90,
size: 50
}]);
rect.exit().remove();
line.exit().remove();
label.exit().remove();
symbol.exit().remove();
rect.enter()
.append("svg:rect")
.attr("class", "horizontal-drag-rect")
.attr("x", 0)
.attr("y", -8)
.attr("height", 8)
.attr('fill', 'none')
.attr('pointer-events', 'all')
.merge(rect)
.attr("width", d => d.w);
line.enter()
.append("svg:line")
.attr("class", "horizontal-drag-line")
.attr("x1", 0)
.merge(line)
.attr("x2", d => d.w);
label.enter()
.append("svg:text")
.attr("class", "horizontal-drag-label")
.attr("text-anchor", "end")
.attr("y", -8)
.merge(label)
.attr("x", d => d.w)
symbol
.enter()
.append("svg:path")
.attr("class", "horizontal-drag-symbol")
.merge(symbol)
.attr("d",
d3.symbol()
.type(d => d.type)
.size(d => d.size)
)
.attr("transform", d => "translate(" + d.x + "," + d.y + ") rotate(" + d.angle + ")");
return g;
}
};
export const CheckPoint = ({
point,
height,
width,
x,
y
}) => {
return g => {
if (isNaN(x) || isNaN(y)) return g;
if (!point.item || !point.item.property('isConnected')) {
point.position = point.position || "bottom";
point.item = g.append('g');
point.item.append("svg:line")
.attr("y1", 0)
.attr("x1", 0)
.attr("style","stroke: rgb(51, 51, 51); stroke-width: 0.5; stroke-dasharray: 2, 2;");
point.item
.append("svg:circle")
.attr("class", " height-focus circle-lower")
.attr("r", 3);
if (point.label) {
point.item.append("svg:text")
.attr("dx", "4px")
.attr("dy", "-4px");
}
}
point.item
.datum({
pos: point.position,
x: x,
y: y
})
.attr("class", d => "point " + d.pos)
.attr("transform", d => "translate(" + d.x + "," + d.y + ")");
point.item.select('line')
.datum({
y2: ({'top': -y, 'bottom': height - y})[point.position],
x2: ({'left': -x, 'right': width - x})[point.position] || 0
})
.attr("y2", d => d.y2)
.attr("x2", d => d.x2)
if (point.label) {
point.item.select('text')
.text(point.label);
}
return g;
}
};
export const Domain = ({
min,
max,
attr,
name,
forceBounds,
scale
}) => function(data) {
attr = (scale && scale.attr) || attr || name;
let domain = data && data.length ? d3.extent(data, d => d[attr]) : [0, 1];
if (typeof min !== "undefined" && (min < domain[0] || forceBounds)) {
domain[0] = min;
}
if (typeof max !== "undefined" && (max > domain[1] || forceBounds)) {
domain[1] = max;
}
return domain;
};
export const Range = ({
axis
}) => function(width, height) {
if (axis == 'x') return [0, width];
else if (axis == 'y') return [height, 0];
};
export const Scale = ({
data,
attr,
min,
max,
forceBounds,
range,
}) => {
return d3.scaleLinear()
.range(range)
.domain(Domain({min, max, attr, forceBounds})(data));
};
export const Bisect = ({
data = [0, 1],
scale,
x,
attr
}) => {
return d3
.bisector(d => d[attr])
.left(data, scale.invert(x));
};
export const Chart = ({
width,
height,
margins = {},
ruler,
}) => {
const w = width - margins.left - margins.right;
const h = height - margins.top - margins.bottom;
// SVG Container
const svg = d3.create("svg:svg").attr("class", "background");
const defs = svg.append("svg:defs");
// SVG Groups
const g = svg.append("g");
const panes = {
grid : g.append("g").attr("class", "grid"),
area : g.append('g').attr("class", "area"),
axis : g.append('g').attr("class", "axis"),
point : g.append('g').attr("class", "point"),
brush : g.append("g").attr("class", "brush"),
tooltip: g.append("g").attr("class", "tooltip").attr('display', 'none'),
ruler : g.append('g').attr('class', 'ruler'),
legend : g.append('g').attr("class", "legend"),
};
// SVG Paths
const mask = panes.area .append("svg:mask") .attr("id", 'elevation-clipper-' + Math.random().toString(36).substr(2, 9)).attr('fill-opacity', 1);
const maskRect = mask .append("svg:rect") .attr('class', 'zoom') .attr('fill', 'white'); // white = transparent
// Canvas Paths
const foreignObject = panes.area .append('svg:foreignObject').attr('mask', 'url(#' + mask.attr('id') + ')');
const canvas = foreignObject.append('xhtml:canvas') .attr('class', 'canvas-plot');
const context = canvas.node().getContext('2d');
// Add tooltip
panes.tooltip.call(Tooltip({ xCoord: 0, yCoord: 0, height: h, width : w, labels: {} }));
// Add the brushing
let brush = d3.brushX().on('start.cursor end.cursor brush.cursor', () => panes.brush.select(".overlay").attr('cursor', null));
// Scales
const scale = (opts) => ({ x: Scale(opts.x), y: Scale(opts.y)});
let utils = {
defs,
mask,
canvas,
context,
brush,
};
let chart = {
svg,
g,
panes,
utils,
scale,
};
// Resize
chart._resize = ({
width,
height,
margins = {},
ruler,
}) => {
const w = width - margins.left - margins.right;
const h = height - margins.top - margins.bottom;
svg .attr("width", width).attr("height", height).attr("viewBox", `0 0 ${width} ${height}`);
g .attr("transform", "translate(" + margins.left + "," + margins.top + ")");
// Fix: https://github.com/Raruto/leaflet-elevation/issues/123
// Fix: https://github.com/Raruto/leaflet-elevation/issues/232
if (
/Mac|iPod|iPhone|iPad/.test(navigator.platform) &&
/AppleWebkit/i.test(navigator.userAgent) &&
!/Chrome/.test(navigator.userAgent)
) {
canvas .style("transform", "translate(" + margins.left + "px," + margins.top + "px)");
}
maskRect .attr("width", w).attr("height", h).attr("x", 0).attr("y", 0);
foreignObject.attr('width', w).attr('height', h);
canvas .attr('width', w).attr('height', h);
if (ruler) {
panes.ruler.call(Ruler({ height: h, width: w }));
}
panes.brush.call(brush.extent( [ [0,0], [w, h] ] ));
panes.brush.select(".overlay").attr('cursor', null);
chart._width = w;
chart._height = h;
chart.svg.dispatch('resize', { detail: { width: w, height: h } } );
};
chart.pane = (name) => (panes[name] || (panes[name] = g.append('g').attr("class", name)));
chart.get = (name) => utils[name];
chart._resize({ width, height, margins});
return chart;
};