Compare commits
119 Commits
be550ebac5
...
ac5dcdc58f
| Author | SHA1 | Date | |
|---|---|---|---|
| ac5dcdc58f | |||
| 296d42e721 | |||
| 9db020ceb0 | |||
| 36da503be9 | |||
| 42751f84d4 | |||
| 39d7c029ea | |||
| f072185074 | |||
| 122ba11a79 | |||
| f1d7799bf1 | |||
| 680f1bff03 | |||
| fa0315650a | |||
| 8f765dd478 | |||
| 1234560512 | |||
| 2711579afb | |||
| 3ac021e45f | |||
| c199eae4ff | |||
| 6073dd0779 | |||
| fee5eda780 | |||
| cc0db6d732 | |||
| f084396e95 | |||
| e4b6eea4b1 | |||
| 75fa966af3 | |||
| a3e4917e89 | |||
| 05fc03e48f | |||
| b00ce507ef | |||
| a69fe09464 | |||
| 77b99801e4 | |||
| da210145e6 | |||
| 21a84b5223 | |||
| a99a884423 | |||
| 6d6b6ba00c | |||
| 380bad6967 | |||
| f3556b6909 | |||
| 452030de5e | |||
| 6b8e3d2089 | |||
| b0467c4571 | |||
| a64e851c33 | |||
| fefc7411c1 | |||
| 00a1bf8ffb | |||
| 317d31bbda | |||
| d339f5307f | |||
| b3085c9b0c | |||
| 96955e0515 | |||
| 9a41f132f8 | |||
| 33c4fe1448 | |||
| d320ff3d93 | |||
| 4ce9cca7e0 | |||
| 29215f0410 | |||
| abc60c6de2 | |||
| c969c5c848 | |||
| 6552d51bcc | |||
| 43e13b53b1 | |||
| 731e9264e3 | |||
| 8839ab52a2 | |||
| f8a199fd6a | |||
| 48ae47d050 | |||
| 3386f0994f | |||
| 75f45c4d87 | |||
| bacd86d836 | |||
| c7af333052 | |||
| a3aa62aee2 | |||
| 6d2e8da805 | |||
| 53e7a21602 | |||
| b6045eda22 | |||
| 6d8af4fdc6 | |||
| 359c17bf29 | |||
| 686e3edd60 | |||
| 2310c2ab0d | |||
| a6fbd0c60d | |||
| 2b82a6822c | |||
| 0bc58ba166 | |||
| e543e0b388 | |||
| 882f04d893 | |||
| da4a95e5ed | |||
| 4679da480c | |||
| 1ca4f18e3d | |||
| add30a5ee9 | |||
| e42c00cc08 | |||
| c9758adbef | |||
| dafe6813ed | |||
| bc0ae23de5 | |||
| 02c748c286 | |||
| 44aed2883d | |||
| 96ed788793 | |||
| b5028e03be | |||
| b56b4c231e | |||
| e3945a4d33 | |||
| cb5b160d3c | |||
| 6c62436753 | |||
| 0bd22233a1 | |||
| 4b4932a636 | |||
| 6474b5e3c6 | |||
| 62cb88e505 | |||
| 332611accb | |||
| 10c982c8bc | |||
| 77c576c434 | |||
| 8f369d9943 | |||
| bc3b6ec3e9 | |||
| 9e53022b4a | |||
| c57b53ccc6 | |||
| 6568ba2ae6 | |||
| 2514cd86c1 | |||
| 8c023760b1 | |||
| a8cd94f47e | |||
| b3509e062b | |||
| 1bfb8a0090 | |||
| d9daf561bf | |||
| 71410801dc | |||
| de9218c3c4 | |||
| e3487557b3 | |||
| 17c0cd5ca9 | |||
| 66da69a57a | |||
| bd1b4fb655 | |||
| d52033b5f0 | |||
| fb5c8ea019 | |||
| ea3ccabe56 | |||
| 3a28e03b6a | |||
| dc8a462031 | |||
| a2945d2d9b |
@@ -1,6 +1,6 @@
|
||||
package org.lucares.pdb.map;
|
||||
|
||||
import java.util.List;
|
||||
import org.lucares.utils.HumanBytes;
|
||||
|
||||
public class PersistentMapStats {
|
||||
private long values = 0;
|
||||
@@ -87,7 +87,7 @@ public class PersistentMapStats {
|
||||
builder.append(String.format("\navg. depth= %.2f", averageDepth));
|
||||
builder.append(String.format("\navg. fill= %.2f", averageFill));
|
||||
builder.append(String.format("\nvalues/node=%.2f", averageValuesInNode));
|
||||
builder.append(String.format("\nfile size= %s\n", toHumanBytes(fileSize)));
|
||||
builder.append(String.format("\nfile size= %s\n", HumanBytes.toHumanBytes(fileSize)));
|
||||
return builder.toString();
|
||||
}
|
||||
|
||||
@@ -100,21 +100,9 @@ public class PersistentMapStats {
|
||||
builder.append(String.format("\navg. depth= %.2f -> %.2f", old.averageDepth, averageDepth));
|
||||
builder.append(String.format("\navg. fill= %.2f -> %.2f", old.averageFill, averageFill));
|
||||
builder.append(String.format("\nvalues/node=%.2f -> %.2f", old.averageValuesInNode, averageValuesInNode));
|
||||
builder.append(String.format("\nfile size= %s -> %s\n", toHumanBytes(old.fileSize), toHumanBytes(fileSize)));
|
||||
builder.append(String.format("\nfile size= %s -> %s\n", HumanBytes.toHumanBytes(old.fileSize),
|
||||
HumanBytes.toHumanBytes(fileSize)));
|
||||
return builder.toString();
|
||||
}
|
||||
|
||||
private static String toHumanBytes(final long bytes) {
|
||||
final List<String> powers = List.of("bytes", "KB", "MB", "GB", "TB", "PB", "EB");
|
||||
|
||||
int power = 1;
|
||||
String result = String.format("%d bytes", bytes);
|
||||
while (bytes >= Math.pow(1024, power) && power < powers.size()) {
|
||||
result = String.format("%.3f", bytes / Math.pow(1024, power));
|
||||
result = result.replaceAll("\\.?0*$", "");
|
||||
result = result + " " + powers.get(power);
|
||||
power = power + 1;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
31
build.gradle
@@ -4,27 +4,27 @@ import org.apache.tools.ant.filters.ReplaceTokens
|
||||
plugins {
|
||||
id 'java'
|
||||
id 'eclipse'
|
||||
id 'com.github.ben-manes.versions' version "0.45.0" // check for dependency updates run: gradlew dependenyUpdates
|
||||
id 'com.github.ben-manes.versions' version "0.51.0" // check for dependency updates run: gradlew dependenyUpdates
|
||||
}
|
||||
|
||||
|
||||
ext {
|
||||
|
||||
javaVersion=17
|
||||
javaVersion=21
|
||||
|
||||
version_log4j2= '2.19.0' // keep in sync with spring-boot-starter-log4j2
|
||||
version_spring = '3.0.2'
|
||||
version_junit = '5.9.2'
|
||||
version_junit_platform = '1.9.2'
|
||||
version_nodejs = '16.17.1' // keep in sync with npm
|
||||
version_npm = '8.15.0' // keep in sync with nodejs
|
||||
version_log4j2= '2.20.0' // keep in sync with spring-boot-starter-log4j2
|
||||
version_spring = '3.3.4'
|
||||
version_junit = '5.10.3'
|
||||
version_junit_platform = '1.10.3'
|
||||
version_nodejs = '20.17.0' // keep in sync with npm
|
||||
version_npm = '10.8.2' // keep in sync with nodejs
|
||||
|
||||
lib_antlr = "org.antlr:antlr4:4.11.1"
|
||||
lib_antlr = "org.antlr:antlr4:4.13.2"
|
||||
|
||||
lib_commons_collections4 = 'org.apache.commons:commons-collections4:4.4'
|
||||
lib_commons_csv= 'org.apache.commons:commons-csv:1.10.0'
|
||||
lib_commons_lang3 = 'org.apache.commons:commons-lang3:3.12.0'
|
||||
lib_jackson_databind = 'com.fasterxml.jackson.core:jackson-databind:2.14.2'
|
||||
lib_commons_csv= 'org.apache.commons:commons-csv:1.12.0'
|
||||
lib_commons_lang3 = 'org.apache.commons:commons-lang3:3.17.0'
|
||||
lib_jackson_databind = 'com.fasterxml.jackson.core:jackson-databind:2.18.0'
|
||||
|
||||
lib_log4j2_core = "org.apache.logging.log4j:log4j-core:${version_log4j2}"
|
||||
lib_log4j2_slf4j_impl = "org.apache.logging.log4j:log4j-slf4j-impl:${version_log4j2}"
|
||||
@@ -73,6 +73,11 @@ subprojects {
|
||||
url 'https://repo.lucares.de/'
|
||||
content { includeGroup "org.lucares" }
|
||||
}
|
||||
maven {
|
||||
url "https://nexus.disco.lab/repository/maven-all/"
|
||||
allowInsecureProtocol = true
|
||||
content { excludeGroup "org.lucares" }
|
||||
}
|
||||
mavenCentral(content: { excludeGroup "org.lucares" })
|
||||
}
|
||||
|
||||
@@ -136,5 +141,5 @@ subprojects {
|
||||
}
|
||||
|
||||
wrapper {
|
||||
gradleVersion = '8.0.1'
|
||||
gradleVersion = '8.9'
|
||||
}
|
||||
|
||||
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
3
gradle/wrapper/gradle-wrapper.properties
vendored
@@ -1,6 +1,7 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.0.1-bin.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip
|
||||
networkTimeout=10000
|
||||
validateDistributionUrl=true
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
|
||||
34
gradlew
vendored
@@ -15,6 +15,8 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
#
|
||||
|
||||
##############################################################################
|
||||
#
|
||||
@@ -55,7 +57,7 @@
|
||||
# Darwin, MinGW, and NonStop.
|
||||
#
|
||||
# (3) This script is generated from the Groovy template
|
||||
# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
||||
# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
||||
# within the Gradle project.
|
||||
#
|
||||
# You can find Gradle at https://github.com/gradle/gradle/.
|
||||
@@ -83,10 +85,9 @@ done
|
||||
# This is normally unused
|
||||
# shellcheck disable=SC2034
|
||||
APP_BASE_NAME=${0##*/}
|
||||
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
|
||||
|
||||
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
|
||||
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s
|
||||
' "$PWD" ) || exit
|
||||
|
||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||
MAX_FD=maximum
|
||||
@@ -133,18 +134,21 @@ location of your Java installation."
|
||||
fi
|
||||
else
|
||||
JAVACMD=java
|
||||
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
if ! command -v java >/dev/null 2>&1
|
||||
then
|
||||
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
|
||||
Please set the JAVA_HOME variable in your environment to match the
|
||||
location of your Java installation."
|
||||
fi
|
||||
fi
|
||||
|
||||
# Increase the maximum file descriptors if we can.
|
||||
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
|
||||
case $MAX_FD in #(
|
||||
max*)
|
||||
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
|
||||
# shellcheck disable=SC3045
|
||||
# shellcheck disable=SC2039,SC3045
|
||||
MAX_FD=$( ulimit -H -n ) ||
|
||||
warn "Could not query maximum file descriptor limit"
|
||||
esac
|
||||
@@ -152,7 +156,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
|
||||
'' | soft) :;; #(
|
||||
*)
|
||||
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
|
||||
# shellcheck disable=SC3045
|
||||
# shellcheck disable=SC2039,SC3045
|
||||
ulimit -n "$MAX_FD" ||
|
||||
warn "Could not set maximum file descriptor limit to $MAX_FD"
|
||||
esac
|
||||
@@ -197,11 +201,15 @@ if "$cygwin" || "$msys" ; then
|
||||
done
|
||||
fi
|
||||
|
||||
# Collect all arguments for the java command;
|
||||
# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
|
||||
# shell script including quotes and variable substitutions, so put them in
|
||||
# double quotes to make sure that they get re-expanded; and
|
||||
# * put everything else in single quotes, so that it's not re-expanded.
|
||||
|
||||
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||
|
||||
# Collect all arguments for the java command:
|
||||
# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
|
||||
# and any embedded shellness will be escaped.
|
||||
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
|
||||
# treated as '${Hostname}' itself on the command line.
|
||||
|
||||
set -- \
|
||||
"-Dorg.gradle.appname=$APP_BASE_NAME" \
|
||||
|
||||
22
gradlew.bat
vendored
@@ -13,6 +13,8 @@
|
||||
@rem See the License for the specific language governing permissions and
|
||||
@rem limitations under the License.
|
||||
@rem
|
||||
@rem SPDX-License-Identifier: Apache-2.0
|
||||
@rem
|
||||
|
||||
@if "%DEBUG%"=="" @echo off
|
||||
@rem ##########################################################################
|
||||
@@ -43,11 +45,11 @@ set JAVA_EXE=java.exe
|
||||
%JAVA_EXE% -version >NUL 2>&1
|
||||
if %ERRORLEVEL% equ 0 goto execute
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
echo.
|
||||
echo Please set the JAVA_HOME variable in your environment to match the
|
||||
echo location of your Java installation.
|
||||
echo. 1>&2
|
||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
|
||||
echo. 1>&2
|
||||
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||
echo location of your Java installation. 1>&2
|
||||
|
||||
goto fail
|
||||
|
||||
@@ -57,11 +59,11 @@ set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||
|
||||
if exist "%JAVA_EXE%" goto execute
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
|
||||
echo.
|
||||
echo Please set the JAVA_HOME variable in your environment to match the
|
||||
echo location of your Java installation.
|
||||
echo. 1>&2
|
||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
|
||||
echo. 1>&2
|
||||
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||
echo location of your Java installation. 1>&2
|
||||
|
||||
goto fail
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package org.lucares.pdb.api;
|
||||
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
public class AbortException extends RuntimeException {
|
||||
|
||||
private static final long serialVersionUID = 7614132985675048490L;
|
||||
@@ -29,4 +31,18 @@ public class AbortException extends RuntimeException {
|
||||
}
|
||||
}
|
||||
|
||||
public static void sleepAbortibly(final long millis) throws AbortException {
|
||||
final long deadline = System.currentTimeMillis() + millis;
|
||||
|
||||
while (System.currentTimeMillis() < deadline) {
|
||||
try {
|
||||
TimeUnit.MILLISECONDS.sleep(Math.min(10, deadline - System.currentTimeMillis()));
|
||||
} catch (final InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
throw new AbortException();
|
||||
}
|
||||
AbortException.abortIfInterrupted();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
2
pdb-js/.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
{
|
||||
}
|
||||
@@ -18,12 +18,15 @@
|
||||
"prefix": "app",
|
||||
"architect": {
|
||||
"build": {
|
||||
"builder": "@angular-devkit/build-angular:browser",
|
||||
"builder": "@angular-devkit/build-angular:application",
|
||||
"options": {
|
||||
"outputPath": "build/generated/resources",
|
||||
"outputPath": {
|
||||
"base": "build/generated/resources"
|
||||
},
|
||||
"index": "src/index.html",
|
||||
"main": "src/main.ts",
|
||||
"polyfills": "src/polyfills.ts",
|
||||
"polyfills": [
|
||||
"src/polyfills.ts"
|
||||
],
|
||||
"tsConfig": "tsconfig.app.json",
|
||||
"inlineStyleLanguage": "scss",
|
||||
"assets": [
|
||||
@@ -33,7 +36,10 @@
|
||||
"styles": [
|
||||
"src/styles.scss"
|
||||
],
|
||||
"scripts": []
|
||||
"scripts": [
|
||||
"node_modules/marked/marked.min.js"
|
||||
],
|
||||
"browser": "src/main.ts"
|
||||
},
|
||||
"configurations": {
|
||||
"production": {
|
||||
@@ -55,12 +61,11 @@
|
||||
"with": "src/environments/environment.prod.ts"
|
||||
}
|
||||
],
|
||||
"outputHashing": "all"
|
||||
"outputHashing": "all",
|
||||
"sourceMap": true
|
||||
},
|
||||
"development": {
|
||||
"buildOptimizer": false,
|
||||
"optimization": false,
|
||||
"vendorChunk": true,
|
||||
"extractLicenses": false,
|
||||
"sourceMap": true,
|
||||
"namedChunks": true
|
||||
@@ -75,10 +80,10 @@
|
||||
},
|
||||
"configurations": {
|
||||
"production": {
|
||||
"browserTarget": "pdb-js:build:production"
|
||||
"buildTarget": "pdb-js:build:production"
|
||||
},
|
||||
"development": {
|
||||
"browserTarget": "pdb-js:build:development"
|
||||
"buildTarget": "pdb-js:build:development"
|
||||
}
|
||||
},
|
||||
"defaultConfiguration": "development"
|
||||
@@ -86,7 +91,7 @@
|
||||
"extract-i18n": {
|
||||
"builder": "@angular-devkit/build-angular:extract-i18n",
|
||||
"options": {
|
||||
"browserTarget": "pdb-js:build"
|
||||
"buildTarget": "pdb-js:build"
|
||||
}
|
||||
},
|
||||
"test": {
|
||||
@@ -109,5 +114,8 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"cli": {
|
||||
"analytics": false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,8 @@ import java.nio.file.Files
|
||||
import java.nio.file.Paths
|
||||
|
||||
plugins {
|
||||
id("com.github.node-gradle.node") version "3.5.1"
|
||||
id("com.github.node-gradle.node") version "7.0.0"
|
||||
id("java-library") // not sure why this is needed - is already set in /build.gradle - but without it the project sometimes (not always) is not configured as a java project
|
||||
}
|
||||
|
||||
|
||||
|
||||
25292
pdb-js/package-lock.json
generated
@@ -9,38 +9,44 @@
|
||||
"test": "ng test",
|
||||
"lint": "ng lint",
|
||||
"e2e": "ng e2e",
|
||||
"releasebuild": "ng build --configuration production"
|
||||
"releasebuild": "ng build --configuration production",
|
||||
"explore": "source-map-explorer build/generated/resources/**/*.js"
|
||||
},
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@angular/animations": "^15.0.2",
|
||||
"@angular/cdk": "^15.0.1",
|
||||
"@angular/common": "^15.0.2",
|
||||
"@angular/compiler": "^15.0.2",
|
||||
"@angular/core": "^15.0.2",
|
||||
"@angular/forms": "^15.0.2",
|
||||
"@angular/material": "^15.0.1",
|
||||
"@angular/platform-browser": "^15.0.2",
|
||||
"@angular/platform-browser-dynamic": "^15.0.2",
|
||||
"@angular/router": "^15.0.2",
|
||||
"moment": "^2.29.1",
|
||||
"@angular/animations": "^18.2.6",
|
||||
"@angular/cdk": "^18.2.6",
|
||||
"@angular/common": "^18.2.6",
|
||||
"@angular/compiler": "^18.2.6",
|
||||
"@angular/core": "^18.2.6",
|
||||
"@angular/forms": "^18.2.6",
|
||||
"@angular/material": "^18.2.6",
|
||||
"@angular/platform-browser": "^18.2.6",
|
||||
"@angular/platform-browser-dynamic": "^18.2.6",
|
||||
"@angular/router": "^18.2.6",
|
||||
"luxon": "^3.4.3",
|
||||
"marked": "^12",
|
||||
"ngx-markdown": "18.0.0",
|
||||
"rxjs": "~7.5.0",
|
||||
"rxjs-compat": "^6.6.7",
|
||||
"tslib": "^2.3.0",
|
||||
"zone.js": "~0.11.4"
|
||||
"zone.js": "^0.14.10"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@angular-devkit/build-angular": "^15.0.2",
|
||||
"@angular/cli": "^15.0.2",
|
||||
"@angular/compiler-cli": "^15.0.2",
|
||||
"@types/jasmine": "~3.10.0",
|
||||
"@angular-devkit/build-angular": "^18.2.6",
|
||||
"@angular/cli": "^18.2.6",
|
||||
"@angular/compiler-cli": "^18.2.6",
|
||||
"@types/jasmine": "~4.3.0",
|
||||
"@types/luxon": "^3.3.2",
|
||||
"@types/marked": "^4.0.8",
|
||||
"@types/node": "^12.11.1",
|
||||
"jasmine-core": "~3.10.0",
|
||||
"karma": "~6.3.0",
|
||||
"karma-chrome-launcher": "~3.1.0",
|
||||
"karma-coverage": "~2.1.0",
|
||||
"karma-jasmine": "~4.0.0",
|
||||
"karma-jasmine-html-reporter": "~1.7.0",
|
||||
"typescript": "4.8"
|
||||
"jasmine-core": "~4.6.0",
|
||||
"karma": "~6.4.0",
|
||||
"karma-chrome-launcher": "~3.2.0",
|
||||
"karma-coverage": "~2.2.0",
|
||||
"karma-jasmine": "~5.1.0",
|
||||
"karma-jasmine-html-reporter": "~2.1.0",
|
||||
"source-map-explorer": "^2.5.3",
|
||||
"typescript": "^5.4.5"
|
||||
}
|
||||
}
|
||||
@@ -1,25 +1,29 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { Routes, RouterModule } from '@angular/router';
|
||||
import { VisualizationPageComponent } from './visualization-page/visualization-page.component';
|
||||
import { MainPageComponent } from './main-page/main-page.component';
|
||||
import { UploadPageComponent } from './upload-page/upload-page.component';
|
||||
import { HelpPageComponent } from './help-page/help-page.component';
|
||||
|
||||
import { NgModule } from "@angular/core";
|
||||
import { RouterModule, Routes } from "@angular/router";
|
||||
import { VisualizationPageComponent } from "./visualization-page/visualization-page.component";
|
||||
import { MainPageComponent } from "./main-page/main-page.component";
|
||||
import { UploadPageComponent } from "./upload-page/upload-page.component";
|
||||
import { HelpPageComponent } from "./help-page/help-page.component";
|
||||
import { DashboardPageComponent } from "./dashboard-page/dashboard-page.component";
|
||||
import { DashboardComponent } from "./dashboard-page/dashboard/dashboard.component";
|
||||
import { CustomizableGridComponent } from "./customizable-grid/customizable-grid.component";
|
||||
|
||||
const routes: Routes = [
|
||||
{ path: '', component: MainPageComponent},
|
||||
{ path: 'vis', component: VisualizationPageComponent },
|
||||
{ path: 'upload', component: UploadPageComponent },
|
||||
{ path: 'help', component: HelpPageComponent },
|
||||
{ path: "", component: MainPageComponent },
|
||||
{ path: "vis", component: VisualizationPageComponent },
|
||||
{ path: "dashboard", component: DashboardPageComponent },
|
||||
{ path: "dashboard/:id", component: DashboardComponent },
|
||||
{ path: "upload", component: UploadPageComponent },
|
||||
{ path: "grid", component: CustomizableGridComponent },
|
||||
{ path: "help", component: HelpPageComponent },
|
||||
// { path: '**', component: PageNotFoundComponent }
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
RouterModule.forRoot(routes, {})
|
||||
RouterModule.forRoot(routes, {}),
|
||||
],
|
||||
//declarations: [VisualizationPageComponent],
|
||||
declarations: [],
|
||||
exports: [RouterModule]
|
||||
exports: [RouterModule],
|
||||
})
|
||||
export class AppRoutingModule {}
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
|
||||
|
||||
<div id="main-toolbar">
|
||||
<a href="/" title="open main page"><img src="assets/img/home.svg" class="icon-small" aria-hidden="false" aria-label="go to main page" /></a>
|
||||
<a href="/vis" title="open visualization page"><img src="assets/img/scatter-chart2.svg" class="icon-small" aria-hidden="false" aria-label="go to visualization page" /></a>
|
||||
<!--<button mat-button [routerLink]="['/']"><img src="assets/img/strip-chart-color.svg" aria-hidden="false" aria-label="go to home page" /> Plotilio</button>-->
|
||||
<a mat-button [routerLink]="['/']"><img src="assets/img/plotilio_64.png" aria-hidden="false" aria-label="go to home page" /></a>
|
||||
<a mat-button [routerLink]="['/vis']">Visualization</a>
|
||||
<a mat-button [routerLink]="['/dashboard']">Dashboards<span class="super-badge">Beta</span></a>
|
||||
</div>
|
||||
|
||||
<router-outlet></router-outlet>
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
|
||||
|
||||
#main-toolbar {
|
||||
height: 2.0em;
|
||||
line-height: 3em;
|
||||
width: 100%;
|
||||
padding-bottom: 0.5em;
|
||||
border-bottom: solid 1px black;
|
||||
background-color: #eee;
|
||||
border-bottom: solid 1px #bbb;
|
||||
}
|
||||
|
||||
#main-toolbar a img {
|
||||
height: 1.5rem;
|
||||
vertical-align: text-bottom;
|
||||
}
|
||||
|
||||
.right{
|
||||
|
||||
@@ -1,41 +1,71 @@
|
||||
import { BrowserModule } from '@angular/platform-browser';
|
||||
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
|
||||
import { NgModule, enableProdMode } from '@angular/core';
|
||||
import { HttpClientModule } from '@angular/common/http';
|
||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
||||
import { BrowserModule } from "@angular/platform-browser";
|
||||
import { BrowserAnimationsModule } from "@angular/platform-browser/animations";
|
||||
import { enableProdMode, NgModule } from "@angular/core";
|
||||
import { provideHttpClient, withInterceptorsFromDi } from "@angular/common/http";
|
||||
import { FormsModule, ReactiveFormsModule } from "@angular/forms";
|
||||
|
||||
import { AppRoutingModule } from './app-routing.module';
|
||||
import { AppComponent } from './app.component';
|
||||
import { MainPageComponent } from './main-page/main-page.component';
|
||||
import { HelpPageComponent } from './help-page/help-page.component';
|
||||
import { UploadPageComponent } from './upload-page/upload-page.component';
|
||||
import { VisualizationPageComponent } from './visualization-page/visualization-page.component';
|
||||
import { AppRoutingModule } from "./app-routing.module";
|
||||
import { AppComponent } from "./app.component";
|
||||
import { MainPageComponent } from "./main-page/main-page.component";
|
||||
import { HelpPageComponent } from "./help-page/help-page.component";
|
||||
import { UploadPageComponent } from "./upload-page/upload-page.component";
|
||||
import { VisualizationPageComponent } from "./visualization-page/visualization-page.component";
|
||||
|
||||
import {MatLegacyAutocompleteModule as MatAutocompleteModule} from '@angular/material/legacy-autocomplete';
|
||||
import {MatLegacyButtonModule as MatButtonModule} from '@angular/material/legacy-button';
|
||||
import {MatLegacyCheckboxModule as MatCheckboxModule} from '@angular/material/legacy-checkbox';
|
||||
import {MatLegacySelectModule as MatSelectModule} from '@angular/material/legacy-select';
|
||||
import { MatLegacyFormFieldModule as MatFormFieldModule } from '@angular/material/legacy-form-field';
|
||||
import { MatLegacyInputModule as MatInputModule } from '@angular/material/legacy-input';
|
||||
import {MatLegacyProgressBarModule as MatProgressBarModule} from '@angular/material/legacy-progress-bar';
|
||||
import {MatLegacyProgressSpinnerModule as MatProgressSpinnerModule} from '@angular/material/legacy-progress-spinner';
|
||||
import {MatLegacyRadioModule as MatRadioModule} from '@angular/material/legacy-radio';
|
||||
import {MatLegacySnackBarModule as MatSnackBarModule} from '@angular/material/legacy-snack-bar';
|
||||
import {MatLegacyTooltipModule as MatTooltipModule} from '@angular/material/legacy-tooltip';
|
||||
import { YAxisDefinitionComponent } from './y-axis-definition/y-axis-definition.component';
|
||||
import { QueryAutocompleteComponent } from './query-autocomplete/query-autocomplete.component';
|
||||
import { LimitByComponent } from './limit-by/limit-by.component';
|
||||
import { PlotDetailsComponent } from './plot-details/plot-details.component';
|
||||
import { PlotViewComponent } from './plot-view/plot-view.component';
|
||||
import { GalleryViewComponent, GalleryItemView, GalleryFilterView } from './gallery-view/gallery-view.component';
|
||||
import { ImageToggleComponent } from './image-toggle/image-toggle.component';
|
||||
import { DatePickerComponent } from "./components/datepicker/date-picker.component";
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
import { MatAutocompleteModule } from "@angular/material/autocomplete";
|
||||
import { MatButtonModule } from "@angular/material/button";
|
||||
import { MatCheckboxModule } from "@angular/material/checkbox";
|
||||
import { MatSelectModule } from "@angular/material/select";
|
||||
import { MatFormFieldModule } from "@angular/material/form-field";
|
||||
import { MatInputModule } from "@angular/material/input";
|
||||
import { MatProgressBarModule } from "@angular/material/progress-bar";
|
||||
import { MatProgressSpinnerModule } from "@angular/material/progress-spinner";
|
||||
import { MatRadioModule } from "@angular/material/radio";
|
||||
import { MatSnackBarModule } from "@angular/material/snack-bar";
|
||||
import { MatTooltipModule } from "@angular/material/tooltip";
|
||||
import { OverlayModule } from "@angular/cdk/overlay";
|
||||
import { YAxisDefinitionComponent } from "./y-axis-definition/y-axis-definition.component";
|
||||
import { QueryAutocompleteComponent } from "./query-autocomplete/query-autocomplete.component";
|
||||
import { LimitByComponent } from "./limit-by/limit-by.component";
|
||||
import { PlotDetailsComponent } from "./plot-details/plot-details.component";
|
||||
import { PlotViewComponent } from "./plot-view/plot-view.component";
|
||||
import {
|
||||
GalleryFilterView,
|
||||
GalleryItemView,
|
||||
GalleryViewComponent,
|
||||
} from "./gallery-view/gallery-view.component";
|
||||
import { ImageToggleComponent } from "./image-toggle/image-toggle.component";
|
||||
import { DashboardPageComponent } from "./dashboard-page/dashboard-page.component";
|
||||
import { NewDashboardComponent } from "./dashboard-page/new-dashboard/new-dashboard.component";
|
||||
import {
|
||||
MAT_DIALOG_DEFAULT_OPTIONS,
|
||||
MatDialogModule,
|
||||
} from "@angular/material/dialog";
|
||||
import { MatTabsModule } from "@angular/material/tabs";
|
||||
import { MatTableModule } from "@angular/material/table";
|
||||
import { MatGridListModule } from "@angular/material/grid-list";
|
||||
import { MatCardModule } from "@angular/material/card";
|
||||
import { MatBadgeModule } from "@angular/material/badge";
|
||||
import { DashboardComponent } from "./dashboard-page/dashboard/dashboard.component";
|
||||
import { AddTextDialogComponent } from "./dashboard-page/dashboard/add-text-dialog/add-text-dialog.component";
|
||||
import { TextWidgetComponent } from "./dashboard-page/dashboard/text-widget/text-widget.component";
|
||||
import { AddPlotDialogComponent } from "./dashboard-page/dashboard/add-plot-dialog/add-plot-dialog.component";
|
||||
import { PlotWidgetComponent } from "./dashboard-page/dashboard/plot-widget/plot-widget.component";
|
||||
import { FullScreenPlotDialogComponent } from "./dashboard-page/dashboard/full-screen-plot-dialog/full-screen-plot-dialog.component";
|
||||
import { CustomizableGridComponent } from "./customizable-grid/customizable-grid.component";
|
||||
|
||||
import { DragDropModule } from "@angular/cdk/drag-drop";
|
||||
import { ConfirmationDialogComponent } from "./confirmation-dialog/confirmation-dialog.component";
|
||||
import { FocusDirective } from "./focus.directive";
|
||||
import { MarkdownModule } from "ngx-markdown";
|
||||
|
||||
@NgModule({ declarations: [
|
||||
AppComponent,
|
||||
MainPageComponent,
|
||||
HelpPageComponent,
|
||||
UploadPageComponent,
|
||||
DatePickerComponent,
|
||||
VisualizationPageComponent,
|
||||
YAxisDefinitionComponent,
|
||||
QueryAutocompleteComponent,
|
||||
@@ -45,30 +75,47 @@ import { ImageToggleComponent } from './image-toggle/image-toggle.component';
|
||||
GalleryViewComponent,
|
||||
GalleryItemView,
|
||||
GalleryFilterView,
|
||||
ImageToggleComponent
|
||||
ImageToggleComponent,
|
||||
DashboardPageComponent,
|
||||
NewDashboardComponent,
|
||||
DashboardComponent,
|
||||
AddTextDialogComponent,
|
||||
TextWidgetComponent,
|
||||
AddPlotDialogComponent,
|
||||
PlotWidgetComponent,
|
||||
FullScreenPlotDialogComponent,
|
||||
CustomizableGridComponent,
|
||||
ConfirmationDialogComponent,
|
||||
FocusDirective,
|
||||
],
|
||||
imports: [
|
||||
bootstrap: [AppComponent], imports: [MarkdownModule.forRoot(),
|
||||
BrowserModule,
|
||||
AppRoutingModule,
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
DragDropModule,
|
||||
MatAutocompleteModule,
|
||||
MatBadgeModule,
|
||||
MatButtonModule,
|
||||
MatCardModule,
|
||||
MatCheckboxModule,
|
||||
MatDialogModule,
|
||||
MatFormFieldModule,
|
||||
MatGridListModule,
|
||||
MatInputModule,
|
||||
MatRadioModule,
|
||||
MatProgressBarModule,
|
||||
MatProgressSpinnerModule,
|
||||
MatSelectModule,
|
||||
MatSnackBarModule,
|
||||
MatTabsModule,
|
||||
MatTableModule,
|
||||
MatTooltipModule,
|
||||
BrowserAnimationsModule,
|
||||
HttpClientModule
|
||||
],
|
||||
providers: [],
|
||||
bootstrap: [AppComponent]
|
||||
})
|
||||
OverlayModule], providers: [{
|
||||
provide: MAT_DIALOG_DEFAULT_OPTIONS,
|
||||
useValue: { hasBackdrop: true },
|
||||
}, provideHttpClient(withInterceptorsFromDi())] })
|
||||
export class AppModule {}
|
||||
|
||||
enableProdMode()
|
||||
enableProdMode();
|
||||
|
||||
188
pdb-js/src/app/components/datepicker/date-picker.component.html
Normal file
@@ -0,0 +1,188 @@
|
||||
<style>
|
||||
.date-picker-overlay {
|
||||
width: 500px;
|
||||
transition: box-shadow 200ms cubic-bezier(0, 0, 0.2, 1);
|
||||
box-shadow: 0 3px 1px -2px rgba(0, 0, 0, 0.2),
|
||||
0 2px 2px 0 rgba(0, 0, 0, 0.14), 0 1px 5px 0 rgba(0, 0, 0, 0.12);
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
.tab-quick {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 2em;
|
||||
}
|
||||
.tab-quick-column {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.date-picker-form-field {
|
||||
width: 23.5em;
|
||||
}
|
||||
</style>
|
||||
|
||||
<button
|
||||
mat-button
|
||||
matTooltip="Date Picker"
|
||||
(click)="isOpen = !isOpen"
|
||||
cdkOverlayOrigin
|
||||
#trigger="cdkOverlayOrigin"
|
||||
[attr.disabled]="isDisabled ? 'disabled' : null"
|
||||
>
|
||||
{{ datePickerControl.value?.display }}
|
||||
</button>
|
||||
|
||||
<ng-template
|
||||
cdkConnectedOverlay
|
||||
[cdkConnectedOverlayOrigin]="trigger"
|
||||
[cdkConnectedOverlayOpen]="isOpen"
|
||||
>
|
||||
<div class="date-picker-overlay">
|
||||
<mat-tab-group
|
||||
animationDuration="0ms"
|
||||
(selectedTabChange)="tabChange()"
|
||||
[(selectedIndex)]="selectedTabIndex"
|
||||
>
|
||||
<mat-tab label="Quick">
|
||||
<div class="tab-quick">
|
||||
<div class="tab-quick-column">
|
||||
<button mat-button (click)="applyQuick('BD/E1D', 'today')">
|
||||
Today
|
||||
</button>
|
||||
<button mat-button (click)="applyQuick('B-1D/E-1D', 'yesterday')">
|
||||
Yesterday
|
||||
</button>
|
||||
<button mat-button (click)="applyQuick('BW/EW', 'this week')">
|
||||
This Week
|
||||
</button>
|
||||
<button mat-button (click)="applyQuick('BM/EM', 'this month')">
|
||||
This Month
|
||||
</button>
|
||||
<button mat-button (click)="applyQuick('BY/EY', 'this year')">
|
||||
This Year
|
||||
</button>
|
||||
</div>
|
||||
<div class="tab-quick-column">
|
||||
<button mat-button (click)="applyQuick('B-7D/ED', 'last 7 days')">
|
||||
Last 7 Days
|
||||
</button>
|
||||
<button mat-button (click)="applyQuick('B-1W/ED', 'last week')">
|
||||
Last Week
|
||||
</button>
|
||||
<button mat-button (click)="applyQuick('B-30D/ED', 'last 30 days')">
|
||||
Last 30 Days
|
||||
</button>
|
||||
<button mat-button (click)="applyQuick('B-1M/E-1M', 'last month')">
|
||||
Last Month
|
||||
</button>
|
||||
<button
|
||||
mat-button
|
||||
(click)="applyQuick('B-3M/E-1M', 'last 3 months')"
|
||||
>
|
||||
Last 3 Months
|
||||
</button>
|
||||
<button mat-button (click)="applyQuick('B-1Y/E-1Y', 'last year')">
|
||||
Last Year
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</mat-tab>
|
||||
|
||||
<mat-tab label="Relative">
|
||||
<mat-form-field
|
||||
class="pdb-form-number-small"
|
||||
(wheel)="scrollRelativeTimeRange($event, 'seconds', 59)"
|
||||
>
|
||||
<mat-label>Seconds:</mat-label>
|
||||
<input
|
||||
matInput
|
||||
name="relative-time-range-seconds"
|
||||
[(ngModel)]="relativeTimeRange.seconds"
|
||||
type="number"
|
||||
min="0"
|
||||
max="59"
|
||||
/>
|
||||
</mat-form-field>
|
||||
<mat-form-field
|
||||
class="pdb-form-number-small"
|
||||
(wheel)="scrollRelativeTimeRange($event, 'minutes', 59)"
|
||||
>
|
||||
<mat-label>Minutes:</mat-label>
|
||||
<input
|
||||
matInput
|
||||
name="relative-time-range-minutes"
|
||||
[(ngModel)]="relativeTimeRange.minutes"
|
||||
type="number"
|
||||
min="0"
|
||||
max="59"
|
||||
/>
|
||||
</mat-form-field>
|
||||
<mat-form-field
|
||||
class="pdb-form-number-small"
|
||||
(wheel)="scrollRelativeTimeRange($event, 'hours', 23)"
|
||||
>
|
||||
<mat-label>Hours:</mat-label>
|
||||
<input
|
||||
matInput
|
||||
name="relative-time-range-hours"
|
||||
[(ngModel)]="relativeTimeRange.hours"
|
||||
type="number"
|
||||
min="0"
|
||||
max="23"
|
||||
/>
|
||||
</mat-form-field>
|
||||
<mat-form-field
|
||||
class="pdb-form-number-small"
|
||||
(wheel)="scrollRelativeTimeRange($event, 'days', 367)"
|
||||
>
|
||||
<mat-label>Days:</mat-label>
|
||||
<input
|
||||
matInput
|
||||
name="relative-time-range-days"
|
||||
[(ngModel)]="relativeTimeRange.days"
|
||||
type="number"
|
||||
min="0"
|
||||
max="367"
|
||||
/>
|
||||
</mat-form-field>
|
||||
<mat-form-field
|
||||
class="pdb-form-number-small"
|
||||
(wheel)="scrollRelativeTimeRange($event, 'months', 11)"
|
||||
>
|
||||
<mat-label>Months:</mat-label>
|
||||
<input
|
||||
matInput
|
||||
name="relative-time-range-months"
|
||||
[(ngModel)]="relativeTimeRange.months"
|
||||
type="number"
|
||||
min="0"
|
||||
max="11"
|
||||
/>
|
||||
</mat-form-field>
|
||||
<mat-form-field
|
||||
class="pdb-form-number-small"
|
||||
(wheel)="scrollRelativeTimeRange($event, 'years', 10)"
|
||||
>
|
||||
<mat-label>Years:</mat-label>
|
||||
<input
|
||||
matInput
|
||||
name="relative-time-range-years"
|
||||
[(ngModel)]="relativeTimeRange.years"
|
||||
type="number"
|
||||
min="0"
|
||||
max="10"
|
||||
/>
|
||||
</mat-form-field>
|
||||
<button mat-button (click)="applyRelativeTimeRange()">Apply</button>
|
||||
</mat-tab>
|
||||
<mat-tab label="Absolute">
|
||||
<mat-form-field class="date-picker-form-field">
|
||||
<mat-label>Date Range:</mat-label>
|
||||
<input matInput [formControl]="dateRange" name="dates" />
|
||||
</mat-form-field>
|
||||
<button mat-button (click)="applyAbsoluteTime()">Apply</button>
|
||||
</mat-tab>
|
||||
</mat-tab-group>
|
||||
</div>
|
||||
</ng-template>
|
||||
183
pdb-js/src/app/components/datepicker/date-picker.component.ts
Normal file
@@ -0,0 +1,183 @@
|
||||
import {
|
||||
Component,
|
||||
EventEmitter,
|
||||
forwardRef,
|
||||
Input,
|
||||
Output,
|
||||
} from "@angular/core";
|
||||
import {
|
||||
ControlValueAccessor,
|
||||
FormControl,
|
||||
NG_VALUE_ACCESSOR,
|
||||
Validators,
|
||||
} from "@angular/forms";
|
||||
|
||||
export type DateType = "QUICK" | "RELATIVE" | "ABSOLUTE";
|
||||
|
||||
export class DateValue {
|
||||
constructor(
|
||||
public type: DateType,
|
||||
public value: string,
|
||||
public display: string,
|
||||
) {}
|
||||
}
|
||||
|
||||
export class DatePickerChange {
|
||||
constructor(public value: DateValue) {}
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: "app-date-picker",
|
||||
templateUrl: "./date-picker.component.html",
|
||||
providers: [
|
||||
{
|
||||
provide: NG_VALUE_ACCESSOR,
|
||||
useExisting: forwardRef(() => DatePickerComponent),
|
||||
multi: true,
|
||||
},
|
||||
],
|
||||
})
|
||||
export class DatePickerComponent implements ControlValueAccessor {
|
||||
isOpen = false;
|
||||
|
||||
relativeTimeRangeUnit = "relativeTimeRangeMinutes";
|
||||
|
||||
relativeTimeRangeAmount = 15;
|
||||
|
||||
relativeTimeRange = {
|
||||
seconds: 0,
|
||||
minutes: 15,
|
||||
hours: 0,
|
||||
days: 0,
|
||||
months: 0,
|
||||
years: 0,
|
||||
};
|
||||
|
||||
dateRange = new FormControl<string>(
|
||||
"2019-10-05 00:00:00 - 2019-10-11 23:59:59",
|
||||
[
|
||||
Validators.pattern(
|
||||
/^\d{4}-\d{2}-\d{2} ([01][0-9]|2[0-3]):\d{2}:\d{2} - \d{4}-\d{2}-\d{2} ([01][0-9]|2[0-3]):\d{2}:\d{2}$/,
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
datePickerControl = new FormControl(
|
||||
new DateValue("QUICK", "BM/EM", "this month"),
|
||||
);
|
||||
|
||||
@Input()
|
||||
isDisabled: boolean = false;
|
||||
|
||||
@Output()
|
||||
readonly dateValueSelected: EventEmitter<DatePickerChange> = new EventEmitter<
|
||||
DatePickerChange
|
||||
>();
|
||||
|
||||
selectedTabIndex = 0;
|
||||
|
||||
_onChange = (_: any) => {};
|
||||
|
||||
_onTouched = (_: any) => {};
|
||||
|
||||
constructor() {}
|
||||
|
||||
getDateValue(): DateValue {
|
||||
return this.datePickerControl.value!;
|
||||
}
|
||||
|
||||
writeValue(obj: DateValue): void {
|
||||
this.datePickerControl.setValue(obj);
|
||||
switch (obj.type) {
|
||||
case "QUICK":
|
||||
break;
|
||||
case "ABSOLUTE":
|
||||
this.dateRange.setValue(obj.value);
|
||||
break;
|
||||
case "RELATIVE":
|
||||
const x = this.relativeTimeRange;
|
||||
// obj.value looks like "P1Y2M3DT4H5M6S" or "PT4H5M6S" or "P1Y2M3D" or "P1YT6S" or ...
|
||||
const matches = obj.value.match(
|
||||
/P(?:(\d+)Y)(?:(\d+)M)(?:(\d+)D)?(?:T(?:(\d+)H)(?:(\d+)M)(?:(\d+)S))?/,
|
||||
) ?? [];
|
||||
|
||||
x.years = Number.parseInt(matches[1] ?? 0);
|
||||
x.months = Number.parseInt(matches[2] ?? 0);
|
||||
x.days = Number.parseInt(matches[3] ?? 0);
|
||||
x.hours = Number.parseInt(matches[4] ?? 0);
|
||||
x.minutes = Number.parseInt(matches[5] ?? 0);
|
||||
x.seconds = Number.parseInt(matches[6] ?? 0);
|
||||
break;
|
||||
default:
|
||||
}
|
||||
}
|
||||
registerOnChange(fn: any): void {
|
||||
this._onChange = fn;
|
||||
}
|
||||
registerOnTouched(fn: any): void {
|
||||
this._onTouched = fn;
|
||||
}
|
||||
setDisabledState?(isDisabled: boolean): void {
|
||||
this.isDisabled = isDisabled;
|
||||
}
|
||||
|
||||
dateDisplay(): string {
|
||||
return this.datePickerControl.value?.display || "no date set";
|
||||
}
|
||||
|
||||
tabChange() {
|
||||
//(<any> window).initSimpleDatePicker(); // breaks form control
|
||||
}
|
||||
|
||||
setDateValue(dateValue: DateValue) {
|
||||
this.datePickerControl.setValue(dateValue);
|
||||
this._onChange(dateValue);
|
||||
this.dateValueSelected.emit(new DatePickerChange(dateValue));
|
||||
//console.log("date value updated: ", dateValue);
|
||||
}
|
||||
|
||||
applyQuick(value: string, display: string) {
|
||||
const newValue = new DateValue("QUICK", value, display);
|
||||
this.setDateValue(newValue);
|
||||
this.isOpen = false;
|
||||
}
|
||||
|
||||
private fixToRange(val: number, min: number, max: number) {
|
||||
return val < min ? min : (val > max ? max : val);
|
||||
}
|
||||
|
||||
applyRelativeTimeRange() {
|
||||
|
||||
const x = this.relativeTimeRange;
|
||||
const years = x.years ? "-"+x.years + "Y" : "";
|
||||
const months = x.months ? "-"+x.months + "M" : "";
|
||||
const days = x.days ? "-"+x.days + "D" : "";
|
||||
const hours = x.hours ? "-"+x.hours + "H" : "";
|
||||
const minutes = x.minutes ? "-"+x.minutes + "m" : "";
|
||||
|
||||
const timeRange = `B${years}${months}${days}${hours}${minutes}/Bm`;
|
||||
|
||||
const newValue = new DateValue("RELATIVE", timeRange, timeRange);
|
||||
this.setDateValue(newValue);
|
||||
this.isOpen = false;
|
||||
}
|
||||
|
||||
applyAbsoluteTime() {
|
||||
const value = <string> this.dateRange.value;
|
||||
const newValue = new DateValue("ABSOLUTE", value, value);
|
||||
this.setDateValue(newValue);
|
||||
this.isOpen = false;
|
||||
}
|
||||
|
||||
scrollRelativeTimeRange(
|
||||
event: WheelEvent,
|
||||
unit: "seconds" | "minutes" | "hours" | "days" | "months" | "years",
|
||||
max: number,
|
||||
) {
|
||||
this.relativeTimeRange[unit] = this.fixToRange(
|
||||
this.relativeTimeRange[unit] + (event.deltaY > 0 ? -1 : 1),
|
||||
0,
|
||||
max,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
<style>
|
||||
div[mat-dialog-content] {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1em;
|
||||
}
|
||||
</style>
|
||||
<h1 *ngIf="data.title" mat-dialog-title>{{data.title}}</h1>
|
||||
<div mat-dialog-content>
|
||||
<img src="assets/img/question-mark-round.svg" class="icon-middle" />
|
||||
<div>{{data.text}}</div>
|
||||
</div>
|
||||
<div mat-dialog-actions align="end">
|
||||
<button mat-button mat-dialog-close cdkFocusRegionStart focus>{{data.btnCancelLabel}}</button>
|
||||
<button mat-raised-button color="warn" mat-dialog-close (click)="onOkClick()" cdkFocusRegionEnd>{{data.btnOkLabel}}</button>
|
||||
</div>
|
||||
@@ -0,0 +1,23 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { ConfirmationDialogComponent } from './confirmation-dialog.component';
|
||||
|
||||
describe('ConfirmationDialogComponent', () => {
|
||||
let component: ConfirmationDialogComponent;
|
||||
let fixture: ComponentFixture<ConfirmationDialogComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [ ConfirmationDialogComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(ConfirmationDialogComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,18 @@
|
||||
import { Component, ElementRef, Inject, ViewChild } from '@angular/core';
|
||||
import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog';
|
||||
|
||||
@Component({
|
||||
selector: 'app-confirmation-dialog',
|
||||
templateUrl: './confirmation-dialog.component.html'
|
||||
})
|
||||
export class ConfirmationDialogComponent {
|
||||
|
||||
constructor(
|
||||
public dialogRef: MatDialogRef<boolean>,
|
||||
@Inject(MAT_DIALOG_DATA) public data: {title:string, text: string, btnOkLabel:string, btnCancelLabel:string}){
|
||||
}
|
||||
|
||||
onOkClick(): void {
|
||||
this.dialogRef.close(true);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
<style>
|
||||
.example-list {
|
||||
list-style-type: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.example-list li {
|
||||
display: table-cell;
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
.example-container {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
min-width: 600px;
|
||||
max-width: 1200px;
|
||||
}
|
||||
|
||||
.example-box {
|
||||
width: var(--box-width);
|
||||
height: var(--box-height);
|
||||
border: solid 1px #ccc;
|
||||
font-size: 30pt;
|
||||
font-weight: bold;
|
||||
color: rgba(0, 0, 0, 0.87);
|
||||
cursor: grab;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
background: #fff;
|
||||
border-radius: 4px;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
transition: box-shadow 200ms cubic-bezier(0, 0, 0.2, 1);
|
||||
box-shadow: 0 3px 1px -2px rgba(0, 0, 0, 0.2), 0 2px 2px 0 rgba(0, 0, 0, 0.14),
|
||||
0 1px 5px 0 rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
|
||||
.example-box--wide {
|
||||
width: 400px;
|
||||
height: var(--box-height);
|
||||
}
|
||||
|
||||
.example-box:active {
|
||||
box-shadow: 0 5px 5px -3px rgba(0, 0, 0, 0.2),
|
||||
0 8px 10px 1px rgba(0, 0, 0, 0.14), 0 3px 14px 2px rgba(0, 0, 0, 0.12);
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.cdk-drop-list {
|
||||
display: flex;
|
||||
padding-right: 10px;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
|
||||
.cdk-drag-preview {
|
||||
box-sizing: border-box;
|
||||
border-color: red;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 5px 5px -3px rgba(0, 0, 0, 0.2),
|
||||
0 8px 10px 1px rgba(0, 0, 0, 0.14), 0 3px 14px 2px rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
|
||||
.cdk-drag-placeholder {
|
||||
opacity: 0.3;
|
||||
border-color: green;
|
||||
}
|
||||
|
||||
.cdk-drag-animating {
|
||||
transition: transform 250ms cubic-bezier(0, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
.cdk-drop-list-dragging {
|
||||
cursor: grabbing !important;
|
||||
}
|
||||
|
||||
button {
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
</style>
|
||||
<h1>Drag&Drop with a flex-wrap</h1>
|
||||
|
||||
<button (click)="add()">Add</button>
|
||||
<button (click)="shuffle()">Shuffle</button>
|
||||
|
||||
<br />
|
||||
|
||||
<ul class="example-list">
|
||||
<li *ngFor="let item of items">{{ item }}</li>
|
||||
</ul>
|
||||
|
||||
<div
|
||||
class="example-container"
|
||||
cdkDropListGroup
|
||||
[ngStyle]="{ '--box-width': boxWidth, '--box-height': boxHeight }"
|
||||
>
|
||||
<div
|
||||
cdkDropList
|
||||
style="outline: dashed 2px black;"
|
||||
(cdkDropListEntered)="onDropListEntered($event)"
|
||||
(cdkDropListDropped)="onDropListDropped()"
|
||||
></div>
|
||||
<div
|
||||
cdkDropList
|
||||
(cdkDropListEntered)="onDropListEntered($event)"
|
||||
(cdkDropListDropped)="onDropListDropped()"
|
||||
*ngFor="let item of items"
|
||||
>
|
||||
<div cdkDrag class="example-box" [ngClass]="{'example-box--wide': item%14==0}">{{ item }}</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,23 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { CustomizableGridComponent } from './customizable-grid.component';
|
||||
|
||||
describe('CustomizableGridComponent', () => {
|
||||
let component: CustomizableGridComponent;
|
||||
let fixture: ComponentFixture<CustomizableGridComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [ CustomizableGridComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(CustomizableGridComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
120
pdb-js/src/app/customizable-grid/customizable-grid.component.ts
Normal file
@@ -0,0 +1,120 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { CdkDragEnter, CdkDropList, moveItemInArray, DragRef} from '@angular/cdk/drag-drop';
|
||||
import { AfterViewInit } from '@angular/core';
|
||||
import { ViewChild } from '@angular/core';
|
||||
|
||||
|
||||
@Component({
|
||||
selector: 'app-customizable-grid',
|
||||
templateUrl: './customizable-grid.component.html'
|
||||
})
|
||||
export class CustomizableGridComponent implements AfterViewInit {
|
||||
@ViewChild(CdkDropList) placeholder!: CdkDropList;
|
||||
|
||||
private target: CdkDropList|null = null;
|
||||
private targetIndex: number = 0;
|
||||
private source: CdkDropList|null = null;
|
||||
private sourceIndex: number = 0;
|
||||
private dragRef: DragRef|null = null;
|
||||
|
||||
items: Array<number> = [1, 2, 3, 4, 5, 6, 7, 8, 9];
|
||||
|
||||
boxWidth = '200px';
|
||||
boxHeight = '200px';
|
||||
|
||||
ngAfterViewInit() {
|
||||
const placeholderElement = this.placeholder.element.nativeElement;
|
||||
|
||||
placeholderElement.style.display = 'none';
|
||||
placeholderElement.parentNode!.removeChild(placeholderElement);
|
||||
}
|
||||
|
||||
add() {
|
||||
this.items.push(this.items.length + 1);
|
||||
}
|
||||
|
||||
shuffle() {
|
||||
this.items.sort(function () {
|
||||
return 0.5 - Math.random();
|
||||
});
|
||||
}
|
||||
|
||||
onDropListDropped() {
|
||||
if (!this.target) {
|
||||
return;
|
||||
}
|
||||
|
||||
const placeholderElement: HTMLElement =
|
||||
this.placeholder.element.nativeElement;
|
||||
const placeholderParentElement: HTMLElement =
|
||||
placeholderElement.parentElement!;
|
||||
|
||||
placeholderElement.style.display = 'none';
|
||||
|
||||
placeholderParentElement.removeChild(placeholderElement);
|
||||
placeholderParentElement.appendChild(placeholderElement);
|
||||
placeholderParentElement.insertBefore(
|
||||
this.source!.element.nativeElement,
|
||||
placeholderParentElement.children[this.sourceIndex]
|
||||
);
|
||||
|
||||
if (this.placeholder._dropListRef.isDragging() && this.dragRef != null) {
|
||||
this.placeholder._dropListRef.exit(this.dragRef);
|
||||
}
|
||||
|
||||
this.target = null;
|
||||
this.source = null;
|
||||
this.dragRef = null;
|
||||
|
||||
if (this.sourceIndex !== this.targetIndex) {
|
||||
moveItemInArray(this.items, this.sourceIndex, this.targetIndex);
|
||||
}
|
||||
}
|
||||
|
||||
onDropListEntered({ item, container }: CdkDragEnter) {
|
||||
if (container == this.placeholder) {
|
||||
return;
|
||||
}
|
||||
|
||||
const placeholderElement: HTMLElement =
|
||||
this.placeholder.element.nativeElement;
|
||||
const sourceElement: HTMLElement = item.dropContainer.element.nativeElement;
|
||||
const dropElement: HTMLElement = container.element.nativeElement;
|
||||
const dragIndex: number = Array.prototype.indexOf.call(
|
||||
dropElement.parentElement!.children,
|
||||
this.source ? placeholderElement : sourceElement
|
||||
);
|
||||
const dropIndex: number = Array.prototype.indexOf.call(
|
||||
dropElement.parentElement!.children,
|
||||
dropElement
|
||||
);
|
||||
|
||||
if (!this.source) {
|
||||
this.sourceIndex = dragIndex;
|
||||
this.source = item.dropContainer;
|
||||
|
||||
placeholderElement.style.width = this.boxWidth + 'px';
|
||||
placeholderElement.style.height = this.boxHeight + 40 + 'px';
|
||||
|
||||
sourceElement.parentElement!.removeChild(sourceElement);
|
||||
}
|
||||
|
||||
this.targetIndex = dropIndex;
|
||||
this.target = container;
|
||||
this.dragRef = item._dragRef;
|
||||
|
||||
placeholderElement.style.display = '';
|
||||
placeholderElement.style.backgroundColor ='pink';
|
||||
|
||||
dropElement.parentElement!.insertBefore(
|
||||
placeholderElement,
|
||||
dropIndex > dragIndex ? dropElement.nextSibling : dropElement
|
||||
);
|
||||
|
||||
this.placeholder._dropListRef.enter(
|
||||
item._dragRef,
|
||||
item.element.nativeElement.offsetLeft,
|
||||
item.element.nativeElement.offsetTop
|
||||
);
|
||||
}
|
||||
}
|
||||
72
pdb-js/src/app/dashboard-page/dashboard-page.component.html
Normal file
@@ -0,0 +1,72 @@
|
||||
<style>
|
||||
:host {
|
||||
padding: 0.5em;
|
||||
}
|
||||
|
||||
.center {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
.center-content {
|
||||
text-align: center;
|
||||
}
|
||||
.is-error {
|
||||
font-size: 3rem;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
}
|
||||
.no-break {
|
||||
white-space: nowrap;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div *ngIf="loading" class="center">
|
||||
<mat-spinner></mat-spinner>
|
||||
</div>
|
||||
<div *ngIf="error" class="center is-error center-content">
|
||||
{{error}}
|
||||
</div>
|
||||
<div *ngIf="!loading && !error">
|
||||
<div class="toolbar">
|
||||
<button mat-button (click)="createNewDashboard()">New</button>
|
||||
</div>
|
||||
<div *ngIf="dataSource.length == 0" class="center center-content is-error">
|
||||
No Dashboard Found
|
||||
</div>
|
||||
<table *ngIf="dataSource.length > 0" mat-table [dataSource]="dataSource" >
|
||||
|
||||
<!-- Icon Column -->
|
||||
<ng-container matColumnDef="icon">
|
||||
<th mat-header-cell *matHeaderCellDef></th>
|
||||
<td mat-cell *matCellDef="let element"><a [routerLink]="['/dashboard', element.id]"><img src="assets/img/dashboard-outline.svg"/></a></td>
|
||||
</ng-container>
|
||||
<!-- Name Column -->
|
||||
<ng-container matColumnDef="name">
|
||||
<th mat-header-cell *matHeaderCellDef> Name </th>
|
||||
<td mat-cell *matCellDef="let element"><a [routerLink]="['/dashboard', element.id]">{{element.name}}</a></td>
|
||||
</ng-container>
|
||||
<!-- Description Column -->
|
||||
<ng-container matColumnDef="description">
|
||||
<th mat-header-cell *matHeaderCellDef>Description</th>
|
||||
<td mat-cell *matCellDef="let element">{{element.description}}</td>
|
||||
</ng-container>
|
||||
<!-- actions Column -->
|
||||
<ng-container matColumnDef="actions">
|
||||
<th mat-header-cell *matHeaderCellDef>Actions</th>
|
||||
<td mat-cell *matCellDef="let element" class="no-break">
|
||||
<button mat-icon-button (click)="edit(element)" aria-label="edit dashboard" title="edit dashboard">
|
||||
<img src="assets/img/edit-outline.svg" class="icon-small" title="delete" />
|
||||
</button>
|
||||
<button mat-icon-button (click)="delete(element)" aria-label="delete dashboard" title="delete dashboard">
|
||||
<img src="assets/img/recycle-bin-line.svg" class="icon-small" title="delete" />
|
||||
</button>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
|
||||
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { DashboardPageComponent } from './dashboard-page.component';
|
||||
|
||||
describe('DashboardPageComponent', () => {
|
||||
let component: DashboardPageComponent;
|
||||
let fixture: ComponentFixture<DashboardPageComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [ DashboardPageComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(DashboardPageComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
103
pdb-js/src/app/dashboard-page/dashboard-page.component.ts
Normal file
@@ -0,0 +1,103 @@
|
||||
import { HttpErrorResponse } from '@angular/common/http';
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
import { MatSnackBar } from '@angular/material/snack-bar';
|
||||
import { ConfirmationDialogComponent } from '../confirmation-dialog/confirmation-dialog.component';
|
||||
import { Dashboard, DashboardCreationData, DashboardList, DashboardService } from '../dashboard.service';
|
||||
import { NewDashboardComponent } from './new-dashboard/new-dashboard.component';
|
||||
|
||||
@Component({
|
||||
selector: 'app-dashboard-page',
|
||||
templateUrl: './dashboard-page.component.html'
|
||||
})
|
||||
export class DashboardPageComponent implements OnInit {
|
||||
|
||||
displayedColumns: string[] = [/*'icon',*/ 'name', 'description','actions'];
|
||||
dataSource: Dashboard[] = [];
|
||||
loading = true;
|
||||
error = "";
|
||||
|
||||
constructor(
|
||||
public dialog: MatDialog,
|
||||
private dashboardService: DashboardService,
|
||||
private snackBar: MatSnackBar){}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.refreshTable();
|
||||
}
|
||||
|
||||
refreshTable() {
|
||||
this.loading = true;
|
||||
this.dashboardService.getDashboards().subscribe({
|
||||
'next':(dashboardList: DashboardList) => {
|
||||
this.dataSource = dashboardList.dashboards;
|
||||
this.loading = false;
|
||||
},
|
||||
'error': (error: HttpErrorResponse) => {
|
||||
if (error.status == 504){
|
||||
this.error = "Server Unreachable";
|
||||
}else{
|
||||
this.error = "Failed to load dashboards";
|
||||
}
|
||||
this.loading = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
createNewDashboard() {
|
||||
const dialogRef = this.dialog.open(NewDashboardComponent, {
|
||||
data: {name: "", description: ""},
|
||||
width: '30em'
|
||||
});
|
||||
|
||||
dialogRef.afterClosed().subscribe((result: DashboardCreationData) => {
|
||||
this.dashboardService.createDashboard(result).subscribe(result => this.refreshTable());
|
||||
});
|
||||
}
|
||||
|
||||
edit(dashboard: Dashboard) {
|
||||
const dialogRef = this.dialog.open(NewDashboardComponent, {
|
||||
data: {name: dashboard.name, description: dashboard.description},
|
||||
hasBackdrop: true
|
||||
});
|
||||
|
||||
dialogRef.afterClosed().subscribe((result?: DashboardCreationData) => {
|
||||
|
||||
if (result) {
|
||||
dashboard.name = result.name;
|
||||
dashboard.description = result.description;
|
||||
this.dashboardService.saveDashboard(dashboard).subscribe({
|
||||
'error': () => {
|
||||
this.snackBar.open("server made a boo boo", "", {
|
||||
duration:5000
|
||||
})
|
||||
},
|
||||
complete: () => {
|
||||
this.refreshTable()
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
delete(dashboard: Dashboard){
|
||||
|
||||
const dialogRef = this.dialog.open(ConfirmationDialogComponent, {
|
||||
data: {title: "", text: "Delete dashboard '"+dashboard.name+"'?", btnOkLabel: "Delete", btnCancelLabel: "Cancel"}
|
||||
});
|
||||
|
||||
dialogRef.afterClosed().subscribe((result: boolean) => {
|
||||
if (result === true) {
|
||||
this.dashboardService.deleteDashboard(dashboard.id).subscribe({
|
||||
'error': (error) => {
|
||||
this.snackBar.open("failed to delete dashboard","", {
|
||||
duration: 5000,
|
||||
verticalPosition: 'top'
|
||||
});
|
||||
},
|
||||
'complete': () => this.refreshTable()
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
<style>
|
||||
:host {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height:100%;
|
||||
}
|
||||
pdb-visualization-page {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: calc(100% - 150px);
|
||||
flex-grow: 1;
|
||||
}
|
||||
.mat-mdc-dialog-content {
|
||||
max-height: unset;
|
||||
}
|
||||
</style>
|
||||
<pdb-visualization-page mat-dialog-content #plot [defaultConfig]="data.config" [galleryEnabled]="false"></pdb-visualization-page>
|
||||
|
||||
<div mat-dialog-actions align="end">
|
||||
<button mat-button mat-dialog-close >Cancel</button>
|
||||
<button class="save-button" mat-button mat-dialog-close (click)="onSaveClick()">Save</button>
|
||||
</div>
|
||||
@@ -0,0 +1,23 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { AddPlotDialogComponent } from './add-plot-dialog.component';
|
||||
|
||||
describe('AddPlotDialogComponent', () => {
|
||||
let component: AddPlotDialogComponent;
|
||||
let fixture: ComponentFixture<AddPlotDialogComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [ AddPlotDialogComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(AddPlotDialogComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,26 @@
|
||||
import { AfterViewInit, Component, ElementRef, Inject, ViewChild } from '@angular/core';
|
||||
import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog';
|
||||
import { PlotConfig } from 'src/app/plot.service';
|
||||
import { VisualizationPageComponent } from 'src/app/visualization-page/visualization-page.component';
|
||||
|
||||
@Component({
|
||||
selector: 'app-add-plot-dialog',
|
||||
templateUrl: './add-plot-dialog.component.html'
|
||||
})
|
||||
export class AddPlotDialogComponent {
|
||||
|
||||
@ViewChild("plot") plotElement! :VisualizationPageComponent;
|
||||
|
||||
|
||||
|
||||
constructor(
|
||||
public dialogRef: MatDialogRef<PlotConfig | undefined>,
|
||||
@Inject(MAT_DIALOG_DATA) public data: {config: PlotConfig, title: string}
|
||||
){
|
||||
}
|
||||
|
||||
onSaveClick(): void {
|
||||
const config = this.plotElement.createPlotConfig();
|
||||
this.dialogRef.close(config);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
<style>
|
||||
|
||||
markdown {
|
||||
--mdc-dialog-supporting-text-color: black;
|
||||
}
|
||||
.mat-mdc-dialog-content {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
height: 22em;
|
||||
}
|
||||
.mat-mdc-dialog-content > div {
|
||||
width:50%;
|
||||
}
|
||||
.preview {
|
||||
margin-left: 0.5em;
|
||||
overflow: auto;
|
||||
}
|
||||
mat-form-field textarea {
|
||||
height: 20em;
|
||||
}
|
||||
</style>
|
||||
<h1 mat-dialog-title>Add Text</h1>
|
||||
<div mat-dialog-content>
|
||||
<div>
|
||||
<mat-form-field class="pdb-form-full-width">
|
||||
<mat-label>Text</mat-label>
|
||||
<textarea matInput [(ngModel)]="text" #textElement focus ></textarea>
|
||||
</mat-form-field>
|
||||
<div>Text field supports <a href="https://spec.commonmark.org/" class="external-link" target="_blank" rel="noopener">Markdown</a>.</div>
|
||||
</div>
|
||||
<div class="preview">
|
||||
<markdown [data]="this.text"></markdown>
|
||||
</div>
|
||||
</div>
|
||||
<div mat-dialog-actions align="end">
|
||||
<button mat-button mat-dialog-close (click)="close()">Cancel</button>
|
||||
<button class="save-button" mat-button mat-dialog-close (click)="onSaveClick()">Save</button>
|
||||
</div>
|
||||
@@ -0,0 +1,23 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { AddTextDialogComponent } from './add-text-dialog.component';
|
||||
|
||||
describe('AddTextDialogComponent', () => {
|
||||
let component: AddTextDialogComponent;
|
||||
let fixture: ComponentFixture<AddTextDialogComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [ AddTextDialogComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(AddTextDialogComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,24 @@
|
||||
import { Component, ElementRef, Inject, ViewChild } from '@angular/core';
|
||||
import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog';
|
||||
|
||||
@Component({
|
||||
selector: 'app-add-text-dialog',
|
||||
templateUrl: './add-text-dialog.component.html'
|
||||
})
|
||||
export class AddTextDialogComponent {
|
||||
text = "";
|
||||
|
||||
@ViewChild('textElement') textElement!: ElementRef;
|
||||
|
||||
constructor(public dialogRef: MatDialogRef<string|undefined>, @Inject(MAT_DIALOG_DATA) public data: {text: string}){
|
||||
this.text = data.text;
|
||||
}
|
||||
|
||||
close(): void {
|
||||
this.dialogRef.close(undefined);
|
||||
}
|
||||
|
||||
onSaveClick(): void {
|
||||
this.dialogRef.close(this.text);
|
||||
}
|
||||
}
|
||||
118
pdb-js/src/app/dashboard-page/dashboard/dashboard.component.html
Normal file
@@ -0,0 +1,118 @@
|
||||
<style>
|
||||
:host {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
.toolbar {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
.toolbar #filter-date-range{
|
||||
flex-grow: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
.center {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
.center-content {
|
||||
text-align: center;
|
||||
}
|
||||
.is-error {
|
||||
font-size: 3rem;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.content {
|
||||
padding: 0.5em;
|
||||
}
|
||||
.dashboard-area {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-evenly;
|
||||
align-items: stretch;
|
||||
}
|
||||
.dashboard-column {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
/* make all columns equal width - flex-basis:0 to make all resizing start from the same size*/
|
||||
flex-grow: 1;
|
||||
flex-shrink: 1;
|
||||
flex-basis: 0;
|
||||
|
||||
}
|
||||
.editable {
|
||||
padding: 0.5em;
|
||||
}
|
||||
|
||||
.editable-hovered {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.editable:hover .editable-hovered{
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
.handle {
|
||||
display: block;
|
||||
height: 1.5em;
|
||||
visibility: hidden;
|
||||
width: fit-content;
|
||||
}
|
||||
[cdkDrag] {
|
||||
position: relative;
|
||||
}
|
||||
[cdkDrag]:hover .handle, .cdk-drop-list-dragging .handle {
|
||||
cursor: grab;
|
||||
visibility: visible;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div *ngIf="dashboard === undefined && !error" class="center">
|
||||
<mat-spinner></mat-spinner>
|
||||
</div>
|
||||
<div *ngIf="error" class="center center-content">
|
||||
<div class="is-error">{{error}}</div>
|
||||
<div>Try another <a [routerLink]="['/dashboard']">dashboard</a>.</div>
|
||||
</div>
|
||||
<div *ngIf="dashboard !== undefined" class="content">
|
||||
<div class="toolbar">
|
||||
<button mat-button (click)="addText()">Add Text</button>
|
||||
<button mat-button (click)="addPlot()">Add Plot</button>
|
||||
<button class="save-button" mat-button (click)="save()" [disabled]="!isDirty()">Save</button>
|
||||
<div id="filter-date-range">
|
||||
Date range: <app-date-picker #datePicker (dateValueSelected)="updateDateRange($event)" ></app-date-picker>
|
||||
</div>
|
||||
</div>
|
||||
<div class="editable">
|
||||
<h1>{{dashboard.name}}<button mat-icon-button (click)="editNameAndDescription()" class="editable-hovered"><img src="/assets/img/edit-outline.svg"/></button></h1>
|
||||
<p>{{dashboard.description}}</p>
|
||||
</div>
|
||||
<div cdkDropListGroup class="dashboard-area">
|
||||
<div
|
||||
cdkDropList
|
||||
class="dashboard-column"
|
||||
*ngFor="let i of [0,1]"
|
||||
[cdkDropListData]="i"
|
||||
(cdkDropListDropped)="drop($event)">
|
||||
<div
|
||||
cdkDrag
|
||||
*ngFor="let id of dashboard.arrangement[i]"
|
||||
[attr.widget-id]="id">
|
||||
<div cdkDragHandle class="handle"><img src="/assets/img/drag_handle.svg" class="icon-small"/></div>
|
||||
<app-text-widget
|
||||
*ngIf="isTextWidget(id)"
|
||||
[data]="getTextWidget(id)!" (deleted)="delete($event)"></app-text-widget>
|
||||
<app-plot-widget
|
||||
*ngIf="isPlotWidget(id)"
|
||||
[data]="getPlotWidget(id)!" (deleted)="delete($event)"></app-plot-widget>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,23 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { DashboardComponent } from './dashboard.component';
|
||||
|
||||
describe('DashboardComponent', () => {
|
||||
let component: DashboardComponent;
|
||||
let fixture: ComponentFixture<DashboardComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [ DashboardComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(DashboardComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
250
pdb-js/src/app/dashboard-page/dashboard/dashboard.component.ts
Normal file
@@ -0,0 +1,250 @@
|
||||
import { CdkDragDrop, moveItemInArray, transferArrayItem } from '@angular/cdk/drag-drop';
|
||||
import { HttpErrorResponse } from '@angular/common/http';
|
||||
import { Component, ElementRef, OnInit, ViewChild } from '@angular/core';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
import { MatSnackBar } from '@angular/material/snack-bar';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { Dashboard, DashboardCreationData, DashboardService, PlotWidget, PlotWidgetRenderData, TextWidget } from 'src/app/dashboard.service';
|
||||
import { PlotConfig, PlotResponse, PlotService } from 'src/app/plot.service';
|
||||
import { NewDashboardComponent } from '../new-dashboard/new-dashboard.component';
|
||||
import { AddPlotDialogComponent } from './add-plot-dialog/add-plot-dialog.component';
|
||||
import { AddTextDialogComponent } from './add-text-dialog/add-text-dialog.component';
|
||||
import { DatePickerChange, DatePickerComponent } from 'src/app/components/datepicker/date-picker.component';
|
||||
|
||||
@Component({
|
||||
selector: 'app-dashboard',
|
||||
templateUrl: './dashboard.component.html'
|
||||
})
|
||||
export class DashboardComponent implements OnInit{
|
||||
|
||||
dashboard?: Dashboard = undefined;
|
||||
|
||||
pristineDashboardJSON?: string = undefined;
|
||||
|
||||
error = "";
|
||||
|
||||
plotWidgetRenderData: PlotWidgetRenderData[] = [];
|
||||
|
||||
@ViewChild("datePicker")
|
||||
datePicker!: DatePickerComponent;
|
||||
|
||||
constructor(
|
||||
private route: ActivatedRoute,
|
||||
private service: DashboardService,
|
||||
private dialog: MatDialog,
|
||||
private snackBar: MatSnackBar,
|
||||
private plotService: PlotService,
|
||||
private element: ElementRef) {}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.service.getDashboard(<string>this.route.snapshot.paramMap.get("id")).subscribe({
|
||||
'next':(dashboard: Dashboard) => {
|
||||
this.dashboard = dashboard;
|
||||
this.pristineDashboardJSON = JSON.stringify(dashboard);
|
||||
this.repairArrangement();
|
||||
|
||||
dashboard.plots.forEach(p => {
|
||||
const submitterId = (<any>window).submitterId + (<any>window).randomId();
|
||||
this.plotWidgetRenderData.push(new PlotWidgetRenderData(p, submitterId));
|
||||
});
|
||||
|
||||
this.loadImages(0, this.plotWidgetRenderData);
|
||||
},
|
||||
'error': (error: HttpErrorResponse) =>{
|
||||
if (error.status == 404) {
|
||||
this.error = "Not Found";
|
||||
}else if (error.status == 504) { // gateway timeout
|
||||
this.error = "Server Unreachable";
|
||||
}else{
|
||||
this.error = "Failed to load dashboard";
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
updateDateRange(e: DatePickerChange) {
|
||||
this.plotWidgetRenderData.forEach(r => {
|
||||
r.widget.config.dateRange = e.value;
|
||||
});
|
||||
this.loadImages(0, this.plotWidgetRenderData);
|
||||
}
|
||||
|
||||
isDirty() {
|
||||
return this.pristineDashboardJSON !== JSON.stringify(this.dashboard);
|
||||
}
|
||||
|
||||
loadImages(index: number, plotWidgetQueue: PlotWidgetRenderData[]) {
|
||||
|
||||
if (index < plotWidgetQueue.length){
|
||||
|
||||
const plot = plotWidgetQueue[index];
|
||||
if (plot.isAborted) {
|
||||
this.loadImages(index +1 , plotWidgetQueue);
|
||||
}else{
|
||||
|
||||
plot.plotResponse = undefined; // remove old image and show loading icon
|
||||
|
||||
const request = PlotWidget.createPlotRequest(plot.widget, plot.submitterId);
|
||||
this.plotService.sendPlotRequest(request).subscribe({
|
||||
next: (response: PlotResponse)=> {
|
||||
plot.plotResponse = response;
|
||||
},
|
||||
error: (error:any)=> {
|
||||
plot.error = error;
|
||||
this.loadImages(index +1 , plotWidgetQueue);
|
||||
},
|
||||
complete: () => {
|
||||
this.loadImages(index +1 , plotWidgetQueue);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private repairArrangement(){
|
||||
const arrangement = this.dashboard!.arrangement || [];
|
||||
for (let i = 0; i < 2; i++){
|
||||
arrangement[i] = arrangement[i] ?? [] ;
|
||||
}
|
||||
this.dashboard?.texts.forEach(t => {
|
||||
if (!this.arrangmentContainsId(arrangement, t.id)){
|
||||
arrangement[0].push(t.id);
|
||||
}
|
||||
});
|
||||
this.dashboard?.plots.forEach(t => {
|
||||
if (!this.arrangmentContainsId(arrangement, t.id)){
|
||||
arrangement[0].push(t.id);
|
||||
}
|
||||
});
|
||||
|
||||
this.dashboard!.arrangement = arrangement;
|
||||
}
|
||||
|
||||
private arrangmentContainsId(arrangement: string[][], id: string): boolean{
|
||||
|
||||
for ( let i = 0; i < arrangement.length; i++){
|
||||
if (arrangement[i].includes(id)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
addText() {
|
||||
this.dialog.open(AddTextDialogComponent,{
|
||||
data: {text:""},
|
||||
width: '800px'
|
||||
}).afterClosed().subscribe((text: string) => {
|
||||
const widget = new TextWidget((<any>window).randomId(),'MEDIUM', text);
|
||||
this.dashboard!.texts.push(widget);
|
||||
this.dashboard!.arrangement[0].push(widget.id);
|
||||
});
|
||||
}
|
||||
|
||||
addPlot() {
|
||||
this.dialog.open(AddPlotDialogComponent,{
|
||||
data: {},
|
||||
width: 'calc(100% - 1em)',
|
||||
height: 'calc(100% - 1em)'
|
||||
}).afterClosed().subscribe((config: PlotConfig | "") => {
|
||||
if (config != "" && config.query.length > 0) {
|
||||
const widget = new PlotWidget((<any>window).randomId(), 'MEDIUM', config);
|
||||
this.dashboard!.plots.push(widget);
|
||||
this.dashboard!.arrangement[0].push(widget.id);
|
||||
this.plotWidgetRenderData.push(new PlotWidgetRenderData(widget, (<any>window).randomId()));
|
||||
this.loadImages(this.plotWidgetRenderData.length-1, this.plotWidgetRenderData);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
save() {
|
||||
const arrangement = <string[][]>[];
|
||||
const dashboardColumns = (<HTMLElement>this.element.nativeElement).querySelectorAll('.dashboard-column');
|
||||
for(let i =0; i < dashboardColumns.length; i++){
|
||||
const ids = [];
|
||||
const column = <HTMLDivElement>dashboardColumns.item(i);
|
||||
for(let c = 0; c <column.children.length; c++) {
|
||||
const element = <Element>column.children.item(c)
|
||||
const id = element!.getAttribute("widget-id");
|
||||
if (id !== null) {
|
||||
ids.push(id);
|
||||
}
|
||||
}
|
||||
arrangement.push(ids);
|
||||
}
|
||||
|
||||
this.dashboard!.arrangement = arrangement;
|
||||
|
||||
this.service.saveDashboard(this.dashboard!).subscribe({
|
||||
'complete': () => {
|
||||
const successMessages = [
|
||||
"dashboard saved",
|
||||
"saving the dashboard was a complete success",
|
||||
"dashboard state securely stored",
|
||||
"the save was successful",
|
||||
"done",
|
||||
"success",
|
||||
"saved"
|
||||
];
|
||||
const randomMessage = successMessages[Math.floor(Math.random()*successMessages.length)];
|
||||
|
||||
this.snackBar.open(randomMessage,"", {
|
||||
duration: 5000,
|
||||
verticalPosition: 'top'
|
||||
});
|
||||
this.pristineDashboardJSON = JSON.stringify(this.dashboard);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
editNameAndDescription() {
|
||||
const dialogRef = this.dialog.open(NewDashboardComponent, {
|
||||
data: {name: this.dashboard!.name, description: this.dashboard!.description},
|
||||
hasBackdrop: true
|
||||
});
|
||||
|
||||
dialogRef.afterClosed().subscribe((result?: DashboardCreationData) => {
|
||||
|
||||
if (result) {
|
||||
this.dashboard!.name = result.name;
|
||||
this.dashboard!.description = result.description;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
isTextWidget(id: string): boolean {
|
||||
return this.getTextWidget(id) !== undefined;
|
||||
}
|
||||
|
||||
isPlotWidget(id: string): boolean {
|
||||
return this.dashboard?.plots.find( x => x.id == id) !== undefined;
|
||||
}
|
||||
|
||||
getTextWidget(id: string): TextWidget | undefined {
|
||||
return this.dashboard?.texts.find( x => x.id == id);
|
||||
}
|
||||
|
||||
getPlotWidget(id: string): PlotWidgetRenderData | undefined {
|
||||
return this.plotWidgetRenderData.find( x => x.widget.id == id);
|
||||
}
|
||||
|
||||
drop(event: CdkDragDrop<number>) {
|
||||
if (event.previousContainer === event.container) {
|
||||
moveItemInArray(this.dashboard!.arrangement[event.container.data], event.previousIndex, event.currentIndex);
|
||||
} else {
|
||||
window.console.log("from ",event.previousContainer.data, " to ", event.container.data);
|
||||
transferArrayItem(
|
||||
this.dashboard!.arrangement[event.previousContainer.data],
|
||||
this.dashboard!.arrangement[event.container.data],
|
||||
event.previousIndex,
|
||||
event.currentIndex,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
delete(dashboardId: string) {
|
||||
this.dashboard!.arrangement[0] = this.dashboard!.arrangement[0].filter(a => a != dashboardId);
|
||||
this.dashboard!.plots = this.dashboard!.plots.filter(p => p.id != dashboardId);
|
||||
this.dashboard!.texts = this.dashboard!.texts.filter(t => t.id != dashboardId);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
<style>
|
||||
button {
|
||||
position: absolute;
|
||||
top: 1rem;
|
||||
right: 1rem;
|
||||
}
|
||||
</style>
|
||||
|
||||
<img [src]="data.imageUrl" (click)="close()" />
|
||||
<button mat-icon-button mat-dialog-close cdkFocusInitial><img src="assets/img/close.svg" class="icon-small" /></button>
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
import { Component, Inject } from '@angular/core';
|
||||
import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog';
|
||||
|
||||
@Component({
|
||||
selector: 'app-full-screen-plot-dialog',
|
||||
templateUrl: './full-screen-plot-dialog.component.html'
|
||||
})
|
||||
export class FullScreenPlotDialogComponent {
|
||||
|
||||
|
||||
constructor(public dialogRef: MatDialogRef<void>, @Inject(MAT_DIALOG_DATA) public data: {imageUrl: string}){}
|
||||
|
||||
close(): void {
|
||||
this.dialogRef.close();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
<style>
|
||||
:host {
|
||||
float: left;
|
||||
}
|
||||
.dashboard-card {
|
||||
position: relative;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
.size-medium {
|
||||
width: 602px;
|
||||
height: 402px;
|
||||
}
|
||||
.size-small {
|
||||
width: 402px;
|
||||
height: 302px;
|
||||
}
|
||||
img.render-img {
|
||||
cursor: zoom-in;
|
||||
}
|
||||
|
||||
.top-left {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
.top-right {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
.dashboard-card .editable-hovered {
|
||||
visibility: hidden;
|
||||
}
|
||||
.dashboard-card:hover .editable-hovered {
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
.aborted-img {
|
||||
flex-grow: 0.3;
|
||||
opacity: 0.3;
|
||||
}
|
||||
.aborted-img img {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.invader {
|
||||
background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABoAAAAUCAYAAACTQC2+AAAAAXNSR0IArs4c6QAAAAZiS0dEAP8A/wD/oL2nkwAAAAlwSFlzAAALEwAACxMBAJqcGAAAAAd0SU1FB9kKGRAxBENShygAAAAZdEVYdENvbW1lbnQAQ3JlYXRlZCB3aXRoIEdJTVBXgQ4XAAAAbUlEQVRIx9WVQQrAMAgEM6X///L2FCihYvRg7F416LCsQdKo0DWKZA4CBGzjev1lRHgezS0lkan3IYr485ZFdo5oJZkbWoRWfSWrJ8p6suvZucsgCS8THsHX+zKiO5uLaO763bpobvoS/e6HfQBzIE0PhAsDxgAAAABJRU5ErkJggg==);
|
||||
width: 26px;
|
||||
height: 20px;
|
||||
display: inline-block;
|
||||
vertical-align: text-bottom;
|
||||
}
|
||||
.spinner {
|
||||
animation: wobble 2s linear infinite;
|
||||
}
|
||||
@keyframes wobble {
|
||||
0% {
|
||||
opacity: 1;
|
||||
}
|
||||
50% {
|
||||
opacity: 0.3;
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<div class="dashboard-card" [ngClass]="{'size-medium' : true}">
|
||||
<div class="editable-hovered top-right">
|
||||
<button mat-icon-button (click)="edit()" ><img src="/assets/img/edit-outline.svg"/></button>
|
||||
<button mat-icon-button (click)="delete()"><img src="/assets/img/recycle-bin-line.svg"/></button>
|
||||
</div>
|
||||
<div *ngIf="!hasRender('main') && !data?.error && !data.isAborted">
|
||||
<button mat-button (click)="abort()"><span class="invader spinner"></span>Cancel</button>
|
||||
</div>
|
||||
<img *ngIf="hasRender('main')" [src]="getImageUrl('main')" (click)="showFullScreenImage()" class="render-img" />
|
||||
<div *ngIf="data?.error && !data?.isAborted">
|
||||
There was an error! This is a good time to panic!
|
||||
</div>
|
||||
<div *ngIf="data?.isAborted" class="aborted-img">
|
||||
<img src="assets/img/image-aborted.svg" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { PlotWidgetComponent } from './plot-widget.component';
|
||||
|
||||
describe('PlotWidgetComponent', () => {
|
||||
let component: PlotWidgetComponent;
|
||||
let fixture: ComponentFixture<PlotWidgetComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [ PlotWidgetComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(PlotWidgetComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,94 @@
|
||||
import { Component, EventEmitter, Input, Output, ViewChild, input } from '@angular/core';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
import { ConfirmationDialogComponent } from 'src/app/confirmation-dialog/confirmation-dialog.component';
|
||||
import { PlotWidget, PlotWidgetRenderData } from 'src/app/dashboard.service';
|
||||
import { PlotViewComponent } from 'src/app/plot-view/plot-view.component';
|
||||
import { PlotConfig, PlotResponse, PlotService } from 'src/app/plot.service';
|
||||
import { AddPlotDialogComponent } from '../add-plot-dialog/add-plot-dialog.component';
|
||||
import { FullScreenPlotDialogComponent } from '../full-screen-plot-dialog/full-screen-plot-dialog.component';
|
||||
|
||||
@Component({
|
||||
selector: 'app-plot-widget',
|
||||
templateUrl: './plot-widget.component.html'
|
||||
})
|
||||
export class PlotWidgetComponent {
|
||||
@Input("data")
|
||||
data!: PlotWidgetRenderData;
|
||||
|
||||
public thumbnailUrl = "";
|
||||
|
||||
|
||||
//@ViewChild("plotView") plotView!: PlotViewComponent;
|
||||
|
||||
@Output()
|
||||
deleted : EventEmitter<string> = new EventEmitter<string>();
|
||||
|
||||
constructor(private dialog: MatDialog, private service: PlotService){}
|
||||
|
||||
hasRender(name: string): boolean{
|
||||
return this.data !== undefined && this.data.plotResponse !== undefined && this.data.plotResponse?.rendered[name] !== undefined;
|
||||
}
|
||||
|
||||
getImageUrl(name: string ): string | undefined {
|
||||
return this.data?.plotResponse?.rendered[name];
|
||||
}
|
||||
|
||||
showFullScreenImage() {
|
||||
this.dialog.open(FullScreenPlotDialogComponent,{
|
||||
width: 'calc(100% - 15px)',
|
||||
height: 'calc(100% - 15px)',
|
||||
data: {'imageUrl': this.getImageUrl('fullScreen')}
|
||||
}).afterClosed().subscribe(() => {
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
abort(){
|
||||
window.console.log("abort");
|
||||
this.data.isAborted = true;
|
||||
this.service.abort(this.data.submitterId).subscribe({
|
||||
complete: () => {
|
||||
window.console.log("cancelled");
|
||||
},
|
||||
error: () => {}
|
||||
});
|
||||
}
|
||||
|
||||
delete() {
|
||||
this.dialog
|
||||
.open(ConfirmationDialogComponent, {
|
||||
data: {title: "", text: "Delete plot?", btnOkLabel: "Delete", btnCancelLabel: "Cancel"}
|
||||
})
|
||||
.afterClosed()
|
||||
.subscribe((result: boolean) => {
|
||||
if (result === true) {
|
||||
this.deleted.emit(this.data.widget.id);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
edit() {
|
||||
this.dialog.open(AddPlotDialogComponent, {
|
||||
data: {config: this.data.widget.config},
|
||||
width: 'calc(100% - 15px)',
|
||||
height: 'calc(100% - 15px)',
|
||||
}).afterClosed().subscribe((config?: PlotConfig) => {
|
||||
if (config !== undefined && config.query.length > 0) {
|
||||
this.data.widget.config = config;
|
||||
|
||||
this.data.error = false;
|
||||
this.data.plotResponse = undefined;
|
||||
|
||||
const request = PlotWidget.createPlotRequest(this.data.widget, this.data.submitterId);
|
||||
this.service.sendPlotRequest(request).subscribe({
|
||||
next: (response: PlotResponse)=> {
|
||||
this.data.plotResponse = response;
|
||||
},
|
||||
error: (error:any)=> {
|
||||
this.data.error = true;
|
||||
},
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
<style>
|
||||
:host {
|
||||
display: block;
|
||||
}
|
||||
.text-widget {
|
||||
position: relative;
|
||||
padding-bottom: 1em;
|
||||
}
|
||||
.text-widget:hover {
|
||||
/*outline: solid 1px black;/**/
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.editable-hovered {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: -2em;
|
||||
}
|
||||
|
||||
.text-widget .editable-hovered {
|
||||
visibility: hidden;
|
||||
}
|
||||
.text-widget:hover .editable-hovered {
|
||||
visibility: visible;
|
||||
}
|
||||
</style>
|
||||
<div class="text-widget">
|
||||
<div class="editable-hovered">
|
||||
<button mat-icon-button (click)="edit()"><img src="/assets/img/edit-outline.svg"/></button>
|
||||
<button mat-icon-button (click)="delete()"><img src="/assets/img/recycle-bin-line.svg"/></button>
|
||||
</div>
|
||||
<markdown [data]="this.data.text"></markdown>
|
||||
</div>
|
||||
@@ -0,0 +1,23 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { TextWidgetComponent } from './text-widget.component';
|
||||
|
||||
describe('TextWidgetComponent', () => {
|
||||
let component: TextWidgetComponent;
|
||||
let fixture: ComponentFixture<TextWidgetComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [ TextWidgetComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(TextWidgetComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,46 @@
|
||||
import { Component, EventEmitter, Input, Output } from '@angular/core';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
import { ConfirmationDialogComponent } from 'src/app/confirmation-dialog/confirmation-dialog.component';
|
||||
import { TextWidget } from 'src/app/dashboard.service';
|
||||
import { AddTextDialogComponent } from '../add-text-dialog/add-text-dialog.component';
|
||||
|
||||
@Component({
|
||||
selector: 'app-text-widget',
|
||||
templateUrl: './text-widget.component.html'
|
||||
})
|
||||
export class TextWidgetComponent {
|
||||
@Input()
|
||||
data! : TextWidget;
|
||||
|
||||
@Output()
|
||||
deleted : EventEmitter<string> = new EventEmitter<string>();
|
||||
|
||||
constructor(private dialog: MatDialog){}
|
||||
|
||||
lines(): string[]{
|
||||
return typeof this.data.text == 'string' ? this.data.text.split(/\r?\n/) : [];
|
||||
}
|
||||
delete() {
|
||||
this.dialog
|
||||
.open(ConfirmationDialogComponent, {
|
||||
data: {title: "", text: "Delete text?", btnOkLabel: "Delete", btnCancelLabel: "Cancel"}
|
||||
})
|
||||
.afterClosed()
|
||||
.subscribe((result: boolean) => {
|
||||
if (result === true) {
|
||||
this.deleted.emit(this.data.id);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
edit() {
|
||||
this.dialog.open(AddTextDialogComponent,{
|
||||
data: {text : this.data.text},
|
||||
width: '800px'
|
||||
}).afterClosed().subscribe((text?: string) => {
|
||||
if (text !== undefined) {
|
||||
this.data.text = text;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
<style>
|
||||
div[mat-dialog-content] {
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
||||
<form [formGroup]="registerForm" >
|
||||
<h1 mat-dialog-title>Create a new dashboard</h1>
|
||||
<div mat-dialog-content>
|
||||
<mat-form-field class="pdb-form-full-width">
|
||||
<mat-label>Name</mat-label>
|
||||
<input matInput [(ngModel)]="data.name" #name formControlName="name" focus maxlength="64" required="required" />
|
||||
<mat-error>Name must be between one and 64 characters.</mat-error>
|
||||
</mat-form-field>
|
||||
<mat-form-field class="pdb-form-full-width">
|
||||
<mat-label>Description</mat-label>
|
||||
<textarea matInput [(ngModel)]="data.description" maxlength="65535" formControlName="description"></textarea>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div mat-dialog-actions align="end">
|
||||
<button mat-button mat-dialog-close>Cancel</button>
|
||||
<button
|
||||
class="save-button"
|
||||
mat-button
|
||||
mat-dialog-close
|
||||
(click)="onSaveClick()"
|
||||
[disabled]="!registerForm.valid">Save</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { NewDashboardComponent } from './new-dashboard.component';
|
||||
|
||||
describe('NewDashboardComponent', () => {
|
||||
let component: NewDashboardComponent;
|
||||
let fixture: ComponentFixture<NewDashboardComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [ NewDashboardComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(NewDashboardComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,31 @@
|
||||
import { Component, ElementRef, Inject, OnInit, ViewChild } from '@angular/core';
|
||||
import {MAT_DIALOG_DATA, MatDialogRef} from '@angular/material/dialog';
|
||||
import { DashboardCreationData } from 'src/app/dashboard.service';
|
||||
import { AbstractControl, FormControl, FormGroup, ValidationErrors, ValidatorFn, Validators } from '@angular/forms';
|
||||
|
||||
@Component({
|
||||
selector: 'app-new-dashboard',
|
||||
templateUrl: './new-dashboard.component.html'
|
||||
})
|
||||
export class NewDashboardComponent implements OnInit {
|
||||
|
||||
|
||||
registerForm = new FormGroup({
|
||||
name: new FormControl('', [Validators.pattern(/^[^\s]+.{0,63}$/), Validators.required]),
|
||||
description: new FormControl('', [Validators.maxLength(65535)]),
|
||||
});
|
||||
|
||||
@ViewChild('name') nameInput!: ElementRef;
|
||||
|
||||
constructor(public dialogRef: MatDialogRef<NewDashboardComponent>,
|
||||
@Inject(MAT_DIALOG_DATA) public data: DashboardCreationData,){
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
//window.setTimeout(() => this.nameInput.nativeElement.focus(), 0);
|
||||
}
|
||||
|
||||
onSaveClick(): void {
|
||||
this.dialogRef.close(this.data);
|
||||
}
|
||||
}
|
||||
16
pdb-js/src/app/dashboard.service.spec.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
|
||||
import { DashboardService } from './dashboard.service';
|
||||
|
||||
describe('DashboardService', () => {
|
||||
let service: DashboardService;
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({});
|
||||
service = TestBed.inject(DashboardService);
|
||||
});
|
||||
|
||||
it('should be created', () => {
|
||||
expect(service).toBeTruthy();
|
||||
});
|
||||
});
|
||||
126
pdb-js/src/app/dashboard.service.ts
Normal file
@@ -0,0 +1,126 @@
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { Injectable } from '@angular/core';
|
||||
import { Observable } from 'rxjs';
|
||||
import { PlotConfig, PlotRequest, PlotResponse, RenderOptions } from './plot.service';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class DashboardService {
|
||||
|
||||
constructor(private http: HttpClient) { }
|
||||
|
||||
createDashboard(data: DashboardCreationData): Observable<Dashboard>{
|
||||
return this.http.post<Dashboard>('//'+window.location.hostname+':'+window.location.port+'/api/dashboards/', data);
|
||||
}
|
||||
|
||||
getDashboards(): Observable<DashboardList>{
|
||||
return this.http.get<DashboardList>('//'+window.location.hostname+':'+window.location.port+'/api/dashboards/');
|
||||
}
|
||||
|
||||
getDashboard(id: string): Observable<Dashboard>{
|
||||
return this.http.get<Dashboard>('//'+window.location.hostname+':'+window.location.port+'/api/dashboards/'+id);
|
||||
}
|
||||
|
||||
saveDashboard(dashboard: Dashboard): Observable<Dashboard>{
|
||||
return this.http.put<Dashboard>('//'+window.location.hostname+':'+window.location.port+'/api/dashboards/'+dashboard.id, dashboard);
|
||||
}
|
||||
|
||||
deleteDashboard(id: string): Observable<void> {
|
||||
return this.http.delete<void>('//'+window.location.hostname+':'+window.location.port+'/api/dashboards/'+id);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export class DashboardCreationData{
|
||||
constructor(public name: string, public description: string){}
|
||||
}
|
||||
|
||||
export interface HasId {
|
||||
id: string;
|
||||
}
|
||||
|
||||
export class Dashboard{
|
||||
constructor(
|
||||
public id: string,
|
||||
public name: string,
|
||||
public description: string,
|
||||
public texts: TextWidget[],
|
||||
public plots: PlotWidget[],
|
||||
public arrangement: string[][]){}
|
||||
}
|
||||
|
||||
export class DashboardList{
|
||||
constructor(public dashboards: [Dashboard]){}
|
||||
}
|
||||
|
||||
export abstract class BaseWidget implements HasId {
|
||||
constructor(public id: string, public type: PlotType, public size: PlotSize) {}
|
||||
}
|
||||
|
||||
export class TextWidget extends BaseWidget {
|
||||
constructor(override id: string, override size: PlotSize, public text: string) {
|
||||
super(id, 'TEXT', size);
|
||||
}
|
||||
}
|
||||
export class PlotWidget extends BaseWidget {
|
||||
constructor(override id: string, override size: 'SMALL'|'MEDIUM'|'LARGE', public config: PlotConfig) {
|
||||
super(id, 'PLOT', size);
|
||||
}
|
||||
|
||||
public static createPlotRequest(widget: PlotWidget, submitterId: string): PlotRequest {
|
||||
|
||||
const height = this.height(widget.size);
|
||||
const width = this.width(widget.size);
|
||||
|
||||
const fullWidth = window.innerWidth-30;
|
||||
const fullHeight = window.innerHeight-30;
|
||||
|
||||
const request = new PlotRequest(
|
||||
submitterId,
|
||||
widget.config,
|
||||
{
|
||||
'main': new RenderOptions(height,width, false, true),
|
||||
'fullScreen': new RenderOptions(fullHeight,fullWidth, false, true)
|
||||
}
|
||||
);
|
||||
return request;
|
||||
}
|
||||
|
||||
|
||||
static height(size: PlotSize): number{
|
||||
switch (size) {
|
||||
case 'SMALL':
|
||||
return 300;
|
||||
case 'MEDIUM':
|
||||
return 400;
|
||||
case 'LARGE':
|
||||
return 600;
|
||||
}
|
||||
}
|
||||
static width(size: PlotSize): number{
|
||||
switch (size) {
|
||||
case 'SMALL':
|
||||
return 400;
|
||||
case 'MEDIUM':
|
||||
return 600;
|
||||
case 'LARGE':
|
||||
return 900;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export type PlotSize = 'SMALL'|'MEDIUM'|'LARGE';
|
||||
|
||||
export type PlotType = 'TEXT'|'PLOT';
|
||||
|
||||
export class PlotWidgetRenderData {
|
||||
public isAborted = false;
|
||||
public error: string|boolean = false;
|
||||
constructor(public widget: PlotWidget, public submitterId: string, public plotResponse?: PlotResponse) {
|
||||
}
|
||||
}
|
||||
|
||||
export class WidgetDimensions{
|
||||
constructor(public width: number, public height: number){}
|
||||
}
|
||||
8
pdb-js/src/app/focus.directive.spec.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { FocusDirective } from './focus.directive';
|
||||
|
||||
describe('FocusDirective', () => {
|
||||
it('should create an instance', () => {
|
||||
const directive = new FocusDirective();
|
||||
expect(directive).toBeTruthy();
|
||||
});
|
||||
});
|
||||
14
pdb-js/src/app/focus.directive.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { AfterViewInit, Directive, ElementRef } from '@angular/core';
|
||||
|
||||
@Directive({
|
||||
selector: '[focus]'
|
||||
})
|
||||
export class FocusDirective implements AfterViewInit {
|
||||
|
||||
constructor(private element: ElementRef) { }
|
||||
|
||||
ngAfterViewInit(): void {
|
||||
this.element.nativeElement.focus();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Component, OnInit, Input, Output, ViewChild, EventEmitter } from '@angular/core';
|
||||
import { MatLegacySnackBar as MatSnackBar } from '@angular/material/legacy-snack-bar';
|
||||
import { PlotService, PlotRequest, PlotResponse, PlotResponseStats, DashTypeAndColor } from '../plot.service';
|
||||
import { MatSnackBar } from '@angular/material/snack-bar';
|
||||
import { PlotService, PlotRequest, PlotResponse, PlotResponseStats, DashTypeAndColor, RenderedImages } from '../plot.service';
|
||||
import { UtilService } from '../utils.service';
|
||||
|
||||
export class GalleryFilterData {
|
||||
@@ -242,9 +242,7 @@ export class GalleryViewComponent implements OnInit {
|
||||
this.galleryItems.length = 0;
|
||||
this.splitByValuesQueue.length = 0;
|
||||
|
||||
request.generateThumbnail = true;
|
||||
|
||||
this.plotService.splitQuery(request.query, splitByField).subscribe({
|
||||
this.plotService.splitQuery(request.config.query, splitByField).subscribe({
|
||||
next: function(valuesForSplitBy){
|
||||
console.log("valuesForSplitBy: " + JSON.stringify(valuesForSplitBy));
|
||||
that.splitByValuesQueue = valuesForSplitBy;
|
||||
@@ -269,11 +267,12 @@ export class GalleryViewComponent implements OnInit {
|
||||
const splitByValue = <string>this.splitByValuesQueue.pop();
|
||||
|
||||
let request = masterRequest.copy();
|
||||
request.query = "("+request.query+") and " + splitByField+"="+ splitByValue;
|
||||
request.config.query = "("+request.config.query+") and " + splitByField+"="+ splitByValue;
|
||||
|
||||
const expectedSequenceId = ++this.sequenceId;
|
||||
|
||||
this.plotService.sendPlotRequest(request).subscribe(function(plotResponse : PlotResponse){
|
||||
this.plotService.sendPlotRequest(request).subscribe({
|
||||
'next':function(plotResponse : PlotResponse){
|
||||
//console.log("response: " + JSON.stringify(plotResponse));
|
||||
if (that.sequenceId != expectedSequenceId){
|
||||
//console.log("ignoring stale response");
|
||||
@@ -282,15 +281,17 @@ export class GalleryViewComponent implements OnInit {
|
||||
|
||||
that.progress = 100 * (that.totalNumberImages - that.splitByValuesQueue.length) / that.totalNumberImages;
|
||||
|
||||
plotResponse.thumbnailUrl = "http://"+window.location.hostname+':'+window.location.port+'/'+plotResponse.thumbnailUrl;
|
||||
plotResponse.imageUrl = "http://"+window.location.hostname+':'+window.location.port+'/'+plotResponse.imageUrl;
|
||||
//plotResponse.thumbnailUrl = "//"+window.location.hostname+':'+window.location.port+'/'+plotResponse.rendered['thumbnail'];
|
||||
//plotResponse.imageUrl = "//"+window.location.hostname+':'+window.location.port+'/'+plotResponse.rendered['main'];
|
||||
let galleryItem = new GalleryItem(splitByValue, plotResponse);
|
||||
that.galleryItems.push(galleryItem);
|
||||
that.sortAndFilterGallery();
|
||||
that.renderGalleryRecursively(masterRequest, splitByField);
|
||||
},
|
||||
'error':
|
||||
(error:any) => {
|
||||
that.showError(error.error.message);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -339,16 +340,16 @@ export class GalleryItemView {
|
||||
|
||||
|
||||
export class GalleryItem {
|
||||
thumbnailUrl: string;
|
||||
imageUrl: string;
|
||||
stats: PlotResponseStats;
|
||||
splitByValue : string;
|
||||
imageUrl: string;
|
||||
thumbnailUrl :string;
|
||||
show : boolean = false;
|
||||
|
||||
constructor(splitByValue: string, plotResponse: PlotResponse){
|
||||
this.thumbnailUrl = plotResponse.thumbnailUrl;
|
||||
this.imageUrl = plotResponse.imageUrl;
|
||||
this.splitByValue = splitByValue;
|
||||
this.stats = plotResponse.stats;
|
||||
this.thumbnailUrl = "//"+window.location.hostname+':'+window.location.port+'/'+plotResponse.rendered['thumbnail'];
|
||||
this.imageUrl = "//"+window.location.hostname+':'+window.location.port+'/'+plotResponse.rendered['main'];
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,9 @@
|
||||
<style>
|
||||
:host {
|
||||
display: flex;
|
||||
}
|
||||
</style>
|
||||
|
||||
<mat-form-field class="pdb-form-mid">
|
||||
<mat-label>Limit By:</mat-label>
|
||||
<mat-select [(value)]="limitBy">
|
||||
@@ -11,6 +17,7 @@
|
||||
|
||||
<mat-form-field class="pdb-form-number"
|
||||
*ngIf="limitBy !== 'NO_LIMIT'">
|
||||
<mat-label><!--empty label needed for layout reasons--></mat-label>
|
||||
<input
|
||||
matInput
|
||||
type="number"
|
||||
|
||||
@@ -8,11 +8,45 @@
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
width: 600px; /* must be multiple of the minmax used in .link-section */
|
||||
}
|
||||
h1 {
|
||||
font-size: 5em;
|
||||
text-align: center;
|
||||
}
|
||||
.link-section {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(300px, 300px)); /* minmax must be fraction of the with used in #main-page-links */
|
||||
}
|
||||
.sub-section {
|
||||
display: inline;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
margin: 2em;
|
||||
}
|
||||
.sub-section > * {
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div id="main-page-links">
|
||||
<a href="/vis" class="button-large" title="open visualization page"><img src="assets/img/scatter-chart2.svg" class="icon-large" aria-hidden="false" aria-label="go to visualization page" /></a>
|
||||
<!--
|
||||
|
||||
<div style="text-align: center;"><img src="assets/img/strip-chart.svg" class="icon-huge" aria-hidden="true"/></div>
|
||||
-->
|
||||
<h1>Plotilio</h1>
|
||||
<div class="link-section">
|
||||
<div class="sub-section">
|
||||
<a [routerLink]="['/vis']" class="button-large" title="Visualization"><img src="assets/img/scatter-chart3.svg" class="icon-large" aria-hidden="false" aria-label="go to visualization page" /></a>
|
||||
<p>Do awesome visualizations.</p>
|
||||
</div>
|
||||
<div class="sub-section">
|
||||
<a [routerLink]="['/dashboard']" class="button-large" title="Dashboards"><img src="assets/img/dashboard-line.svg" class="icon-large" aria-hidden="false" aria-label="go to dashboard page" /></a>
|
||||
<p>Create the most sophisticated dashboards.</p>
|
||||
<em>This feature is still in development. You may loose data without warning.</em>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!--a href="/upload" class="button-large" title="upload data"><img src="assets/img/upload.svg" class="icon-large" aria-hidden="false" aria-label="upload data" /></a-->
|
||||
</div>
|
||||
|
||||
@@ -1,33 +1,4 @@
|
||||
|
||||
.plot-details-plotType {
|
||||
background-image: url(/assets/img/pointTypes.png);
|
||||
width: 9px;
|
||||
height: 7px;
|
||||
transform: scale(1.5);
|
||||
}
|
||||
|
||||
.plot-details-plotType_0 {background-position-x: 0px;}
|
||||
.plot-details-plotType_1 {background-position-x: -10px;}
|
||||
.plot-details-plotType_2 {background-position-x: -20px;}
|
||||
.plot-details-plotType_3 {background-position-x: -30px;}
|
||||
.plot-details-plotType_4 {background-position-x: -40px;}
|
||||
.plot-details-plotType_5 {background-position-x: -50px;}
|
||||
.plot-details-plotType_6 {background-position-x: -60px;}
|
||||
.plot-details-plotType_7 {background-position-x: -70px;}
|
||||
.plot-details-plotType_8 {background-position-x: -80px;}
|
||||
.plot-details-plotType_9 {background-position-x: -90px;}
|
||||
.plot-details-plotType_10 {background-position-x:-100px;}
|
||||
.plot-details-plotType_11 {background-position-x:-110px;}
|
||||
.plot-details-plotType_12 {background-position-x:-120px;}
|
||||
|
||||
.plot-details-plotType_0051c2 {background-position-y: 0px;}
|
||||
.plot-details-plotType_bf8300 {background-position-y: -8px;}
|
||||
.plot-details-plotType_9400d3 {background-position-y: -16px;}
|
||||
.plot-details-plotType_00c254 {background-position-y: -24px;}
|
||||
.plot-details-plotType_e6e600 {background-position-y: -32px;}
|
||||
.plot-details-plotType_e51e10 {background-position-y: -40px;}
|
||||
.plot-details-plotType_57a1c2 {background-position-y: -48px;}
|
||||
.plot-details-plotType_bd36c2 {background-position-y: -56px;}
|
||||
|
||||
|
||||
.gallery-item-details td {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Component, OnInit, Input, Output, ViewChild, EventEmitter, ɵpublishDefaultGlobalUtils } from '@angular/core';
|
||||
import { Component, Input } from '@angular/core';
|
||||
import { DashTypeAndColor, PlotResponseStats, DataSeriesStats } from '../plot.service';
|
||||
import { UtilService } from '../utils.service';
|
||||
|
||||
|
||||
@@ -1,3 +1,16 @@
|
||||
|
||||
<!---->
|
||||
<div cdkDrag
|
||||
[ngClass]="{'hidden': !imageUrl || showStats || dataSeries().length == 0}"
|
||||
class="plot-view--legend"
|
||||
[cdkDragFreeDragPosition]="legendInitialPosition">
|
||||
<div cdkDragHandle></div>
|
||||
<div class="plot-view--legend-content">
|
||||
<ol>
|
||||
<li *ngFor="let stat of dataSeries()"><div class="{{ pointTypeClass(stat.dashTypeAndColor) }}" title="{{ stat.name }}"></div>{{ stat.name }}</li>
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
*ngIf="imageUrl">
|
||||
<div
|
||||
|
||||
@@ -21,3 +21,60 @@ img {
|
||||
box-shadow: 5px 5px 10px 0px #e0e0e0;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.plot-view--legend {
|
||||
border: solid 1px #ccc;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: #fff;
|
||||
border-radius: 4px;
|
||||
position: fixed;
|
||||
z-index: 1;
|
||||
transition: box-shadow 200ms cubic-bezier(0, 0, 0.2, 1);
|
||||
box-shadow: 0 3px 1px -2px rgba(0, 0, 0, 0.2),
|
||||
0 2px 2px 0 rgba(0, 0, 0, 0.14),
|
||||
0 1px 5px 0 rgba(0, 0, 0, 0.12);
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
.plot-view--legend:active {
|
||||
box-shadow: 0 5px 5px -3px rgba(0, 0, 0, 0.2),
|
||||
0 8px 10px 1px rgba(0, 0, 0, 0.14),
|
||||
0 3px 14px 2px rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
.plot-view--legend div[cdkDragHandle] {
|
||||
visibility: hidden;
|
||||
height: 1.2rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.plot-view--legend:hover div[cdkDragHandle] {
|
||||
cursor: move;
|
||||
visibility: visible;
|
||||
background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAQAAAAECAIAAAAmkwkpAAAAF0lEQVQI12P4//8/AwMDhGSEUFCAUwYAJl4R8Z1D4wIAAAAASUVORK5CYII=);
|
||||
}
|
||||
|
||||
.plot-view--legend-content {
|
||||
max-height: 30em;
|
||||
overflow: auto;
|
||||
max-width: 60em;
|
||||
overflow: auto;
|
||||
resize: both;
|
||||
padding-bottom: 0.5em;
|
||||
}
|
||||
|
||||
.plot-view--legend ol {
|
||||
padding-inline-start: 0.7em;
|
||||
}
|
||||
|
||||
.plot-view--legend ol li {
|
||||
list-style-type: none;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center
|
||||
}
|
||||
.plot-view--legend ol li .plot-details-plotType{
|
||||
margin-right: 0.3em;
|
||||
flex-shrink: 0;
|
||||
flex-grow: 0;
|
||||
}
|
||||
@@ -1,19 +1,29 @@
|
||||
import { Component, OnInit, Output, EventEmitter } from '@angular/core';
|
||||
import { DataType, AxesTypes, PlotResponseStats } from '../plot.service';
|
||||
import { Component, Output, EventEmitter } from '@angular/core';
|
||||
import { DataType, AxesTypes, PlotResponseStats, PlotConfig, PlotService, PlotResponse, PlotRequest, RenderOptions, DataSeriesStats, DashTypeAndColor } from '../plot.service';
|
||||
import { MatSnackBar } from '@angular/material/snack-bar';
|
||||
//import * as moment from 'moment';
|
||||
import { WidgetDimensions } from '../dashboard.service';
|
||||
import { Overlay } from "@angular/cdk/overlay";
|
||||
|
||||
import { DateTime, Duration } from "luxon";
|
||||
import { DateValue } from '../components/datepicker/date-picker.component';
|
||||
import { Observable } from 'rxjs';
|
||||
|
||||
@Component({
|
||||
selector: 'pdb-plot-view',
|
||||
templateUrl: './plot-view.component.html',
|
||||
styleUrls: ['./plot-view.component.scss']
|
||||
})
|
||||
export class PlotViewComponent implements OnInit {
|
||||
export class PlotViewComponent {
|
||||
|
||||
readonly DATE_PATTERN = "yyyy-MM-dd HH:mm:ss"; // for moment-JS
|
||||
|
||||
readonly gnuplotLMargin = 110; // The left margin configured for gnuplot
|
||||
readonly gnuplotRMargin = 110; // The right margin configured for gnuplot
|
||||
readonly gnuplotTMargin = 57; // The top margin configured for gnuplot
|
||||
readonly gnuplotBMargin = 76; // The bottom margin configured for gnuplot
|
||||
|
||||
isOpen = false;
|
||||
|
||||
imageUrl! : string;
|
||||
stats: PlotResponseStats | null = null;
|
||||
@@ -21,10 +31,10 @@ export class PlotViewComponent implements OnInit {
|
||||
axes!: AxesTypes;
|
||||
|
||||
@Output()
|
||||
zoomRange : EventEmitter<SelectionRange> = new EventEmitter<SelectionRange>();
|
||||
loadingEvent : EventEmitter<LoadingEvent> = new EventEmitter<LoadingEvent>();
|
||||
|
||||
@Output()
|
||||
zoomWithDateAnchor : EventEmitter<DateAnchor> = new EventEmitter<DateAnchor>();
|
||||
dateRangeUpdateEvent : EventEmitter<DateValue> = new EventEmitter<DateValue>();
|
||||
|
||||
in_drag_mode = false;
|
||||
drag_start_x = 0;
|
||||
@@ -41,9 +51,18 @@ export class PlotViewComponent implements OnInit {
|
||||
|
||||
showStats = false;
|
||||
|
||||
constructor() { }
|
||||
config? : PlotConfig;
|
||||
|
||||
ngOnInit() {
|
||||
legendInitialPosition = {x:115,y:60};
|
||||
|
||||
constructor(private service : PlotService, private snackBar: MatSnackBar, private overlay: Overlay) { }
|
||||
|
||||
|
||||
showError(message:string) {
|
||||
this.snackBar.open(message, "", {
|
||||
duration: 5000,
|
||||
verticalPosition: 'top'
|
||||
});
|
||||
}
|
||||
|
||||
hideZoomInSlider() {
|
||||
@@ -147,7 +166,7 @@ export class PlotViewComponent implements OnInit {
|
||||
const startPercentOfDateRange = startPxWithinPlotArea / widthPlotArea;
|
||||
const endPercentOfDateRange = endPxWithinPlotArea / widthPlotArea;
|
||||
|
||||
this.zoomRange.emit(new SelectionRange(startPercentOfDateRange, endPercentOfDateRange));
|
||||
this.zoomRange(new SelectionRange(startPercentOfDateRange, endPercentOfDateRange));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -162,6 +181,23 @@ export class PlotViewComponent implements OnInit {
|
||||
}
|
||||
}
|
||||
|
||||
setDateRange(startDate: any, endDate: any) {
|
||||
const formattedStartDate = startDate.toFormat(this.DATE_PATTERN);
|
||||
const formattedEndDate = endDate.toFormat(this.DATE_PATTERN);
|
||||
|
||||
const newDateRange = formattedStartDate+" - "+formattedEndDate;
|
||||
const newDateValue = new DateValue('ABSOLUTE', newDateRange, newDateRange);
|
||||
this.dateRangeUpdateEvent.emit(newDateValue);
|
||||
}
|
||||
|
||||
zoomRange(range: SelectionRange) {
|
||||
this.shiftDate(this.config?.dateRange!, range.startPercentOfDateRange, range.endPercentOfDateRange-1);
|
||||
}
|
||||
|
||||
zoomWithDateAnchor(dateAnchor: DateAnchor){
|
||||
this.shiftDateByAnchor(this.config?.dateRange!, dateAnchor.cursorPercentOfDateRange, dateAnchor.zoomFactor);
|
||||
}
|
||||
|
||||
zoomByScroll(event: WheelEvent) {
|
||||
if (this.isInImage(event) && event.deltaY != 0 && this.axes.hasXAxis(DataType.Time)) {
|
||||
this.in_drag_mode = false;
|
||||
@@ -175,7 +211,7 @@ export class PlotViewComponent implements OnInit {
|
||||
|
||||
const zoomFactor = event.deltaY < 0 ? 0.5 : 2;
|
||||
|
||||
this.zoomWithDateAnchor.emit(new DateAnchor(cursorPercentOfDateRange, zoomFactor));
|
||||
this.zoomWithDateAnchor(new DateAnchor(cursorPercentOfDateRange, zoomFactor));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -186,6 +222,153 @@ export class PlotViewComponent implements OnInit {
|
||||
hideDetails() {
|
||||
this.showStats = false;
|
||||
}
|
||||
|
||||
getAxes(): AxesTypes{
|
||||
const plotTypes = this.service.getPlotTypes();
|
||||
|
||||
this.config?.aggregates
|
||||
|
||||
const x = new Array<DataType>();
|
||||
const y = new Array<DataType>();
|
||||
|
||||
for(var i = 0; i < this.config!.aggregates.length; i++){
|
||||
const aggregateType = this.config?.aggregates[i];
|
||||
const plotType = plotTypes.find((p) => p.id == aggregateType);
|
||||
if (plotType) {
|
||||
if (!x.includes(plotType.xAxis)) {
|
||||
x.push(plotType.xAxis);
|
||||
}
|
||||
if (!y.includes(plotType.yAxis)) {
|
||||
y.push(plotType.yAxis);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return new AxesTypes(x,y);
|
||||
}
|
||||
|
||||
|
||||
plot(config : PlotConfig, dimension: WidgetDimensions | (()=>WidgetDimensions)){
|
||||
this.config = config;
|
||||
this.axes = this.getAxes();
|
||||
|
||||
const request = this.createPlotRequest(dimension);
|
||||
|
||||
this.imageUrl = '';
|
||||
this.stats = null;
|
||||
|
||||
document.dispatchEvent(new Event("invadersStart", {}));
|
||||
this.loadingEvent.emit(new LoadingEvent(true));
|
||||
const x = this.service.sendPlotRequest(request).subscribe({
|
||||
next: (plotResponse: PlotResponse) => {
|
||||
|
||||
this.imageUrl = "http://"+window.location.hostname+':'+window.location.port+'/'+plotResponse.rendered['main'];
|
||||
this.stats = plotResponse.stats;
|
||||
document.dispatchEvent(new Event("invadersPause", {}));
|
||||
this.loadingEvent.emit(new LoadingEvent(false));
|
||||
},
|
||||
error: (error:any) => {
|
||||
this.imageUrl = '';
|
||||
this.stats = null;
|
||||
this.showError(error.error.message);
|
||||
document.dispatchEvent(new Event("invadersPause", {}));
|
||||
this.loadingEvent.emit(new LoadingEvent(false));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
createPlotRequest( dimension: WidgetDimensions | (()=>WidgetDimensions)): PlotRequest {
|
||||
|
||||
const actualDimension = typeof dimension === "function" ? dimension() : dimension;
|
||||
|
||||
const request = new PlotRequest(
|
||||
(<any>window).submitterId,
|
||||
this.config!,
|
||||
{
|
||||
'main': new RenderOptions(actualDimension.height, actualDimension.width, false, true)
|
||||
});
|
||||
return request;
|
||||
}
|
||||
|
||||
/**
|
||||
* Zoom in/out by zoomFaktor, so that the anchorInPercentOfDateRange keeps the same position.
|
||||
*
|
||||
* shiftDateByAnchor(dateRangeAsString, 0.20, 0.5) zooms in by 50%, so that the date that was at 20% before the zoom is still at 20% after the zoom
|
||||
* shiftDateByAnchor(dateRangeAsString, 0.33, 2) zooms out by 50%, so that the date that was at 33% before the zoom is still at 33% after the zoom
|
||||
*/
|
||||
shiftDateByAnchor(dateValue:DateValue, anchorInPercentOfDateRange:number, zoomFactor:number)
|
||||
{
|
||||
const dateRangeParsed = this.parseDateRange(dateValue);
|
||||
dateRangeParsed.subscribe({
|
||||
next: (dataRange: DateRange) => {
|
||||
const dateRangeInSeconds = Math.floor(dataRange.duration.toMillis()/1000);
|
||||
|
||||
const anchorTimestampInSeconds = dataRange.startDate.plus(Math.floor(dateRangeInSeconds*anchorInPercentOfDateRange)*1000);
|
||||
const newDateRangeInSeconds = dateRangeInSeconds * zoomFactor;
|
||||
|
||||
const newStartDate = anchorTimestampInSeconds.minus(newDateRangeInSeconds*anchorInPercentOfDateRange*1000);
|
||||
const newEndDate = newStartDate.plus({seconds: newDateRangeInSeconds});;
|
||||
|
||||
this.setDateRange(newStartDate, newEndDate);
|
||||
},
|
||||
error: (err: any) => {
|
||||
window.console.error("failed to parse DateValue into DateRange: ", err);
|
||||
}
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Zoom in/out or shift date by adding factorStartDate*dateRangeInSeconds seconds to the start date
|
||||
* and factorEndDate*dateRangeInSeconds seconds to the end date.
|
||||
*
|
||||
* shiftDate(dateRangeAsString, 0.25, -0.25) will zoom in, making the range half its size
|
||||
* shiftDate(dateRangeAsString, -0.5, 0.5) will zoom out, making the range double its size
|
||||
* shiftDate(dateRangeAsString, -0.5, -0.5) will move the range by half its size to older values
|
||||
* shiftDate(dateRangeAsString, 1, 1) will move the range by its size to newer values
|
||||
*/
|
||||
shiftDate(dateValue: DateValue, factorStartDate: number, factorEndDate: number)
|
||||
{
|
||||
this.parseDateRange(dateValue).subscribe(
|
||||
dateRangeParsed => {
|
||||
const dateRangeInSeconds = Math.floor(dateRangeParsed.duration.toMillis()/1000);
|
||||
|
||||
const newStartDate = dateRangeParsed.startDate.plus({seconds: dateRangeInSeconds*factorStartDate});
|
||||
const newEndDate = dateRangeParsed.endDate.plus({seconds: dateRangeInSeconds*factorEndDate});
|
||||
|
||||
this.setDateRange(newStartDate, newEndDate);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
parseDateRange(dateValue : DateValue) : Observable<DateRange> {
|
||||
return this.service.toDateRange(dateValue);
|
||||
/*
|
||||
.pipe(map((dateRangeAsString:string) => {
|
||||
const startDate = DateTime.fromFormat(dateRangeAsString.slice(0, 19), this.DATE_PATTERN );
|
||||
const endDate = DateTime.fromFormat(dateRangeAsString.slice(22, 41), this.DATE_PATTERN );
|
||||
|
||||
|
||||
return {
|
||||
startDate: startDate,
|
||||
endDate: endDate,
|
||||
duration: endDate.diff(startDate),
|
||||
};
|
||||
}));
|
||||
*/
|
||||
}
|
||||
|
||||
|
||||
|
||||
dataSeries(): Array<DataSeriesStats> {
|
||||
return this.stats ? this.stats.dataSeriesStats : [];
|
||||
}
|
||||
|
||||
pointTypeClass(typeAndColor: DashTypeAndColor): string {
|
||||
return "plot-details-plotType"
|
||||
+" plot-details-plotType_"+typeAndColor.pointType
|
||||
+" plot-details-plotType_"+typeAndColor.color.toLocaleLowerCase();
|
||||
}
|
||||
}
|
||||
|
||||
export class SelectionRange {
|
||||
@@ -207,3 +390,14 @@ export class DateAnchor {
|
||||
this.zoomFactor = zoomFactor;
|
||||
}
|
||||
}
|
||||
|
||||
export class LoadingEvent {
|
||||
constructor(public loading: boolean){}
|
||||
}
|
||||
|
||||
export class DateRange {
|
||||
constructor(
|
||||
public startDate: DateTime,
|
||||
public endDate: DateTime,
|
||||
public duration: Duration){}
|
||||
}
|
||||
@@ -1,64 +1,228 @@
|
||||
import { Injectable, OnInit } from '@angular/core';
|
||||
import { HttpClient, HttpParams } from '@angular/common/http';
|
||||
import { Observable } from 'rxjs';
|
||||
import { map } from 'rxjs/operators';
|
||||
|
||||
import { Injectable, OnInit } from "@angular/core";
|
||||
import { HttpClient, HttpParams } from "@angular/common/http";
|
||||
import { Observable } from "rxjs";
|
||||
import { map } from "rxjs/operators";
|
||||
import { DateValue } from "./components/datepicker/date-picker.component";
|
||||
import { DateRange } from "./plot-view/plot-view.component";
|
||||
import { DateTime } from "luxon";
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
providedIn: "root",
|
||||
})
|
||||
export class PlotService {
|
||||
|
||||
readonly DATE_PATTERN = "yyyy-MM-dd'T'HH:mm:ss";
|
||||
|
||||
plotTypes: Array<PlotType>;
|
||||
|
||||
constructor(private http: HttpClient) {
|
||||
this.plotTypes = new Array<PlotType>();
|
||||
this.plotTypes.push(new PlotType("SCATTER","Scatter","scatter-chart2",true,DataType.Time,DataType.Duration));
|
||||
this.plotTypes.push(new PlotType("CUM_DISTRIBUTION", "Cumulative Distribution", "cumulative-distribution-chart", true, DataType.Percent, DataType.Duration));
|
||||
this.plotTypes.push(new PlotType("HISTOGRAM", "Histogram", "histogram", true, DataType.HistogramBin, DataType.HistogramCount));
|
||||
this.plotTypes.push(new PlotType("PARALLEL", "Parallel Requests", "parallel-requests-chart", true, DataType.Time, DataType.Count));
|
||||
this.plotTypes.push(new PlotType("BAR", "Bar (number of requests)", "bar-chart", true, DataType.Group, DataType.Count));
|
||||
this.plotTypes.push(new PlotType("BOX", "Box", "box-plot", true, DataType.Time, DataType.Duration));
|
||||
this.plotTypes.push(
|
||||
new PlotType(
|
||||
"SCATTER",
|
||||
"Scatter",
|
||||
"scatter-chart2",
|
||||
true,
|
||||
DataType.Time,
|
||||
DataType.Duration,
|
||||
),
|
||||
);
|
||||
this.plotTypes.push(
|
||||
new PlotType(
|
||||
"CUM_DISTRIBUTION",
|
||||
"Cumulative Distribution",
|
||||
"cumulative-distribution-chart",
|
||||
true,
|
||||
DataType.Percent,
|
||||
DataType.Duration,
|
||||
),
|
||||
);
|
||||
this.plotTypes.push(
|
||||
new PlotType(
|
||||
"HISTOGRAM",
|
||||
"Histogram",
|
||||
"histogram",
|
||||
true,
|
||||
DataType.HistogramBin,
|
||||
DataType.HistogramCount,
|
||||
),
|
||||
);
|
||||
this.plotTypes.push(
|
||||
new PlotType(
|
||||
"PARALLEL",
|
||||
"Parallel Requests",
|
||||
"parallel-requests-chart",
|
||||
true,
|
||||
DataType.Time,
|
||||
DataType.Count,
|
||||
),
|
||||
);
|
||||
this.plotTypes.push(
|
||||
new PlotType(
|
||||
"BAR",
|
||||
"Bar (number of requests)",
|
||||
"bar-chart",
|
||||
true,
|
||||
DataType.Group,
|
||||
DataType.Count,
|
||||
),
|
||||
);
|
||||
this.plotTypes.push(
|
||||
new PlotType(
|
||||
"BOX",
|
||||
"Box",
|
||||
"box-plot",
|
||||
true,
|
||||
DataType.Time,
|
||||
DataType.Duration,
|
||||
),
|
||||
);
|
||||
|
||||
this.plotTypes.push(new PlotType("HEATMAP", "Heatmap", "heatmap", false, DataType.Other, DataType.Other));
|
||||
this.plotTypes.push(new PlotType("CONTOUR", "Contour", "contour-chart", false, DataType.Time, DataType.Duration));
|
||||
this.plotTypes.push(new PlotType("RIDGELINES", "Ridgelines", "ridgelines", false, DataType.Other, DataType.Other));
|
||||
this.plotTypes.push(new PlotType("QQ", "Quantile-Quantile", "quantile-quantile", false, DataType.Other, DataType.Other));
|
||||
this.plotTypes.push(new PlotType("VIOLIN", "Violin", "violin-chart", false, DataType.Group, DataType.Duration));
|
||||
this.plotTypes.push(new PlotType("STRIP", "Strip", "strip-chart", false, DataType.Group, DataType.Duration));
|
||||
this.plotTypes.push(new PlotType("PIE", "Pie", "pie-chart", false, DataType.Other, DataType.Other));
|
||||
this.plotTypes.push(new PlotType("STEP_FIT", "Step Fit", "step-fit", false, DataType.Other, DataType.Other));
|
||||
this.plotTypes.push(new PlotType("LAG", "Lag", "lag-plot", false, DataType.Other, DataType.Other));
|
||||
this.plotTypes.push(new PlotType("ACF", "ACF", "acf-plot", false, DataType.Other, DataType.Other));
|
||||
this.plotTypes.push(
|
||||
new PlotType(
|
||||
"HEATMAP",
|
||||
"Heatmap",
|
||||
"heatmap",
|
||||
false,
|
||||
DataType.Other,
|
||||
DataType.Other,
|
||||
),
|
||||
);
|
||||
this.plotTypes.push(
|
||||
new PlotType(
|
||||
"CONTOUR",
|
||||
"Contour",
|
||||
"contour-chart",
|
||||
false,
|
||||
DataType.Time,
|
||||
DataType.Duration,
|
||||
),
|
||||
);
|
||||
this.plotTypes.push(
|
||||
new PlotType(
|
||||
"RIDGELINES",
|
||||
"Ridgelines",
|
||||
"ridgelines",
|
||||
false,
|
||||
DataType.Other,
|
||||
DataType.Other,
|
||||
),
|
||||
);
|
||||
this.plotTypes.push(
|
||||
new PlotType(
|
||||
"QQ",
|
||||
"Quantile-Quantile",
|
||||
"quantile-quantile",
|
||||
false,
|
||||
DataType.Other,
|
||||
DataType.Other,
|
||||
),
|
||||
);
|
||||
this.plotTypes.push(
|
||||
new PlotType(
|
||||
"VIOLIN",
|
||||
"Violin",
|
||||
"violin-chart",
|
||||
false,
|
||||
DataType.Group,
|
||||
DataType.Duration,
|
||||
),
|
||||
);
|
||||
this.plotTypes.push(
|
||||
new PlotType(
|
||||
"STRIP",
|
||||
"Strip",
|
||||
"strip-chart",
|
||||
false,
|
||||
DataType.Group,
|
||||
DataType.Duration,
|
||||
),
|
||||
);
|
||||
this.plotTypes.push(
|
||||
new PlotType(
|
||||
"PIE",
|
||||
"Pie",
|
||||
"pie-chart",
|
||||
false,
|
||||
DataType.Other,
|
||||
DataType.Other,
|
||||
),
|
||||
);
|
||||
this.plotTypes.push(
|
||||
new PlotType(
|
||||
"STEP_FIT",
|
||||
"Step Fit",
|
||||
"step-fit",
|
||||
false,
|
||||
DataType.Other,
|
||||
DataType.Other,
|
||||
),
|
||||
);
|
||||
this.plotTypes.push(
|
||||
new PlotType(
|
||||
"LAG",
|
||||
"Lag",
|
||||
"lag-plot",
|
||||
false,
|
||||
DataType.Other,
|
||||
DataType.Other,
|
||||
),
|
||||
);
|
||||
this.plotTypes.push(
|
||||
new PlotType(
|
||||
"ACF",
|
||||
"ACF",
|
||||
"acf-plot",
|
||||
false,
|
||||
DataType.Other,
|
||||
DataType.Other,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
getPlotTypes(): Array<PlotType> {
|
||||
return this.plotTypes.filter(plotType => plotType.active);
|
||||
return this.plotTypes.filter((plotType) => plotType.active);
|
||||
}
|
||||
|
||||
getTagFields(): Observable<Array<string>> {
|
||||
return this.http.get<Array<string>>('//'+window.location.hostname+':'+window.location.port+'/api/fields');
|
||||
return this.http.get<Array<string>>(
|
||||
"//" + window.location.hostname + ":" + window.location.port +
|
||||
"/api/fields",
|
||||
);
|
||||
}
|
||||
|
||||
autocomplete(query: string, caretIndex: number, resultMode: ResultMode): Observable<AutocompleteResult>
|
||||
{
|
||||
autocomplete(
|
||||
query: string,
|
||||
caretIndex: number,
|
||||
resultMode: ResultMode,
|
||||
): Observable<AutocompleteResult> {
|
||||
const options = {
|
||||
params: new HttpParams()
|
||||
.set('caretIndex', ""+caretIndex)
|
||||
.set('query', query)
|
||||
.set('resultMode', resultMode)
|
||||
.set("caretIndex", "" + caretIndex)
|
||||
.set("query", query)
|
||||
.set("resultMode", resultMode),
|
||||
};
|
||||
return this.http.get<AutocompleteResult>('//'+window.location.hostname+':'+window.location.port+'/api/autocomplete', options);
|
||||
return this.http.get<AutocompleteResult>(
|
||||
"//" + window.location.hostname + ":" + window.location.port +
|
||||
"/api/autocomplete",
|
||||
options,
|
||||
);
|
||||
}
|
||||
|
||||
abort(submitterId: string): Observable<void> {
|
||||
return this.http.delete<void>('//'+window.location.hostname+':'+window.location.port+'/api/plots/'+submitterId)
|
||||
return this.http.delete<void>(
|
||||
"//" + window.location.hostname + ":" + window.location.port +
|
||||
"/api/plots/" + submitterId,
|
||||
);
|
||||
}
|
||||
|
||||
sendPlotRequest(plotRequest: PlotRequest): Observable<PlotResponse> {
|
||||
|
||||
//console.log("send plot request: "+ JSON.stringify(plotRequest));
|
||||
const result = this.http.post<PlotResponse>('//'+window.location.hostname+':'+window.location.port+'/api/plots', plotRequest);
|
||||
const result = this.http.post<PlotResponse>(
|
||||
"//" + window.location.hostname + ":" + window.location.port +
|
||||
"/api/plots",
|
||||
plotRequest,
|
||||
);
|
||||
return result.pipe(map(this.enrichStats));
|
||||
}
|
||||
|
||||
@@ -77,36 +241,48 @@ export class PlotService {
|
||||
}
|
||||
|
||||
getFilterDefaults(): Observable<FilterDefaults> {
|
||||
return this.http.get<FilterDefaults>('//'+window.location.hostname+':'+window.location.port+'/api/filters/defaults')
|
||||
return this.http.get<FilterDefaults>(
|
||||
"//" + window.location.hostname + ":" + window.location.port +
|
||||
"/api/filters/defaults",
|
||||
);
|
||||
}
|
||||
|
||||
splitQuery(query: string, splitBy: string): Observable<Array<string>> {
|
||||
|
||||
const q = "(" + query + ") and " + splitBy + "=";
|
||||
return this.autocomplete(q, q.length + 1, ResultMode.FULL_VALUES).pipe(
|
||||
map(
|
||||
(autocompleteResult: AutocompleteResult) => autocompleteResult.proposals.map((suggestion:Suggestion) => suggestion.value)
|
||||
)
|
||||
(autocompleteResult: AutocompleteResult) =>
|
||||
autocompleteResult.proposals.map((suggestion: Suggestion) =>
|
||||
suggestion.value
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
toDateRange(dateValue: DateValue): Observable<DateRange> {
|
||||
return this.http.post<{start: string, end:string, startEpochMilli: number, endEpochMilli: number}>("//" + window.location.hostname+":" + window.location.port +"/api/dates",dateValue)
|
||||
.pipe(map((data) => {
|
||||
const startDate = DateTime.fromFormat(data.start.slice(0, -1), this.DATE_PATTERN );
|
||||
const endDate = DateTime.fromFormat(data.end.slice(0, -1), this.DATE_PATTERN );
|
||||
|
||||
|
||||
return {
|
||||
startDate: startDate,
|
||||
endDate: endDate,
|
||||
duration: endDate.diff(startDate),
|
||||
};
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export class PlotType {
|
||||
id: string;
|
||||
name: string;
|
||||
icon: string
|
||||
active: boolean;
|
||||
xAxis: DataType;
|
||||
yAxis: DataType;
|
||||
|
||||
constructor(id: string, name: string, icon: string, active: boolean, xAxis: DataType, yAxis: DataType) {
|
||||
this.id = id;
|
||||
this.name = name;
|
||||
this.icon = icon;
|
||||
this.active = active;
|
||||
this.xAxis = xAxis;
|
||||
this.yAxis = yAxis;
|
||||
constructor(
|
||||
public id: string,
|
||||
public name: string,
|
||||
public icon: string,
|
||||
public active: boolean,
|
||||
public xAxis: DataType,
|
||||
public yAxis: DataType,
|
||||
) {
|
||||
}
|
||||
|
||||
compatible(others: Array<PlotType>): boolean {
|
||||
@@ -140,7 +316,7 @@ export enum DataType {
|
||||
Metric,
|
||||
HistogramBin,
|
||||
HistogramCount,
|
||||
Other
|
||||
Other,
|
||||
}
|
||||
|
||||
export class AxesTypes {
|
||||
@@ -186,10 +362,10 @@ export class AxesTypes {
|
||||
const x2 = this.getXAxisDataType(2);
|
||||
const y2 = this.getYAxisDataType(2);
|
||||
|
||||
return (x1 ? "x1:"+DataType[x1] : "")
|
||||
+ (y1 ? " y1:"+DataType[y1] : "")
|
||||
+ (x2 ? " x2:"+DataType[x2] : "")
|
||||
+ (y2 ? " y2:"+DataType[y2] : "");
|
||||
return (x1 ? "x1:" + DataType[x1] : "") +
|
||||
(y1 ? " y1:" + DataType[y1] : "") +
|
||||
(x2 ? " x2:" + DataType[x2] : "") +
|
||||
(y2 ? " y2:" + DataType[y2] : "");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -197,38 +373,57 @@ export class Suggestion {
|
||||
constructor(
|
||||
public value: string,
|
||||
public newQuery: string,
|
||||
public newCaretPosition: number){}
|
||||
public newCaretPosition: number,
|
||||
) {}
|
||||
}
|
||||
|
||||
|
||||
export class AutocompleteResult {
|
||||
constructor(public proposals: Array<Suggestion>) {}
|
||||
}
|
||||
|
||||
export type RenderOptionsMap = {
|
||||
[key: string]: RenderOptions;
|
||||
};
|
||||
|
||||
export type RenderedImages = {
|
||||
[key: string]: string;
|
||||
};
|
||||
|
||||
export class PlotRequest {
|
||||
constructor(
|
||||
public submitterId: string,
|
||||
public config: PlotConfig,
|
||||
public renders: RenderOptionsMap,
|
||||
) {}
|
||||
|
||||
copy(): PlotRequest {
|
||||
return JSON.parse(JSON.stringify(this));
|
||||
}
|
||||
}
|
||||
|
||||
export class PlotConfig {
|
||||
constructor(
|
||||
public query: string,
|
||||
public height : number,
|
||||
public width : number,
|
||||
public thumbnailMaxWidth : number = 300,
|
||||
public thumbnailMaxHeight : number = 200,
|
||||
public groupBy: Array<string>,
|
||||
public limitBy: string,
|
||||
public limit: number,
|
||||
public y1: YAxisDefinition,
|
||||
public y2: YAxisDefinition | undefined,
|
||||
public dateRange : string,
|
||||
public dateRange: DateValue,
|
||||
public aggregates: Array<string>,
|
||||
public keyOutside : boolean = false,
|
||||
public generateThumbnail : boolean,
|
||||
public intervalUnit: string,
|
||||
public intervalValue: number,
|
||||
public submitterId: string,
|
||||
public renderBarChartTickLabels: boolean = false){}
|
||||
|
||||
copy(): PlotRequest {
|
||||
return JSON.parse(JSON.stringify(this));
|
||||
public renderBarChartTickLabels: boolean = false,
|
||||
) {}
|
||||
}
|
||||
|
||||
export class RenderOptions {
|
||||
constructor(
|
||||
public height: number,
|
||||
public width: number,
|
||||
public showKey: boolean,
|
||||
public renderLabels: boolean,
|
||||
) {}
|
||||
}
|
||||
|
||||
export class YAxisDefinition {
|
||||
@@ -236,14 +431,15 @@ export class YAxisDefinition {
|
||||
public axisScale: string,
|
||||
public rangeMin: number,
|
||||
public rangeMax: number,
|
||||
public rangeUnit : string){}
|
||||
public rangeUnit: string,
|
||||
) {}
|
||||
}
|
||||
|
||||
export class PlotResponse {
|
||||
constructor(
|
||||
public imageUrl : string,
|
||||
public stats: PlotResponseStats,
|
||||
public thumbnailUrl : string){}
|
||||
public rendered: RenderedImages,
|
||||
) {}
|
||||
}
|
||||
|
||||
export class PlotResponseStats {
|
||||
@@ -253,7 +449,8 @@ export class PlotResponseStats {
|
||||
public average: number,
|
||||
public plottedValues: number,
|
||||
public maxAvgRatio: number,
|
||||
public dataSeriesStats : Array<DataSeriesStats>){}
|
||||
public dataSeriesStats: Array<DataSeriesStats>,
|
||||
) {}
|
||||
}
|
||||
|
||||
export class DataSeriesStats {
|
||||
@@ -264,23 +461,26 @@ export class DataSeriesStats {
|
||||
public average: number,
|
||||
public plottedValues: number,
|
||||
public dashTypeAndColor: DashTypeAndColor,
|
||||
public percentiles: Map<string, number>){}
|
||||
public percentiles: Map<string, number>,
|
||||
) {}
|
||||
}
|
||||
|
||||
export class DashTypeAndColor {
|
||||
constructor(
|
||||
public color: string,
|
||||
public pointType: number) {}
|
||||
public pointType: number,
|
||||
) {}
|
||||
}
|
||||
|
||||
export class FilterDefaults {
|
||||
constructor(
|
||||
public groupBy: Array<string>,
|
||||
public fields: Array<string>,
|
||||
public splitBy: string){}
|
||||
public splitBy: string,
|
||||
) {}
|
||||
}
|
||||
|
||||
export enum ResultMode {
|
||||
CUT_AT_DOT = "CUT_AT_DOT",
|
||||
FULL_VALUES = "FULL_VALUES"
|
||||
FULL_VALUES = "FULL_VALUES",
|
||||
}
|
||||
|
||||
@@ -4,5 +4,6 @@
|
||||
border-radius: 5px;
|
||||
box-sizing: border-box;
|
||||
padding: 5px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { Component, OnInit, Input, ViewChild } from '@angular/core';
|
||||
import {UntypedFormControl} from '@angular/forms';
|
||||
import { Component, OnInit, Input, ViewChild, AfterViewInit } from '@angular/core';
|
||||
import {FormControl, UntypedFormControl} from '@angular/forms';
|
||||
import {Observable} from 'rxjs';
|
||||
import {startWith, map} from 'rxjs/operators';
|
||||
import {MatLegacyAutocompleteTrigger as MatAutocompleteTrigger } from '@angular/material/legacy-autocomplete';
|
||||
import {MatAutocompleteTrigger } from '@angular/material/autocomplete';
|
||||
import { PlotService, PlotType, AutocompleteResult, Suggestion, ResultMode } from '../plot.service';
|
||||
|
||||
@Component({
|
||||
@@ -12,7 +12,7 @@ import { PlotService, PlotType, AutocompleteResult, Suggestion, ResultMode } fro
|
||||
})
|
||||
export class QueryAutocompleteComponent implements OnInit {
|
||||
|
||||
queryField = new UntypedFormControl('');
|
||||
queryField = new FormControl<Suggestion>(new Suggestion("","",0));
|
||||
|
||||
suggestions = new UntypedFormControl();
|
||||
|
||||
@@ -20,26 +20,33 @@ export class QueryAutocompleteComponent implements OnInit {
|
||||
|
||||
query : string = "";
|
||||
|
||||
suggestionFetcherEnabled = true;
|
||||
|
||||
@ViewChild(MatAutocompleteTrigger)
|
||||
autocomplete!: MatAutocompleteTrigger;
|
||||
|
||||
|
||||
constructor(private plotService: PlotService) {}
|
||||
constructor(private plotService: PlotService) {
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
const that = this;
|
||||
this.query = "";
|
||||
this.queryField.valueChanges.subscribe(function(value){
|
||||
this.queryField.valueChanges.subscribe((value) =>{
|
||||
if (value != null) {
|
||||
if (typeof value == "string") {
|
||||
that.query = value;
|
||||
this.query = value;
|
||||
}else{
|
||||
that.query = value.newQuery;
|
||||
this.query = value.newQuery;
|
||||
|
||||
var el : HTMLInputElement = <HTMLInputElement>document.getElementById('query-autocomplete-input');
|
||||
const el : HTMLInputElement = <HTMLInputElement>document.getElementById('query-autocomplete-input');
|
||||
el.selectionStart=value.newCaretPosition;
|
||||
el.selectionEnd=value.newCaretPosition;
|
||||
}
|
||||
|
||||
that.fetchSuggestions(value.newCaretPosition);
|
||||
|
||||
if (this.suggestionFetcherEnabled) {
|
||||
this.fetchSuggestions(value.newCaretPosition);
|
||||
}
|
||||
}
|
||||
});
|
||||
this.filteredSuggestions = this.suggestions.valueChanges.pipe(
|
||||
@@ -61,23 +68,21 @@ export class QueryAutocompleteComponent implements OnInit {
|
||||
const that = this;
|
||||
const query = typeof this.queryField.value == "string"
|
||||
? this.queryField.value
|
||||
: this.queryField.value.newQuery;
|
||||
: this.queryField.value!.newQuery;
|
||||
|
||||
this.plotService
|
||||
.autocomplete(query, caretIndex, ResultMode.CUT_AT_DOT)
|
||||
.subscribe(
|
||||
(data: AutocompleteResult) => {// success path
|
||||
//console.log(JSON.stringify(data.proposals));
|
||||
.subscribe({
|
||||
next: (data: AutocompleteResult) => {
|
||||
that.suggestions.setValue(data.proposals);
|
||||
|
||||
that.autocomplete.openPanel();
|
||||
},
|
||||
(error:any) => console.log(error)
|
||||
error: (error:any) => console.log(error)
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
displaySuggestion(suggestion?: Suggestion): string {
|
||||
//console.log("suggestion: "+JSON.stringify(suggestion));
|
||||
return suggestion ? suggestion.newQuery : '';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,29 +4,41 @@
|
||||
</div>
|
||||
|
||||
<div id="date-box">
|
||||
<mat-form-field>
|
||||
<app-date-picker #datePicker></app-date-picker>
|
||||
<!--
|
||||
<mat-form-field class="pdb-form-full-width">
|
||||
<mat-label>Date Range:</mat-label>
|
||||
<input matInput id="search-date-range" value="dateRange" name="dates" />
|
||||
</mat-form-field>
|
||||
-->
|
||||
</div>
|
||||
|
||||
<div id="filters">
|
||||
<div id="filterpanel">
|
||||
|
||||
|
||||
<mat-form-field>
|
||||
<mat-form-field class="pdb-form-full-width">
|
||||
<mat-label>Type:</mat-label>
|
||||
<mat-select multiple [(ngModel)]="selectedPlotType" (ngModelChange)="changePlotType($event)">
|
||||
<mat-option *ngFor="let plotType of plotTypes" [value]="plotType" [disabled]="!plotType.active">
|
||||
<img src="assets/img/{{plotType.icon}}.svg" class="icon-select" /> {{plotType.name}}
|
||||
<mat-select
|
||||
multiple
|
||||
[(ngModel)]="selectedPlotType"
|
||||
(ngModelChange)="changePlotType($event)"
|
||||
>
|
||||
<mat-option
|
||||
*ngFor="let plotType of plotTypes"
|
||||
[value]="plotType"
|
||||
[disabled]="!plotType.active"
|
||||
>
|
||||
<img src="assets/img/{{ plotType.icon }}.svg" class="icon-select" />
|
||||
{{ plotType.name }}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field>
|
||||
<mat-form-field class="pdb-form-full-width">
|
||||
<mat-label>Group By:</mat-label>
|
||||
<mat-select multiple [(value)]="groupBy">
|
||||
<mat-option *ngFor="let tagField of tagFields" [value]="tagField">{{tagField.name}}</mat-option>
|
||||
<mat-option *ngFor="let tagField of tagFields" [value]="tagField">{{
|
||||
tagField.name
|
||||
}}</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
|
||||
@@ -47,32 +59,62 @@
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div [hidden]="!selectedPlotTypesContains(['BAR', 'BOX'])">
|
||||
<mat-checkbox [(ngModel)]="renderBarChartTickLabels">Show Tic Labels (bar chart)</mat-checkbox>
|
||||
<mat-checkbox [(ngModel)]="renderBarChartTickLabels"
|
||||
>Show Tic Labels (bar chart)</mat-checkbox
|
||||
>
|
||||
</div>
|
||||
<pdb-y-axis-definition #y1AxisDefinitionComponent yIndex="1"></pdb-y-axis-definition>
|
||||
<pdb-y-axis-definition #y2AxisDefinitionComponent yIndex="2" [hidden]="!y2AxisAvailable"></pdb-y-axis-definition>
|
||||
<pdb-y-axis-definition
|
||||
#y1AxisDefinitionComponent
|
||||
yIndex="1"
|
||||
></pdb-y-axis-definition>
|
||||
<pdb-y-axis-definition
|
||||
#y2AxisDefinitionComponent
|
||||
yIndex="2"
|
||||
[hidden]="!y2AxisAvailable"
|
||||
></pdb-y-axis-definition>
|
||||
|
||||
<mat-checkbox [(ngModel)]="enableGallery">Gallery</mat-checkbox>
|
||||
<mat-checkbox
|
||||
*ngIf="galleryEnabled"
|
||||
[(ngModel)]="enableGallery"
|
||||
(click)="toggleGallery($event)"
|
||||
>Gallery</mat-checkbox
|
||||
>
|
||||
|
||||
<mat-form-field *ngIf="enableGallery">
|
||||
<mat-form-field *ngIf="enableGallery" class="pdb-form-full-width">
|
||||
<mat-label>Split By:</mat-label>
|
||||
<mat-select [(value)]="splitBy">
|
||||
<mat-option *ngFor="let tagField of tagFields" [value]="tagField">{{tagField.name}}</mat-option>
|
||||
<mat-option *ngFor="let tagField of tagFields" [value]="tagField">{{
|
||||
tagField.name
|
||||
}}</mat-option>
|
||||
</mat-select>
|
||||
<mat-error *ngIf="splitBy == null || true">
|
||||
Please select a value!
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
|
||||
|
||||
<div id="plot-button-bar">
|
||||
<a
|
||||
mat-icon-button
|
||||
[routerLink]="['/vis']"
|
||||
[queryParams]="{ config: serializedConfig() }"
|
||||
target="_blank"
|
||||
aria-label="open new window with the same search"
|
||||
title="open new window with the same search"
|
||||
><img src="assets/img/link.svg" aria-hidden="true"
|
||||
/></a>
|
||||
<button
|
||||
*ngIf="!enableGallery && !plotJobActive"
|
||||
[disabled]="plotJobActive"
|
||||
mat-button
|
||||
matTooltip="Create Plot"
|
||||
(click)="plot()">
|
||||
<img src="assets/img/scatter-chart2.svg" class="icon-inline" aria-hidden="true" title="create plot" />
|
||||
(click)="plot()"
|
||||
>
|
||||
<img
|
||||
src="assets/img/scatter-chart2.svg"
|
||||
class="icon-inline"
|
||||
aria-hidden="true"
|
||||
title="create plot"
|
||||
/>
|
||||
Plot
|
||||
</button>
|
||||
<button
|
||||
@@ -80,15 +122,24 @@
|
||||
mat-button
|
||||
matTooltip="Create Gallery"
|
||||
(click)="gallery()"
|
||||
[disabled]="this.splitBy == null">
|
||||
<img src="assets/img/four-squares-line.svg" class="icon-inline" aria-hidden="true" title="Create Gallery (only active if 'Split' is set)" />
|
||||
[disabled]="this.splitBy == null"
|
||||
>
|
||||
<img
|
||||
src="assets/img/four-squares-line.svg"
|
||||
class="icon-inline"
|
||||
aria-hidden="true"
|
||||
title="Create Gallery (only active if 'Split' is set)"
|
||||
/>
|
||||
Gallery
|
||||
</button>
|
||||
<button
|
||||
*ngIf="plotJobActive"
|
||||
mat-button
|
||||
(click)="abort()"
|
||||
matTooltip="abort"><img src="assets/img/close.svg" class="icon-inline" /> Abort</button>
|
||||
matTooltip="abort"
|
||||
>
|
||||
<img src="assets/img/close.svg" class="icon-inline" /> Abort
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -96,13 +147,9 @@
|
||||
<div id="results">
|
||||
<pdb-plot-view
|
||||
#plotView
|
||||
(zoomRange)="zoomRange($event)"
|
||||
(zoomWithDateAnchor)="zoomWithDateAnchor($event)"></pdb-plot-view>
|
||||
<pdb-gallery-view
|
||||
#galleryView>
|
||||
</pdb-gallery-view>
|
||||
(loadingEvent)="loading($event)"
|
||||
(dateRangeUpdateEvent)="updateDateRange($event)"
|
||||
></pdb-plot-view>
|
||||
<pdb-gallery-view #galleryView> </pdb-gallery-view>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
:host{
|
||||
font-weight: bold;
|
||||
width: 100%;
|
||||
height: calc(100% - 29px);
|
||||
height: calc(100% - 43px);
|
||||
position: absolute;
|
||||
display: grid;
|
||||
}
|
||||
@@ -15,11 +15,11 @@
|
||||
grid:
|
||||
"query-box query-box date-box" auto
|
||||
"filters results results" 1fr
|
||||
/ 17.7em 3fr 21em;
|
||||
/ 25.5em 3fr auto;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 1000px) {
|
||||
@media screen and (max-width: 900px) {
|
||||
#visualization {
|
||||
display: grid;
|
||||
margin: 0;
|
||||
@@ -27,7 +27,7 @@
|
||||
grid:
|
||||
"query-box" auto
|
||||
"date-box" auto
|
||||
"filters" auto
|
||||
"filters" min-content
|
||||
"results" 1fr
|
||||
/ 1fr;
|
||||
}
|
||||
@@ -43,9 +43,14 @@
|
||||
|
||||
#date-box{
|
||||
grid-area: date-box;
|
||||
margin-right: 1em;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
#filters {
|
||||
grid-area: filters;
|
||||
overflow: auto;
|
||||
@@ -64,5 +69,7 @@
|
||||
}
|
||||
|
||||
#plot-button-bar {
|
||||
text-align: right;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,113 +1,212 @@
|
||||
import { Component, OnInit, ViewChild } from '@angular/core';
|
||||
import { PlotService, PlotType, PlotRequest, PlotResponse, TagField, FilterDefaults, DataType, YAxisDefinition, AxesTypes } from '../plot.service';
|
||||
import { UntypedFormControl, Validators } from '@angular/forms';
|
||||
import { MatLegacySnackBar as MatSnackBar } from '@angular/material/legacy-snack-bar';
|
||||
import { LimitByComponent } from '../limit-by/limit-by.component';
|
||||
import { YAxisDefinitionComponent } from '../y-axis-definition/y-axis-definition.component';
|
||||
import { QueryAutocompleteComponent } from '../query-autocomplete/query-autocomplete.component';
|
||||
import { PlotViewComponent, SelectionRange, DateAnchor } from '../plot-view/plot-view.component';
|
||||
import { GalleryViewComponent } from '../gallery-view/gallery-view.component';
|
||||
import * as moment from 'moment';
|
||||
import {
|
||||
AfterViewInit,
|
||||
Component,
|
||||
Input,
|
||||
OnInit,
|
||||
ViewChild,
|
||||
} from "@angular/core";
|
||||
import {
|
||||
AxesTypes,
|
||||
DataType,
|
||||
FilterDefaults,
|
||||
PlotConfig,
|
||||
PlotRequest,
|
||||
PlotService,
|
||||
PlotType,
|
||||
RenderOptions,
|
||||
RenderOptionsMap,
|
||||
Suggestion,
|
||||
TagField,
|
||||
} from "../plot.service";
|
||||
import { UntypedFormControl } from "@angular/forms";
|
||||
import { MatSnackBar } from "@angular/material/snack-bar";
|
||||
import { LimitByComponent } from "../limit-by/limit-by.component";
|
||||
import { YAxisDefinitionComponent } from "../y-axis-definition/y-axis-definition.component";
|
||||
import { QueryAutocompleteComponent } from "../query-autocomplete/query-autocomplete.component";
|
||||
import {
|
||||
DateRange,
|
||||
LoadingEvent,
|
||||
PlotViewComponent,
|
||||
} from "../plot-view/plot-view.component";
|
||||
import { GalleryViewComponent } from "../gallery-view/gallery-view.component";
|
||||
import { WidgetDimensions } from "../dashboard.service";
|
||||
import {
|
||||
DatePickerComponent,
|
||||
DateValue,
|
||||
} from "../components/datepicker/date-picker.component";
|
||||
|
||||
@Component({
|
||||
selector: 'pdb-visualization-page',
|
||||
templateUrl: './visualization-page.component.html',
|
||||
styleUrls: ['./visualization-page.component.scss']
|
||||
selector: "pdb-visualization-page",
|
||||
templateUrl: "./visualization-page.component.html",
|
||||
styleUrls: ["./visualization-page.component.scss"],
|
||||
})
|
||||
export class VisualizationPageComponent implements OnInit {
|
||||
|
||||
export class VisualizationPageComponent implements OnInit, AfterViewInit {
|
||||
readonly DATE_PATTERN = "YYYY-MM-DD HH:mm:ss"; // for moment-JS
|
||||
|
||||
dateRange = new UntypedFormControl('2019-10-05 00:00:00 - 2019-10-11 23:59:59');
|
||||
@Input()
|
||||
defaultConfig?: PlotConfig;
|
||||
|
||||
@Input()
|
||||
galleryEnabled = true;
|
||||
|
||||
dateRange = new UntypedFormControl(
|
||||
"2019-10-05 00:00:00 - 2019-10-11 23:59:59",
|
||||
);
|
||||
|
||||
selectedPlotType = new Array<PlotType>();
|
||||
plotTypes: Array<any> = [];
|
||||
plotTypes: PlotType[] = [];
|
||||
|
||||
tagFields: Array<TagField> = new Array<TagField>();
|
||||
|
||||
groupBy = new Array<TagField>();
|
||||
|
||||
@ViewChild('limitbycomponent')
|
||||
@ViewChild("limitbycomponent")
|
||||
private limitbycomponent!: LimitByComponent;
|
||||
|
||||
|
||||
@ViewChild('y1AxisDefinitionComponent', { read: YAxisDefinitionComponent })
|
||||
@ViewChild("y1AxisDefinitionComponent", { read: YAxisDefinitionComponent })
|
||||
private y1AxisDefinitionComponent!: YAxisDefinitionComponent;
|
||||
|
||||
@ViewChild('y2AxisDefinitionComponent', { read: YAxisDefinitionComponent })
|
||||
@ViewChild("y2AxisDefinitionComponent", { read: YAxisDefinitionComponent })
|
||||
private y2AxisDefinitionComponent!: YAxisDefinitionComponent;
|
||||
|
||||
@ViewChild('query')
|
||||
@ViewChild("query")
|
||||
query!: QueryAutocompleteComponent;
|
||||
|
||||
@ViewChild('plotView')
|
||||
@ViewChild("plotView")
|
||||
plotView!: PlotViewComponent;
|
||||
|
||||
@ViewChild('galleryView')
|
||||
@ViewChild("galleryView")
|
||||
galleryView!: GalleryViewComponent;
|
||||
|
||||
@ViewChild("datePicker")
|
||||
datePicker!: DatePickerComponent;
|
||||
|
||||
enableGallery = false;
|
||||
splitBy: TagField | undefined = undefined;
|
||||
y2AxisAvailable = false;
|
||||
|
||||
intervalUnit = 'NO_INTERVAL';
|
||||
intervalUnit = "NO_INTERVAL";
|
||||
intervalValue = 1;
|
||||
renderBarChartTickLabels = false;
|
||||
|
||||
submitterId = crypto.randomUUID();
|
||||
|
||||
plotJobActive = false;
|
||||
|
||||
constructor(private plotService: PlotService, private snackBar: MatSnackBar) {
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
if (!this.defaultConfig && params.get("config")) {
|
||||
const config = JSON.parse(params.get("config")!);
|
||||
this.defaultConfig = config;
|
||||
}
|
||||
}
|
||||
|
||||
showError(message: string) {
|
||||
this.snackBar.open(message, "", {
|
||||
duration: 5000,
|
||||
verticalPosition: 'top'
|
||||
verticalPosition: "top",
|
||||
});
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
const that = this;
|
||||
(<any> window).initDatePicker();
|
||||
|
||||
this.plotTypes = this.plotService.getPlotTypes();
|
||||
this.selectedPlotType.push(this.plotTypes[0]);
|
||||
|
||||
that.plotService.getFilterDefaults().subscribe(function(filterDefaults: FilterDefaults) {
|
||||
this.plotService.getFilterDefaults().subscribe(
|
||||
(filterDefaults: FilterDefaults) => {
|
||||
filterDefaults.fields.forEach((name: string) => {
|
||||
this.tagFields.push(new TagField(name));
|
||||
}, (error: any) => {
|
||||
this.showError(error.error.message);
|
||||
});
|
||||
|
||||
filterDefaults.fields.forEach(function(name:string) {
|
||||
that.tagFields.push(new TagField(name));
|
||||
const groupByDefaults = this.defaultConfig
|
||||
? this.defaultConfig.groupBy
|
||||
: filterDefaults.groupBy;
|
||||
this.groupBy = this.tagFields.filter((val) =>
|
||||
groupByDefaults.includes(val.name)
|
||||
);
|
||||
this.splitBy = this.tagFields.find((val) =>
|
||||
filterDefaults.splitBy == val.name
|
||||
);
|
||||
|
||||
if (this.defaultConfig) {
|
||||
this.plot();
|
||||
}
|
||||
},
|
||||
(error: any) => {
|
||||
that.showError(error.error.message);
|
||||
});
|
||||
);
|
||||
}
|
||||
|
||||
that.groupBy = that.tagFields.filter(val => filterDefaults.groupBy.includes(val.name));
|
||||
that.splitBy = that.tagFields.find(val => filterDefaults.splitBy == val.name);
|
||||
});
|
||||
ngAfterViewInit(): void {
|
||||
if (this.defaultConfig) {
|
||||
const c = this.defaultConfig;
|
||||
this.query.suggestionFetcherEnabled = false;
|
||||
this.query.queryField.setValue(
|
||||
new Suggestion(c.query, c.query, c.query.length),
|
||||
);
|
||||
this.query.suggestionFetcherEnabled = true;
|
||||
|
||||
this.selectedPlotType = this.plotTypes.filter((pt) =>
|
||||
c.aggregates.includes(pt.id)
|
||||
);
|
||||
this.changePlotType(this.selectedPlotType);
|
||||
this.updateDateRange(c.dateRange, false);
|
||||
this.limitbycomponent.limitBy = c.limitBy;
|
||||
this.limitbycomponent.limit = c.limit;
|
||||
this.intervalUnit = c.intervalUnit;
|
||||
this.intervalValue = c.intervalValue;
|
||||
this.y1AxisDefinitionComponent.yAxisScale = c.y1.axisScale;
|
||||
this.y1AxisDefinitionComponent.minYValue = c.y1.rangeMin;
|
||||
this.y1AxisDefinitionComponent.maxYValue = c.y1.rangeMax;
|
||||
this.y1AxisDefinitionComponent.yAxisUnit = c.y1.rangeUnit;
|
||||
|
||||
if (c.y2) {
|
||||
this.y2AxisDefinitionComponent.yAxisScale = c.y2.axisScale;
|
||||
this.y2AxisDefinitionComponent.minYValue = c.y2.rangeMin;
|
||||
this.y2AxisDefinitionComponent.maxYValue = c.y2.rangeMax;
|
||||
this.y2AxisDefinitionComponent.yAxisUnit = c.y2.rangeUnit;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
toggleGallery(event: Event) {
|
||||
this.galleryView.show = this.enableGallery;
|
||||
}
|
||||
|
||||
loading(event: LoadingEvent) {
|
||||
this.plotJobActive = event.loading;
|
||||
}
|
||||
|
||||
updateDateRange(newDateRange: DateValue, updatePlot = true) {
|
||||
this.datePicker.setDateValue(newDateRange);
|
||||
if (updatePlot) {
|
||||
this.plot();
|
||||
}
|
||||
}
|
||||
|
||||
changePlotType(selectedPlotTypes: Array<PlotType>) {
|
||||
const compatiblePlotTypes = this.plotTypes.filter(pt => pt.compatible(selectedPlotTypes));
|
||||
this.plotTypes.forEach(pt => pt.active=false);
|
||||
compatiblePlotTypes.forEach(pt => pt.active=true);
|
||||
const compatiblePlotTypes = this.plotTypes.filter((pt) =>
|
||||
pt.compatible(selectedPlotTypes)
|
||||
);
|
||||
this.plotTypes.forEach((pt) => pt.active = false);
|
||||
compatiblePlotTypes.forEach((pt) => pt.active = true);
|
||||
|
||||
const axesTypes = this.getAxes();
|
||||
this.y2AxisAvailable = axesTypes.y.length == 2;
|
||||
}
|
||||
|
||||
selectedPlotTypesContains(plotTypeIds: Array<string>) {
|
||||
return this.selectedPlotType.filter(pt => plotTypeIds.includes(pt.id)).length > 0;
|
||||
return this.selectedPlotType.filter((pt) => plotTypeIds.includes(pt.id))
|
||||
.length > 0;
|
||||
}
|
||||
|
||||
|
||||
dateRangeAsString() : string {
|
||||
return (<HTMLInputElement>document.getElementById("search-date-range")).value;
|
||||
dateRangeAsString(): DateValue {
|
||||
return this.datePicker.getDateValue();
|
||||
}
|
||||
|
||||
gallery() {
|
||||
if (this.splitBy != null) {
|
||||
this.plotView.imageUrl = '';
|
||||
this.plotView.imageUrl = "";
|
||||
this.plotView.stats = null;
|
||||
this.galleryView.show = true;
|
||||
const request = this.createPlotRequest();
|
||||
@@ -118,7 +217,6 @@ export class VisualizationPageComponent implements OnInit {
|
||||
}
|
||||
|
||||
getAxes(): AxesTypes {
|
||||
|
||||
const x = new Array<DataType>();
|
||||
const y = new Array<DataType>();
|
||||
|
||||
@@ -136,160 +234,79 @@ export class VisualizationPageComponent implements OnInit {
|
||||
}
|
||||
|
||||
abort() {
|
||||
this.plotService.abort(this.submitterId).subscribe({
|
||||
this.plotService.abort((<any> window).submitterId).subscribe({
|
||||
complete: () => {
|
||||
//this.plotView.imageUrl = '';
|
||||
//this.plotView.stats = null;
|
||||
//this.plotJobActive = false;
|
||||
//this.showError("Job aborted");
|
||||
//document.dispatchEvent(new Event("invadersPause", {}));
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
plot() {
|
||||
const that = this;
|
||||
|
||||
that.plotView.imageUrl = '';
|
||||
that.plotView.stats = null;
|
||||
this.plotJobActive = true;
|
||||
this.plotView.axes = this.getAxes();
|
||||
console.log(JSON.stringify(this.getAxes()));
|
||||
that.galleryView.show=false;
|
||||
document.dispatchEvent(new Event("invadersStart", {}));
|
||||
|
||||
const request = this.createPlotRequest();
|
||||
|
||||
this.plotService.sendPlotRequest(request).subscribe({
|
||||
next: (plotResponse: PlotResponse) => {
|
||||
this.plotView.imageUrl = "http://"+window.location.hostname+':'+window.location.port+'/'+plotResponse.imageUrl;
|
||||
this.plotView.stats = plotResponse.stats;
|
||||
this.plotJobActive = false;
|
||||
document.dispatchEvent(new Event("invadersPause", {}));
|
||||
},
|
||||
error: (error:any) => {
|
||||
console.log(JSON.stringify(error));
|
||||
this.plotView.imageUrl = '';
|
||||
this.plotView.stats = null;
|
||||
this.plotJobActive = false;
|
||||
this.showError(error.error.message);
|
||||
document.dispatchEvent(new Event("invadersPause", {}));
|
||||
}
|
||||
});
|
||||
const config = this.createPlotConfig();
|
||||
this.plotView.plot(config, this.plotDimensionSupplier);
|
||||
}
|
||||
|
||||
createPlotRequest(): PlotRequest {
|
||||
plotDimensionSupplier(): WidgetDimensions {
|
||||
const results = document.getElementById("results");
|
||||
return new WidgetDimensions(
|
||||
results != null ? results.offsetWidth - 1 : 1024,
|
||||
results != null ? results.offsetHeight - 1 : 1024,
|
||||
);
|
||||
}
|
||||
|
||||
createPlotConfig(): PlotConfig {
|
||||
const aggregates = new Array<string>();
|
||||
this.selectedPlotType.forEach(a => aggregates.push(a.id));
|
||||
this.selectedPlotType.forEach((a) => aggregates.push(a.id));
|
||||
|
||||
const y1 = this.y1AxisDefinitionComponent.getAxisDefinition();
|
||||
const y2 = this.y2AxisDefinitionComponent ? this.y2AxisDefinitionComponent.getAxisDefinition() : undefined;
|
||||
const results = document.getElementById("results");
|
||||
const y2 = this.y2AxisDefinitionComponent
|
||||
? this.y2AxisDefinitionComponent.getAxisDefinition()
|
||||
: undefined;
|
||||
|
||||
const request = new PlotRequest(
|
||||
const config = new PlotConfig(
|
||||
this.query.query,
|
||||
results != null ? results.offsetHeight-1: 1024,
|
||||
results != null ? results.offsetWidth-1 : 1024,
|
||||
300, // thumbnailMaxWidth
|
||||
200, // thumbnailMaxHeight
|
||||
this.groupBy.map(o => o.name),
|
||||
this.groupBy.map((o) => o.name),
|
||||
this.limitbycomponent.limitBy,
|
||||
this.limitbycomponent.limit,
|
||||
y1,
|
||||
y2,
|
||||
this.dateRangeAsString(), // dateRange
|
||||
this.datePicker.getDateValue(), // dateRange
|
||||
aggregates, // aggregates
|
||||
false, // keyOutside
|
||||
this.enableGallery, // generateThumbnail
|
||||
this.intervalUnit,
|
||||
this.intervalValue,
|
||||
this.submitterId,
|
||||
this.renderBarChartTickLabels);
|
||||
this.renderBarChartTickLabels,
|
||||
);
|
||||
return config;
|
||||
}
|
||||
|
||||
createPlotRequest(): PlotRequest {
|
||||
const results = document.getElementById("results");
|
||||
|
||||
const config = this.createPlotConfig();
|
||||
|
||||
const renderOptions: RenderOptionsMap = {
|
||||
"main": new RenderOptions(
|
||||
results!.offsetHeight - 1,
|
||||
results!.offsetWidth - 1,
|
||||
true,
|
||||
true,
|
||||
),
|
||||
"thumbnail": new RenderOptions(200, 300, false, false),
|
||||
};
|
||||
|
||||
const request = new PlotRequest(
|
||||
(<any> window).submitterId,
|
||||
config,
|
||||
renderOptions,
|
||||
);
|
||||
return request;
|
||||
}
|
||||
|
||||
/**
|
||||
* Zoom in/out by zoomFaktor, so that the anchorInPercentOfDateRange keeps the same position.
|
||||
*
|
||||
* shiftDateByAnchor(dateRangeAsString, 0.20, 0.5) zooms in by 50%, so that the date that was at 20% before the zoom is still at 20% after the zoom
|
||||
* shiftDateByAnchor(dateRangeAsString, 0.33, 2) zooms out by 50%, so that the date that was at 33% before the zoom is still at 33% after the zoom
|
||||
*/
|
||||
shiftDateByAnchor(dateRange:string, anchorInPercentOfDateRange:number, zoomFactor:number)
|
||||
{
|
||||
const dateRangeParsed = this.parseDateRange(dateRange);
|
||||
const dateRangeInSeconds = dateRangeParsed.duration.asSeconds();
|
||||
|
||||
const anchorTimestampInSeconds = dateRangeParsed.startDate.clone().add(Math.floor(dateRangeInSeconds*anchorInPercentOfDateRange), "seconds");
|
||||
const newDateRangeInSeconds = dateRangeInSeconds * zoomFactor;
|
||||
|
||||
const newStartDate = anchorTimestampInSeconds.clone().subtract(newDateRangeInSeconds*anchorInPercentOfDateRange, "seconds");
|
||||
const newEndDate = newStartDate.clone().add({seconds: newDateRangeInSeconds});;
|
||||
|
||||
this.setDateRange(newStartDate, newEndDate);
|
||||
serializedConfig(): string {
|
||||
try {
|
||||
const config = this.createPlotConfig();
|
||||
return JSON.stringify(config);
|
||||
} catch (e) {
|
||||
return "";
|
||||
}
|
||||
|
||||
/**
|
||||
* Zoom in/out or shift date by adding factorStartDate*dateRangeInSeconds seconds to the start date
|
||||
* and factorEndDate*dateRangeInSeconds seconds to the end date.
|
||||
*
|
||||
* shiftDate(dateRangeAsString, 0.25, -0.25) will zoom in, making the range half its size
|
||||
* shiftDate(dateRangeAsString, -0.5, 0.5) will zoom out, making the range double its size
|
||||
* shiftDate(dateRangeAsString, -0.5, -0.5) will move the range by half its size to older values
|
||||
* shiftDate(dateRangeAsString, 1, 1) will move the range by its size to newer values
|
||||
*/
|
||||
shiftDate(dateRange: string, factorStartDate: number, factorEndDate: number)
|
||||
{
|
||||
const dateRangeParsed = this.parseDateRange(dateRange);
|
||||
const dateRangeInSeconds = dateRangeParsed.duration.asSeconds();
|
||||
|
||||
const newStartDate = dateRangeParsed.startDate.add({seconds: dateRangeInSeconds*factorStartDate});
|
||||
const newEndDate = dateRangeParsed.endDate.add({seconds: dateRangeInSeconds*factorEndDate});
|
||||
|
||||
this.setDateRange(newStartDate, newEndDate);
|
||||
}
|
||||
|
||||
parseDateRange(dateRangeAsString : string) : DateRange {
|
||||
const startDate = moment(dateRangeAsString.slice(0, 19));
|
||||
const endDate = moment(dateRangeAsString.slice(22, 41));
|
||||
|
||||
return {
|
||||
startDate: startDate,
|
||||
endDate: endDate,
|
||||
duration: moment.duration(endDate.diff(startDate))
|
||||
};
|
||||
}
|
||||
|
||||
setDateRange(startDate: any, endDate: any) {
|
||||
const formattedStartDate = startDate.format(this.DATE_PATTERN);
|
||||
const formattedEndDate = endDate.format(this.DATE_PATTERN);
|
||||
|
||||
const newDateRange = formattedStartDate+" - "+formattedEndDate;
|
||||
|
||||
(<HTMLInputElement>document.getElementById("search-date-range")).value = newDateRange;
|
||||
this.plot();
|
||||
}
|
||||
|
||||
zoomRange(range: SelectionRange) {
|
||||
this.shiftDate(this.dateRangeAsString(), range.startPercentOfDateRange, range.endPercentOfDateRange-1);
|
||||
}
|
||||
|
||||
zoomWithDateAnchor(dateAnchor: DateAnchor){
|
||||
this.shiftDateByAnchor(this.dateRangeAsString(), dateAnchor.cursorPercentOfDateRange, dateAnchor.zoomFactor);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export class DateRange {
|
||||
startDate: any;
|
||||
endDate: any;
|
||||
duration: any;
|
||||
}
|
||||
|
||||
/*
|
||||
export class AxesUsed {
|
||||
x1: DataType;
|
||||
y1: DataType;
|
||||
x2: DataType;
|
||||
y2: DataType;
|
||||
}
|
||||
*/
|
||||
|
||||
@@ -7,14 +7,19 @@
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
|
||||
<div>
|
||||
<mat-form-field class="pdb-form-mid">
|
||||
<mat-label>Y{{yIndex}}-Axis Unit:</mat-label>
|
||||
<mat-select [(value)]="yAxisUnit">
|
||||
<mat-optgroup label="⸺numbers⸺">
|
||||
<mat-optgroup label="—numbers—">
|
||||
<mat-option value="AUTOMATIC_NUMBER">auto (number)</mat-option>
|
||||
<mat-option value="NO_UNIT">no unit</mat-option>
|
||||
</mat-optgroup>
|
||||
<mat-optgroup label="⸺time⸺">
|
||||
<mat-optgroup label="—bytes—">
|
||||
<mat-option value="AUTOMATIC_BYTES">auto (bytes)</mat-option>
|
||||
<mat-option value="BYTES">bytes</mat-option>
|
||||
</mat-optgroup>
|
||||
<mat-optgroup label="—time—">
|
||||
<mat-option value="AUTOMATIC_TIME">auto (time)</mat-option>
|
||||
<mat-option value="MILLISECONDS">millis</mat-option>
|
||||
<mat-option value="SECONDS">seconds</mat-option>
|
||||
@@ -24,10 +29,13 @@
|
||||
</mat-optgroup>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
<mat-form-field *ngIf="yAxisUnit !== 'AUTOMATIC_TIME' && yAxisUnit !== 'AUTOMATIC_NUMBER'" class="pdb-form-number">
|
||||
<mat-form-field *ngIf="yAxisUnit !== 'AUTOMATIC_TIME' && yAxisUnit !== 'AUTOMATIC_NUMBER' && yAxisUnit !== 'AUTOMATIC_BYTES'" class="pdb-form-number">
|
||||
<mat-label>Min:</mat-label>
|
||||
<input matInput type="number" placeholder="Min" min="0" [(ngModel)]="minYValue">
|
||||
</mat-form-field>
|
||||
<mat-form-field *ngIf="yAxisUnit !== 'AUTOMATIC_TIME' && yAxisUnit !== 'AUTOMATIC_NUMBER'" class="pdb-form-number">
|
||||
<mat-form-field *ngIf="yAxisUnit !== 'AUTOMATIC_TIME' && yAxisUnit !== 'AUTOMATIC_NUMBER' && yAxisUnit !== 'AUTOMATIC_BYTES'" class="pdb-form-number">
|
||||
<mat-label>Max:</mat-label>
|
||||
<input matInput type="number" placeholder="Max" min="0" [(ngModel)]="maxYValue">
|
||||
</mat-form-field>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
1
pdb-js/src/assets/img/bookmark-add-line.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" enable-background="new 0 0 24 24" height="24" viewBox="0 0 24 24" width="24"><rect fill="none" height="24" width="24"/><path d="M17,11v6.97l-5-2.14l-5,2.14V5h6V3H7C5.9,3,5,3.9,5,5v16l7-3l7,3V11H17z M21,7h-2v2h-2V7h-2V5h2V3h2v2h2V7z"/></svg>
|
||||
|
After Width: | Height: | Size: 280 B |
1
pdb-js/src/assets/img/bookmark-line.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24"><path d="M0 0h24v24H0V0z" fill="none"/><path d="M17 3H7c-1.1 0-2 .9-2 2v16l7-3 7 3V5c0-1.1-.9-2-2-2zm0 15l-5-2.18L7 18V5h10v13z"/></svg>
|
||||
|
After Width: | Height: | Size: 219 B |
1
pdb-js/src/assets/img/bookmarks-line.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24"><path d="M0 0h24v24H0V0z" fill="none"/><path d="M15 7v12.97l-4.21-1.81-.79-.34-.79.34L5 19.97V7h10m4-6H8.99C7.89 1 7 1.9 7 3h10c1.1 0 2 .9 2 2v13l2 1V3c0-1.1-.9-2-2-2zm-4 4H5c-1.1 0-2 .9-2 2v16l7-3 7 3V7c0-1.1-.9-2-2-2z"/></svg>
|
||||
|
After Width: | Height: | Size: 311 B |
1
pdb-js/src/assets/img/dashboard-outline.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24"><path d="M0 0h24v24H0V0z" fill="none"/><path d="M19 5v2h-4V5h4M9 5v6H5V5h4m10 8v6h-4v-6h4M9 17v2H5v-2h4M21 3h-8v6h8V3zM11 3H3v10h8V3zm10 8h-8v10h8V11zm-10 4H3v6h8v-6z"/></svg>
|
||||
|
After Width: | Height: | Size: 258 B |
21
pdb-js/src/assets/img/drag_handle.svg
Normal file
@@ -0,0 +1,21 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="512" height="512" viewBox="0 0 32 32">
|
||||
<g transform="translate(0.5,0.5)" style="fill:black">
|
||||
<circle cx="4" cy="9" r="2"/>
|
||||
<circle cx="12" cy="9" r="2"/>
|
||||
<circle cx="20" cy="9" r="2"/>
|
||||
<circle cx="28" cy="9" r="2"/>
|
||||
|
||||
<circle cx="4" cy="15" r="2"/>
|
||||
<circle cx="12" cy="15" r="2"/>
|
||||
<circle cx="20" cy="15" r="2"/>
|
||||
<circle cx="28" cy="15" r="2"/>
|
||||
|
||||
<circle cx="4" cy="21" r="2"/>
|
||||
<circle cx="12" cy="21" r="2"/>
|
||||
<circle cx="20" cy="21" r="2"/>
|
||||
<circle cx="28" cy="21" r="2"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
||||
|
||||
|
||||
|
After Width: | Height: | Size: 534 B |
1
pdb-js/src/assets/img/edit-note-outline.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" enable-background="new 0 0 24 24" height="24" viewBox="0 0 24 24" width="24"><rect fill="none" height="24" width="24"/><path d="M3,10h11v2H3V10z M3,8h11V6H3V8z M3,16h7v-2H3V16z M18.01,12.87l0.71-0.71c0.39-0.39,1.02-0.39,1.41,0l0.71,0.71 c0.39,0.39,0.39,1.02,0,1.41l-0.71,0.71L18.01,12.87z M17.3,13.58l-5.3,5.3V21h2.12l5.3-5.3L17.3,13.58z"/></svg>
|
||||
|
After Width: | Height: | Size: 386 B |
1
pdb-js/src/assets/img/edit-outline.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24"><path d="M0 0h24v24H0V0z" fill="none"/><path d="M14.06 9.02l.92.92L5.92 19H5v-.92l9.06-9.06M17.66 3c-.25 0-.51.1-.7.29l-1.83 1.83 3.75 3.75 1.83-1.83c.39-.39.39-1.02 0-1.41l-2.34-2.34c-.2-.2-.45-.29-.71-.29zm-3.6 3.19L3 17.25V21h3.75L17.81 9.94l-3.75-3.75z"/></svg>
|
||||
|
After Width: | Height: | Size: 348 B |
@@ -1,491 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
width="64"
|
||||
height="64"
|
||||
shape-rendering="geometricPrecision"
|
||||
text-rendering="geometricPrecision"
|
||||
image-rendering="optimizeQuality"
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
viewBox="0 0 640 640"
|
||||
version="1.1"
|
||||
id="svg4"
|
||||
sodipodi:docname="histogram.svg"
|
||||
inkscape:version="0.92.3 (2405546, 2018-03-11)">
|
||||
<metadata
|
||||
id="metadata10">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title></dc:title>
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<defs
|
||||
id="defs8">
|
||||
<inkscape:path-effect
|
||||
effect="bspline"
|
||||
id="path-effect5303"
|
||||
is_visible="true"
|
||||
weight="33.333333"
|
||||
steps="2"
|
||||
helper_size="0"
|
||||
apply_no_weight="true"
|
||||
apply_with_weight="true"
|
||||
only_selected="false" />
|
||||
<inkscape:path-effect
|
||||
effect="bspline"
|
||||
id="path-effect5299"
|
||||
is_visible="true"
|
||||
weight="33.333333"
|
||||
steps="2"
|
||||
helper_size="0"
|
||||
apply_no_weight="true"
|
||||
apply_with_weight="true"
|
||||
only_selected="false" />
|
||||
<inkscape:path-effect
|
||||
effect="bspline"
|
||||
id="path-effect5295"
|
||||
is_visible="true"
|
||||
weight="33.333333"
|
||||
steps="2"
|
||||
helper_size="0"
|
||||
apply_no_weight="true"
|
||||
apply_with_weight="true"
|
||||
only_selected="false" />
|
||||
<inkscape:path-effect
|
||||
effect="bspline"
|
||||
id="path-effect5291"
|
||||
is_visible="true"
|
||||
weight="33.333333"
|
||||
steps="2"
|
||||
helper_size="0"
|
||||
apply_no_weight="true"
|
||||
apply_with_weight="true"
|
||||
only_selected="false" />
|
||||
<inkscape:path-effect
|
||||
effect="bspline"
|
||||
id="path-effect5241"
|
||||
is_visible="true"
|
||||
weight="33.333333"
|
||||
steps="2"
|
||||
helper_size="0"
|
||||
apply_no_weight="true"
|
||||
apply_with_weight="true"
|
||||
only_selected="false" />
|
||||
<inkscape:path-effect
|
||||
effect="bspline"
|
||||
id="path-effect5237"
|
||||
is_visible="true"
|
||||
weight="33.333333"
|
||||
steps="2"
|
||||
helper_size="0"
|
||||
apply_no_weight="true"
|
||||
apply_with_weight="true"
|
||||
only_selected="false" />
|
||||
<inkscape:path-effect
|
||||
effect="bspline"
|
||||
id="path-effect5140"
|
||||
is_visible="true"
|
||||
weight="33.333333"
|
||||
steps="2"
|
||||
helper_size="0"
|
||||
apply_no_weight="true"
|
||||
apply_with_weight="true"
|
||||
only_selected="false" />
|
||||
<inkscape:path-effect
|
||||
effect="bspline"
|
||||
id="path-effect5136"
|
||||
is_visible="true"
|
||||
weight="33.333333"
|
||||
steps="2"
|
||||
helper_size="0"
|
||||
apply_no_weight="true"
|
||||
apply_with_weight="true"
|
||||
only_selected="false" />
|
||||
<inkscape:path-effect
|
||||
effect="bspline"
|
||||
id="path-effect5132"
|
||||
is_visible="true"
|
||||
weight="33.333333"
|
||||
steps="2"
|
||||
helper_size="0"
|
||||
apply_no_weight="true"
|
||||
apply_with_weight="true"
|
||||
only_selected="false" />
|
||||
<inkscape:path-effect
|
||||
effect="bspline"
|
||||
id="path-effect4918"
|
||||
is_visible="true"
|
||||
weight="33.333333"
|
||||
steps="2"
|
||||
helper_size="0"
|
||||
apply_no_weight="true"
|
||||
apply_with_weight="true"
|
||||
only_selected="false" />
|
||||
<inkscape:path-effect
|
||||
effect="bspline"
|
||||
id="path-effect4895"
|
||||
is_visible="true"
|
||||
weight="33.333333"
|
||||
steps="2"
|
||||
helper_size="0"
|
||||
apply_no_weight="true"
|
||||
apply_with_weight="true"
|
||||
only_selected="false" />
|
||||
<inkscape:path-effect
|
||||
effect="bspline"
|
||||
id="path-effect4872"
|
||||
is_visible="true"
|
||||
weight="33.333333"
|
||||
steps="2"
|
||||
helper_size="0"
|
||||
apply_no_weight="true"
|
||||
apply_with_weight="true"
|
||||
only_selected="false" />
|
||||
<inkscape:path-effect
|
||||
effect="bspline"
|
||||
id="path-effect4868"
|
||||
is_visible="true"
|
||||
weight="33.333333"
|
||||
steps="2"
|
||||
helper_size="0"
|
||||
apply_no_weight="true"
|
||||
apply_with_weight="true"
|
||||
only_selected="false" />
|
||||
<inkscape:path-effect
|
||||
effect="bspline"
|
||||
id="path-effect4864"
|
||||
is_visible="true"
|
||||
weight="33.333333"
|
||||
steps="2"
|
||||
helper_size="0"
|
||||
apply_no_weight="true"
|
||||
apply_with_weight="true"
|
||||
only_selected="false" />
|
||||
<inkscape:path-effect
|
||||
effect="bspline"
|
||||
id="path-effect4731"
|
||||
is_visible="true"
|
||||
weight="33.333333"
|
||||
steps="2"
|
||||
helper_size="0"
|
||||
apply_no_weight="true"
|
||||
apply_with_weight="true"
|
||||
only_selected="false" />
|
||||
<inkscape:path-effect
|
||||
effect="bspline"
|
||||
id="path-effect3922"
|
||||
is_visible="true"
|
||||
weight="33.3333"
|
||||
steps="2"
|
||||
helper_size="0"
|
||||
apply_no_weight="true"
|
||||
apply_with_weight="true"
|
||||
only_selected="false" />
|
||||
<inkscape:path-effect
|
||||
effect="bspline"
|
||||
id="path-effect3918"
|
||||
is_visible="true"
|
||||
weight="33.333333"
|
||||
steps="2"
|
||||
helper_size="0"
|
||||
apply_no_weight="true"
|
||||
apply_with_weight="true"
|
||||
only_selected="false" />
|
||||
<inkscape:path-effect
|
||||
effect="bspline"
|
||||
id="path-effect3914"
|
||||
is_visible="true"
|
||||
weight="33.333333"
|
||||
steps="2"
|
||||
helper_size="0"
|
||||
apply_no_weight="true"
|
||||
apply_with_weight="true"
|
||||
only_selected="false" />
|
||||
<inkscape:path-effect
|
||||
effect="bend_path"
|
||||
id="path-effect205"
|
||||
is_visible="true"
|
||||
bendpath="M 0,0 H 1"
|
||||
prop_scale="1.0426743"
|
||||
scale_y_rel="true"
|
||||
vertical="false"
|
||||
bendpath-nodetypes="cc" />
|
||||
<inkscape:path-effect
|
||||
effect="bspline"
|
||||
id="path-effect4918-3"
|
||||
is_visible="true"
|
||||
weight="33.333333"
|
||||
steps="2"
|
||||
helper_size="0"
|
||||
apply_no_weight="true"
|
||||
apply_with_weight="true"
|
||||
only_selected="false" />
|
||||
<inkscape:path-effect
|
||||
effect="bspline"
|
||||
id="path-effect5136-6"
|
||||
is_visible="true"
|
||||
weight="33.333333"
|
||||
steps="2"
|
||||
helper_size="0"
|
||||
apply_no_weight="true"
|
||||
apply_with_weight="true"
|
||||
only_selected="false" />
|
||||
<inkscape:path-effect
|
||||
effect="bspline"
|
||||
id="path-effect5140-0"
|
||||
is_visible="true"
|
||||
weight="33.333333"
|
||||
steps="2"
|
||||
helper_size="0"
|
||||
apply_no_weight="true"
|
||||
apply_with_weight="true"
|
||||
only_selected="false" />
|
||||
<inkscape:path-effect
|
||||
effect="bspline"
|
||||
id="path-effect5136-61"
|
||||
is_visible="true"
|
||||
weight="33.333333"
|
||||
steps="2"
|
||||
helper_size="0"
|
||||
apply_no_weight="true"
|
||||
apply_with_weight="true"
|
||||
only_selected="false" />
|
||||
<inkscape:path-effect
|
||||
effect="bspline"
|
||||
id="path-effect5140-8"
|
||||
is_visible="true"
|
||||
weight="33.333333"
|
||||
steps="2"
|
||||
helper_size="0"
|
||||
apply_no_weight="true"
|
||||
apply_with_weight="true"
|
||||
only_selected="false" />
|
||||
<inkscape:path-effect
|
||||
effect="bspline"
|
||||
id="path-effect5136-2"
|
||||
is_visible="true"
|
||||
weight="33.333333"
|
||||
steps="2"
|
||||
helper_size="0"
|
||||
apply_no_weight="true"
|
||||
apply_with_weight="true"
|
||||
only_selected="false" />
|
||||
<inkscape:path-effect
|
||||
effect="bspline"
|
||||
id="path-effect5140-02"
|
||||
is_visible="true"
|
||||
weight="33.333333"
|
||||
steps="2"
|
||||
helper_size="0"
|
||||
apply_no_weight="true"
|
||||
apply_with_weight="true"
|
||||
only_selected="false" />
|
||||
<inkscape:path-effect
|
||||
effect="bspline"
|
||||
id="path-effect5136-5"
|
||||
is_visible="true"
|
||||
weight="33.333333"
|
||||
steps="2"
|
||||
helper_size="0"
|
||||
apply_no_weight="true"
|
||||
apply_with_weight="true"
|
||||
only_selected="false" />
|
||||
<inkscape:path-effect
|
||||
effect="bspline"
|
||||
id="path-effect5140-9"
|
||||
is_visible="true"
|
||||
weight="33.333333"
|
||||
steps="2"
|
||||
helper_size="0"
|
||||
apply_no_weight="true"
|
||||
apply_with_weight="true"
|
||||
only_selected="false" />
|
||||
<inkscape:path-effect
|
||||
effect="bspline"
|
||||
id="path-effect5303-9"
|
||||
is_visible="true"
|
||||
weight="33.333333"
|
||||
steps="2"
|
||||
helper_size="0"
|
||||
apply_no_weight="true"
|
||||
apply_with_weight="true"
|
||||
only_selected="false" />
|
||||
<inkscape:path-effect
|
||||
effect="bspline"
|
||||
id="path-effect5299-7"
|
||||
is_visible="true"
|
||||
weight="33.333333"
|
||||
steps="2"
|
||||
helper_size="0"
|
||||
apply_no_weight="true"
|
||||
apply_with_weight="true"
|
||||
only_selected="false" />
|
||||
<inkscape:path-effect
|
||||
effect="bspline"
|
||||
id="path-effect5303-1"
|
||||
is_visible="true"
|
||||
weight="33.333333"
|
||||
steps="2"
|
||||
helper_size="0"
|
||||
apply_no_weight="true"
|
||||
apply_with_weight="true"
|
||||
only_selected="false" />
|
||||
<inkscape:path-effect
|
||||
effect="bspline"
|
||||
id="path-effect5299-2"
|
||||
is_visible="true"
|
||||
weight="33.333333"
|
||||
steps="2"
|
||||
helper_size="0"
|
||||
apply_no_weight="true"
|
||||
apply_with_weight="true"
|
||||
only_selected="false" />
|
||||
<inkscape:path-effect
|
||||
effect="bspline"
|
||||
id="path-effect5303-19"
|
||||
is_visible="true"
|
||||
weight="33.333333"
|
||||
steps="2"
|
||||
helper_size="0"
|
||||
apply_no_weight="true"
|
||||
apply_with_weight="true"
|
||||
only_selected="false" />
|
||||
<inkscape:path-effect
|
||||
effect="bspline"
|
||||
id="path-effect5299-4"
|
||||
is_visible="true"
|
||||
weight="33.333333"
|
||||
steps="2"
|
||||
helper_size="0"
|
||||
apply_no_weight="true"
|
||||
apply_with_weight="true"
|
||||
only_selected="false" />
|
||||
<inkscape:path-effect
|
||||
effect="bspline"
|
||||
id="path-effect5303-4"
|
||||
is_visible="true"
|
||||
weight="33.333333"
|
||||
steps="2"
|
||||
helper_size="0"
|
||||
apply_no_weight="true"
|
||||
apply_with_weight="true"
|
||||
only_selected="false" />
|
||||
<inkscape:path-effect
|
||||
effect="bspline"
|
||||
id="path-effect5299-5"
|
||||
is_visible="true"
|
||||
weight="33.333333"
|
||||
steps="2"
|
||||
helper_size="0"
|
||||
apply_no_weight="true"
|
||||
apply_with_weight="true"
|
||||
only_selected="false" />
|
||||
</defs>
|
||||
<sodipodi:namedview
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1"
|
||||
objecttolerance="10"
|
||||
gridtolerance="10"
|
||||
guidetolerance="10"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:window-width="1533"
|
||||
inkscape:window-height="1145"
|
||||
id="namedview6"
|
||||
showgrid="true"
|
||||
inkscape:snap-grids="true"
|
||||
inkscape:zoom="14.75"
|
||||
inkscape:cx="31.243745"
|
||||
inkscape:cy="34.779661"
|
||||
inkscape:window-x="67"
|
||||
inkscape:window-y="27"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="svg4"
|
||||
inkscape:snap-to-guides="false"
|
||||
inkscape:snap-global="true">
|
||||
<inkscape:grid
|
||||
type="xygrid"
|
||||
id="grid3910" />
|
||||
</sodipodi:namedview>
|
||||
<path
|
||||
style="fill:#9400d3;stroke-width:6.5;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;fill-opacity:0"
|
||||
d="m 43.72308,28.757127 c 0.282929,-0.521415 5.177783,-8.662034 5.207841,-8.661149 0.01984,5.84e-4 1.278626,1.930246 2.797293,4.288136 l 2.761213,4.287072 -0.727315,0.04249 c -0.400023,0.02337 -2.40528,0.06184 -4.456128,0.08548 -2.050847,0.02364 -4.166544,0.06175 -4.701548,0.08468 L 43.6317,28.92553 Z"
|
||||
id="path4801"
|
||||
inkscape:connector-curvature="0"
|
||||
transform="scale(10)" />
|
||||
<path
|
||||
style="fill:#9400d3;fill-opacity:0;stroke-width:6.5;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none"
|
||||
d="m 11.170391,48.874963 c 0,-0.11777 5.07961,-8.576658 5.150332,-8.576658 0.136168,0 5.561825,8.36129 5.479243,8.443871 -0.08038,0.08038 -10.629575,0.21216 -10.629575,0.132787 z"
|
||||
id="path4803"
|
||||
inkscape:connector-curvature="0"
|
||||
transform="scale(10)" />
|
||||
<path
|
||||
style="fill:#9400d3;fill-opacity:0;stroke-width:6.5;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none"
|
||||
d="m 11.170391,48.874963 c 0,-0.11777 5.07961,-8.576658 5.150332,-8.576658 0.136168,0 5.561825,8.36129 5.479243,8.443871 -0.08038,0.08038 -10.629575,0.21216 -10.629575,0.132787 z"
|
||||
id="path4805"
|
||||
inkscape:connector-curvature="0"
|
||||
transform="scale(10)" />
|
||||
<rect
|
||||
style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:30.00000191;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="rect5590"
|
||||
width="70"
|
||||
height="100"
|
||||
x="0"
|
||||
y="440" />
|
||||
<rect
|
||||
style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:30.00000191;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="rect5592"
|
||||
width="70"
|
||||
height="190"
|
||||
x="90"
|
||||
y="350" />
|
||||
<rect
|
||||
style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:29.99999809;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="rect5594"
|
||||
width="70.847458"
|
||||
height="400"
|
||||
x="180"
|
||||
y="139.99998"
|
||||
ry="0" />
|
||||
<rect
|
||||
style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:30.00000191;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="rect5596"
|
||||
width="70"
|
||||
height="500"
|
||||
x="270"
|
||||
y="40" />
|
||||
<rect
|
||||
style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:30.00000191;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="rect5598"
|
||||
width="70"
|
||||
height="400"
|
||||
x="360"
|
||||
y="140" />
|
||||
<rect
|
||||
style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:30.00000191;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="rect5600"
|
||||
width="70"
|
||||
height="250"
|
||||
x="450"
|
||||
y="290" />
|
||||
<rect
|
||||
style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:30.00000191;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="rect5592-4"
|
||||
width="70"
|
||||
height="150"
|
||||
x="540"
|
||||
y="390" />
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="640" height="640" viewBox="0 0 640 640" version="1.1">
|
||||
<rect width="70" height="100" x="0" y="440"/>
|
||||
<rect width="70" height="190" x="90" y="350"/>
|
||||
<rect width="70" height="400" x="180" y="139"/>
|
||||
<rect width="70" height="500" x="270" y="40"/>
|
||||
<rect width="70" height="400" x="360" y="140"/>
|
||||
<rect width="70" height="250" x="450" y="290"/>
|
||||
<rect width="70" height="150" x="540" y="390"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 496 B |
5
pdb-js/src/assets/img/image-aborted.svg
Normal file
@@ -0,0 +1,5 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" shape-rendering="geometricPrecision" text-rendering="geometricPrecision" image-rendering="optimizeQuality" fill-rule="evenodd" clip-rule="evenodd" viewBox="-30 -30 700 700">
|
||||
<path d="M-.012 65.611h640.024v508.778H-.012V65.611zm180.026 132.273c22.996 0 41.635 18.638 41.635 41.634 0 22.997-18.638 41.635-41.635 41.635-22.996 0-41.634-18.638-41.634-41.635 0-22.996 18.638-41.634 41.634-41.634zm175.207 178.679l83.269-143.978 88.466 223.763h-412.86v-27.756l34.702-1.725 34.69-85.005 17.338 60.722h52.052l45.095-116.222 57.248 90.201zM47.528 107.764h544.944v424.47H47.528v-424.47z"/>
|
||||
|
||||
<line x1="10" y1="630" x2="630" y2="10" style="stroke:#000; stroke-width: 60px; stroke-linecap: round;" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 758 B |
1
pdb-js/src/assets/img/launch.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24"><path d="M0 0h24v24H0z" fill="none"/><path d="M19 19H5V5h7V3H5c-1.11 0-2 .9-2 2v14c0 1.1.89 2 2 2h14c1.1 0 2-.9 2-2v-7h-2v7zM14 3v2h3.59l-9.83 9.83 1.41 1.41L19 6.41V10h2V3h-7z"/></svg>
|
||||
|
After Width: | Height: | Size: 268 B |
1
pdb-js/src/assets/img/move.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="512" height="512" viewBox="0 0 512 512"><title>ionicons-v5-g</title><polyline points="176 112 256 32 336 112" style="fill:none;stroke:#000;stroke-linecap:round;stroke-linejoin:round;stroke-width:32px"/><line x1="255.98" y1="32" x2="256" y2="480" style="fill:none;stroke:#000;stroke-linecap:round;stroke-linejoin:round;stroke-width:32px"/><polyline points="176 400 256 480 336 400" style="fill:none;stroke:#000;stroke-linecap:round;stroke-linejoin:round;stroke-width:32px"/><polyline points="400 176 480 256 400 336" style="fill:none;stroke:#000;stroke-linecap:round;stroke-linejoin:round;stroke-width:32px"/><polyline points="112 176 32 256 112 336" style="fill:none;stroke:#000;stroke-linecap:round;stroke-linejoin:round;stroke-width:32px"/><line x1="32" y1="256" x2="480" y2="256" style="fill:none;stroke:#000;stroke-linecap:round;stroke-linejoin:round;stroke-width:32px"/></svg>
|
||||
|
After Width: | Height: | Size: 928 B |
BIN
pdb-js/src/assets/img/plotilio.xcf
Normal file
BIN
pdb-js/src/assets/img/plotilio_64.png
Normal file
|
After Width: | Height: | Size: 3.6 KiB |
1
pdb-js/src/assets/img/resize.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="512" height="512" viewBox="0 0 512 512"><title>ionicons-v5-c</title><polyline points="304 96 416 96 416 208" style="fill:none;stroke:#000;stroke-linecap:round;stroke-linejoin:round;stroke-width:32px"/><line x1="405.77" y1="106.2" x2="111.98" y2="400.02" style="fill:none;stroke:#000;stroke-linecap:round;stroke-linejoin:round;stroke-width:32px"/><polyline points="208 416 96 416 96 304" style="fill:none;stroke:#000;stroke-linecap:round;stroke-linejoin:round;stroke-width:32px"/></svg>
|
||||
|
After Width: | Height: | Size: 532 B |
1
pdb-js/src/assets/img/save-outline.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="512" height="512" viewBox="0 0 512 512"><title>ionicons-v5-p</title><path d="M380.93,57.37A32,32,0,0,0,358.3,48H94.22A46.21,46.21,0,0,0,48,94.22V417.78A46.21,46.21,0,0,0,94.22,464H417.78A46.36,46.36,0,0,0,464,417.78V153.7a32,32,0,0,0-9.37-22.63ZM256,416a64,64,0,1,1,64-64A63.92,63.92,0,0,1,256,416Zm48-224H112a16,16,0,0,1-16-16V112a16,16,0,0,1,16-16H304a16,16,0,0,1,16,16v64A16,16,0,0,1,304,192Z" style="fill:none;stroke:#000;stroke-linecap:round;stroke-linejoin:round;stroke-width:32px"/></svg>
|
||||
|
After Width: | Height: | Size: 542 B |
23
pdb-js/src/assets/img/scatter-chart3.svg
Normal file
@@ -0,0 +1,23 @@
|
||||
<svg
|
||||
width="64"
|
||||
height="64"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<defs>
|
||||
<g id="cross">
|
||||
<rect x="4" y="0" width="4" height="12" style="fill: #0051c2;" />
|
||||
<rect x="0" y="4" width="12" height="4" style="fill: #0051c2;" />
|
||||
</g>
|
||||
</defs>
|
||||
|
||||
<use x="12" y="10" xlink:href="#cross" />
|
||||
<use x="30" y="6" xlink:href="#cross" />
|
||||
<use x="26" y="32" xlink:href="#cross" />
|
||||
<use x="12" y="38" xlink:href="#cross" />
|
||||
<use x="44" y="20" xlink:href="#cross" />
|
||||
|
||||
<path d="M4,0
|
||||
L4,60
|
||||
L64,60"
|
||||
style="stroke:black; stroke-width: 6px; fill:none;"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 641 B |
34
pdb-js/src/assets/img/strip-chart-color.svg
Normal file
@@ -0,0 +1,34 @@
|
||||
<svg
|
||||
width="512"
|
||||
height="512"
|
||||
viewBox="0 0 64 64"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<defs>
|
||||
<g id="cross">
|
||||
<rect x="4" y="0" width="4" height="12" style="fill: #0051c2;" />
|
||||
<rect x="0" y="4" width="12" height="4" style="fill: #0051c2;" />
|
||||
</g>
|
||||
<g id="triangle">
|
||||
<path d="M6,2 L12,12 L 0,12" style="fill: #c18400" />
|
||||
</g>
|
||||
<g id="square">
|
||||
<rect x="4" y="0" width="10" height="10" style="fill: #0051c2;" />
|
||||
</g>
|
||||
|
||||
</defs>
|
||||
|
||||
|
||||
<use x="12" y="7" xlink:href="#triangle" />
|
||||
<use x="20" y="18" xlink:href="#triangle" />
|
||||
<use x="12" y="38" xlink:href="#triangle" />
|
||||
|
||||
<use x="30" y="6" xlink:href="#square" />
|
||||
<use x="32" y="32" xlink:href="#square" />
|
||||
<use x="44" y="20" xlink:href="#square" />
|
||||
|
||||
<path d="M4,0
|
||||
L4,60
|
||||
L64,60"
|
||||
style="stroke:black; stroke-width: 6px; fill:none;"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 907 B |
@@ -7,32 +7,24 @@ var invaders_stepSize = 5;
|
||||
var invaders_margin = 30;
|
||||
var invaders_city_height = 75;
|
||||
var invaders_pause = true;
|
||||
var invaders_parentDivId = 'invaders_area';
|
||||
var invaders_area = 'invaders_area';
|
||||
var invaders_kills = 0;
|
||||
var invaders_points = 0;
|
||||
var invaders_points_kill = 10;
|
||||
var invaders_points_lost = -50;
|
||||
var invaders_game_over = false;
|
||||
var invaders_loop_count = 0;
|
||||
var invaders_parentDivId = "invaders";
|
||||
|
||||
function initInvaders(parentDivId) {
|
||||
invaders_parentDivId = parentDivId;
|
||||
|
||||
// create a copy of the parentDiv
|
||||
// and set it at the exact same position
|
||||
var parent = $('#'+parentDivId);
|
||||
var height = parent.height();
|
||||
var width = parent.width();
|
||||
$('body').append('<div id="'+invaders_parentDivId+'"><div id="invaders_points">Points: 0</div><div id="invaders_kills">Kills: 0</div><div id="invaders_game_over"><div class="invader_notify">Game Over</div></div></div>');
|
||||
$('#'+invaders_parentDivId).offset({ top: parent.offset().top, left: parent.offset().left})
|
||||
$('#'+invaders_parentDivId).height(parent.height());
|
||||
$('#'+invaders_parentDivId).width(parent.width());
|
||||
|
||||
$('#'+invaders_parentDivId)
|
||||
$('body').append('<div id="'+invaders_area+'"><div id="invaders_points">Points: 0</div><div id="invaders_kills">Kills: 0</div><div id="invaders_game_over"><div class="invader_notify">Game Over</div></div></div>');
|
||||
|
||||
$('.invader_notify').click(function() {
|
||||
// restart the game
|
||||
$('#'+invaders_parentDivId).remove();
|
||||
initInvaders(parentDivId);
|
||||
$('#'+invaders_area).remove();
|
||||
initInvaders(invaders_parentDivId);
|
||||
invaders_game_over = false;
|
||||
invaders_kills = 0;
|
||||
invaders_points = 0;
|
||||
@@ -49,7 +41,7 @@ function gameOver() {
|
||||
}
|
||||
|
||||
function pauseInvaders() {
|
||||
$('#'+invaders_parentDivId).hide();
|
||||
$('#'+invaders_area).hide();
|
||||
clearIntervals();
|
||||
}
|
||||
|
||||
@@ -60,15 +52,25 @@ function clearIntervals() {
|
||||
|
||||
function startInvaders() {
|
||||
|
||||
$('#'+invaders_parentDivId).show();
|
||||
// move invaders_area to the same position as the parent div
|
||||
const parent = $('#'+invaders_parentDivId);
|
||||
const height = parent.height();
|
||||
const width = parent.width();
|
||||
const area = document.getElementById(invaders_area);
|
||||
area.style.top=parent.offset().top+"px";
|
||||
area.style.left=parent.offset().left+"px";
|
||||
area.style.height=parent.height()+"px";
|
||||
area.style.width=parent.width()+"px";
|
||||
|
||||
$('#'+invaders_area).show();
|
||||
|
||||
if (!invaders_game_over) {
|
||||
if (invaders_count == 0) {
|
||||
addInvader(invaders_parentDivId);
|
||||
addInvader(invaders_area);
|
||||
}
|
||||
|
||||
clearIntervals();
|
||||
invaders_game_move=window.setInterval("moveRandomly('"+invaders_parentDivId+"')",100);
|
||||
invaders_game_move=window.setInterval("moveRandomly('"+invaders_area+"')",100);
|
||||
invaders_game_new=window.setInterval("addInvader()", 1000);
|
||||
}
|
||||
}
|
||||
@@ -77,7 +79,7 @@ function startInvaders() {
|
||||
function addInvader()
|
||||
{
|
||||
var id = 'invader_' + invaders_count++;
|
||||
var parent = $('#'+invaders_parentDivId);
|
||||
var parent = $('#'+invaders_area);
|
||||
var height = parent.height();
|
||||
var width = parent.width();
|
||||
var top = 10; // start at the top
|
||||
|
||||
@@ -17,7 +17,8 @@
|
||||
<body>
|
||||
<app-root></app-root>
|
||||
<script>
|
||||
$( document ).ready(function() {
|
||||
|
||||
function initDatePicker() {
|
||||
$('input[name="dates"]').daterangepicker({
|
||||
timePicker: true,
|
||||
minDate: "2017-01-01",
|
||||
@@ -39,13 +40,30 @@
|
||||
'Last 30 Days': [moment().subtract(29, 'days').startOf('day'), moment().endOf('day')],
|
||||
'This Month': [moment().startOf('month'), moment().endOf('month').endOf('day')],
|
||||
'Last Month': [moment().subtract(1, 'month').startOf('month'), moment().subtract(1, 'month').endOf('month')],
|
||||
'Last 3 Months': [moment().subtract(3, 'month').startOf('month'),moment().endOf('month').endOf('day')],
|
||||
'Last 3 Months': [moment().subtract(3, 'month').startOf('month'),moment().subtract(1, 'month').endOf('month').endOf('day')],
|
||||
'This Year': [moment().startOf('year'),moment().endOf('month').endOf('day')],
|
||||
'Last Year': [moment().subtract(1, 'year').startOf('year'),moment().subtract(1, 'year').endOf('year')],
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function initSimpleDatePicker() {
|
||||
$('input[name="dates"]').daterangepicker({
|
||||
timePicker: true,
|
||||
minDate: "2017-01-01",
|
||||
maxDate: "2029-12-31",
|
||||
maxYear: parseInt(moment().format('YYYY'),10),
|
||||
timePicker24Hour: true,
|
||||
timePickerSeconds: true,
|
||||
showDropdowns: true, // drop downs for selecting year and month
|
||||
locale: {
|
||||
format: 'YYYY-MM-DD HH:mm:ss',
|
||||
"firstDay": 1 // monday is the first day of the week
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
$( document ).ready(function() {
|
||||
initInvaders('results');
|
||||
document.addEventListener("invadersPause", function(event) {
|
||||
pauseInvaders();
|
||||
|
||||
@@ -8,5 +8,11 @@ if (environment.production) {
|
||||
enableProdMode();
|
||||
}
|
||||
|
||||
(<any>window).randomId = () => {
|
||||
return Math.random().toString(36).replace('0.', '') + Math.random().toString(36).replace('0.', '');
|
||||
};
|
||||
|
||||
(<any>window).submitterId = (<any>window).randomId();
|
||||
|
||||
platformBrowserDynamic().bootstrapModule(AppModule)
|
||||
.catch(err => console.error(err));
|
||||
|
||||
@@ -13,33 +13,35 @@
|
||||
// If you specify typography styles for the components you use elsewhere, you should delete this line.
|
||||
// If you don't need the default component typographies but still want the hierarchy styles,
|
||||
// you can delete this line and instead use:
|
||||
// `@include mat.legacy-typography-hierarchy(mat.define-legacy-typography-config());`
|
||||
@include mat.all-legacy-component-typographies();
|
||||
@include mat.legacy-core();
|
||||
// `@include mat.legacy-typography-hierarchy(mat.define-typography-config());`
|
||||
@include mat.all-component-typographies();
|
||||
@include mat.core();
|
||||
|
||||
// Define the palettes for your theme using the Material Design palettes available in palette.scss
|
||||
// (imported above). For each palette, you can optionally specify a default, lighter, and darker
|
||||
// hue. Available color palettes: https://material.io/design/color/
|
||||
$candy-app-primary: mat.define-palette(mat.$blue-palette);
|
||||
$candy-app-accent: mat.define-palette(mat.$blue-palette, A200, A100, A400);
|
||||
$candy-app-primary: mat.m2-define-palette(mat.$m2-blue-palette);
|
||||
$candy-app-accent: mat.m2-define-palette(mat.$m2-blue-palette, A200, A100, A400);
|
||||
|
||||
// The warn palette is optional (defaults to red).
|
||||
$candy-app-warn: mat.define-palette(mat.$red-palette);
|
||||
$candy-app-warn: mat.m2-define-palette(mat.$m2-red-palette);
|
||||
|
||||
// Create the theme object. A theme consists of configurations for individual
|
||||
// theming systems such as "color" or "typography".
|
||||
$candy-app-theme: mat.define-light-theme((
|
||||
$candy-app-theme: mat.m2-define-light-theme((
|
||||
color: (
|
||||
primary: $candy-app-primary,
|
||||
accent: $candy-app-accent,
|
||||
warn: $candy-app-warn,
|
||||
)
|
||||
),
|
||||
//typography: mat.define-typography-config(),
|
||||
density: -1,
|
||||
));
|
||||
|
||||
// Include theme styles for core and each component used in your app.
|
||||
// Alternatively, you can import and @include the theme mixins for each component
|
||||
// that you are using.
|
||||
@include mat.all-legacy-component-themes($candy-app-theme);
|
||||
@include mat.all-component-themes($candy-app-theme);
|
||||
|
||||
|
||||
|
||||
@@ -63,6 +65,14 @@ grey
|
||||
*/
|
||||
$background-color: #CBD7F4;
|
||||
|
||||
.mat-mdc-option span.mdc-list-item__primary-text,
|
||||
.mdc-list-item__primary-text {
|
||||
font-size: 1rem;
|
||||
}
|
||||
mat-form-field .mat-mdc-option span.mdc-list-item__primary-text{
|
||||
--mdc-typography-subtitle1-font-size: 14px;
|
||||
}
|
||||
|
||||
*, body {
|
||||
font-family: Arial;
|
||||
font-size: 14px;
|
||||
@@ -81,26 +91,41 @@ h2 {
|
||||
margin-block-end: 0.83rem;
|
||||
}
|
||||
|
||||
.hidden {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.icon-inline {
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
vertical-align: text-bottom;
|
||||
}
|
||||
.mat-button .mat-button-wrapper .icon-inline {
|
||||
vertical-align: text-top;
|
||||
}
|
||||
|
||||
button[disabled] .icon-inline {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
button.save-button {
|
||||
background-color: #ff9900;
|
||||
}
|
||||
button.save-button:disabled {
|
||||
background-color: inherit;
|
||||
}
|
||||
|
||||
.icon-tiny {
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
}
|
||||
|
||||
.icon-small {
|
||||
width: 1.5em;
|
||||
height: 1.5em;
|
||||
margin: 0.2em;
|
||||
}
|
||||
.icon-small:hover {
|
||||
background-color: #eee;
|
||||
|
||||
.icon-middle {
|
||||
width: 2.5em;
|
||||
height: 2.5em;
|
||||
margin: 0.2em;
|
||||
}
|
||||
|
||||
.icon-large {
|
||||
@@ -108,23 +133,21 @@ button[disabled] .icon-inline {
|
||||
height: 8em;
|
||||
margin: 1em;
|
||||
}
|
||||
.icon-huge {
|
||||
width: 12em;
|
||||
height: 12em;
|
||||
margin: 1em;
|
||||
}
|
||||
|
||||
.icon-select {
|
||||
width: 1.5em;
|
||||
height: 1.5em;
|
||||
vertical-align: text-bottom;
|
||||
}
|
||||
.mat-option-disabled .icon-select {
|
||||
opacity: 0.25;
|
||||
}
|
||||
|
||||
mat-option.mat-option {
|
||||
height: 2em;
|
||||
line-height: 2em;
|
||||
}
|
||||
|
||||
mat-option.mat-option.mat-active {
|
||||
background-color: #ccc;
|
||||
a ,a:visited {
|
||||
color: blue;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
a.external-link:after {
|
||||
@@ -150,41 +173,109 @@ a.external-link:after {
|
||||
right: 0;
|
||||
}
|
||||
|
||||
body .mat-select-panel, body .mat-autocomplete-panel {
|
||||
max-height: 300px;
|
||||
}
|
||||
|
||||
mat-form-field {
|
||||
mat-form-field.pdb-form-full-width {
|
||||
width: 100%;
|
||||
margin-right: 1ex;
|
||||
}
|
||||
mat-form-field:last-child {
|
||||
margin-right: 0ex;
|
||||
mat-form-field.pdb-form-number-small {
|
||||
width: 4.5em;
|
||||
}
|
||||
mat-form-field.pdb-form-number {
|
||||
width: 3.5em;
|
||||
width: 5.5em;
|
||||
}
|
||||
mat-form-field.pdb-form-number-long {
|
||||
width: 7em;
|
||||
}
|
||||
.pdb-form-icon-small {
|
||||
display: inline-block;
|
||||
width: 2em;
|
||||
vertical-align: middle;
|
||||
}
|
||||
mat-form-field.pdb-form-mid {
|
||||
width: 7.5em;
|
||||
width: 9.5em;
|
||||
}
|
||||
mat-form-field.pdb-form-wide {
|
||||
width: 10em;
|
||||
}
|
||||
|
||||
|
||||
|
||||
pdb-visualization-page .mat-mdc-form-field-subscript-wrapper,
|
||||
app-add-text-dialog .mat-mdc-form-field-subscript-wrapper {
|
||||
display: none;/**/
|
||||
}
|
||||
|
||||
.errorPanel {
|
||||
padding: 1ex;
|
||||
background-color: map-get(mat.$red-palette, 100);
|
||||
background-color: map-get(mat.$m2-red-palette, 100);
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.super-badge {
|
||||
position: absolute;
|
||||
font-size: 0.8em;
|
||||
color: #0051c2;
|
||||
top: 0.2em;
|
||||
line-height: 1em;
|
||||
}
|
||||
|
||||
a.external-link:after {
|
||||
content: "";
|
||||
display: inline-block;
|
||||
background: url("assets/img/external-link.svg") no-repeat;
|
||||
background-size: 1em;
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
margin-left: 0.3em;
|
||||
vertical-align: text-top;
|
||||
}
|
||||
|
||||
|
||||
/* styles for markdown*/
|
||||
markdown blockquote {
|
||||
border-left: 3px grey solid;
|
||||
display: block;
|
||||
margin: 1em 0 1em 2em;
|
||||
padding: 0.5em 0 0.5em 0.5em;
|
||||
}
|
||||
|
||||
markdown table {
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
markdown table th, markdown table td {
|
||||
border: solid 1px black;
|
||||
padding: 0.4em;
|
||||
}
|
||||
markdown thead {
|
||||
border-bottom: 2px black solid;
|
||||
}
|
||||
markdown tfoot {
|
||||
border-top: 2px black solid;
|
||||
}
|
||||
|
||||
markdown pre {
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
|
||||
.plot-details-plotType {
|
||||
background-image: url(/assets/img/pointTypes.png);
|
||||
width: 9px;
|
||||
height: 7px;
|
||||
transform: scale(1.5);
|
||||
}
|
||||
|
||||
.plot-details-plotType_0 {background-position-x: 0px;}
|
||||
.plot-details-plotType_1 {background-position-x: -10px;}
|
||||
.plot-details-plotType_2 {background-position-x: -20px;}
|
||||
.plot-details-plotType_3 {background-position-x: -30px;}
|
||||
.plot-details-plotType_4 {background-position-x: -40px;}
|
||||
.plot-details-plotType_5 {background-position-x: -50px;}
|
||||
.plot-details-plotType_6 {background-position-x: -60px;}
|
||||
.plot-details-plotType_7 {background-position-x: -70px;}
|
||||
.plot-details-plotType_8 {background-position-x: -80px;}
|
||||
.plot-details-plotType_9 {background-position-x: -90px;}
|
||||
.plot-details-plotType_10 {background-position-x:-100px;}
|
||||
.plot-details-plotType_11 {background-position-x:-110px;}
|
||||
.plot-details-plotType_12 {background-position-x:-120px;}
|
||||
|
||||
.plot-details-plotType_0051c2 {background-position-y: 0px;}
|
||||
.plot-details-plotType_bf8300 {background-position-y: -8px;}
|
||||
.plot-details-plotType_9400d3 {background-position-y: -16px;}
|
||||
.plot-details-plotType_00c254 {background-position-y: -24px;}
|
||||
.plot-details-plotType_e6e600 {background-position-y: -32px;}
|
||||
.plot-details-plotType_e51e10 {background-position-y: -40px;}
|
||||
.plot-details-plotType_57a1c2 {background-position-y: -48px;}
|
||||
.plot-details-plotType_bd36c2 {background-position-y: -56px;}
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
"baseUrl": "./",
|
||||
"outDir": "./dist/out-tsc",
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"esModuleInterop": true,
|
||||
"strict": true,
|
||||
"noImplicitOverride": true,
|
||||
"noPropertyAccessFromIndexSignature": true,
|
||||
@@ -12,7 +13,6 @@
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"sourceMap": true,
|
||||
"declaration": false,
|
||||
"downlevelIteration": true,
|
||||
"experimentalDecorators": true,
|
||||
"moduleResolution": "node",
|
||||
"importHelpers": true,
|
||||
|
||||
@@ -93,7 +93,7 @@ public class BarChartAggregatorForIntervals implements CustomAggregator, Indexed
|
||||
}
|
||||
|
||||
private boolean showLabel(final int index, final int numberOfBuckets) {
|
||||
final int width = settings.getWidth();
|
||||
final int width = settings.getMaxWidth();
|
||||
final int widthInPx = width - GnuplotSettings.GNUPLOT_LEFT_RIGHT_MARGIN;
|
||||
|
||||
final long maxLabels = Math.max(1, widthInPx / (GnuplotSettings.TICKS_FONT_SIZE * 8));
|
||||
|
||||
@@ -0,0 +1,143 @@
|
||||
package org.lucares.pdb.plot.api;
|
||||
|
||||
import java.time.DayOfWeek;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.OffsetDateTime;
|
||||
import java.time.ZoneOffset;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.time.temporal.TemporalAdjusters;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import org.lucares.pdb.api.DateTimeRange;
|
||||
import org.lucares.utils.Preconditions;
|
||||
|
||||
public class DateTimeRangeParser {
|
||||
|
||||
private static final DateTimeFormatter DATE_FORMAT = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
|
||||
|
||||
public static DateTimeRange parse(final OffsetDateTime offsetTime, final String datePeriod) {
|
||||
|
||||
final String[] startEnd = datePeriod.split(Pattern.quote("/"));
|
||||
final String start = startEnd[0];
|
||||
final String end = startEnd[1];
|
||||
|
||||
final OffsetDateTime startTime = parseInternal(offsetTime, start);
|
||||
final OffsetDateTime endTime = parseInternal(offsetTime, end);
|
||||
|
||||
return new DateTimeRange(startTime, endTime);
|
||||
}
|
||||
|
||||
private static OffsetDateTime parseInternal(final OffsetDateTime offsetTime, final String timeDefinition) {
|
||||
|
||||
final Pattern regex = Pattern.compile("(?<beginEnd>[BE])(?<amountUnit>(\\-?[0-9]*[mHDWMY])+)",
|
||||
Pattern.MULTILINE);
|
||||
|
||||
final Matcher matcher = regex.matcher(timeDefinition);
|
||||
|
||||
if (matcher.matches()) {
|
||||
|
||||
final String beginEnd = matcher.group("beginEnd");
|
||||
final boolean begin = "B".equals(beginEnd);
|
||||
|
||||
OffsetDateTime result = offsetTime;
|
||||
|
||||
final String amountUnitString = matcher.group("amountUnit");
|
||||
final Pattern regexAmountUnit = Pattern.compile("(?<amount>\\-?[0-9]*)(?<unit>[mHDWMY])");
|
||||
final Matcher m = regexAmountUnit.matcher(amountUnitString);
|
||||
while (m.find()) {
|
||||
final String amountString = m.group("amount");
|
||||
final String unitString = m.group("unit");
|
||||
final int amount = amountString.equals("") ? 0 : Integer.parseInt(amountString);
|
||||
|
||||
switch (unitString) {
|
||||
case "m": {
|
||||
final ChronoUnit unit = ChronoUnit.MINUTES;
|
||||
if (begin) {
|
||||
result = result.plus(amount, unit).truncatedTo(unit);
|
||||
} else {
|
||||
result = result.plus(amount + 1, unit).truncatedTo(unit).minusSeconds(1);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "H": {
|
||||
final ChronoUnit unit = ChronoUnit.HOURS;
|
||||
if (begin) {
|
||||
result = result.plus(amount, unit).truncatedTo(unit);
|
||||
} else {
|
||||
result = result.plus(amount + 1, unit).truncatedTo(unit).minusSeconds(1);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "D": {
|
||||
final ChronoUnit unit = ChronoUnit.DAYS;
|
||||
if (begin) {
|
||||
result = result.plus(amount, unit).truncatedTo(unit);
|
||||
} else {
|
||||
result = result.plus(amount + 1, unit).truncatedTo(unit).minusSeconds(1);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "W": {
|
||||
final DayOfWeek firstDayOfWeek = DayOfWeek.MONDAY;
|
||||
final DayOfWeek lastDayOfWeek = DayOfWeek
|
||||
.of(((firstDayOfWeek.getValue() - 1 + 6) % DayOfWeek.values().length) + 1); // weird
|
||||
// computation,
|
||||
// because
|
||||
// DayOfWeek
|
||||
// goes from 1
|
||||
// to 7
|
||||
final ChronoUnit unit = ChronoUnit.WEEKS;
|
||||
if (begin) {
|
||||
result = result.plus(amount, unit).with(TemporalAdjusters.previousOrSame(firstDayOfWeek))
|
||||
.truncatedTo(ChronoUnit.DAYS);
|
||||
} else {
|
||||
result = result.plus(amount, unit).with(TemporalAdjusters.nextOrSame(lastDayOfWeek))
|
||||
.plus(1, ChronoUnit.DAYS).truncatedTo(ChronoUnit.DAYS).minusSeconds(1);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "M": {
|
||||
final ChronoUnit unit = ChronoUnit.MONTHS;
|
||||
if (begin) {
|
||||
result = result.plus(amount, unit).truncatedTo(ChronoUnit.DAYS).withDayOfMonth(1);
|
||||
} else {
|
||||
result = result.plus(amount, unit).truncatedTo(ChronoUnit.DAYS).withDayOfMonth(1)
|
||||
.plus(1, ChronoUnit.MONTHS).minusSeconds(1);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "Y": {
|
||||
final ChronoUnit unit = ChronoUnit.YEARS;
|
||||
if (begin) {
|
||||
result = result.plus(amount, unit).truncatedTo(ChronoUnit.DAYS).withDayOfYear(1);
|
||||
} else {
|
||||
result = result.plus(amount, unit).truncatedTo(ChronoUnit.DAYS).withDayOfYear(1)
|
||||
.plus(1, ChronoUnit.YEARS).minusSeconds(1);
|
||||
}
|
||||
break;
|
||||
}
|
||||
default:
|
||||
throw new IllegalArgumentException("Unexpected value: " + unitString);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
throw new IllegalArgumentException("invalid input: " + timeDefinition);
|
||||
}
|
||||
|
||||
public static DateTimeRange parseAbsolute(final String dateRangeAsString) {
|
||||
final String[] startEnd = dateRangeAsString.split(Pattern.quote(" - "));
|
||||
Preconditions.checkEqual(startEnd.length, 2, "invalid date range: ''{0}''", dateRangeAsString);
|
||||
|
||||
final String startString = startEnd[0];
|
||||
final String endString = startEnd[1];
|
||||
|
||||
final OffsetDateTime start = LocalDateTime.parse(startString, DATE_FORMAT).atOffset(ZoneOffset.UTC);
|
||||
final OffsetDateTime end = LocalDateTime.parse(endString, DATE_FORMAT).atOffset(ZoneOffset.UTC);
|
||||
|
||||
return new DateTimeRange(start, end);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package org.lucares.pdb.plot.api;
|
||||
|
||||
public class DateValue {
|
||||
public enum DateType {
|
||||
QUICK, RELATIVE, ABSOLUTE
|
||||
}
|
||||
|
||||
private DateType type;
|
||||
|
||||
private String display;
|
||||
|
||||
private String value;
|
||||
|
||||
public DateType getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
public void setType(final DateType type) {
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
public String getDisplay() {
|
||||
return display;
|
||||
}
|
||||
|
||||
public void setDisplay(final String display) {
|
||||
this.display = display;
|
||||
}
|
||||
|
||||
public String getValue() {
|
||||
return value;
|
||||
}
|
||||
|
||||
public void setValue(final String value) {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -63,7 +63,7 @@ public class HistogramAggregator implements CustomAggregator {
|
||||
final char separator = ',';
|
||||
final char newline = '\n';
|
||||
|
||||
final int numBins = plotSettings.getWidth() / 8;
|
||||
final int numBins = plotSettings.getMaxWidth() / 8;
|
||||
final int binWidth = Math.max((int) (max) / numBins, 1);
|
||||
|
||||
final ToBins toBins = new ToBins(numBins, binWidth);
|
||||
|
||||
@@ -1,52 +1,49 @@
|
||||
package org.lucares.pdb.plot.api;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.OffsetDateTime;
|
||||
import java.time.ZoneOffset;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.TreeMap;
|
||||
|
||||
import org.lucares.pdb.api.DateTimeRange;
|
||||
import org.lucares.recommind.logs.GnuplotAxis;
|
||||
import org.lucares.utils.Preconditions;
|
||||
import org.lucares.recommind.logs.GnuplotSettings;
|
||||
|
||||
public class PlotSettings {
|
||||
|
||||
private static final DateTimeFormatter DATE_FORMAT = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
|
||||
public static final DateTimeFormatter DATE_FORMAT = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
|
||||
|
||||
private String query;
|
||||
|
||||
private int height;
|
||||
|
||||
private int width;
|
||||
|
||||
private int thumbnailMaxWidth = 0;
|
||||
|
||||
private int thumbnailMaxHeight = 0;
|
||||
|
||||
private List<String> groupBy;
|
||||
|
||||
private Limit limitBy;
|
||||
|
||||
private int limit;
|
||||
|
||||
private String dateRangeAsString;
|
||||
private DateValue dateValue;
|
||||
|
||||
private YAxisDefinition y1;
|
||||
private YAxisDefinition y2;
|
||||
|
||||
private AggregateHandlerCollection aggregates;
|
||||
|
||||
private boolean keyOutside;
|
||||
|
||||
private boolean generateThumbnail;
|
||||
|
||||
private Interval interval;
|
||||
|
||||
private boolean renderBarChartTickLabels;
|
||||
|
||||
private Map<String, RenderOptions> renders = new TreeMap<>();
|
||||
|
||||
public void setRenders(final Map<String, RenderOptions> renders) {
|
||||
this.renders = renders;
|
||||
}
|
||||
|
||||
public Map<String, RenderOptions> getRenders() {
|
||||
return renders;
|
||||
}
|
||||
|
||||
public String getQuery() {
|
||||
return query;
|
||||
}
|
||||
@@ -55,38 +52,6 @@ public class PlotSettings {
|
||||
this.query = query;
|
||||
}
|
||||
|
||||
public int getHeight() {
|
||||
return height;
|
||||
}
|
||||
|
||||
public void setHeight(final int height) {
|
||||
this.height = height;
|
||||
}
|
||||
|
||||
public int getWidth() {
|
||||
return width;
|
||||
}
|
||||
|
||||
public void setWidth(final int width) {
|
||||
this.width = width;
|
||||
}
|
||||
|
||||
public int getThumbnailMaxWidth() {
|
||||
return thumbnailMaxWidth;
|
||||
}
|
||||
|
||||
public void setThumbnailMaxWidth(final int thumbnailMaxWidth) {
|
||||
this.thumbnailMaxWidth = thumbnailMaxWidth;
|
||||
}
|
||||
|
||||
public int getThumbnailMaxHeight() {
|
||||
return thumbnailMaxHeight;
|
||||
}
|
||||
|
||||
public void setThumbnailMaxHeight(final int thumbnailMaxHeight) {
|
||||
this.thumbnailMaxHeight = thumbnailMaxHeight;
|
||||
}
|
||||
|
||||
public List<String> getGroupBy() {
|
||||
return groupBy;
|
||||
}
|
||||
@@ -111,33 +76,33 @@ public class PlotSettings {
|
||||
this.limit = limit;
|
||||
}
|
||||
|
||||
public String getDateRange() {
|
||||
return dateRangeAsString;
|
||||
public DateValue getDateRange() {
|
||||
return dateValue;
|
||||
}
|
||||
|
||||
public void setDateRange(final String dateRangeAsString) {
|
||||
this.dateRangeAsString = dateRangeAsString;
|
||||
public void setDateRange(final DateValue dateValue) {
|
||||
this.dateValue = dateValue;
|
||||
}
|
||||
|
||||
public DateTimeRange dateRange() {
|
||||
|
||||
final String[] startEnd = dateRangeAsString.split(Pattern.quote(" - "));
|
||||
Preconditions.checkEqual(startEnd.length, 2, "invalid date range: ''{0}''", dateRangeAsString);
|
||||
|
||||
final OffsetDateTime startDate = LocalDateTime.parse(startEnd[0], DATE_FORMAT).atOffset(ZoneOffset.UTC);
|
||||
final OffsetDateTime endDate = LocalDateTime.parse(startEnd[1], DATE_FORMAT).atOffset(ZoneOffset.UTC);
|
||||
|
||||
return new DateTimeRange(startDate, endDate);
|
||||
switch (this.dateValue.getType()) {
|
||||
case RELATIVE:
|
||||
case QUICK:
|
||||
final DateTimeRange dateTimeRange = DateTimeRangeParser.parse(OffsetDateTime.now(), dateValue.getValue());
|
||||
return dateTimeRange;
|
||||
case ABSOLUTE:
|
||||
return DateTimeRangeParser.parseAbsolute(dateValue.getValue());
|
||||
}
|
||||
throw new UnsupportedOperationException();
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "PlotSettings [query=" + query + ", height=" + height + ", width=" + width + ", thumbnailMaxWidth="
|
||||
+ thumbnailMaxWidth + ", thumbnailMaxHeight=" + thumbnailMaxHeight + ", groupBy=" + groupBy
|
||||
+ ", limitBy=" + limitBy + ", limit=" + limit + ", dateRangeAsString=" + dateRangeAsString + ", y1="
|
||||
+ y1 + " y2=" + y2 + ", aggregates=" + aggregates + ", keyOutside=" + keyOutside
|
||||
+ ", generateThumbnail=" + generateThumbnail + "]";
|
||||
return "PlotSettings [query=" + query + ", groupBy=" + groupBy + ", limitBy=" + limitBy + ", limit=" + limit
|
||||
+ ", dateRangeAsString=" + dateValue + ", y1=" + y1 + " y2=" + y2 + ", aggregates=" + aggregates
|
||||
+ ", renders=" + renders + "]";
|
||||
}
|
||||
|
||||
public void setAggregates(final AggregateHandlerCollection aggregates) {
|
||||
@@ -148,22 +113,6 @@ public class PlotSettings {
|
||||
return aggregates;
|
||||
}
|
||||
|
||||
public void setKeyOutside(final boolean keyOutside) {
|
||||
this.keyOutside = keyOutside;
|
||||
}
|
||||
|
||||
public boolean isKeyOutside() {
|
||||
return keyOutside;
|
||||
}
|
||||
|
||||
public void setGenerateThumbnail(final boolean generateThumbnail) {
|
||||
this.generateThumbnail = generateThumbnail;
|
||||
}
|
||||
|
||||
public boolean isGenerateThumbnail() {
|
||||
return generateThumbnail;
|
||||
}
|
||||
|
||||
public YAxisDefinition getY1() {
|
||||
return y1;
|
||||
}
|
||||
@@ -207,4 +156,32 @@ public class PlotSettings {
|
||||
this.renderBarChartTickLabels = renderBarChartTickLabels;
|
||||
}
|
||||
|
||||
public int getMaxWidth() {
|
||||
int maxWidth = 1;
|
||||
|
||||
for (final RenderOptions renderOptions : renders.values()) {
|
||||
int width = renderOptions.getWidth();
|
||||
if (renderOptions.isRenderLabels()) {
|
||||
width -= GnuplotSettings.GNUPLOT_LEFT_RIGHT_MARGIN;
|
||||
}
|
||||
maxWidth = Math.max(maxWidth, width);
|
||||
}
|
||||
|
||||
return maxWidth;
|
||||
}
|
||||
|
||||
public int getMaxHeight() {
|
||||
int maxHeight = 1;
|
||||
|
||||
for (final RenderOptions renderOptions : renders.values()) {
|
||||
int height = renderOptions.getHeight();
|
||||
if (renderOptions.isRenderLabels()) {
|
||||
height -= GnuplotSettings.GNUPLOT_TOP_BOTTOM_MARGIN;
|
||||
}
|
||||
maxHeight = Math.max(maxHeight, height);
|
||||
}
|
||||
|
||||
return maxHeight;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -7,6 +7,8 @@ public enum RangeUnit {
|
||||
|
||||
NO_UNIT(false, Type.Number, "Value"),
|
||||
|
||||
AUTOMATIC_BYTES(true, Type.Number, "Value"),
|
||||
|
||||
BYTES(false, Type.Number, "Value"),
|
||||
|
||||
AUTOMATIC_TIME(true, Type.Duration, "Duration"),
|
||||
@@ -43,6 +45,10 @@ public enum RangeUnit {
|
||||
return type == Type.Number || type == Type.HistogramCount;
|
||||
}
|
||||
|
||||
public boolean isBytes() {
|
||||
return this == BYTES || this == AUTOMATIC_BYTES;
|
||||
}
|
||||
|
||||
public String getLabel() {
|
||||
return axisLabel;
|
||||
}
|
||||
@@ -51,11 +57,9 @@ public enum RangeUnit {
|
||||
return type;
|
||||
}
|
||||
|
||||
public int valueForUnit(final int value) {
|
||||
public long valueForUnit(final long value) {
|
||||
|
||||
switch (this) {
|
||||
case AUTOMATIC_NUMBER:
|
||||
return Integer.MAX_VALUE;
|
||||
case NO_UNIT:
|
||||
case BYTES:
|
||||
return value;
|
||||
@@ -69,10 +73,12 @@ public enum RangeUnit {
|
||||
return value * 60 * 60 * 1000;
|
||||
case DAYS:
|
||||
return value * 24 * 60 * 60 * 1000;
|
||||
case AUTOMATIC_NUMBER:
|
||||
case AUTOMATIC_TIME:
|
||||
return Integer.MAX_VALUE;
|
||||
case AUTOMATIC_BYTES:
|
||||
return Long.MAX_VALUE;
|
||||
}
|
||||
return Integer.MAX_VALUE;
|
||||
return Long.MAX_VALUE;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
package org.lucares.pdb.plot.api;
|
||||
|
||||
public class RenderOptions {
|
||||
private int height;
|
||||
private int width;
|
||||
private boolean showKey;
|
||||
private boolean renderLabels;
|
||||
|
||||
public int getHeight() {
|
||||
return height;
|
||||
}
|
||||
|
||||
public void setHeight(final int height) {
|
||||
this.height = height;
|
||||
}
|
||||
|
||||
public int getWidth() {
|
||||
return width;
|
||||
}
|
||||
|
||||
public void setWidth(final int width) {
|
||||
this.width = width;
|
||||
}
|
||||
|
||||
public boolean isShowKey() {
|
||||
return showKey;
|
||||
}
|
||||
|
||||
public void setShowKey(final boolean showKey) {
|
||||
this.showKey = showKey;
|
||||
}
|
||||
|
||||
public boolean isRenderLabels() {
|
||||
return renderLabels;
|
||||
}
|
||||
|
||||
public void setRenderLabels(final boolean renderLabels) {
|
||||
this.renderLabels = renderLabels;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -14,7 +14,6 @@ import java.util.concurrent.TimeUnit;
|
||||
import org.lucares.collections.Sparse2DLongArray;
|
||||
import org.lucares.pdb.api.RuntimeIOException;
|
||||
import org.lucares.recommind.logs.GnuplotAxis;
|
||||
import org.lucares.recommind.logs.GnuplotSettings;
|
||||
import org.lucares.recommind.logs.LambdaFriendlyWriter;
|
||||
import org.lucares.recommind.logs.LongUtils;
|
||||
|
||||
@@ -38,15 +37,15 @@ public class ScatterAggregator implements CustomAggregator {
|
||||
|
||||
this.tmpDir = tmpDir;
|
||||
useMillis = (toEpochMilli - fromEpochMilli) < TimeUnit.MINUTES.toMillis(5);
|
||||
plotAreaWidthInPx = plotSettings.getWidth() - GnuplotSettings.GNUPLOT_LEFT_RIGHT_MARGIN;
|
||||
plotAreaHeightInPx = plotSettings.getHeight() - GnuplotSettings.GNUPLOT_TOP_BOTTOM_MARGIN;
|
||||
plotAreaWidthInPx = plotSettings.getMaxWidth();
|
||||
plotAreaHeightInPx = plotSettings.getMaxHeight();
|
||||
epochMillisPerPixel = Math.max(1, (toEpochMilli - fromEpochMilli) / plotAreaWidthInPx);
|
||||
|
||||
final YAxisDefinition yAxisDefinition = plotSettings.getyAxisDefinition(yAxis);
|
||||
|
||||
final boolean automaticRange = yAxisDefinition.getRangeUnit().isAutomatic();
|
||||
minValue = automaticRange ? 0 : yAxisDefinition.getRangeMinForUnit();
|
||||
maxValue = automaticRange ? Long.MAX_VALUE : yAxisDefinition.getRangeMaxForUnit();
|
||||
minValue = automaticRange ? 0 : yAxisDefinition.rangeMinForUnit();
|
||||
maxValue = automaticRange ? Long.MAX_VALUE : yAxisDefinition.rangeMaxForUnit();
|
||||
valuesPerPixel = yAxisDefinition.getAxisScale() == AxisScale.LINEAR && !automaticRange
|
||||
? Math.max(1, (maxValue - minValue) / plotAreaHeightInPx)
|
||||
: 1;
|
||||
|
||||
@@ -3,8 +3,8 @@ package org.lucares.pdb.plot.api;
|
||||
public class YAxisDefinition {
|
||||
private AxisScale axisScale = AxisScale.LINEAR;
|
||||
|
||||
private int rangeMin = 0;
|
||||
private int rangeMax = 300;
|
||||
private long rangeMin = 0;
|
||||
private long rangeMax = 300;
|
||||
private RangeUnit rangeUnit = RangeUnit.AUTOMATIC_TIME;
|
||||
|
||||
public AxisScale getAxisScale() {
|
||||
@@ -15,15 +15,15 @@ public class YAxisDefinition {
|
||||
this.axisScale = axisScale;
|
||||
}
|
||||
|
||||
public long getRangeMinForUnit() {
|
||||
public long rangeMinForUnit() {
|
||||
return rangeUnit.valueForUnit(rangeMin);
|
||||
}
|
||||
|
||||
public long getRangeMaxForUnit() {
|
||||
public long rangeMaxForUnit() {
|
||||
return rangeUnit.valueForUnit(rangeMax);
|
||||
}
|
||||
|
||||
public int getRangeMin() {
|
||||
public long getRangeMin() {
|
||||
return rangeMin;
|
||||
}
|
||||
|
||||
@@ -31,15 +31,15 @@ public class YAxisDefinition {
|
||||
return !rangeUnit.isAutomatic() && rangeMin >= 0 && rangeMax >= 0 && rangeMin < rangeMax;
|
||||
}
|
||||
|
||||
public void setRangeMin(final int rangeMin) {
|
||||
public void setRangeMin(final long rangeMin) {
|
||||
this.rangeMin = rangeMin;
|
||||
}
|
||||
|
||||
public int getRangeMax() {
|
||||
public long getRangeMax() {
|
||||
return rangeMax;
|
||||
}
|
||||
|
||||
public void setRangeMax(final int rangeMax) {
|
||||
public void setRangeMax(final long rangeMax) {
|
||||
this.rangeMax = rangeMax;
|
||||
}
|
||||
|
||||
@@ -51,8 +51,4 @@ public class YAxisDefinition {
|
||||
this.rangeUnit = rangeUnit;
|
||||
}
|
||||
|
||||
public boolean isLogscale() {
|
||||
return axisScale == AxisScale.LOG10;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -52,15 +52,15 @@ public class AxisTime {
|
||||
|
||||
final int graphOffset = yAxisDefinition.getAxisScale() == AxisScale.LINEAR ? 0 : 1;
|
||||
if (yAxisDefinition.hasRange()) {
|
||||
final long min = Math.max(yAxisDefinition.getRangeMinForUnit(), graphOffset);
|
||||
final long max = yAxisDefinition.getRangeMaxForUnit();
|
||||
final long min = Math.max(yAxisDefinition.rangeMinForUnit(), graphOffset);
|
||||
final long max = yAxisDefinition.rangeMaxForUnit();
|
||||
result.setFrom(String.valueOf(min));
|
||||
result.setTo(String.valueOf(max));
|
||||
} else {
|
||||
result.setFrom(String.valueOf(graphOffset));
|
||||
}
|
||||
|
||||
result.setLogscale(yAxisDefinition.isLogscale());
|
||||
result.setLogscale(yAxisDefinition.getAxisScale() == AxisScale.LOG10);
|
||||
|
||||
result.setTics(YAxisTicks.computeYTicks(settings, yAxis, dataSeries));
|
||||
result.setShowGrid(yAxis == GnuplotAxis.Y1);
|
||||
|
||||
@@ -50,10 +50,9 @@ public class GnuplotFileGenerator implements Appender {
|
||||
|
||||
appendln(result, "set nokey");
|
||||
} else {
|
||||
if (settings.isKeyOutside()) {
|
||||
appendfln(result, "set key outside");
|
||||
} else {
|
||||
|
||||
if (!settings.isShowKey()) {
|
||||
appendfln(result, "set nokey");
|
||||
}
|
||||
// make sure left and right margins are always the same
|
||||
// this is need to be able to zoom in by selecting a region
|
||||
// (horizontal: 1 unit = 10px; vertical: 1 unit = 19px)
|
||||
@@ -62,7 +61,6 @@ public class GnuplotFileGenerator implements Appender {
|
||||
appendln(result, "set tmargin 3"); // margin 3 -> 57px - marker (1)
|
||||
appendln(result, "set bmargin 4"); // margin 4 -> 76
|
||||
}
|
||||
}
|
||||
|
||||
// appendfln(result, "set xrange [-1:1]");
|
||||
appendfln(result, "set boxwidth 0.5");
|
||||
|
||||
@@ -30,7 +30,7 @@ public class GnuplotSettings {
|
||||
private YAxisDefinition y1;
|
||||
private YAxisDefinition y2;
|
||||
private AggregateHandlerCollection aggregates;
|
||||
private boolean keyOutside = false;
|
||||
private boolean showKey = false;
|
||||
|
||||
private AxisSettings xAxisSettings = new AxisSettings();
|
||||
private boolean renderLabels = true;
|
||||
@@ -101,12 +101,12 @@ public class GnuplotSettings {
|
||||
return aggregates;
|
||||
}
|
||||
|
||||
public void setKeyOutside(final boolean keyOutside) {
|
||||
this.keyOutside = keyOutside;
|
||||
public void setShowKey(final boolean showKey) {
|
||||
this.showKey = showKey;
|
||||
}
|
||||
|
||||
public boolean isKeyOutside() {
|
||||
return keyOutside;
|
||||
public boolean isShowKey() {
|
||||
return showKey;
|
||||
}
|
||||
|
||||
public void renderLabels(final boolean renderLabels) {
|
||||
|
||||