4814052 [rkeene@sledge /home/rkeene/tmp]$ cat -n line-chart.sh
  1 #! /usr/bin/env bash
  2 
  3 function timeSeriesToLineSVG() {
  4     local series
  5     local seriesIdx color label
  6     local awkVars awkFuncs
  7     local lowTime highTime lowValue highValue
  8     local seriesLowTime seriesHighTime seriesLowValue seriesHighValue
  9     local graphWidth graphHeight offsetLeft offsetRight offsetTop offsetBottom
 10     local graphLabelX graphLabelY
 11     local imageWidth imageHeight
 12     local svgStyleSheet
 13     local graphColors
 14     local ticksX ticksY defaultTicksX defaultTicksY minTicksX minTicksY
 15 
 16     # Determine graph parameters
 17     graphWidth=500
 18     graphHeight=150
 19     graphLabelX='Time'
 20     graphLabelY='Value'
 21     offsetLeft=150
 22     offsetRight=20
 23     offsetTop=10
 24     offsetBottom=40
 25     defaultTicksX=10
 26     defaultTicksY=10
 27     minTicksX=4
 28     minTicksY=2
 29     graphColors=(
 30         '#ffa500'
 31         '#9acd32'
 32         '#f1ca3a'
 33         '#e41a1c'
 34         '#1c91c0'
 35         '#43459d'
 36         '#984ea3'
 37         '#ff7f00'
 38         '#ffff33'
 39         '#a65628'
 40         '#f781bf'
 41     )
 42 
 43     # Get graph options
 44     while true; do
 45         case "$1" in
 46             -width)
 47                 shift
 48                 graphWidth="$1"
 49                 shift
 50                 ;;
 51             -height)
 52                 shift
 53                 graphHeight="$1"
 54                 shift
 55                 ;;
 56             -labelx)
 57                 shift
 58                 graphLabelX="$1"
 59                 shift
 60                 ;;
 61             -labely)
 62                 shift
 63                 graphLabelY="$1"
 64                 shift
 65                 ;;
 66             -style)
 67                 shift
 68                 svgStyleSheet="$1"
 69                 shift
 70                 ;;
 71             -colors)
 72                 shift
 73                 graphColors=($1)
 74                 shift
 75                 ;;
 76             -ticksx)
 77                 shift
 78                 ticksX="$1"
 79                 shift
 80                 ;;
 81             -ticksy)
 82                 ticksY="$1"
 83                 ;;
 84             *)
 85                 break
 86                 ;;
 87         esac
 88     done
 89 
 90     # Helper functions for awk
 91     awkFuncs='
 92         function relativeToAbsoluteX(relPositionX) {
 93             relPositionX = int(graphWidth * relPositionX + 0.5);
 94             relPositionX += offsetLeft;
 95             return(relPositionX);
 96         }
 97 
 98         function relativeToAbsoluteY(relPositionY) {
 99             relPositionY = int(graphHeight * relPositionY + 0.5);
100             relPositionY = (relPositionY - graphHeight) * -1;
101             relPositionY += offsetTop;
102             return(relPositionY);
103         }
104     '
105 
106     # Determine ranges for all series
107     for series in "$@"; do
108         series="$(echo "${series}" | cut -f 2 -d =)"
109 
110         read -r seriesLowTime seriesHighTime seriesLowValue seriesHighValue seriesElements < <(echo "${series}" | tr ' '
	$'\n' | awk -F , "${awkVars[@]}" "${awkFuncs}"'
111             BEGIN{
112                 dataIndex = 0;
113             }
114 
115             {
116                 time = $1;
117                 value = $2;
118 
119                 if (dataIndex == 0) {
120                     lowValue = value;
121                     highValue = value;
122                     lowTime = time;
123                     highTime = time;
124                 }
125 
126                 if (value < lowValue) {
127                     lowValue = value;
128                 }
129 
130                 if (value > highValue) {
131                     highValue = value;
132                 
133                 }
134 
135                 if (time < lowTime) {
136                     lowTime = time;
137                 }
138                 if (time > highTime) {
139                     highTime = time;
140                 }
141 
142                 dataIndex++;
143             }
144 
145             END{
146                 print lowTime, highTime, lowValue, highValue, dataIndex
147             }
148         ')
149 
150         if [ -z "${lowTime}" ]; then
151             lowTime="${seriesLowTime}"
152             highTime="${seriesHighTime}"
153             lowValue="${seriesLowValue}"
154             highValue="${seriesHighValue}"
155             highSeriesElements="${seriesElements}"
156         else
157             read -r lowTime highTime lowValue highValue highSeriesElements < <(awk \
158                 -v highTime="${highTime}" -v lowTime="${lowTime}" -v lowValue="${lowValue}" -v highValue="${highValue}"
	\
159                 -v seriesHighTime="${seriesHighTime}" -v seriesLowTime="${seriesLowTime}" -v
	seriesLowValue="${seriesLowValue}" -v seriesHighValue="${seriesHighValue}" \
160                 -v seriesElements="${seriesElements}" -v highSeriesElements="${highSeriesElements}" '
161                     END{
162                         if (seriesLowTime < lowTime) {
163                             lowTime = seriesLowTime;
164                         }
165 
166                         if (seriesHighTime > highTime) {
167                             highTime = seriesHighTime;
168                         }
169 
170                         if (seriesLowValue < lowValue) {
171                             lowValue = seriesLowValue;
172                         }
173 
174                         if (seriesHighValue > highValue) {
175                             highValue = seriesHighValue;
176                         }
177 
178                         if (seriesElements > highSeriesElements) {
179                             highSeriesElements = seriesElements;
180                         }
181 
182                         print lowTime, highTime, lowValue, highValue, highSeriesElements;
183                     }
184                 ' </dev/null)
185             
186         fi
187     done
188 
189     if [ "${highTime}" = "${lowTime}" ]; then
190         highTime=$[$highTime + 1]
191         lowTime=$[$lowTime - 1]
192     fi
193 
194     if [ "${highValue}" = "${lowValue}" ]; then
195         highValue=$[$highValue + 1]
196     fi
197 
198     if [ -z "${ticksX}" ]; then
199         highSeriesElements=$[$highSeriesElements - 1]
200 
201         ticksX="${defaultTicksX}"
202 
203         if [ "${highSeriesElements}" -lt "${defaultTicksX}" ]; then
204             ticksX="${highSeriesElements}"
205         fi
206 
207         if [ "${ticksX}" -lt "${minTicksX}" ]; then
208             ticksX="${minTicksX}"
209         fi
210     fi
211 
212     if [ -z "${ticksY}" ]; then
213         ticksY="${defaultTicksY}"
214     fi
215 
216     # Adjust the value ranges so the chart doesn't just ride at the top and bottom
217     read -r highValue lowValue < <(awk -v highValue="${highValue}" -v lowValue="${lowValue}" '
218         END{
219             origLowValue = lowValue;
220 
221             adjustValue = (highValue - lowValue) * 0.05;
222             if (adjustValue > 1) {
223                 adjustValue = int(adjustValue)
224             }
225 
226             if (adjustValue < 0.00001) {
227                 adjustValue = 1;
228             }
229 
230             highValue  += adjustValue;
231             lowValue   -= adjustValue;
232 
233             if (lowValue < 0) {
234                 if (origLowValue >= 0) {
235                     lowValue = 0;
236                 }
237             }
238 
239             print highValue, lowValue;
240         }
241     ' </dev/null)
242 
243     # Emit SVG
244     imageWidth=$[${offsetLeft} + ${offsetRight} + ${graphWidth}]
245     imageHeight=$[${offsetTop} + ${offsetBottom} + ${graphHeight}]
246     awkVars=(
247         -v lowTime="${lowTime}" -v highTime="${highTime}" -v lowValue="${lowValue}" -v highValue="${highValue}"
248         -v offsetLeft="${offsetLeft}" -v offsetRight="${offsetRight}"
249         -v offsetTop="${offsetTop}" -v offsetBottom="${offsetBottom}"
250         -v graphWidth="${graphWidth}" -v graphHeight="${graphHeight}"
251         -v graphLabelX="${graphLabelX}" -v graphLabelY="${graphLabelY}"
252         -v ticksX="${ticksX}" -v ticksY="${ticksY}"
253     )
254     echo '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 '"${imageWidth}" "${imageHeight}"'"
	height="'"${imageHeight}"'px" width="'"${imageWidth}"'px">'
255     if [ -n "${svgStyleSheet}" ]; then
256         echo "<style><![CDATA[${svgStyleSheet}]]></style>"
257     fi
258 
259     ## Emit the data for each series
260     seriesIdx='-1'
261     for series in "$@"; do
262         ((seriesIdx++)) || :
263         color="${graphColors[${seriesIdx}]}"
264         label="$(echo "${series}" | cut -f 1 -d =)"
265         series="$(echo "${series}" | cut -f 2 -d = | tr ' ' $'\n' | sort -t , -k 1n,1n)"
266 
267         ### Draw the legend
268         awk "${awkVars[@]}" -v label="${label}" -v color="${color}" -v seriesIdx="${seriesIdx}" "${awkFuncs}"'
269             END{
270                 print "<polyline fill=\"" color "\" stroke=\"#dedede\" stroke-width=\"1\" points=\""
271 
272                 logoSize = 10;
273                 spacingSize = 5;
274 
275                 /* Border */
276                 legendMarkerX = logoSize;
277                 legendMarkerY = offsetTop + logoSize;
278 
279                 legendMarkerY += seriesIdx * 2 * logoSize 
280 
281                 print legendMarkerX "," legendMarkerY
282                 print legendMarkerX "," legendMarkerY + logoSize
283                 print legendMarkerX + logoSize "," legendMarkerY + logoSize
284                 print legendMarkerX + logoSize "," legendMarkerY
285                 print legendMarkerX "," legendMarkerY
286 
287                 print "\"/>"
288                 print "<text class=\"labelText\" x=\"" (legendMarkerX + logoSize + spacingSize) "\" y=\"" legendMarkerY
	+ (logoSize / 2) "\" dominant-baseline=\"central\">" label "</text>"
289             }
290         ' </dev/null
291 
292         ### Plot the data
293         #### If there is only one data point in the series, draw a circle at the point
294         seriesElementsTwo="$(echo "${series}" | head -n 2 | wc -l)"
295         if [ "${seriesElementsTwo}" = '1' ]; then
296             echo "${series}" | awk -v color="${color}" -F , "${awkVars[@]}" "${awkFuncs}"'
297                 {
298                     time = $1
299                     positionX = (time - lowTime) / (highTime - lowTime);
300                     positionX = relativeToAbsoluteX(positionX);
301 
302                     value = $2
303                     positionY = (value - lowValue) / (highValue - lowValue);
304                     positionY = relativeToAbsoluteY(positionY);
305 
306                     print "<circle cx=\"" positionX "\" cy=\"" positionY "\" r=\"5\" stroke-width=\"3\" stroke=\"" color
	"\" fill=\"none\"/>"
307                 }
308             '
309             continue
310         fi
311 
312         echo "<polyline stroke-linejoin=\"round\" stroke-linecap=\"round\" fill=\"none\" stroke=\"${color}\"
	stroke-width=\"3\" points=\""
313         echo "${series}" | awk -F , "${awkVars[@]}" "${awkFuncs}"'
314             BEGIN{
315                 dataIndex = 0;
316             }
317 
318             {
319                 time = $1;
320                 value = $2;
321 
322                 seriesTime[dataIndex]  = time;
323                 seriesValue[dataIndex] = value;
324 
325                 dataIndex++;
326             }
327 
328             END{
329                 for (idx in seriesTime) {
330                     time = seriesTime[idx];
331                     positionX = (time - lowTime) / (highTime - lowTime);
332                     positionX = relativeToAbsoluteX(positionX);
333 
334                     value = seriesValue[idx];
335                     positionY = (value - lowValue) / (highValue - lowValue);
336                     positionY = relativeToAbsoluteY(positionY);
337 
338                     print positionX "," positionY;
339                 }
340             }
341         '
342         echo '"/>'
343     done
344 
345     ## Emit the grid
346     awk "${awkVars[@]}" "${awkFuncs}"'
347         END{
348             # Various parameters (may be exposed later)
349             textHeight = 14;
350             spacingSize = 13;
351             tickSizeX = 6;
352             tickSizeY = 6;
353 
354             # Axis bars
355             axisHalfWidth = 1;
356             print "<polyline fill=\"none\" stroke=\"#aaaaaa\" stroke-width=\"" axisHalfWidth * 2 "\" points=\""
357             print relativeToAbsoluteX(0) - axisHalfWidth , relativeToAbsoluteY(1) - axisHalfWidth;
358             print relativeToAbsoluteX(0) - axisHalfWidth, relativeToAbsoluteY(0) + axisHalfWidth;
359             print relativeToAbsoluteX(1) + axisHalfWidth, relativeToAbsoluteY(0) + axisHalfWidth;
360             print "\"/>"
361 
362             # Axis labels
363             ## X
364             positionY = relativeToAbsoluteY(0) + axisHalfWidth + textHeight + (tickSizeX / 2);
365             print "<text class=\"axisLabel\" dominant-baseline=\"hanging\" text-anchor=\"middle\" x=\""
	relativeToAbsoluteX(0.5) "\" y=\"" positionY "\">" graphLabelX "</text>"
366 
367             ## Y
368             positionX = relativeToAbsoluteX(1) + (2 * axisHalfWidth);
369             print "<text class=\"axisLabel\" style=\"writing-mode: tb; glyph-orientation-vertical: 90;\"
	dominant-baseline=\"text-after-edge\" text-anchor=\"middle\" x=\"" positionX "\" y=\"" relativeToAbsoluteY(0.5) "\">"
	graphLabelY "</text>"
370 
371             # Step ticks
372             ## X
373             lastValueXYear = ""
374             lastValueXMonthDay = ""
375             for (tick = 1; tick <= (ticksX - 1); tick++) {
376                 relValueX = (tick / ticksX)
377                 valueX = int((highTime - lowTime) * relValueX + lowTime + 0.5);
378                 dateCommandYear = "date -d @" valueX " +%Y";
379                 dateCommandMonthDay = "date -d @" valueX " +%m-%d";
380                 dateCommandYear | getline valueXYear
381                 dateCommandMonthDay | getline valueXMonthDay
382                 close(dateCommandYear);
383                 close(dateCommandMonthDay);
384 
385                 if (valueXYear == lastValueXYear) {
386                     if (valueXMonthDay == lastValueXMonthDay && lastValueXPresentation != "year") {
387                         dateCommandTime = "date -d @" valueX " +%H:%M";
388                         dateCommandTime | getline valueXTime
389                         close(dateCommandTime);
390 
391                         valueXStr = valueXTime;
392 
393                         lastValueXPresentation = "hour";
394                     } else {
395                         valueXStr = valueXMonthDay;
396 
397                         lastValueXPresentation = "month";
398                     }
399                 } else {
400                     valueXStr = valueXYear;
401 
402                     lastValueXPresentation = "year";
403                 }
404 
405                 lastValueXYear = valueXYear;
406                 lastValueXMonthDay = valueXMonthDay;
407 
408                 valueX = valueXStr;
409 
410                 positionX = relativeToAbsoluteX(relValueX);
411                 positionY = relativeToAbsoluteY(0) + axisHalfWidth;
412 
413                 print "<line x1=\"" positionX "\" x2=\"" positionX "\" y1=\"" positionY + (tickSizeX / 2) "\" y2=\""
	positionY - (tickSizeX / 2) "\" stroke=\"#000000\"/>";
414                 print "<text class=\"axisTick\" x=\"" positionX "\" y=\"" positionY + (tickSizeX / 2) + axisHalfWidth 
	"\" dominant-baseline=\"hanging\" text-anchor=\"middle\" style=\"font-size: " textHeight "px\">" valueX "</text>"
415             }
416 
417             ## Y
418 
419             lastValueY = "";
420             usePreciseValues = 0;
421             if ((highValue - lowValue) < ticksY) {
422                 usePreciseValues = 1;
423             }
424             for (tick = 1; tick <= (ticksY - 1); tick++) {
425                 relValueY = (tick / ticksY)
426                 valueY = int((highValue - lowValue) * relValueY + lowValue + 0.5);
427 
428                 if (valueY == lastValueY || usePreciseValues == 1) {
429                     valueY = (highValue - lowValue) * relValueY + lowValue;
430 
431                     usePrceiseValues = 1;
432                 }
433                 lastValueY = valueY;
434 
435                 positionX = relativeToAbsoluteX(0) - axisHalfWidth;
436                 positionY = relativeToAbsoluteY(relValueY);
437 
438                 print "<line x1=\"" positionX - (tickSizeY / 2) "\" x2=\"" positionX + (tickSizeY / 2) "\" y1=\""
	positionY "\" y2=\"" positionY "\" stroke=\"#000000\"/>";
439                 print "<text class=\"axisTick\" x=\"" positionX - (tickSizeY / 2) - (spacingSize / 3) "\" y=\""
	positionY "\" dominant-baseline=\"central\" text-anchor=\"end\" style=\"font-size: " textHeight "px\">" valueY
	"</text>"
440             }
441 
442         }
443     ' </dev/null
444 
445     echo '</svg>'
446 }
447 
448 function svgToPNG() {
449     local svg
450     local width height
451     local pngFile
452     local htmlImage
453     local tmpfileHTML tmpfilePNG
454 
455     pngFile="$1"
456 
457     svg="$(cat)"
458     if [ -z "${svg}" ]; then
459         return 0
460     fi
461 
462     read -r width height < <(echo "${svg}" | sed '
463         /<svg/{
464             h
465             s/.*width="//
466             s/".*$//
467             s/px.*$//
468             x
469             s/.*height="//
470             s/".*$//
471             s/px.*$//
472             H
473             x
474             q
475         }
476     ' | tr $'\n' ' '; echo)
477 
478     htmlImage="$(
479         echo -n '<img width="'"${width}"'" height="'"${height}"'" src="data:image/svg+xml;base64,'
480         echo "${svg}" | base64 | tr -d $'\n'
481         echo '">'
482     )"
483 
484     # Use Google Chrome to render it to a PNG if requested and possible
485     tmpfileHTML="$(mktemp -u).html"
486     (
487         echo "<html><head><style>body { margin: 0; }</style></head><body>"
488         echo "${htmlImage}"
489         echo "</body></html>"
490     ) > "${tmpfileHTML}"
491 
492     tmpfilePNG="${tmpfileHTML}.png"
493     rm -f "${tmpfilePNG}"
494     google-chrome --headless --disable-gpu --screenshot="${tmpfilePNG}" --hide-scrollbars
	--window-size="${width}x${height}" "file://${tmpfileHTML}" >/dev/null 2>/dev/null </dev/null
495 
496     if [ -s "${tmpfilePNG}" ]; then
497         if [ -n "${pngFile}" ]; then
498             mv "${tmpfilePNG}" "${pngFile}"
499         else
500             cat "${tmpfilePNG}"
501         fi
502     else
503         return 1
504     fi
505 
506     rm -f "${tmpfileHTML}" "${tmpfilePNG}"
507 
508     return 0
509 }
510 
511 outputMode="png"
512 if [ "$1" = '-svg' ]; then
513     shift
514     outputMode='svg'
515 fi
516 
517 
518 timeSeriesToLineSVG "$@" | (
519     if [ "${outputMode}" = 'svg' ]; then
520         cat
521     else
522         svgToPNG
523     fi
524 )
4814053 [rkeene@sledge /home/rkeene/tmp]$

Click here to go back to the directory listing.
Click here to download this file.
last modified: 2019-03-04 18:24:38