Pomoc Ookla speedtest na QNap

Debcu

Passing Basics
Beginner
22 Sierpień 2018
11
1
3
Łódź
QNAP
TS-x53
Ethernet
1 GbE
Witajcie,
jak w temacie, czy jest możliwość aby na QNap'ie zainstalować jakiś tester prędkości Internetu, np. Ookla speedtest?
 
Rozwiązanie to pozwala na cykliczne odpalanie (CRON), logowanie wyników do np. pliku (znacznik czasu, ping, download, upload) oraz prezentację (np. w postaci wykresów).
Możesz się podzielić know-how ? ja na ten moment mam tylko logowanie do pliku co godzine .. a wolałbym ładne wykresy ;)
Chciałem popełnić ten post jako mały tutorial, ale nie ma chyba odpowiedniego działu. ;)
Poniżej po krótce przedstawiłem dwa niezależne sposoby generowania wykresów, jeden w postaci statycznych obrazków (Python + Matplotlib), drugi w postaci interaktywnych wykresów (PHP + JavaScript). Gdybyś napotkał na jakieś ograniczenia w kwestii skonfigurowania wyglądu pod swoje potrzeby lub potrzebował wsparcia to chętnie pomogę.

Badanie szybkości łącza internetowego oraz wizualizacja wyników w postaci wykresów

1. Test łącza internetowego

W celu przeprowadzenia testu łacza internetowego, instalujemy narzędzie 'speedtest-cli' (sivel/speedtest-cli) oraz przygotowujemy skrypt (speedtest.sh), za pomocą którego będziemy dokonywali pomiarów.

Kod:
#!/bin/sh
COMMAND=$(speedtest --simple)

DATA=$(echo $COMMAND | tr "[:alpha:]+[/:]" "\n")
DATA=$(echo $DATA | tr "[:blank:]" ,)

PING=$(printf "%s" $DATA | cut -d, -f 1)
DOWNLOAD=$(printf "%s" $DATA | cut -d, -f 2)
UPLOAD=$(printf "%s" $DATA | cut -d, -f 3)

[ -z $PING ] && PING=0
[ -z $DOWNLOAD ] && DOWNLOAD=0
[ -z $UPLOAD ] && UPLOAD=0

DATETIME=$(date +"%Y-%m-%d %H:%M:%S")

printf "$DATETIME,%.1f,%.2f,%.2f" $PING $DOWNLOAD $UPLOAD
echo

Skrypt sprawdzi szybkość naszego łącza i wygeneruje wynik w formacie 'data czas,ping,download,upload'. Samo wywołanie speedtest'a można oczywiście wzbogacić np. o podanie identyfikatora serwera, dla którego chcemy wykonać test (np. speedtest --simple --server 15317). Wyniki poszczególnych testów przechowywać będziemy w pliku CSV, np. speedtest.csv. W tym celu dodajemy odpowiedni wpis w harmonogramie zadań.

Kod:
crontab -b

Przykład wpisu uruchamiający test łącza co 15 minut.

Kod:
0,15,30,45 * * * * /usr/local/bin/speedtest.sh >> /var/log/speedtest.csv

Po zmianie harmonogramu, warto pamiętać o zrestartowaniu usługi CRON. Działające zadanie zaowocuje odkładaniem wyników kolejnych testów.

Kod:
2019-03-25 12:00:28,5.6,41.89,39.27
2019-03-25 12:15:29,9.8,41.90,39.27
2019-03-25 12:30:28,5.5,41.90,41.69
2019-03-25 12:45:28,5.6,41.90,41.31
2019-03-25 13:00:28,6.1,41.88,41.28
2019-03-25 13:15:28,6.0,41.90,41.41
2019-03-25 13:30:29,6.2,41.90,41.09
2019-03-25 13:45:29,9.6,41.79,40.67
2019-03-25 14:00:28,5.8,41.88,41.07

Jak widać, przeprowadzenie testu łącza trwa około 28-29 sekund. Warto o tym pamiętać przy ograniczaniu wyników do przedziału czasu od-do.

2. Wizualizacja wyników testu szybkości łącza w postaci statycznych obrazków

Generowanie obrazków możliwe jest m.in. poprzez wykorzystanie biblioteki dla języka Python o nazwie Matplotlib (Matplotlib: Python plotting — Matplotlib 3.0.3 documentation).

Potrzebujemy skryptu (speedtest_generate_chart.sh), który wygeneruje nam wykresy na podstawie zgromadzonych danych i będzie możliwy do cyklicznego wywoływania w harmonogramie zadań.

Kod:
#!/bin/sh
LOG_FILE='/var/log/speedtest.csv'
DATETIME_FROM=$(date -d 'today' +'%Y-%m-%d 00:00:00')
DATETIME_TO=$(date -d 'today' +'%Y-%m-%d %H:%M:%S')
OUTPUT_PATH=$TMPDIR
OUTPUT_FILE='chart.png'
CHART_GENERATOR='/usr/local/bin/speedtest_generate_chart.py'
CHART_TYPE='summary'

while getopts 'f:l:m:o:p:t:' flag; do
  case "${flag}" in
      f) DATETIME_FROM=$OPTARG ;;
      l) LOG_FILE=$OPTARG ;;
      m) CHART_TYPE=$OPTARG ;;
      o) OUTPUT_FILE=$OPTARG ;;
      p) OUTPUT_PATH=$OPTARG ;;
      t) DATETIME_TO=$OPTARG ;;
      *) exit 1 ;;
  esac
done

if [ ! -f $LOG_FILE ] ; then
  echo "Log file $LOG_FILE does not exist, aborting."
  exit
fi

if [ ! -s $LOG_FILE ] ; then
  echo "Log file $LOG_FILE is empty, aborting."
  exit
fi

[ -z $OUTPUT_PATH ] && OUTPUT_PATH='/tmp'

if [ ! -d $OUTPUT_PATH ] ; then
  echo "Output path $OUTPUT_PATH does not exist, aborting."
  exit
fi

if [ ! -w $OUTPUT_PATH ] ; then
  echo "Output path $OUTPUT_PATH is not writable, aborting."
  exit
fi

TMP_LOG_FILE=$(mktemp)

awk -F, -v datetime_from="$DATETIME_FROM" -v datetime_to="$DATETIME_TO" '$1>=datetime_from && $1<=datetime_to' $LOG_FILE > $TMP_LOG_FILE

python3 $CHART_GENERATOR $TMP_LOG_FILE $CHART_TYPE "$OUTPUT_PATH/$OUTPUT_FILE"

rm $TMP_LOG_FILE

Parametry skryptu:
- LOG_FILE - ścieżka do pliku z danymi (domyślnie '/var/log/speedtest.csv')
- DATETIME_FROM - data od (domyślnie dziś, godzina 00:00)
- DATETIME_TO - data do (domyślnie "teraz")
- OUTPUT_PATH - ścieżka dla pliku wyjściowego (domyślnie systemowy folder dla plików tymczasowych)
- OUTPUT_FILE - nazwa pliku wyjściowego (domyślnie 'chart.png')
- CHART_GENERATOR - skrypt do generowania pliku wyjściowego ('domyślnie /usr/local/bin/speedtest_generate_chart.py')
- CHAR;(YPE - typ wykresu (domyślnie 'summary', możliwe tryby to 'ping', 'download', 'upload' oraz 'summary')

Potrzebujemy również skryptu (speedtest_generate_chart.py) do wygenerowania pliku z wykresem. Poniższy skrypt ma zaszytą w sobie konfigurację, którą można dostosować do własnych potrzeb, np. poprzez zmianę kolorów linii czy uruchomienie trybu "ciemnego" (darkBackground = True).

Kod:
#!/usr/bin/python
import sys
import os
import datetime
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
from matplotlib.dates import DateFormatter
from matplotlib.dates import HourLocator

logFile = sys.argv[1]
type = sys.argv[2]
outputFile = sys.argv[3]

if not os.path.exists(logFile):
   sys.exit(1)

if not os.path.isfile(logFile):
   sys.exit(1)

timestamps = []
pings = []
downloads = []
uploads = []

logFileDataSeparator = ','
logFileDateTimeFormat = '%Y-%m-%d %H:%M:%S'

darkBackground = None

formatStringPing = 'r-'
formatStringDownload = 'g-'
formatStringUpload = 'b-'
formatStringDateFormatter = '%H'

xLabel = 'Time [h]'
yLabelPing = 'Ping [ms]'
yLabelDownload = 'Download [Mbit/s]'
yLabelUpload = 'Upload [Mbit/s]'
yLabelSpeed = 'Speed [Mbit/s]'

grid = True
gridColor = 'lightgray'
gridLineStyle = ':'
gridLineWidth = 1

with open(logFile, "r") as file:
   for line in file:
       line = line.rstrip()
       columns = line.split(logFileDataSeparator)
       timestamps.append(datetime.datetime.strptime(columns[0], logFileDateTimeFormat))
       pings.append(columns[1])
       downloads.append(columns[2])
       uploads.append(columns[3])

if darkBackground:
   plt.style.use('dark_background')

if type == 'ping':
   fig, ax = plt.subplots()
   ax.set_xlabel(xLabel)
   ax.set_ylabel(yLabelPing)
   ax.plot_date(timestamps, pings, fmt=formatStringPing, xdate=True, ydate=False)
   ax.set_ylim(bottom=0)
   if grid:
       ax.grid(None, color=gridColor, linestyle=gridLineStyle, linewidth=gridLineWidth)
   ax.tick_params(labelsize='medium', width=1, labeltop=None, labelright=None)
   ax.xaxis.set_major_locator(HourLocator())
   ax.xaxis.set_major_formatter(DateFormatter(formatStringDateFormatter))
   plt.savefig(outputFile)
elif type == 'download':
   fig, ax = plt.subplots()
   ax.set_xlabel(xLabel)
   ax.set_ylabel(yLabelDownload)
   ax.plot_date(timestamps, downloads, fmt=formatStringDownload, xdate=True, ydate=False)
   ax.set_ylim(bottom=0)
   if grid:
       ax.grid(None, color=gridColor, linestyle=gridLineStyle, linewidth=gridLineWidth)
   ax.tick_params(labelsize='medium', width=1, labeltop=None, labelright=None)
   ax.xaxis.set_major_locator(HourLocator())
   ax.xaxis.set_major_formatter(DateFormatter(formatStringDateFormatter))
   plt.savefig(outputFile)
elif type == 'upload':
   fig, ax = plt.subplots()
   ax.set_xlabel(xLabel)
   ax.set_ylabel(yLabelUpload)
   ax.plot_date(timestamps, uploads, fmt=formatStringUpload, xdate=True, ydate=False)
   ax.set_ylim(bottom=0)
   if grid:
       ax.grid(None, color=gridColor, linestyle=gridLineStyle, linewidth=gridLineWidth)
   ax.tick_params(labelsize='medium', width=1, labeltop=None, labelright=None)
   ax.xaxis.set_major_locator(HourLocator())
   ax.xaxis.set_major_formatter(DateFormatter(formatStringDateFormatter))
   plt.savefig(outputFile)
elif type == 'summary':
   fig, ax1 = plt.subplots()
   ax1.set_xlabel(xLabel)
   ax1.set_ylabel(yLabelSpeed)
   ax1.tick_params(axis='y', labelsize='medium', width=1)
   ax1.plot_date(timestamps, downloads, fmt=formatStringDownload, xdate=True, ydate=False)
   ax1.plot_date(timestamps, uploads, fmt=formatStringUpload, xdate=True, ydate=False)
   ax1.set_ylim(bottom=0)
   ax1.xaxis.set_major_locator(HourLocator())
   ax1.xaxis.set_major_formatter(DateFormatter(formatStringDateFormatter))
   if grid:
       ax1.grid(None, color=gridColor, linestyle=gridLineStyle, linewidth=gridLineWidth)
   ax2 = ax1.twinx()
   ax2.set_ylabel(yLabelPing)
   ax2.plot_date(timestamps, pings, fmt=formatStringPing, xdate=True, ydate=False)
   fig.tight_layout()
   plt.savefig(outputFile)

Domyślnie generowany jest plik z wykresami dla bieżącego dnia. Niemniej, skrypt może posłużyć do generowania wykresów na podstawie danych z dowolnego przedziału czasowego. Korzystając z odpowiednich wpisów w harmonogramie zadań, możemy cyklicznie generować pliki, np. o godzinie 1 w nocy wygenerować plik(i) na podstawie danych z dnia poprzedniego i np. odłożyć je w pożądanym udzialel lub "wysłać w świat". Pliki (obrazki) można udostępnić/osadzić np. na stronie internetowej. Jak to mówią - "Sky is the limit".

Poniżej przykładowe wywołanie skryptu generującego wykres tylko z szybkością pobierania (download) dla 16 marca 2019 roku oraz plik wynikowy.

Kod:
/usr/local/bin/speedtest_generate_chart.sh -l /var/log/speedtest.csv -o 20190316_download.png -f '2019-03-16 00:00:00' -t '2019-03-17 00:00:00' -m download

upload_2019-3-31_23-8-50.png


Dorzucam również porównanie motywów - wykresy dotyczące prędkości (download/upload) nie są może zbyt urodziwe, ale taki urok łącza symetrycznego.

upload_2019-3-31_23-9-7.png

upload_2019-3-31_23-9-22.png



3. Wizualizacja wyników testu szybkości łącza w postaci interaktywnych wykresów na stronie WWW

Potrzebujemy skryptu PHP (speedtest_csv2json.php), który będzie przetwarzał plik z wynikami testów z formatu CSV do formatu JSON.

Kod:
<?php

$datetimeFrom = isset($_GET['from']) ? strtotime($_GET['from']) : false;
$datetimeTo = isset($_GET['to']) ? strtotime($_GET['to']) : false;

$csvFile = '/var/log/speedtest.csv';

$data = [];

if (file_exists($csvFile)) {
  $keys = ['datetime', 'ping', 'download', 'upload'];

  foreach (file($csvFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) as $line) {
      $append = true;
      $values = explode(',', $line);
      $datetime = strtotime($values[0]);

      if ($datetime) {
          if ($datetimeFrom and $datetime < $datetimeFrom) {
              $append = false;
          }
          if ($append and $datetimeTo and $datetime > $datetimeTo) {
              $append = false;
          }
      }

      if ($append) {
          array_push($data, array_combine($keys, $values));
      }
  }
}

$json = json_encode($data);

print_r($json);

Skrypt domyślnie przetworzy cały plik z danymi. Ograniczenie danych do wybranego przedział czasu możliwe jest za pomocą parametrów 'from' i 'to'.

Kod:
speedtest_csv2json.php?from=20190325120000&to=20190325140100

Kod:
[{"datetime":"2019-03-25 12:00:28","ping":"5.6","download":"41.89","upload":"39.27"},{"datetime":"2019-03-25 12:15:29","ping":"9.8","download":"41.90","upload":"39.27"},{"datetime":"2019-03-25 12:30:28","ping":"5.5","download":"41.90","upload":"41.69"},{"datetime":"2019-03-25 12:45:28","ping":"5.6","download":"41.90","upload":"41.31"},{"datetime":"2019-03-25 13:00:28","ping":"6.1","download":"41.88","upload":"41.28"},{"datetime":"2019-03-25 13:15:28","ping":"6.0","download":"41.90","upload":"41.41"},{"datetime":"2019-03-25 13:30:29","ping":"6.2","download":"41.90","upload":"41.09"},{"datetime":"2019-03-25 13:45:29","ping":"9.6","download":"41.79","upload":"40.67"},{"datetime":"2019-03-25 14:00:28","ping":"5.8","download":"41.88","upload":"41.07"}]

Dane przygotowane i odpowiednio sformatowane, pora więc na skrypty, które na ich podstawie wygenerują interaktywny wykres. W tym celu skorzystamy z biblioteki D3.js (D3.js - Data-Driven Documents) w wersji 3. Zamieszczone poniżej skrypty przygotowałem w taki sposób, by pozwalały na możliwie dowolne (konkretne) dopasowanie wizualne (rozmiar, kolory, prezentowane dane, itd.).

Skrypt PHP (speedtest_chart.config.php) do generowania konfiguracji wykresu.

Kod:
<?php

$allowCustomization = true;

$config = array(
	'selector' => 'speedtestChart',
	'width' => 600,
	'height' => 400,
	'marginLeft' => 50,
	'marginRight' => 50,
	'marginTop' => 30,
	'marginBottom' => 30,
	'url' => 'speedtest_csv2json.php', # can be also an URL like 'http://SOMEWHERE/speedtest_csv2json.php'
	'color' => 'black',
	'backgroundColor' => 'white',
	'datetimeFormat' => '%Y-%m-%d %H:%M:%S',
	'ping' => true,
	'pingStroke' => 'red',
	'pingStrokeWidth' => 'red',
	'download' => true,
	'downloadStroke' => 'green',
	'downloadStrokeWidth' => '1px',
	'upload' => true,
	'uploadStroke' => 'blue',
	'uploadStrokeWidth' => '1px',
	'dateLine' => true,
	'dateLineStroke' => 'steelblue',
	'dateLineStrokeWidth' => '1px',
	'tooltip' => true,
	'tooltipLeft' => 20,
	'tooltipTop' => 20,
	'xAxisOrient' => 'bottom',
	'xAxisTicks' => 5,
	'xAxisTickFormat' => '%Y-%m-%d',
	'xAxisTickPadding' => 10,
	'xAxisTickLineStroke' => 'black',
	'xAxisTickLineStrokeWidth' => '1px',
	'xAxisTickLineStrokeDasharray' => '2,2',
	'xAxisTickLineOpacity' => '0.5',
	'yAxisSpeedOrient' => 'left',
	'yAxisSpeedTicks' => 5,
	'yAxisSpeedTickFormat' => 'd',
	'yAxisSpeedTickPadding' => 10,
	'yAxisPingOrient' => 'right',
	'yAxisPingTicks' => 5,
	'yAxisPingTickFormat' => 'd',
	'yAxisPingTickPadding' => 10,
	'yAxisTickLineStroke' => 'black',
	'yAxisTickLineStrokeWidth' => '1px',
	'yAxisTickLineStrokeDasharray' => '2,2',
	'yAxisTickLineOpacity' => '0.5',
);

if ($allowCustomization) {
	foreach ($_GET as $key => $value) {
		if (isset($config[$key])) {
			switch ($key) {
				case 'url':
				case 'color':
				case 'backgroundColor':
				case 'selector':
				case 'dateLineStroke':
				case 'dateLineStrokeWidth':
				case 'datetimeFormat':
				case 'downloadStroke':
				case 'downloadStrokeWidth':
				case 'uploadStroke':
				case 'uploadStrokeWidth':
				case 'pingStroke':
				case 'pingStrokeWidth':
				case 'xAxisOrient':
				case 'xAxisTickFormat':
				case 'xAxisTickLineStroke':
				case 'xAxisTickLineStrokeWidth':
				case 'xAxisTickLineStrokeDasharray':
				case 'xAxisTickLineOpacity':
				case 'yAxisSpeedOrient':
				case 'yAxisSpeedTickFormat':
				case 'yAxisPingOrient':
				case 'yAxisPingTickFormat':
				case 'yAxisTickLineStroke':
				case 'yAxisTickLineStrokeWidth':
				case 'yAxisTickLineStrokeDasharray':
				case 'yAxisTickLineOpacity':
					$config[$key] = htmlentities($value, ENT_QUOTES);
					break;
				case 'height':
				case 'width':
				case 'marginLeft':
				case 'marginRight':
				case 'marginTop':
				case 'marginBottom':
				case 'tooltipTop':
				case 'xAxisTicks':
				case 'xAxisTickPadding':
				case 'yAxisSpeedTicks':
				case 'yAxisSpeedTickPadding':
				case 'yAxisPingTicks':
				case 'yAxisPingTickPadding':
					$config[$key] = (int) $value;
					break;
				case 'download':
				case 'upload':
				case 'ping':
				case 'dateLine':
				case 'tooltip':
					$config[$key] = (bool) $value;
					break;
			}
		}
	}
}

$data = array(
	'width' => $config['width'],
	'height' => $config['height'],
	'margin' => array(
		'left' => $config['marginLeft'],
		'right' => $config['marginRight'],
		'top' => $config['marginTop'],
		'bottom' => $config['marginBottom'],
	),
	'url' => $config['url'],
	'color' => $config['color'],
	'backgroundColor' => $config['backgroundColor'],
	'datetimeFormat' => $config['datetimeFormat'],
	'selector' => '#' . $config['selector'],
	'ping' => array(
		'display' => $config['ping'] ? true : false,
		'stroke' => $config['pingStroke'],
		'strokeWidth' => $config['pingStrokeWidth'],
	),
	'download' => array(
		'display' => $config['download'] ? true : false,
		'stroke' => $config['downloadStroke'],
		'strokeWidth' => $config['downloadStrokeWidth'],
	),
	'upload' => array(
		'display' => $config['upload'] ? true : false,
		'stroke' => $config['uploadStroke'],
		'strokeWidth' => $config['uploadStrokeWidth'],
	),
	'dateLine' => array(
		'display' => $config['dateLine'] ? true : false,
		'stroke' => $config['dateLineStroke'],
		'strokeWidth' => $config['dateLineStrokeWidth'],
	),
	'tooltip' => array(
		'display' => $config['tooltip'] ? true : false,
		'left' => $config['tooltipLeft'],
		'top' => $config['tooltipTop'],
		'rowSeparator' => '<br/>',
	),
	'xAxis' => array(
		'orient' => $config['xAxisOrient'],
		'ticks' => $config['xAxisTicks'],
		'tick' => array(
			'format' => $config['xAxisTickFormat'],
			'padding' => $config['xAxisTickPadding'],
			'line' => array(
				'stroke' => $config['xAxisTickLineStroke'],
				'strokeWidth' => $config['xAxisTickLineStrokeWidth'],
				'strokeDasharray' => $config['xAxisTickLineStrokeDasharray'],
				'opacity' => $config['xAxisTickLineOpacity'],
			),
		),
	),
	'yAxis' => array(
		'speed' => array(
			'orient' => $config['yAxisSpeedOrient'],
			'ticks' => $config['yAxisSpeedTicks'],
			'tick' => array(
				'format' => $config['yAxisSpeedTickFormat'],
				'padding' => $config['yAxisSpeedTickPadding'],
			),
		),
		'ping' => array(
			'orient' => $config['yAxisPingOrient'],
			'ticks' => $config['yAxisPingTicks'],
			'tick' => array(
				'format' => $config['yAxisPingTickFormat'],
				'padding' => $config['yAxisPingTickPadding'],
			),
		),
		'tick' => array(
			'line' => array(
				'stroke' => $config['yAxisTickLineStroke'],
				'strokeWidth' => $config['yAxisTickLineStrokeWidth'],
				'strokeDasharray' => $config['yAxisTickLineStrokeDasharray'],
				'opacity' => $config['yAxisTickLineOpacity'],
			),
		),
	),
);

$json = json_encode($data);

print_r($json);

Korzystanie z powyższego skryptu jest opcjonalne - w kodzie strony można osadzić własną konfigurację. Poniżej wynik wywołania skryptu bez parametrów, który zwraca domyślną konfigurację dla wykresu.

Kod:
{
  "width": 600,
  "height": 400,
  "margin": {
    "left": 50,
    "right": 50,
    "top": 30,
    "bottom": 30
  },
  "url": "speedtest_csv2json.php",
  "color": "black",
  "backgroundColor": "white",
  "datetimeFormat": "%Y-%m-%d %H:%M:%S",
  "selector": "#speedtestChart",
  "ping": {
    "display": true,
    "stroke": "red",
    "strokeWidth": "red"
  },
  "download": {
    "display": true,
    "stroke": "green",
    "strokeWidth": "1px"
  },
  "upload": {
    "display": true,
    "stroke": "blue",
    "strokeWidth": "1px"
  },
  "dateLine": {
    "display": true,
    "stroke": "steelblue",
    "strokeWidth": "1px"
  },
  "tooltip": {
    "display": true,
    "left": 20,
    "top": 20,
    "rowSeparator": ""
  },
  "xAxis": {
    "orient": "bottom",
    "ticks": 5,
    "tick": {
      "format": "%Y-%m-%d",
      "padding": 10,
      "line": {
        "stroke": "black",
        "strokeWidth": "1px",
        "strokeDasharray": "2,2",
        "opacity": "0.5"
      }
    }
  },
  "yAxis": {
    "speed": {
      "orient": "left",
      "ticks": 5,
      "tick": {
        "format": "d",
        "padding": 10
      }
    },
    "ping": {
      "orient": "right",
      "ticks": 5,
      "tick": {
        "format": "d",
        "padding": 10
      }
    },
    "tick": {
      "line": {
        "stroke": "black",
        "strokeWidth": "1px",
        "strokeDasharray": "2,2",
        "opacity": "0.5"
      }
    }
  }
}

Skrypt JS (speedtest_chart.js) do generowania wykresu.

Kod:
d3.speedtestChart = function(config) {
	var chartWidth = config.width - config.margin.left - config.margin.right,
		chartHeight = config.height - config.margin.top - config.margin.bottom,
		parseDate = d3.time.format(config.datetimeFormat).parse,
		x = d3.time.scale().range([0, chartWidth]),
		xAxis = d3.svg.axis().scale(x)
			.orient(config.xAxis.orient)
			.ticks(config.xAxis.ticks)
			.tickFormat(d3.time.format(config.xAxis.tick.format))
			.innerTickSize(-chartHeight)
			.outerTickSize(0)
			.tickPadding(config.xAxis.tick.padding),
		ySpeed = d3.scale.linear().range([chartHeight, 0]),
		yAxisSpeed = d3.svg.axis().scale(ySpeed)
			.orient(config.yAxis.speed.orient)
			.ticks(config.yAxis.speed.ticks)
			.tickFormat(d3.format(config.yAxis.speed.tick.format))
			.innerTickSize(-chartWidth)
			.outerTickSize(0)
			.tickPadding(config.yAxis.speed.tick.padding),
		downloadLine = d3.svg.line()
			.x(function(d) { return x(d.datetime); })
			.y(function(d) { return ySpeed(d.download); }),
		uploadLine = d3.svg.line()
			.x(function(d) { return x(d.datetime); })
			.y(function(d) { return ySpeed(d.upload); }),
		yPing = d3.scale.linear().range([chartHeight, 0]),
		yAxisPing = d3.svg.axis().scale(yPing)
			.orient(config.yAxis.ping.orient)
			.ticks(config.yAxis.ping.ticks)
			.tickFormat(d3.format(config.yAxis.ping.tick.format))
			.innerTickSize((config.download.display || config.upload.display) ? 0 : -chartWidth)
			.outerTickSize(0)
			.tickPadding(config.yAxis.ping.tick.padding),
		pingLine = d3.svg.line()
			.x(function(d) { return x(d.datetime); })
			.y(function(d) { return yPing(d.ping); }),
		draw = function(config) {
			var svgWrapper = d3.select(config.selector)
					.append('div')
					.attr('id', config.selector.replace('#', '') + '-chart-wrapper')
					.style({
						position: 'relative',
					}),
				svg = svgWrapper
					.append('svg')
						.attr('width', chartWidth + config.margin.left + config.margin.right)
						.attr('height', chartHeight + config.margin.top + config.margin.bottom)
						.style('background-color', config.backgroundColor)
						.style({
							'background-color': config.backgroundColor,
							color: config.color,
						})
					.append('g')
						.attr('transform', 'translate(' + config.margin.left + ',' + config.margin.top + ')'),
				dateLine = svg.append('path')
					.attr('class', 'date-line')
					.style({
						stroke: config.dateLine.stroke,
						'stroke-width': config.dateLine.strokeWidth,
					}),
				tooltip = svgWrapper
					.append('div')
					.attr('id', config.selector.replace('#', '') + '-chart-tooltip')
					.style({
						display: 'none',
						position: 'absolute',
						'text-align': 'left',
						padding: '8px',
						'background-color': config.backgroundColor,
						color: config.color,
						border: '1px solid ' + config.color,
						'border-radius': '4px',
						'box-shadow': '0px 0px 5px 0px rgba(0,0,0,0.3)',
						'z-index': 1,
					}),
				showTooltip = function() {
						d3.select(config.selector + '-chart-tooltip')
							.style('display', 'inline');
					},
				hideTooltip = function() {
						d3.select(config.selector + '-chart-tooltip')
							.style('display', 'none');
					},
				tooltipDate = null,
				tooltipText = [],
				getTooltipDate = function() {
						return tooltipDate;
					},
				setTooltipDate = function(str) {
						tooltipDate = str;
					},
				setTooltipText = function(texts) {
						if (!(texts instanceof Array)) {
							texts = [];
						}
						if (texts.length) {
							texts = texts.filter(function(text) {
								return text;
							});
						}
						tooltipText = texts;
					},
				refreshTooltipText = function() {
						if (config.tooltip.display) {
							let content = tooltipText.slice();
							content.unshift(getTooltipDate());
							content = content.join(config.tooltip.rowSeparator);
							d3.select(config.selector + '-chart-tooltip')
								.html(content);
						}
					};

			svg
				.append('rect')
				.attr('y', 0)
				.attr('width', chartWidth)
				.attr('height', chartHeight)
				.style('opacity', 0);

			d3.json(config.url, function(data) {
				data.forEach(function(d) {
					d.datetime = parseDate(d.datetime);
					d.ping = +d.ping;
					d.download = +d.download;
					d.upload = +d.upload;
				});

				x.domain(d3.extent(data, function(d) { return d.datetime; }));
				ySpeed.domain([0, d3.max(data, function(d) { return Math.max(d.download, d.upload); })]);
				yPing.domain([0, d3.max(data, function(d) { return d.ping; })]);

				svg
					.append('g')
					.attr('class', 'x axis')
					.attr('transform', 'translate(0,' + chartHeight + ')')
					.style({
						stroke: config.color,
						'stroke-width': '1px',
						'shape-rendering': 'crispEdges',
					})
					.call(xAxis);

				if (config.download.display || config.upload.display) {
					svg.append('g')
						.attr('class', 'y axis')
						.style({
							stroke: config.color,
							'stroke-width': '1px',
							'shape-rendering': 'crispEdges',
						})
						.call(yAxisSpeed);
				}

				if (config.download.display) {
					svg.append('path')
						.attr('class', 'line')
						.attr('d', downloadLine(data))
						.style({
							stroke: config.download.stroke,
							'stroke-width': config.download.strokeWidth,
							fill: 'none',
						});
				}

				if (config.upload.display) {
					svg.append('path')
						.attr('class', 'line')
						.attr('d', uploadLine(data))
						.style({
							stroke: config.upload.stroke,
							'stroke-width': config.upload.strokeWidth,
							fill: 'none',
						});
				}

				if (config.ping.display) {
					svg.append('g')				
						.attr('class', 'y axis')	
						.attr('transform', 'translate(' + ((config.download.display || config.upload.display) ? chartWidth : 0) + ', 0)')	
						.style({
							stroke: config.color,
							'stroke-width': '1px',
							'shape-rendering': 'crispEdges',
						})
						.call(yAxisPing);

					svg.append('path')
						.attr('class', 'line')
						.attr('d', pingLine(data))
						.style({
							stroke: config.ping.stroke,
							'stroke-width': config.ping.strokeWidth,
							fill: 'none',
						});
				}

				svg
					.selectAll('.x.axis > .tick > line')
					.style({
						stroke: config.xAxis.tick.line.stroke,
						'stroke-width': config.xAxis.tick.line.strokeWidth,
						'stroke-dasharray': config.xAxis.tick.line.strokeDasharray,
						opacity: config.xAxis.tick.line.opacity,
					});

				svg
					.selectAll('.x.axis > .tick > text')
					.style({
						stroke: 'none',
						fill: config.color,
					});

				svg
					.selectAll('.y.axis > .tick > line')
					.style({
						stroke: config.yAxis.tick.line.stroke,
						'stroke-width': config.yAxis.tick.line.strokeWidth,
						'stroke-dasharray': config.yAxis.tick.line.strokeDasharray,
						opacity: config.yAxis.tick.line.opacity,
					});

				svg
					.selectAll('.y.axis > .tick > text')
					.style({
						stroke: 'none',
						fill: config.color,
					});
			
				let bisectDate = d3.bisector(function(d) {
						return d.datetime;
					}).left,
					onMouseOver = function() {
							if (config.tooltip.display) {
								let coordinates = d3.mouse(this);
								if (coordinates[0] >= 0 && coordinates[0] <= chartWidth && coordinates[1] <= chartHeight) {
									showTooltip();
								}
							}
						},
					onMouseMove = function() {
							let coordinates = d3.mouse(this);
							if (coordinates[0] >= 0 && coordinates[0] <= chartWidth && coordinates[1] <= chartHeight) {
								if (config.dateLine.display) {
									dateLine.style('display', null)
										.attr('d', function () {
											return 'M' + coordinates[0] + ',' + (chartHeight) + ' ' + coordinates[0] + ', 0';
										});
								}
								if (config.tooltip.display) {
									let date = moment(x.invert(coordinates[0])).format('YYYY-MM-DD HH:mm:ss'),
										index = bisectDate(data, parseDate(date)),
										dataLeft = data[index - 1],
										dataRight = data[index],
										dataNearest = null;
									if (typeof dataLeft !== 'undefined') {
										if (typeof dataRight !== 'undefined') {
											dataNearest = parseDate(date) - dataLeft.datetime > dataRight.datetime - parseDate(date) ? dataRight : dataLeft;
										} else {
											dataNearest = dataLeft;
										}
									} else {
										if (typeof dataRight !== 'undefined') {
											dataNearest = dataRight;
										}
									}
									if (typeof dataNearest !== 'undefined') {
										setTooltipDate(moment(dataNearest.datetime).format('YYYY-MM-DD HH:mm'));
										let tooltipText = [];
										if (config.ping) {
											tooltipText.push('Ping: ' + dataNearest.ping);
										}
										if (config.download) {
											tooltipText.push('Download: ' + dataNearest.download);
										}
										if (config.upload) {
											tooltipText.push('Upload: ' + dataNearest.upload);
										}
										setTooltipText(tooltipText);
										d3.select(config.selector + '-chart-tooltip')
											.style({
												left: (coordinates[0] + config.margin.left + config.tooltip.left) + 'px',
												top: (coordinates[1] + config.margin.top + config.tooltip.top) + 'px',
											});
										refreshTooltipText();
									}
								}
							}
						},
					onMouseOut = function() {
							if (config.dateLine.display) {
								dateLine.style('display', 'none');
							}
							if (config.tooltip.display) {
								hideTooltip();
								setTooltipText([]);
							}
						};

				svg
					.on('mouseover', onMouseOver)
					.on('mousemove', onMouseMove)
					.on('mouseout', onMouseOut);
			});
		};

	return draw(config);
};
Osadzenie powyższego skryptu na stronie pozwala na skorzystanie z funkcji d3.speedtestChart, która w swoim jedynym parametrze przyjmuje konfigurację wykresu.

Przykładowa strona, która prezentuje wykresy.

Kod:
<!doctype html>
<html lang="en">
  <head>
      <meta charset="utf-8">
      <script src="https://code.jquery.com/jquery-3.3.1.min.js"></script>
      <script src="http://d3js.org/d3.v3.min.js"></script>
      <script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.24.0/moment.min.js"></script>
      <script src="speedtest_chart.js"></script>
      <script>
          $(document).ready(function() {
              $.get('speedtest_chart_config.php?selector=summaryChart', function(configJSON) {
                  var summaryChartConfig = JSON.parse(configJSON);
                  d3.speedtestChart(summaryChartConfig);
              });
              $.get('speedtest_chart_config.php?selector=downloadChart&download=1&upload=0&ping=0&backgroundColor=black&color=white&xAxisTickLineStroke=white&yAxisTickLineStroke=white', function(configJSON) {
                  var downloadChartConfig = JSON.parse(configJSON);
                  d3.speedtestChart(downloadChartConfig);
              });
              $.get('speedtest_chart_config.php?selector=uploadChart&download=0&upload=1&ping=0', function(configJSON) {
                  var uploadChartConfig = JSON.parse(configJSON);
                  d3.speedtestChart(uploadChartConfig);
              });
              $.get('speedtest_chart_config.php?selector=pingChart&download=0&upload=0&ping=1&yAxisPingOrient=left&backgroundColor=black&color=white&xAxisTickLineStroke=white&yAxisTickLineStroke=white', function(configJSON) {
                  var pingChartConfig = JSON.parse(configJSON);
                  d3.speedtestChart(pingChartConfig);
              });
          });
      </script>
      <style>
          body {
              font-family: Arial;
              font-size: 11px;
          }
      </style>
  </head>
  <body>
      <div id="summaryChart"></div>
      <div id="downloadChart"></div>
      <div id="uploadChart"></div>
      <div id="pingChart"></div>
  </body>
</html>

Wygenerowane wykresy prezentują się następująco.

upload_2019-3-31_23-15-18.png


Wykresy po najechaniu na nie kursorem myszy, domyślnie prezentują pływający znacznik czasu oraz tooltip z danymi testu.

upload_2019-3-31_23-15-33.png

upload_2019-3-31_23-15-43.png


Na koniec mała ciekawostka. Poniżej wykres z Pi-hole'a, na którym widać ile zapytań DNS generuje testowanie łącza.

upload_2019-3-31_23-15-59.png
 
hmm

coś nie idzie

wrzuciłem te pliki na maszyne , crontab działa sobie radośnie

ale csv2json sypie błędem:

PHP:
PHP Parse error:  syntax error, unexpected '$datetimeFrom' (T_VARIABLE) in /var/www/html/speedtest_csv2json.php on line 3

Coś brakuje ?

Wywołanie :
upload_2019-4-7_22-21-37.png


php w wersji 7 włączone w apache
 
To wygląda na błąd przy kopiowaniu/wstawianiu kodu lub kodowania pliku (BOM). Daj znać - jak nie będzie bangla to wieczorem wystawię Ci wszystkie skrypty w formie plików.
 
Pytam, bo nie wiem czy nie trzeba czegoś dorzucić. Brakuje np. etykiet dla osi (zwłaszcza Y), a zastanawiałem się jeszcze nad dodaniem brusha (ten mniejszy wykres z możliwością wyboru zakresu).
 
Pięknie, hehe.;)
Widzę, że używasz przykładowych wykresów - jak nie potrzebujesz wszystkich czy chcesz je bardziej dopasować pod kątem wyglądu to daj znać.
Jeśli chodzi o dołożenie labeli i brusha, pamiętam o tym, ale czekam na chwilę przestoju.
 

Użytkownicy znaleźli tą stronę używając tych słów:

  1. speed test