In this SVG chart, the line is too flat. How can I exaggerate the difference in Y values of the points so that it appears more "zig-zaggy", with the lowest point of the line at the bottom of the graph and the highest point at the top?
svg {
display: flex;
width: calc(100% + 4em);
transform: translateX(-2em);
clip-path: polygon(2em 0, calc(100% - 2em) 0, calc(100% - 2em) 100%, 2em 100%);
}
polyline {
transform: scaleY(-1);
}
<svg viewBox="0 -100 900 100" class="chart" style="height: 100vh; width: 100vw;">
<defs>
<marker id="red-circle" viewBox="0 0 10 10" refX="5" refY="5" orient="auto">
<circle fill="red" cx="5" cy="5" r="5" />
</marker>
</defs>
<polyline fill="#FEF9CC" stroke="#FED225" stroke-width="2" points="
0, 0
100, 23
200, 21
300, 20
400, 20
500, 23
600, 28
700, 30
800, 30
900, 30
0, -99999
0, 0
" marker-start="url(#red-circle)" marker-end="url(#red-circle)" marker-mid="url(#red-circle)" />
</svg>
I received the formula here on Reddit. Here is how I implemented it:
const exaggerate = function () {
const polyline = document.querySelector('polyline');
const points = [...polyline.points].slice(2, -2);
// slice off (ignore) first 2 and last 2 because
// they are purely for closing the loop
// so that it can be filled with a color
const ys = points.map(({ y }) => y);
const y_min = Math.min(...ys);
const y_max = Math.max(...ys);
points.forEach((point) => {
point.y = (point.y - y_min) * 100 / (y_max - y_min);
});
}
exaggerate();
svg {
display: flex;
width: calc(100% + 4em);
transform: translateX(-2em);
clip-path: polygon(2em 0, calc(100% - 2em) 0, calc(100% - 2em) 100%, 2em 100%);
}
polyline {
transform: scaleY(-1);
}
<svg viewBox="0 -100 900 100" class="chart" style="height: 100vh; width: 100vw;">
<defs>
<marker id="red-circle" viewBox="0 0 10 10" refX="5" refY="5" orient="auto">
<circle fill="red" cx="5" cy="5" r="5" />
</marker>
https://stackoverflow.com/questions/66473797/how-to-exaggerate-the-y-axis-of-an-svg-plot# </defs>
<polyline fill="#FEF9CC" stroke="#FED225" stroke-width="2" points="
0, 0
100, 23
200, 21
300, 20
400, 20
500, 23
600, 28
700, 30
800, 30
900, 30
0, -99999
0, 0
" marker-start="url(#red-circle)" marker-end="url(#red-circle)" marker-mid="url(#red-circle)" />
</svg>
EDIT:
Came across this question and answer as well, for anyone interested.
Related
I have svg stroke animation https://codesandbox.io/s/magical-hill-r92ong
But it's starts from bottom-right position, can i start it from upper center like on screenshot (red dot) ?
I try to set stroke-dashoffset with negative value it's helps to set start point, but stroke animation is not going to the end
As I've commented you need to rewrite the d attribute so that it starts where you want the animation to begin.
For example you may try this:
body {
font-family: sans-serif;
}
svg path {
animation: anim 3s ease-in-out forwards infinite;
}
#keyframes anim {
0% {
stroke-dasharray: 0 672;
}
100% {
stroke-dasharray: 672 672;
}
}
<svg width="218" height="196" viewBox="0 0 218 196" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M106.732 31.0933C107.743 32.8343 110.257 32.8343 111.268 31.0933C115.823 22.9236 121.557 13.3943 128.468 8.90454C136.579 3.63485 145.927 1 156.511 1C165.086 1 173.004 2.54327 180.266 5.6298C187.605 8.71634 194.017 13.1203 199.502 18.8417C205.064 24.5631 209.352 31.3007 212.365 39.0547C215.455 46.8087 217 55.3531 217 64.688C217 79.7443 212.867 94.6123 204.601 109.292C196.335 123.972 184.67 138.313 169.605 152.315
C154.541 166.243 136.811 179.567 116.416 192.29C115.335 193.043 114.099 193.683 112.708 194.21C111.318 194.737 110.082 195 109 195C107.996 195 106.798 194.737 105.408 194.21C103.94 193.683 102.665 193.043 101.584 192.29C81.1888 179.567 63.4592 166.243 48.3948 152.315C33.3304 138.313 21.6652 123.972 13.3991 109.292C5.13304 94.6123 1 79.7443 1 64.688C1 55.3531 2.54506 46.8087 5.63519 39.0547C8.64807 31.3007 12.9356 24.5631 18.4979 18.8417C23.9828 13.1203 30.3948 8.71634 37.7339 5.6298C44.9957 2.54327 52.9142 1 61.4893 1C72.073 1 81.4206 3.63485 89.5322 8.90454C96.4432 13.3943 102.177 22.9236 106.732 31.0933Z" stroke="black" stroke-linecap="round" stroke-linejoin="round"></path>
</svg>
Shift M (starting point)
Shifting the starting point is actually not too complicated – provided you're using absolute commands and your path doesn't contain any shorthand commands (more details below):
The upper center command would be the 16nth or 17nth command:
/**
* 1st chunk - becomes 2nd
* M will be replaced by
* last C command end coordinates in this chunk
*/
M 169.605 152.315
C 154.541 166.243 136.811 179.567 116.416 192.29
C 115.335 193.043 114.099 193.683 112.708 194.21
C 111.318 194.737 110.082 195 109 195
C 107.996 195 106.798 194.737 105.408 194.21
C 103.94 193.683 102.665 193.043 101.584 192.29
C 81.189 179.567 63.459 166.243 48.395 152.315
C 33.33 138.313 21.665 123.972 13.399 109.292
C 5.133 94.612 1 79.744 1 64.688
C 1 55.353 2.545 46.809 5.635 39.055
C 8.648 31.301 12.936 24.563 18.498 18.842
C 23.983 13.12 30.395 8.716 37.734 5.63
C 44.996 2.543 52.914 1 61.489 1
C 72.073 1 81.421 3.635 89.532 8.905
C 96.443 13.394 102.177 22.924 106.732 31.093
C 107.743 32.834 110.257 32.834 111.268 31.093
/**
* 2nd chunk - new M: 111.268 31.093 = previous C command end point
*/
C 115.823 22.924 121.557 13.394 128.468 8.905
C 136.579 3.635 145.927 1 156.511 1
C 165.086 1 173.004 2.543 180.266 5.63
C 187.605 8.716 194.017 13.12 199.502 18.842
C 205.064 24.563 209.352 31.301 212.365 39.055
C 215.455 46.809 217 55.353 217 64.688
C 217 79.744 212.867 94.612 204.601 109.292
C 196.335 123.972 184.67 138.313 169.605 152.315
/** append to final path data */
Z
Reordered path
M 111.268 31.093
C 115.823 22.924 121.557 13.394 128.468 8.905
C 136.579 3.635 145.927 1 156.511 1
C 165.086 1 173.004 2.543 180.266 5.63
C 187.605 8.716 194.017 13.12 199.502 18.842
C 205.064 24.563 209.352 31.301 212.365 39.055
C 215.455 46.809 217 55.353 217 64.688
C 217 79.744 212.867 94.612 204.601 109.292
C 196.335 123.972 184.67 138.313 169.605 152.315
C 154.541 166.243 136.811 179.567 116.416 192.29
C 115.335 193.043 114.099 193.683 112.708 194.21
C 111.318 194.737 110.082 195 109 195
C 107.996 195 106.798 194.737 105.408 194.21
C 103.94 193.683 102.665 193.043 101.584 192.29
C 81.189 179.567 63.459 166.243 48.395 152.315
C 33.33 138.313 21.665 123.972 13.399 109.292
C 5.133 94.612 1 79.744 1 64.688
C 1 55.353 2.545 46.809 5.635 39.055
C 8.648 31.301 12.936 24.563 18.498 18.842
C 23.983 13.12 30.395 8.716 37.734 5.63
C 44.996 2.543 52.914 1 61.489 1
C 72.073 1 81.421 3.635 89.532 8.905
C 96.443 13.394 102.177 22.924 106.732 31.093
C 107.743 32.834 110.257 32.834 111.268 31.093
Z
However, this approach won't work if your path contains relative (lowercase commands) or shorthand commands such as H, V (horizontal/vertical linetos ), S (cubic curveto), T (quadratic bézier curves).
JS aproach 1: Shift starting points using getPathData() (polyfilled)
getPathData() and setPathData() methods are based the w3c working draft of the SVG Paths specification to provide a standardized way of parsing <path> d attributes to an array of commands as well as applying the manipulated data once again via svgelement.setPathData(pathData) – so it's "kinda official" (as a successor/replacement for pathSegList())
Still (2023) not natively supported by major browsers you can use Jarek Foksa's polyfill
let pathData = path.getPathData({normalize:true});
inputShift.setAttribute('max', pathData.length-1);
inputShift.addEventListener("input", (e) => {
let off = +e.currentTarget.value;
if(off>=pathData.length-1){
off=0;
inputShift.value=off;
}else if(off==0 ){
off=pathData.length-1;
inputShift.value=off;
}
let pathDataShift = roundPathData(shiftSvgStartingPoint(pathData, off), 3);
path.setPathData(pathDataShift);
svgOut.value = path.getAttribute("d");
});
/**
* shift starting point
*/
function shiftSvgStartingPoint(pathData, offset) {
let pathDataL = pathData.length;
let newStartIndex = 0;
if (offset == 0) {
return pathData;
}
//exclude Z/z (closepath) command if present
let lastCommand = pathData[pathDataL - 1]["type"];
let trimRight = lastCommand.toLowerCase() == "z" ? 1 : 0;
// M start offset
newStartIndex =
offset + 1 < pathData.length - 1
? offset + 1
: pathData.length - 1 - trimRight;
// slice array to reorder
let pathDataStart = pathData.slice(newStartIndex);
let pathDataEnd = pathData.slice(0, newStartIndex);
// remove original M
pathDataEnd.shift();
let pathDataEndL = pathDataEnd.length;
let pathDataEndLastValues = pathDataEnd[pathDataEndL - 1]["values"];
let pathDataEndLastXY = [
pathDataEndLastValues[pathDataEndLastValues.length - 2],
pathDataEndLastValues[pathDataEndLastValues.length - 1]
];
//remove z(close path) from original pathdata array
if (trimRight) {
pathDataStart.pop();
pathDataEnd.push({
type: "Z",
values: []
});
}
// prepend new M command and concatenate array chunks
pathData = [
{
type: "M",
values: pathDataEndLastXY
}
]
.concat(pathDataStart)
.concat(pathDataEnd);
return pathData;
}
// just rounding to prevent awful floating point values
function roundPathData(pathData, decimals = -1) {
pathData.forEach((com, c) => {
if (decimals >= 0) {
com.values.forEach((val, v) => {
pathData[c].values[v] = +val.toFixed(decimals);
});
}
});
return pathData;
}
svg{
width:20em;
overflow:visible;
}
#path {
marker-start: url(#markerStart);
marker-mid: url(#markerRound);
stroke-width: 0.33%;
}
textarea{
display:block;
width:100%;
min-height:30em;
}
<p><label>Shift starting point <input type="range" id="inputShift" steps="1" min="0" max="100" value="0"></label></p>
<svg id="svgPrev" viewBox="1 1 216 194">
<path id="path" d="M169.6 152.3c-15.1 13.9-32.8 27.3-53.2 40c-1.1 0.7-2.3 1.4-3.7 1.9s-2.6 0.8-3.7 0.8s-2.2-0.3-3.6-0.8s-2.7-1.2-3.8-1.9c-20.4-12.7-38.1-26.1-53.2-40s-26.7-28.3-35-43s-12.4-29.6-12.4-44.6c0-9.3 1.5-17.9 4.6-25.6s7.3-14.5 12.9-20.3s11.9-10.1 19.2-13.2s15.2-4.6 23.8-4.6c10.6 0 19.9 2.6 28 7.9c6.9 4.5 12.7 14 17.2 22.2c1 1.7 3.6 1.7 4.6 0c4.5-8.2 10.3-17.7 17.2-22.2c8.1-5.3 17.4-7.9 28-7.9c8.6 0 16.5 1.5 23.8 4.6s13.7 7.5 19.2 13.2s9.9 12.5 12.9 20.3s4.6 16.3 4.6 25.6c0 15-4.1 29.9-12.4 44.6s-19.9 29-35 43z"></path>
</svg>
<h3>Output</h3>
<textarea id="svgOut" ></textarea>
<!-- markers to show commands -->
<svg id="svgMarkers" style="width:0; height:0; position:absolute; z-index:-1;float:left;">
<defs>
<marker id="markerStart" overflow="visible" viewBox="0 0 10 10" refX="5" refY="5" markerUnits="strokeWidth" markerWidth="10" markerHeight="10" orient="auto-start-reverse">
<circle cx="5" cy="5" r="10" fill="green"></circle>
<marker id="markerRound" overflow="visible" viewBox="0 0 10 10" refX="5" refY="5" markerUnits="strokeWidth" markerWidth="10" markerHeight="10" orient="auto-start-reverse">
<circle cx="5" cy="5" r="2.5" fill="red"></circle>
</marker>
</defs>
</svg>
<script src="https://cdn.jsdelivr.net/npm/path-data-polyfill#1.0.4/path-data-polyfill.min.js"></script>
How it works
parse the <path> d attribute to an array of commands
convert them to absolute coordinates via getPathData({normalize:true})
this option also converts shorthands like v,h,s and quadratic commands to cubic q, t as well as arcto commands a! So it's a rather "aggresive/lossy" conversion.
basically just splitting the pathData to array chunks and sorting (with the aforementioned changes for new preceeding M commands)
JS aproach 2: Retain Q, A commands (also based on getPathdata())
In this case you'll need a more advanced normalizing.
convert to all absolute command coordinates
normalize shorthand commands to their longhand counterpart such as s => c, t => q, v, h => l
let pathData = pathDataToLonghands(path.getPathData());
inputShift.setAttribute('max', pathData.length - 1);
inputShift.addEventListener("input", (e) => {
let off = +e.currentTarget.value;
if (off >= pathData.length - 1) {
off = 0;
inputShift.value = off;
} else if (off == 0) {
off = pathData.length - 1;
inputShift.value = off;
}
let pathDataShift = shiftSvgStartingPoint(pathData, off);
pathDataShift = roundPathData(pathDataShift, 3)
path.setPathData(pathDataShift);
svgOut.value = path.getAttribute("d");
});
/**
* shift starting point
*/
function shiftSvgStartingPoint(pathData, offset) {
let pathDataL = pathData.length;
let newStartIndex = 0;
if (offset == 0) {
return pathData;
}
//exclude Z/z (closepath) command if present
let lastCommand = pathData[pathDataL - 1]["type"];
let trimRight = lastCommand.toLowerCase() == "z" ? 1 : 0;
// M start offset
newStartIndex =
offset + 1 < pathData.length - 1 ?
offset + 1 :
pathData.length - 1 - trimRight;
// slice array to reorder
let pathDataStart = pathData.slice(newStartIndex);
let pathDataEnd = pathData.slice(0, newStartIndex);
// remove original M
pathDataEnd.shift();
let pathDataEndL = pathDataEnd.length;
let pathDataEndLastValues = pathDataEnd[pathDataEndL - 1]["values"];
let pathDataEndLastXY = [
pathDataEndLastValues[pathDataEndLastValues.length - 2],
pathDataEndLastValues[pathDataEndLastValues.length - 1]
];
//remove z(close path) from original pathdata array
if (trimRight) {
pathDataStart.pop();
pathDataEnd.push({
type: "Z",
values: []
});
}
// prepend new M command and concatenate array chunks
pathData = [{
type: "M",
values: pathDataEndLastXY
}]
.concat(pathDataStart)
.concat(pathDataEnd);
return pathData;
}
/**
* decompose/convert shorthands to "longhand" commands:
* H, V, S, T => L, L, C, Q
* reversed method: pathDataToShorthands()
*/
function pathDataToLonghands(pathData) {
pathData = pathDataToAbsolute(pathData);
let pathDataLonghand = [];
let comPrev = {
type: "M",
values: pathData[0].values
};
pathDataLonghand.push(comPrev);
for (let i = 1; i < pathData.length; i++) {
let com = pathData[i];
let type = com.type;
let values = com.values;
let valuesL = values.length;
let valuesPrev = comPrev.values;
let valuesPrevL = valuesPrev.length;
let [x, y] = [values[valuesL - 2], values[valuesL - 1]];
let cp1X, cp1Y, cpN1X, cpN1Y, cpN2X, cpN2Y, cp2X, cp2Y;
let [prevX, prevY] = [
valuesPrev[valuesPrevL - 2],
valuesPrev[valuesPrevL - 1]
];
switch (type) {
case "H":
comPrev = {
type: "L",
values: [values[0], prevY]
};
break;
case "V":
comPrev = {
type: "L",
values: [prevX, values[0]]
};
break;
case "T":
[cp1X, cp1Y] = [valuesPrev[0], valuesPrev[1]];
[prevX, prevY] = [
valuesPrev[valuesPrevL - 2],
valuesPrev[valuesPrevL - 1]
];
// new control point
cpN1X = prevX + (prevX - cp1X);
cpN1Y = prevY + (prevY - cp1Y);
comPrev = {
type: "Q",
values: [cpN1X, cpN1Y, x, y]
};
break;
case "S":
[cp1X, cp1Y] = [valuesPrev[0], valuesPrev[1]];
[cp2X, cp2Y] =
valuesPrevL > 2 ? [valuesPrev[2], valuesPrev[3]] : [valuesPrev[0], valuesPrev[1]];
[prevX, prevY] = [
valuesPrev[valuesPrevL - 2],
valuesPrev[valuesPrevL - 1]
];
// new control points
cpN1X = 2 * prevX - cp2X;
cpN1Y = 2 * prevY - cp2Y;
cpN2X = values[0];
cpN2Y = values[1];
comPrev = {
type: "C",
values: [cpN1X, cpN1Y, cpN2X, cpN2Y, x, y]
};
break;
default:
comPrev = {
type: type,
values: values
};
}
pathDataLonghand.push(comPrev);
}
return pathDataLonghand;
}
/**
* This is just a port of Dmitry Baranovskiy's
* pathToRelative/Absolute methods used in snap.svg
* https://github.com/adobe-webplatform/Snap.svg/
*/
function pathDataToAbsolute(pathData, decimals = -1) {
let M = pathData[0].values;
let x = M[0],
y = M[1],
mx = x,
my = y;
// loop through commands
for (let i = 1; i < pathData.length; i++) {
let cmd = pathData[i];
let type = cmd.type;
let typeAbs = type.toUpperCase();
let values = cmd.values;
if (type != typeAbs) {
type = typeAbs;
cmd.type = type;
// check current command types
switch (typeAbs) {
case "A":
values[5] = +(values[5] + x);
values[6] = +(values[6] + y);
break;
case "V":
values[0] = +(values[0] + y);
break;
case "H":
values[0] = +(values[0] + x);
break;
case "M":
mx = +values[0] + x;
my = +values[1] + y;
default:
// other commands
if (values.length) {
for (let v = 0; v < values.length; v++) {
// even value indices are y coordinates
values[v] = values[v] + (v % 2 ? y : x);
}
}
}
}
// is already absolute
let vLen = values.length;
switch (type) {
case "Z":
x = +mx;
y = +my;
break;
case "H":
x = values[0];
break;
case "V":
y = values[0];
break;
case "M":
mx = values[vLen - 2];
my = values[vLen - 1];
default:
x = values[vLen - 2];
y = values[vLen - 1];
}
}
// round coordinates
if (decimals >= 0) {
pathData = roundPathData(pathData, decimals);
}
return pathData;
}
// just rounding to prevent awful floating point values
function roundPathData(pathData, decimals = -1) {
pathData.forEach((com, c) => {
if (decimals >= 0) {
com.values.forEach((val, v) => {
pathData[c].values[v] = +val.toFixed(decimals);
});
}
});
return pathData;
}
svg {
width: 20em;
overflow: visible;
}
#path {
marker-start: url(#markerStart);
marker-mid: url(#markerRound);
stroke-width: 0.33%;
}
textarea {
display: block;
width: 100%;
min-height: 30em;
}
<p><label>Shift starting point <input type="range" id="inputShift" steps="1" min="0" max="100" value="0"></label></p>
<svg id="svgPrev" viewBox="1 1 216 194">
<path id="path" d="
M 50 0
Q 36.4 0 24.8 6.8
t -18 18
t -6.8 25.2
C 0 63.8 5.6 76.3 14.65 85.35
s 21.55 14.65 35.35 14.65
A 50 50 0 0 0100 50
h -12.5
v -25
H 50
V 0
z "></path>
</svg>
<h3>Output</h3>
<textarea id="svgOut"></textarea>
<!-- markers to show commands -->
<svg id="svgMarkers" style="width:0; height:0; position:absolute; z-index:-1;float:left;">
<defs>
<marker id="markerStart" overflow="visible" viewBox="0 0 10 10" refX="5" refY="5" markerUnits="strokeWidth" markerWidth="10" markerHeight="10" orient="auto-start-reverse">
<circle cx="5" cy="5" r="5" fill="green"></circle>
<marker id="markerRound" overflow="visible" viewBox="0 0 10 10" refX="5" refY="5" markerUnits="strokeWidth" markerWidth="10" markerHeight="10" orient="auto-start-reverse">
<circle cx="5" cy="5" r="2.5" fill="red"></circle>
</marker>
</defs>
</svg>
<script src="https://cdn.jsdelivr.net/npm/path-data-polyfill#1.0.4/path-data-polyfill.min.js"></script>
You might try my codepen example path direction and starting point sanitizer
Alternative: stroke-dashoffset
Besides, you could also use a stroke-dashoffset as described here:
"How to change start point of svg line animation"
body {
font-family: sans-serif;
margin:1em;
}
svg {
overflow: visible;
height:75vmin;
width:auto;
}
svg path {
stroke-dashoffset: 260;
animation: anim 3s ease-in-out forwards infinite;
}
#keyframes anim {
0% {
stroke-dasharray: 0 672;
}
100% {
stroke-dasharray: 672 0;
}
}
<svg width="218" height="196" viewBox="0 0 218 196" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M169.605 152.315C154.541 166.243 136.811 179.567 116.416 192.29C115.335 193.043 114.099 193.683 112.708 194.21C111.318 194.737 110.082 195 109 195C107.996 195 106.798 194.737 105.408 194.21C103.94 193.683 102.665 193.043 101.584 192.29C81.1888 179.567 63.4592 166.243 48.3948 152.315C33.3304 138.313 21.6652 123.972 13.3991 109.292C5.13304 94.6123 1 79.7443 1 64.688C1 55.3531 2.54506 46.8087 5.63519 39.0547C8.64807 31.3007 12.9356 24.5631 18.4979 18.8417C23.9828 13.1203 30.3948 8.71634 37.7339 5.6298C44.9957 2.54327 52.9142 1 61.4893 1C72.073 1 81.4206 3.63485 89.5322 8.90454C96.4432 13.3943 102.177 22.9236 106.732 31.0933C107.743 32.8343 110.257 32.8343 111.268 31.0933C115.823 22.9236 121.557 13.3943 128.468 8.90454C136.579 3.63485 145.927 1 156.511 1C165.086 1 173.004 2.54327 180.266 5.6298C187.605 8.71634 194.017 13.1203 199.502 18.8417C205.064 24.5631 209.352 31.3007 212.365 39.0547C215.455 46.8087 217 55.3531 217 64.688C217 79.7443 212.867 94.6123 204.601 109.292C196.335 123.972 184.67 138.313 169.605 152.315Z" stroke="black" stroke-linecap="round" stroke-linejoin="round"></path>
</svg>
I would like to change the CSS style of the grid line in which the value is zero.
How can I do?
My chart:
I would like to create a chart like this:
The easiest thing would be to change the Marker color based on value of the point. You can set the color/background/border color property of the series markers to a function instead of a string and return desired color:
$("#chart").kendoChart({
series: [ {
type: "line",
color: "#82D225",
markers: {
visible: true,
border: {
color: function(point){return point.value<=0 ? "red" : "#82D225"; }
}
},
data: [3300, 3200, 0, -300, -100, 200],
}]
});
Here is a DEMO
Changing the color of the line as opposed to the markers is more complicated. You can apply a gradient to the line with color for above 0 and color for below 0.
Define an SVG gradient anywhere on the page:
<div style="height: 0px;">
<svg>
<defs>
<linearGradient id="theGrad" x1="0" x2="0" y1="0" y2="1">
<stop stop-color="#82D225" offset="0%"></stop>
<stop id="stop1" stop-color="#82D225" offset="40%"></stop>
<stop id="stop2" stop-color="red" offset="40%"></stop>
<stop stop-color="red" offset="100%"></stop>
</linearGradient>
</defs>
</svg>
</div>
Then in script find min and max of your dataset and update the gradient stop offsets:
var data = [3300, 3200, 0, -300, -100, 200];
var max = Math.max(...data);
var min = Math.min(...data);
var color = "url(#theGrad)";
var NeedGradient = max > 0 && min <= 0;
if (NeedGradient){
var range = max - min;
var stop = (max - 0) * 100 / range;
stop = Math.round(stop);
$("#stop1, #stop2").attr("offset", stop + "%");
} else {
max <=0 ? color = "red" : color = "#82D225";
}
Finally, on chart render, apply the gradient to the line:
$("#chart").kendoChart({
series: [ {
type: "line",
color: function(point){return point.value<=0 ? "red" : "#82D225"; },
data: data,
}],
render: function(e) {
$('#chart svg g [clip-path="url(#kdef2)"] path').css("stroke", color);
}
});
Here is another DEMO
I have an inline svg with transparent fill and a dark stroke. When it's hoverd, I want to fill it with the dark color.
The problem is that when I increase the stroke-width to make the stoke more visible, the stroke goes out the width box of the element like in the screenshot below.
So how to give the svg element an extra space for the stroke to grow?
svg {
width: 10rem;
fill: transparent;
stroke: #222;
stroke-width: 30;
}
svg:hover {
fill: #222;
}
<svg
viewBox="0 -28 512.00002 512"
xmlns="http://www.w3.org/2000/svg"
>
<path d="m471.382812 44.578125c-26.503906-28.746094-62.871093-44.578125-102.410156-44.578125-29.554687 0-56.621094 9.34375-80.449218 27.769531-12.023438 9.300781-22.917969 20.679688-32.523438 33.960938-9.601562-13.277344-20.5-24.660157-32.527344-33.960938-23.824218-18.425781-50.890625-27.769531-80.445312-27.769531-39.539063 0-75.910156 15.832031-102.414063 44.578125-26.1875 28.410156-40.613281 67.222656-40.613281 109.292969 0 43.300781 16.136719 82.9375 50.78125 124.742187 30.992188 37.394531 75.535156 75.355469 127.117188 119.3125 17.613281 15.011719 37.578124 32.027344 58.308593 50.152344 5.476563 4.796875 12.503907 7.4375 19.792969 7.4375 7.285156 0 14.316406-2.640625 19.785156-7.429687 20.730469-18.128907 40.707032-35.152344 58.328125-50.171876 51.574219-43.949218 96.117188-81.90625 127.109375-119.304687 34.644532-41.800781 50.777344-81.4375 50.777344-124.742187 0-42.066407-14.425781-80.878907-40.617188-109.289063zm0 0"/>
</svg>
Adjust the viewBox attribute:
viewBox="-25 -53 562.00002 562"
Adds space for a 25px margin.
Note that you have to decrease the top/left coordinates by 25 and increase the bottom/right coordinates by double the amount as your width/height must cater for 2 times the margin width.
svg {
width: 10rem;
fill: transparent;
stroke: #222;
stroke-width: 30;
}
svg:hover {
fill: #222;
}
<svg
viewBox="-25 -53 562.00002 562"
xmlns="http://www.w3.org/2000/svg"
>
<path d="m471.382812 44.578125c-26.503906-28.746094-62.871093-44.578125-102.410156-44.578125-29.554687 0-56.621094 9.34375-80.449218 27.769531-12.023438 9.300781-22.917969 20.679688-32.523438 33.960938-9.601562-13.277344-20.5-24.660157-32.527344-33.960938-23.824218-18.425781-50.890625-27.769531-80.445312-27.769531-39.539063 0-75.910156 15.832031-102.414063 44.578125-26.1875 28.410156-40.613281 67.222656-40.613281 109.292969 0 43.300781 16.136719 82.9375 50.78125 124.742187 30.992188 37.394531 75.535156 75.355469 127.117188 119.3125 17.613281 15.011719 37.578124 32.027344 58.308593 50.152344 5.476563 4.796875 12.503907 7.4375 19.792969 7.4375 7.285156 0 14.316406-2.640625 19.785156-7.429687 20.730469-18.128907 40.707032-35.152344 58.328125-50.171876 51.574219-43.949218 96.117188-81.90625 127.109375-119.304687 34.644532-41.800781 50.777344-81.4375 50.777344-124.742187 0-42.066407-14.425781-80.878907-40.617188-109.289063zm0 0"/>
</svg>
In a QML application I have an item that is moving around the screen (not rotating). I want to display an indicator that rotates around this item, pointing away from the center of the screen, a fixed distance away from the center of the item.
The following simplified QML application performs this goal, by making the indicator a child of the item, and translating it to the desired location. However, when I try to rotate the indicator (the commented-out code) I cannot find any values for origin.x and .y that work. It feels like the QML scene graph calculates X/Y positioning in a way unlike any I've experienced.
import QtQuick 2.7
import QtQuick.Window 2.2
Window {
id: win
visible:true; width:600; height:300
property real padding: 50
property real angle: 0
property real _rads: angle * Math.PI/180
Timer {
interval:50; running:true; repeat:true
onTriggered:win.angle = (new Date/50) % 360
}
Rectangle {
id:object; color:'blue'
width:50; height:width
property real xOffset: Math.cos(_rads)
property real yOffset: Math.sin(_rads)
x: win.width/2 + xOffset * (win.width/2 - padding*2)
y: win.height/2 + yOffset * (win.height/2 - padding*2)
Rectangle {
id:indicator; color:'red'
property real centerOffset: 40
width:10; height:width*2
x: object.width/2 + object.xOffset * centerOffset - width/2
y: object.height/2 + object.yOffset * centerOffset - height/2
// transform: Rotation { origin.x:0; origin.y:0; angle:win.angle }
}
}
}
I've tried making the indicator not be a child of the item. I've tried using Translate in the transform stack instead of X/Y positions. All of them result in amusing-but-incorrect rotations.
How can I simply rotate the indicator around its own center, or otherwise achieve my goal?
You might think of it as a clock and build yourself a clockhand.
import QtQuick 2.7
import QtQuick.Window 2.2
Window {
id: win
visible:true; width:600; height:300
property real padding: 50
property real angle: 0
property real _rads: angle * Math.PI/180
Timer {
interval:50; running:true; repeat:true
onTriggered:win.angle = (new Date/50) % 360
}
Rectangle {
id:object; color:'blue'
width:50; height:width
property real xOffset: Math.cos(_rads)
property real yOffset: Math.sin(_rads)
x: win.width/2 + xOffset * (win.width/2 - padding*2)
y: win.height/2 + yOffset * (win.height/2 - padding*2)
Text {
width: 250
height: 250
x: -100
y: -100
text: '▲'
color: 'red'
font.pixelSize: 20
horizontalAlignment: Qt.AlignHCenter
verticalAlignment: Qt.AlignTop
transform: Rotation {
angle: win.angle + 90
origin.x: 125
origin.y: 125
}
}
Text {
x: 15
y: -125
width: 20
height: 20
text: '▲'
color: 'red'
font.pixelSize: 20
horizontalAlignment: Qt.AlignHCenter
verticalAlignment: Qt.AlignVCenter
transform: Rotation {
angle: win.angle + 90
origin.x: 10
origin.y: 150
}
}
Rectangle {
id: clockhand
width: 1
height: 100
color: 'black'
anchors {
centerIn: parent
}
rotation: win.angle + 90
Text {
text: '▲'
color: 'red'
anchors {
horizontalCenter: parent.horizontalCenter
bottom: parent.top
bottomMargin: -5
}
font.pixelSize: 20
}
}
}
}
Just turn the Clockhand into an Item and remove the color, to make it invisible.
Similar to #derM I have a solution that makes use of Item.rotation. However, with regard to the rest, I have generalized it by avoiding cos and sin since they are not required.
To demonstrate I create 3 SVG images crosshairs.svg, marker.svg, and triangle.svg. I placed crosshairs.svg in the center of the screen. I animate marker.svg by making it bounce around the screen "pong" style. Now, the secret sauce is the placement and orientation of triangle.svg.
To place an offset a triangle relative to the marker. I put the triangle Image inside an Item component. The Item component has no area, it merely has x, y, and rotation set. The Image component is placed relative to the Item and we need to compute its relative placement.
Because the triangle.svg is 16x16, I placed it at (20, -8) relative to the marker. If I had chosen (-8, -8) the SVG would sit on top of the marker. Because I put it at (20, -8) it puts it beyond the marker. Lastly, I compute the rotation using Math.atan2() on the vector between the marker and the crosshairs:
Item {
x: marker.x
y: marker.y
rotation: Math.atan2(
marker.y - crosshairs.y,
marker.x - crosshairs.x
) * 180 / Math.PI
Image {
source: "triangle.svg"
x: 20
y: -8
//cache: false
}
}
Here's a full working demo:
import QtQuick 2.15
import QtQuick.Controls 2.15
Page {
Timer {
interval: 50
running: true
repeat: true
onTriggered: marker.animate()
}
Rectangle {
id: frame
anchors.centerIn: parent
width: parent.width / 2
height: parent.height / 2
color: "#ffe"
border.color: "grey"
clip: true
Image {
id: crosshairs
anchors.centerIn: parent
source: "crosshair.svg"
//cache: false
}
Item {
id: marker
x: parent.width/2
y: parent.height/2
property int dx: 2
property int dy: 2
property int size: 20
Image {
anchors.centerIn: parent
source: "marker.svg"
//cache: false
}
function animate() {
x += dx;
y += dy;
if (x + size / 2 >= parent.width || x - size / 2 <= 0) {
dx = -dx;
x += dx;
x += dx;
}
if (y + size / 2 >= parent.height || y - size / 2 <= 0) {
dy = -dy;
py += dy;
py += dy;
}
}
}
Item {
x: marker.x
y: marker.y
rotation: Math.atan2(
marker.y - crosshairs.y,
marker.x - crosshairs.x
) * 180 / Math.PI
Image {
source: "triangle.svg"
x: 20
y: -8
//cache: false
}
}
}
}
//crosshair.svg
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 12 12"><path stroke="grey" d="M6 0L6 12M 0 6L 12 6"/></svg>
//marker.svg
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><path stroke="grey" fill="#ffe" d="M0 0 L20 0 L20 20 L0 20 z"/></svg>
//triangle.svg
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path stroke="grey" fill="red" d="M16 8 L0 0 L 0 16z"/></svg>
You can Try it Online!
How do you rotate, using Matrix4x4 transform, a QML item around another axis than z, with the center of the item as origin of the transformation?
To rotate around the y axis with (0,0) as origin, I tried naively:
Image {
source: "..."
width: 100
height: 100
transform: Matrix4x4 {
property real a: Math.PI / 4
matrix: Qt.matrix4x4(
Math.cos(a), 0, -Math.sin(a), 0,
0, 1, 0, 0,
Math.sin(a), 0, Math.cos(a), 0,
0, 0, 0, 1)
}
}
As a result, I get a cut width item whereas I am looking for a perspective effect.
Can anyone explain how the transformation matrix of QML items works?
Here's comment from Unity 8:
// Rotating 3 times at top/bottom because that increases the perspective.
// This is a hack, but as QML does not support real 3D coordinates
// getting a higher perspective can only be done by a hack. This is the most
// readable/understandable one I could come up with.
Link to source code: https://github.com/ubports/unity8/blob/xenial/qml/Launcher/LauncherDelegate.qml#L287
The thing to be careful about is there appears to be clipping around z >= 0, where the object is technically in front of your monitor. To ensure the object stays on screen, you need to translate it so that it remains behind the monitor. In the following example, because I know the object is 300x300 and that it is centered, I know that I only need to push it into the screen by 150 pixels.
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
Page {
property real xrotation: 0
property real yrotation: 0
property real zrotation: 0
Image {
width: 300
height: 300
anchors.centerIn: parent
source: "image-32.svg"
sourceSize: Qt.size(width, height)
transform: Matrix4x4 {
matrix: (() => {
let m = Qt.matrix4x4();
m.translate(Qt.vector3d(150, 150, -150));
m.rotate(zrotation, Qt.vector3d(0, 0, 1));
m.rotate(yrotation, Qt.vector3d(0, 1, 0));
m.rotate(xrotation, Qt.vector3d(1, 0, 0));
m.translate(Qt.vector3d(-150, -150, 0));
return m;
})()
}
}
Timer {
running: xbutton.pressed
repeat: true
interval: 100
onTriggered: xrotation += 5
}
Timer {
running: ybutton.pressed
repeat: true
interval: 100
onTriggered: yrotation += 5
}
Timer {
running: zbutton.pressed
repeat: true
interval: 100
onTriggered: zrotation += 5
}
footer: Frame {
RowLayout {
width: parent.width
Button {
id: xbutton
text: "X"
}
Button {
id: ybutton
text: "Y"
}
Button {
id: zbutton
text: "Z"
}
Button {
text: "Reset"
onClicked: {
xrotation = yrotation = zrotation = 0;
}
}
}
}
}
// image-32.svg
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"><path d="M2 5v22h28V5zm27 21H3v-5.474l4.401-3.5 1.198.567L14 13.106l5 4.531 3.506-3.123L29 20.39zm-5.997-12.422a.652.652 0 0 0-.926-.033L19 16.293l-4.554-4.131a.652.652 0 0 0-.857-.013L8.45 16.417l-.826-.391a.642.642 0 0 0-.72.117L3 19.248V6h26v13.082zM19 8a2 2 0 1 0 2 2 2.002 2.002 0 0 0-2-2zm0 3a1 1 0 1 1 1-1 1.001 1.001 0 0 1-1 1z"/><path fill="none" d="M0 0h32v32H0z"/></svg>
You can Try it Online!