Small improvements
This commit is contained in:
parent
9be5dc8281
commit
8b3c441878
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -59,6 +59,7 @@
|
|||
display: flex;
|
||||
max-width: 100%;
|
||||
overflow: auto;
|
||||
transition: right 0.1s;
|
||||
|
||||
.inner {
|
||||
background-color: var(--box-bg-color);
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
@ -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} />
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
|
@ -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 = {
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
}
|
||||
|
|
@ -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(() => {
|
||||
|
|
|
|||
|
|
@ -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)]
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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)
|
||||
)
|
||||
}
|
||||
|
|
@ -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]
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in New Issue