Small improvements

This commit is contained in:
Jan Zípek 2024-03-28 15:58:28 +01:00
parent 9be5dc8281
commit 8b3c441878
Signed by: kamen
GPG Key ID: A17882625B33AC31
18 changed files with 304 additions and 46 deletions

View File

@ -5,7 +5,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Graphicek</title>
<!-- TODO: This should be loaded locally -->
<script src="https://cdn.plot.ly/plotly-2.14.0.min.js"></script>
<script src="https://cdn.plot.ly/plotly-2.30.0.min.js"></script>
</head>
<body>
<div id="application"></div>

View File

@ -11,7 +11,7 @@
"license": "ISC",
"devDependencies": {
"@preact/preset-vite": "^2.3.0",
"@types/plotly.js": "^2.12.2",
"@types/plotly.js": "^2.29.2",
"@typescript-eslint/eslint-plugin": "^5.7.0",
"@typescript-eslint/parser": "^5.7.0",
"eslint": "^8.5.0",

View File

@ -107,3 +107,18 @@ section.content {
display: flex;
flex-direction: column;
}
@media only screen and (max-width: 800px) {
header.header {
> .inner > .menu-button {
font-size: 175%;
}
.dashboard-head {
> button,
.filter-button {
font-size: 150%;
}
}
}
}

View File

@ -59,6 +59,7 @@
display: flex;
max-width: 100%;
overflow: auto;
transition: right 0.1s;
.inner {
background-color: var(--box-bg-color);

View File

@ -1,5 +1,5 @@
:root {
--main-font: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;
--main-font: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
--font-size: 14px;
--border-radius: 0.15rem;
--main-bg-color: #eee;
@ -14,7 +14,11 @@
--button-remove-fg-color: #f00;
--header-bg-color: #fff;
--header-spacer-color: #ddd;
--header-shadow: linear-gradient(0deg, rgba(255, 255, 255, 0) 5%, rgba(190, 190, 190, 0.6) 100%);
--header-shadow: linear-gradient(
0deg,
rgba(255, 255, 255, 0) 5%,
rgba(190, 190, 190, 0.6) 100%
);
--box-bg-color: #fff;
--box-fg-color: #111;
--box-border-color: #ddd;
@ -30,8 +34,16 @@
--graph-axis-fg-color: #777;
--graph-grid-color: rgb(238, 238, 238);
--link-fg-color: #3988ff;
--menu-shadow: linear-gradient(270deg, rgba(255, 255, 255, 0) 0%, rgba(90, 90, 90, 0.2) 100%);
--filters-menu-shadow: linear-gradient(90deg, rgba(255, 255, 255, 0) 0%, rgba(90, 90, 90, 0.2) 100%);
--menu-shadow: linear-gradient(
270deg,
rgba(255, 255, 255, 0) 0%,
rgba(90, 90, 90, 0.2) 100%
);
--filters-menu-shadow: linear-gradient(
90deg,
rgba(255, 255, 255, 0) 0%,
rgba(90, 90, 90, 0.2) 100%
);
--input-border-color: #ddd;
--input-focus-border-color: #3988ff;
--input-bg-color: #fff;
@ -58,14 +70,19 @@
--button-bg-color: #0b3c9f;
--button-fg-color: #eee;
--button-cancel-fg-color: #ccc;
--modal-overlay-bg-color: rgba(128, 128, 128, 0.3);
--confirm-overlay-bg-color: rgba(128, 128, 128, 0.5);
--modal-overlay-bg-color: rgba(0, 0, 0, 0.3);
--confirm-overlay-bg-color: rgba(0, 0, 0, 0.5);
--input-border-color: #333;
--input-focus-border-color: #666;
--input-bg-color: #222;
--input-fg-color: #ccc;
--input-disabled-fg-color: #bbb;
--input-disabled-bg-color: #2a2a2a;
--menu-shadow: linear-gradient(
270deg,
rgba(0, 0, 0, 0) 0%,
rgba(0, 0, 0, 0.2) 100%
);
}
.sensor-item .auth .auth-value .label {

View File

@ -0,0 +1,65 @@
export type ColorPoint = {
value: number
color: string
}
type Props = {
value: ColorPoint[]
onChange: (v: ColorPoint[]) => void
}
export const ColorPointsEditor = ({ value, onChange }: Props) => {
const handleColorChange = (index: number) => (e: Event) => {
const target = e.currentTarget as HTMLInputElement | null
if (target) {
const newValue = [...value]
newValue[index] = {
...value[index],
color: target.value,
}
onChange(newValue)
}
}
const handleValueChange = (index: number) => (e: Event) => {
const target = e.currentTarget as HTMLInputElement | null
if (target) {
const newValue = [...value]
newValue[index] = {
...value[index],
value: +target.value,
}
onChange(newValue)
}
}
const handleAdd = () => {
onChange([...value, { value: 0, color: '#fff' }])
}
return (
<>
{value.map((v, i) => (
<div key={i}>
<input
type="number"
value={v.value}
step="any"
onChange={handleValueChange(i)}
/>
<input type="color" value={v.color} onChange={handleColorChange(i)} />
</div>
))}
<button type="button" onClick={handleAdd}>
Add
</button>
</>
)
}

View File

@ -2,6 +2,7 @@ import { SensorInfo } from '@/api/sensors'
import { DashboardGraphData } from '@/utils/dashboard/parseDashboard'
import { useForm } from '@/utils/hooks/useForm'
import { omit } from '@/utils/omit'
import { ColorPoint, ColorPointsEditor } from './ColorPointsEditor'
type Props = {
sensors: SensorInfo[]
@ -12,11 +13,16 @@ type Props = {
export const GraphSettings = ({ value, onChange, sensors }: Props) => {
const { register, watch } = useForm({
defaultValue: () => ({
...(value && omit(value, ['type'])),
...(value && omit(value, ['type', 'colorPoints'])),
}),
onChange: (v) => onChange({ ...v, type: 'graph' }),
})
const colorPoints = value?.colorPoints ?? []
const setColorPoints = (colorPoints: ColorPoint[]) =>
onChange({ ...value, type: 'graph', colorPoints })
const colorMode = watch('colorMode')
return (
@ -69,6 +75,7 @@ export const GraphSettings = ({ value, onChange, sensors }: Props) => {
<select {...register('colorMode')}>
<option value="">None</option>
<option value="static">Static</option>
{/*<option value="points">Gradient</option>*/}
</select>
</div>
@ -78,6 +85,10 @@ export const GraphSettings = ({ value, onChange, sensors }: Props) => {
<input type="color" {...register('staticColor')} />
</div>
)}
{colorMode === 'points' && (
<ColorPointsEditor value={colorPoints} onChange={setColorPoints} />
)}
</>
)
}

View File

@ -1,9 +1,9 @@
import { getSensorValues } from '@/api/sensorValues'
import { useDashboardContext } from '@/pages/dashboard/contexts/DashboardContext'
import { BoxDefinition } from '@/pages/dashboard/types'
import { DashboardGraphData } from '@/utils/dashboard/parseDashboard'
import { max } from '@/utils/max'
import { min } from '@/utils/min'
import { DashboardGraphData } from '@/utils/dashboard/parseDashboard'
import { RefObject } from 'preact'
import { useEffect, useRef } from 'preact/hooks'
import { useQuery } from 'react-query'
@ -64,6 +64,7 @@ export const BoxGraphContent = ({ box, data, refreshRef }: Props) => {
const fill = data.fill ?? undefined
const colorMode = data.colorMode
const staticColor = data.staticColor
// const colorPoints = data.colorPoints ?? []
const x = values.data.map((v) => new Date(v.timestamp * 1000))
const y = values.data.map((v) => v.value)

View File

@ -57,9 +57,10 @@ export const DashboardHeader = () => {
<FiltersIcon />
</div>
{filtersShown && (
<>
<div className="filters-panel">
<div
className="filters-panel"
style={{ right: !filtersShown ? '-20rem' : '0' }}
>
<div className="shadow" />
<div className="inner">
<div
@ -71,14 +72,14 @@ export const DashboardHeader = () => {
<DashboardSwitch />
<DashboardFilters />
<DashboardFilters onClose={() => setFiltersShown(false)} />
</div>
</div>
{filtersShown && (
<div
className="menu-overlay"
onClick={() => setFiltersShown(false)}
></div>
</>
)}
</>
)}

View File

@ -0,0 +1,44 @@
import { hex2Rgb } from '@/utils/hex2Rgb'
import { interpolateColors } from '@/utils/interpolateColors'
import { ColorPoint } from '../components/BoxSettings/components/ColorPointsEditor'
import { rgb2Hex } from '@/utils/rgb2Hex'
type Props = {
colorPoints: ColorPoint[]
value: number
}
export const getColorFromColorPoints = ({ colorPoints, value }: Props) => {
let previousPoint = colorPoints[0]
let currentPoint = colorPoints[0]
let index = 0
while (currentPoint.value < value) {
if (index === colorPoints.length - 1) {
currentPoint = colorPoints[colorPoints.length - 1]
previousPoint = colorPoints[colorPoints.length - 1]
break
}
currentPoint = colorPoints[index]
previousPoint = colorPoints[index + 1]
index++
}
if (currentPoint.color !== previousPoint.color) {
const diff = currentPoint.value - previousPoint.value
const rate = (value - previousPoint.value) / diff
return rgb2Hex(
interpolateColors(
hex2Rgb(previousPoint.color),
hex2Rgb(currentPoint.color),
rate
)
)
} else {
return rgb2Hex(hex2Rgb(currentPoint.color))
}
}

View File

@ -30,8 +30,9 @@ export type DashboardGraphData = {
| 'tonextx'
| 'toself'
| 'tonext'
colorMode?: 'static'
colorMode?: 'static' | 'points'
staticColor?: string
colorPoints?: { value: number; color: string }[]
}
export type DashboardDialData = {

View File

@ -0,0 +1,11 @@
export function hex2Rgb(hex: string): [number, number, number] {
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex)
return result
? [
parseInt(result[1], 16),
parseInt(result[2], 16),
parseInt(result[3], 16),
]
: [0, 0, 0]
}

View File

@ -84,7 +84,7 @@ export const useForm = <TValue extends BaseValue = BaseValue>({
)
const watch = useCallback(
(name: keyof TValue) => {
<TName extends keyof TValue>(name: TName) => {
const [value, setValue] = useState(internalRef.current.value[name] ?? '')
useEffect(() => {

View File

@ -0,0 +1,44 @@
export function hsl2Rgb(
color: [number, number, number]
): [number, number, number] {
let l = color[2]
if (color[1] == 0) {
l = Math.round(l * 255)
return [l, l, l]
} else {
function hue2rgb(p: number, q: number, t: number) {
if (t < 0) {
t += 1
}
if (t > 1) {
t -= 1
}
if (t < 1 / 6) {
return p + (q - p) * 6 * t
}
if (t < 1 / 2) {
return q
}
if (t < 2 / 3) {
return p + (q - p) * (2 / 3 - t) * 6
}
return p
}
const s = color[1]
const q = l < 0.5 ? l * (1 + s) : l + s - l * s
const p = 2 * l - q
const r = hue2rgb(p, q, color[0] + 1 / 3)
const g = hue2rgb(p, q, color[0])
const b = hue2rgb(p, q, color[0] - 1 / 3)
return [Math.round(r * 255), Math.round(g * 255), Math.round(b * 255)]
}
}

View File

@ -0,0 +1,13 @@
export const interpolateColors = (
color1: [number, number, number],
color2: [number, number, number],
factor = 0.5
) => {
const result = color1.slice() as [number, number, number]
for (let i = 0; i < 3; i++) {
result[i] = Math.round(result[i] + factor * (color2[i] - color1[i]))
}
return result
}

View File

@ -0,0 +1,6 @@
export function rgb2Hex(rgb: [number, number, number]) {
return (
'#' +
((1 << 24) + (rgb[0] << 16) + (rgb[1] << 8) + rgb[2]).toString(16).slice(1)
)
}

View File

@ -0,0 +1,37 @@
export function rgb2Hsl(
color: [number, number, number]
): [number, number, number] {
const r = color[0] / 255
const g = color[1] / 255
const b = color[2] / 255
const max = Math.max(r, g, b),
min = Math.min(r, g, b)
let h = 0,
s
const l = (max + min) / 2
if (max == min) {
h = s = 0 // achromatic
} else {
const d = max - min
s = l > 0.5 ? d / (2 - max - min) : d / (max + min)
switch (max) {
case r:
h = (g - b) / d + (g < b ? 6 : 0)
break
case g:
h = (b - r) / d + 2
break
case b:
h = (r - g) / d + 4
break
}
h /= 6
}
return [h, s, l]
}

View File

@ -664,13 +664,6 @@ __metadata:
languageName: node
linkType: hard
"@types/d3@npm:^3":
version: 3.5.47
resolution: "@types/d3@npm:3.5.47"
checksum: f49657cb30705125de0de0617194f78ca5c05c1e1900485f2ea4c438d5b7744ff7d53b31e814caf986b994d21997edcda9fd845df115193fca2667bd096d0fdf
languageName: node
linkType: hard
"@types/json-schema@npm:^7.0.9":
version: 7.0.11
resolution: "@types/json-schema@npm:7.0.11"
@ -685,12 +678,10 @@ __metadata:
languageName: node
linkType: hard
"@types/plotly.js@npm:^2.12.2":
version: 2.12.2
resolution: "@types/plotly.js@npm:2.12.2"
dependencies:
"@types/d3": ^3
checksum: aa6a1bcd78869a79f299a5cd58886a3e5d9eaa16561f0da90eb77c172972c7b0699ba537bf60579e2904fc281b42dbd3692cdba6b994d63eaf1c2f0d5bc2cd38
"@types/plotly.js@npm:^2.29.2":
version: 2.29.2
resolution: "@types/plotly.js@npm:2.29.2"
checksum: 45ebf6219b3b92acfea5ef6086f7a71e2e20e689dc950d509fbc689113b8ff6db9c4242f2c4518b86ab7385a3f406a39c5dbcde3bdbbe7cc7a2362f0c111b9eb
languageName: node
linkType: hard
@ -1188,7 +1179,7 @@ __metadata:
resolution: "client@workspace:."
dependencies:
"@preact/preset-vite": ^2.3.0
"@types/plotly.js": ^2.12.2
"@types/plotly.js": ^2.29.2
"@typescript-eslint/eslint-plugin": ^5.7.0
"@typescript-eslint/parser": ^5.7.0
eslint: ^8.5.0