mirror of
https://github.com/tiqi-group/pydase.git
synced 2025-04-20 08:20:02 +02:00
Merge pull request #84 from tiqi-group/75-numberslider-component-is-not-working
75 numberslider component is not working
This commit is contained in:
commit
b4edc31030
121
README.md
121
README.md
@ -42,8 +42,7 @@
|
|||||||
- [Saving and restoring the service state for service persistence](#understanding-service-persistence)
|
- [Saving and restoring the service state for service persistence](#understanding-service-persistence)
|
||||||
- [Automated task management with built-in start/stop controls and optional autostart](#understanding-tasks-in-pydase)
|
- [Automated task management with built-in start/stop controls and optional autostart](#understanding-tasks-in-pydase)
|
||||||
- [Support for units](#understanding-units-in-pydase)
|
- [Support for units](#understanding-units-in-pydase)
|
||||||
<!-- * Event-based callback functionality for real-time updates
|
<!-- Support for additional servers for specific use-cases -->
|
||||||
- Support for additional servers for specific use-cases -->
|
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
@ -286,26 +285,132 @@ if __name__ == "__main__":
|
|||||||
|
|
||||||
#### `NumberSlider`
|
#### `NumberSlider`
|
||||||
|
|
||||||
This component provides an interactive slider interface for adjusting numerical values on the frontend. It supports both floats and integers. The values adjusted on the frontend are synchronized with the backend in real-time, ensuring consistent data representation.
|
The `NumberSlider` component in the `pydase` package provides an interactive slider interface for adjusting numerical values on the frontend. It is designed to support both numbers and quantities and ensures that values adjusted on the frontend are synchronized with the backend.
|
||||||
|
|
||||||
The slider can be customized with initial values, minimum and maximum limits, and step sizes to fit various use cases.
|
To utilize the `NumberSlider`, users should implement a class that derives from `NumberSlider`. This class can then define the initial values, minimum and maximum limits, step sizes, and additional logic as needed.
|
||||||
|
|
||||||
|
Here's an example of how to implement and use a custom slider:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
import pydase
|
import pydase
|
||||||
from pydase.components import NumberSlider
|
import pydase.components
|
||||||
|
|
||||||
|
|
||||||
|
class MySlider(pydase.components.NumberSlider):
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
value: float = 0.0,
|
||||||
|
min_: float = 0.0,
|
||||||
|
max_: float = 100.0,
|
||||||
|
step_size: float = 1.0,
|
||||||
|
) -> None:
|
||||||
|
super().__init__(value, min_, max_, step_size)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def min(self) -> float:
|
||||||
|
return self._min
|
||||||
|
|
||||||
|
@min.setter
|
||||||
|
def min(self, value: float) -> None:
|
||||||
|
self._min = value
|
||||||
|
|
||||||
|
@property
|
||||||
|
def max(self) -> float:
|
||||||
|
return self._max
|
||||||
|
|
||||||
|
@max.setter
|
||||||
|
def max(self, value: float) -> None:
|
||||||
|
self._max = value
|
||||||
|
|
||||||
|
@property
|
||||||
|
def step_size(self) -> float:
|
||||||
|
return self._step_size
|
||||||
|
|
||||||
|
@step_size.setter
|
||||||
|
def step_size(self, value: float) -> None:
|
||||||
|
self._step_size = value
|
||||||
|
|
||||||
|
@property
|
||||||
|
def value(self) -> float:
|
||||||
|
return self._value
|
||||||
|
|
||||||
|
@value.setter
|
||||||
|
def value(self, value: float) -> None:
|
||||||
|
if value < self._min or value > self._max:
|
||||||
|
raise ValueError("Value is either below allowed min or above max value.")
|
||||||
|
self._value = value
|
||||||
|
|
||||||
|
|
||||||
class MyService(pydase.DataService):
|
class MyService(pydase.DataService):
|
||||||
slider = NumberSlider(value=3.5, min=0, max=10, step_size=0.1, type="float")
|
def __init__(self) -> None:
|
||||||
|
super().__init__()
|
||||||
|
self.voltage = MySlider()
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
service = MyService()
|
service_instance = MyService()
|
||||||
pydase.Server(service).run()
|
service_instance.voltage.value = 5
|
||||||
|
print(service_instance.voltage.value) # Output: 5
|
||||||
|
pydase.Server(service_instance).run()
|
||||||
```
|
```
|
||||||
|
|
||||||
|
In this example, `MySlider` overrides the `min`, `max`, `step_size`, and `value` properties. Users can make any of these properties read-only by omitting the corresponding setter method.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
|
- Accessing parent class resources in `NumberSlider`
|
||||||
|
|
||||||
|
In scenarios where you need the slider component to interact with or access resources from its parent class, you can achieve this by passing a callback function to it. This method avoids directly passing the entire parent class instance (`self`) and offers a more encapsulated approach. The callback function can be designed to utilize specific attributes or methods of the parent class, allowing the slider to perform actions or retrieve data in response to slider events.
|
||||||
|
|
||||||
|
Here's an illustrative example:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from collections.abc import Callable
|
||||||
|
|
||||||
|
import pydase
|
||||||
|
import pydase.components
|
||||||
|
|
||||||
|
|
||||||
|
class MySlider(pydase.components.NumberSlider):
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
value: float,
|
||||||
|
on_change: Callable[[float], None],
|
||||||
|
) -> None:
|
||||||
|
super().__init__(value=value)
|
||||||
|
self._on_change = on_change
|
||||||
|
|
||||||
|
# ... other properties ...
|
||||||
|
|
||||||
|
@property
|
||||||
|
def value(self) -> float:
|
||||||
|
return self._value
|
||||||
|
|
||||||
|
@value.setter
|
||||||
|
def value(self, new_value: float) -> None:
|
||||||
|
if new_value < self._min or new_value > self._max:
|
||||||
|
raise ValueError("Value is either below allowed min or above max value.")
|
||||||
|
self._value = new_value
|
||||||
|
self._on_change(new_value)
|
||||||
|
|
||||||
|
|
||||||
|
class MyService(pydase.DataService):
|
||||||
|
def __init__(self) -> None:
|
||||||
|
self.voltage = MySlider(
|
||||||
|
5,
|
||||||
|
on_change=self.handle_voltage_change,
|
||||||
|
)
|
||||||
|
|
||||||
|
def handle_voltage_change(self, new_voltage: float) -> None:
|
||||||
|
print(f"Voltage changed to: {new_voltage}")
|
||||||
|
# Additional logic here
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
service_instance = MyService()
|
||||||
|
my_service.voltage.value = 7 # Output: "Voltage changed to: 7"
|
||||||
|
pydase.Server(service_instance).run()
|
||||||
|
````
|
||||||
|
|
||||||
#### `ColouredEnum`
|
#### `ColouredEnum`
|
||||||
|
|
||||||
This component provides a way to visually represent different states or categories in a data service using colour-coded options. It behaves similarly to a standard `Enum`, but the values encode colours in a format understood by CSS. The colours can be defined using various methods like Hexadecimal, RGB, HSL, and more.
|
This component provides a way to visually represent different states or categories in a data service using colour-coded options. It behaves similarly to a standard `Enum`, but the values encode colours in a format understood by CSS. The colours can be defined using various methods like Hexadecimal, RGB, HSL, and more.
|
||||||
|
Binary file not shown.
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 20 KiB |
@ -97,10 +97,10 @@ export const GenericComponent = React.memo(
|
|||||||
parentPath={parentPath}
|
parentPath={parentPath}
|
||||||
docString={attribute.doc}
|
docString={attribute.doc}
|
||||||
readOnly={attribute.readonly}
|
readOnly={attribute.readonly}
|
||||||
value={attribute.value['value']['value']}
|
value={attribute.value['value']}
|
||||||
min={attribute.value['min']['value']}
|
min={attribute.value['min']}
|
||||||
max={attribute.value['max']['value']}
|
max={attribute.value['max']}
|
||||||
stepSize={attribute.value['step_size']['value']}
|
stepSize={attribute.value['step_size']}
|
||||||
isInstantUpdate={isInstantUpdate}
|
isInstantUpdate={isInstantUpdate}
|
||||||
addNotification={addNotification}
|
addNotification={addNotification}
|
||||||
/>
|
/>
|
||||||
|
@ -8,6 +8,29 @@ import { LevelName } from './NotificationsComponent';
|
|||||||
|
|
||||||
// TODO: add button functionality
|
// TODO: add button functionality
|
||||||
|
|
||||||
|
export type QuantityObject = {
|
||||||
|
type: 'Quantity';
|
||||||
|
readonly: boolean;
|
||||||
|
value: {
|
||||||
|
magnitude: number;
|
||||||
|
unit: string;
|
||||||
|
};
|
||||||
|
doc?: string;
|
||||||
|
};
|
||||||
|
export type IntObject = {
|
||||||
|
type: 'int';
|
||||||
|
readonly: boolean;
|
||||||
|
value: number;
|
||||||
|
doc?: string;
|
||||||
|
};
|
||||||
|
export type FloatObject = {
|
||||||
|
type: 'float';
|
||||||
|
readonly: boolean;
|
||||||
|
value: number;
|
||||||
|
doc?: string;
|
||||||
|
};
|
||||||
|
export type NumberObject = IntObject | FloatObject | QuantityObject;
|
||||||
|
|
||||||
interface NumberComponentProps {
|
interface NumberComponentProps {
|
||||||
name: string;
|
name: string;
|
||||||
type: 'float' | 'int';
|
type: 'float' | 'int';
|
||||||
@ -18,12 +41,6 @@ interface NumberComponentProps {
|
|||||||
isInstantUpdate: boolean;
|
isInstantUpdate: boolean;
|
||||||
unit?: string;
|
unit?: string;
|
||||||
showName?: boolean;
|
showName?: boolean;
|
||||||
customEmitUpdate?: (
|
|
||||||
name: string,
|
|
||||||
parent_path: string,
|
|
||||||
value: number,
|
|
||||||
callback?: (ack: unknown) => void
|
|
||||||
) => void;
|
|
||||||
addNotification: (message: string, levelname?: LevelName) => void;
|
addNotification: (message: string, levelname?: LevelName) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -123,10 +140,6 @@ export const NumberComponent = React.memo((props: NumberComponentProps) => {
|
|||||||
|
|
||||||
// Whether to show the name infront of the component (false if used with a slider)
|
// Whether to show the name infront of the component (false if used with a slider)
|
||||||
const showName = props.showName !== undefined ? props.showName : true;
|
const showName = props.showName !== undefined ? props.showName : true;
|
||||||
// If emitUpdate is passed, use this instead of the setAttribute from the socket
|
|
||||||
// Also used when used with a slider
|
|
||||||
const emitUpdate =
|
|
||||||
props.customEmitUpdate !== undefined ? props.customEmitUpdate : setAttribute;
|
|
||||||
|
|
||||||
const renderCount = useRef(0);
|
const renderCount = useRef(0);
|
||||||
// Create a state for the cursor position
|
// Create a state for the cursor position
|
||||||
@ -263,7 +276,7 @@ export const NumberComponent = React.memo((props: NumberComponentProps) => {
|
|||||||
selectionEnd
|
selectionEnd
|
||||||
));
|
));
|
||||||
} else if (key === 'Enter' && !isInstantUpdate) {
|
} else if (key === 'Enter' && !isInstantUpdate) {
|
||||||
emitUpdate(name, parentPath, Number(newValue));
|
setAttribute(name, parentPath, Number(newValue));
|
||||||
return;
|
return;
|
||||||
} else {
|
} else {
|
||||||
console.debug(key);
|
console.debug(key);
|
||||||
@ -272,7 +285,7 @@ export const NumberComponent = React.memo((props: NumberComponentProps) => {
|
|||||||
|
|
||||||
// Update the input value and maintain the cursor position
|
// Update the input value and maintain the cursor position
|
||||||
if (isInstantUpdate) {
|
if (isInstantUpdate) {
|
||||||
emitUpdate(name, parentPath, Number(newValue));
|
setAttribute(name, parentPath, Number(newValue));
|
||||||
}
|
}
|
||||||
|
|
||||||
setInputString(newValue);
|
setInputString(newValue);
|
||||||
@ -284,7 +297,7 @@ export const NumberComponent = React.memo((props: NumberComponentProps) => {
|
|||||||
const handleBlur = () => {
|
const handleBlur = () => {
|
||||||
if (!isInstantUpdate) {
|
if (!isInstantUpdate) {
|
||||||
// If not in "instant update" mode, emit an update when the input field loses focus
|
// If not in "instant update" mode, emit an update when the input field loses focus
|
||||||
emitUpdate(name, parentPath, Number(inputString));
|
setAttribute(name, parentPath, Number(inputString));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -3,19 +3,19 @@ import { InputGroup, Form, Row, Col, Collapse, ToggleButton } from 'react-bootst
|
|||||||
import { setAttribute } from '../socket';
|
import { setAttribute } from '../socket';
|
||||||
import { DocStringComponent } from './DocStringComponent';
|
import { DocStringComponent } from './DocStringComponent';
|
||||||
import { Slider } from '@mui/material';
|
import { Slider } from '@mui/material';
|
||||||
import { NumberComponent } from './NumberComponent';
|
import { NumberComponent, NumberObject } from './NumberComponent';
|
||||||
import { getIdFromFullAccessPath } from '../utils/stringUtils';
|
import { getIdFromFullAccessPath } from '../utils/stringUtils';
|
||||||
import { LevelName } from './NotificationsComponent';
|
import { LevelName } from './NotificationsComponent';
|
||||||
|
|
||||||
interface SliderComponentProps {
|
interface SliderComponentProps {
|
||||||
name: string;
|
name: string;
|
||||||
min: number;
|
min: NumberObject;
|
||||||
max: number;
|
max: NumberObject;
|
||||||
parentPath?: string;
|
parentPath?: string;
|
||||||
value: number;
|
value: NumberObject;
|
||||||
readOnly: boolean;
|
readOnly: boolean;
|
||||||
docString: string;
|
docString: string;
|
||||||
stepSize: number;
|
stepSize: NumberObject;
|
||||||
isInstantUpdate: boolean;
|
isInstantUpdate: boolean;
|
||||||
addNotification: (message: string, levelname?: LevelName) => void;
|
addNotification: (message: string, levelname?: LevelName) => void;
|
||||||
}
|
}
|
||||||
@ -30,7 +30,6 @@ export const SliderComponent = React.memo((props: SliderComponentProps) => {
|
|||||||
min,
|
min,
|
||||||
max,
|
max,
|
||||||
stepSize,
|
stepSize,
|
||||||
readOnly,
|
|
||||||
docString,
|
docString,
|
||||||
isInstantUpdate,
|
isInstantUpdate,
|
||||||
addNotification
|
addNotification
|
||||||
@ -58,52 +57,41 @@ export const SliderComponent = React.memo((props: SliderComponentProps) => {
|
|||||||
addNotification(`${parentPath}.${name}.stepSize changed to ${stepSize}.`);
|
addNotification(`${parentPath}.${name}.stepSize changed to ${stepSize}.`);
|
||||||
}, [props.stepSize]);
|
}, [props.stepSize]);
|
||||||
|
|
||||||
const emitSliderUpdate = (
|
|
||||||
name: string,
|
|
||||||
parentPath: string,
|
|
||||||
value: number,
|
|
||||||
callback?: (ack: unknown) => void,
|
|
||||||
min: number = props.min,
|
|
||||||
max: number = props.max,
|
|
||||||
stepSize: number = props.stepSize
|
|
||||||
) => {
|
|
||||||
setAttribute(
|
|
||||||
name,
|
|
||||||
parentPath,
|
|
||||||
{
|
|
||||||
value: value,
|
|
||||||
min: min,
|
|
||||||
max: max,
|
|
||||||
step_size: stepSize
|
|
||||||
},
|
|
||||||
callback
|
|
||||||
);
|
|
||||||
};
|
|
||||||
const handleOnChange = (event, newNumber: number | number[]) => {
|
const handleOnChange = (event, newNumber: number | number[]) => {
|
||||||
// This will never be the case as we do not have a range slider. However, we should
|
// This will never be the case as we do not have a range slider. However, we should
|
||||||
// make sure this is properly handled.
|
// make sure this is properly handled.
|
||||||
if (Array.isArray(newNumber)) {
|
if (Array.isArray(newNumber)) {
|
||||||
newNumber = newNumber[0];
|
newNumber = newNumber[0];
|
||||||
}
|
}
|
||||||
emitSliderUpdate(name, parentPath, newNumber);
|
setAttribute(`${name}.value`, parentPath, newNumber);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleValueChange = (newValue: number, valueType: string) => {
|
const handleValueChange = (newValue: number, valueType: string) => {
|
||||||
switch (valueType) {
|
setAttribute(`${name}.${valueType}`, parentPath, newValue);
|
||||||
case 'min':
|
|
||||||
emitSliderUpdate(name, parentPath, value, undefined, newValue);
|
|
||||||
break;
|
|
||||||
case 'max':
|
|
||||||
emitSliderUpdate(name, parentPath, value, undefined, min, newValue);
|
|
||||||
break;
|
|
||||||
case 'stepSize':
|
|
||||||
emitSliderUpdate(name, parentPath, value, undefined, min, max, newValue);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const deconstructNumberDict = (
|
||||||
|
numberDict: NumberObject
|
||||||
|
): [number, boolean, string | null] => {
|
||||||
|
let numberMagnitude: number;
|
||||||
|
let numberUnit: string | null = null;
|
||||||
|
const numberReadOnly = numberDict.readonly;
|
||||||
|
|
||||||
|
if (numberDict.type === 'int' || numberDict.type === 'float') {
|
||||||
|
numberMagnitude = numberDict.value;
|
||||||
|
} else if (numberDict.type === 'Quantity') {
|
||||||
|
numberMagnitude = numberDict.value.magnitude;
|
||||||
|
numberUnit = numberDict.value.unit;
|
||||||
|
}
|
||||||
|
|
||||||
|
return [numberMagnitude, numberReadOnly, numberUnit];
|
||||||
|
};
|
||||||
|
|
||||||
|
const [valueMagnitude, valueReadOnly, valueUnit] = deconstructNumberDict(value);
|
||||||
|
const [minMagnitude, minReadOnly] = deconstructNumberDict(min);
|
||||||
|
const [maxMagnitude, maxReadOnly] = deconstructNumberDict(max);
|
||||||
|
const [stepSizeMagnitude, stepSizeReadOnly] = deconstructNumberDict(stepSize);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="sliderComponent" id={id}>
|
<div className="sliderComponent" id={id}>
|
||||||
{process.env.NODE_ENV === 'development' && (
|
{process.env.NODE_ENV === 'development' && (
|
||||||
@ -120,15 +108,15 @@ export const SliderComponent = React.memo((props: SliderComponentProps) => {
|
|||||||
style={{ margin: '0px 0px 10px 0px' }}
|
style={{ margin: '0px 0px 10px 0px' }}
|
||||||
aria-label="Always visible"
|
aria-label="Always visible"
|
||||||
// valueLabelDisplay="on"
|
// valueLabelDisplay="on"
|
||||||
disabled={readOnly}
|
disabled={valueReadOnly}
|
||||||
value={value}
|
value={valueMagnitude}
|
||||||
onChange={(event, newNumber) => handleOnChange(event, newNumber)}
|
onChange={(event, newNumber) => handleOnChange(event, newNumber)}
|
||||||
min={min}
|
min={minMagnitude}
|
||||||
max={max}
|
max={maxMagnitude}
|
||||||
step={stepSize}
|
step={stepSizeMagnitude}
|
||||||
marks={[
|
marks={[
|
||||||
{ value: min, label: `${min}` },
|
{ value: minMagnitude, label: `${minMagnitude}` },
|
||||||
{ value: max, label: `${max}` }
|
{ value: maxMagnitude, label: `${maxMagnitude}` }
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
@ -136,13 +124,13 @@ export const SliderComponent = React.memo((props: SliderComponentProps) => {
|
|||||||
<NumberComponent
|
<NumberComponent
|
||||||
isInstantUpdate={isInstantUpdate}
|
isInstantUpdate={isInstantUpdate}
|
||||||
parentPath={parentPath}
|
parentPath={parentPath}
|
||||||
name={name}
|
name={`${name}.value`}
|
||||||
docString=""
|
docString=""
|
||||||
readOnly={readOnly}
|
readOnly={valueReadOnly}
|
||||||
type="float"
|
type="float"
|
||||||
value={value}
|
value={valueMagnitude}
|
||||||
|
unit={valueUnit}
|
||||||
showName={false}
|
showName={false}
|
||||||
customEmitUpdate={emitSliderUpdate}
|
|
||||||
addNotification={() => null}
|
addNotification={() => null}
|
||||||
/>
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
@ -178,7 +166,8 @@ export const SliderComponent = React.memo((props: SliderComponentProps) => {
|
|||||||
<Form.Label>Min Value</Form.Label>
|
<Form.Label>Min Value</Form.Label>
|
||||||
<Form.Control
|
<Form.Control
|
||||||
type="number"
|
type="number"
|
||||||
value={min}
|
value={minMagnitude}
|
||||||
|
disabled={minReadOnly}
|
||||||
onChange={(e) => handleValueChange(Number(e.target.value), 'min')}
|
onChange={(e) => handleValueChange(Number(e.target.value), 'min')}
|
||||||
/>
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
@ -187,7 +176,8 @@ export const SliderComponent = React.memo((props: SliderComponentProps) => {
|
|||||||
<Form.Label>Max Value</Form.Label>
|
<Form.Label>Max Value</Form.Label>
|
||||||
<Form.Control
|
<Form.Control
|
||||||
type="number"
|
type="number"
|
||||||
value={max}
|
value={maxMagnitude}
|
||||||
|
disabled={maxReadOnly}
|
||||||
onChange={(e) => handleValueChange(Number(e.target.value), 'max')}
|
onChange={(e) => handleValueChange(Number(e.target.value), 'max')}
|
||||||
/>
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
@ -196,8 +186,9 @@ export const SliderComponent = React.memo((props: SliderComponentProps) => {
|
|||||||
<Form.Label>Step Size</Form.Label>
|
<Form.Label>Step Size</Form.Label>
|
||||||
<Form.Control
|
<Form.Control
|
||||||
type="number"
|
type="number"
|
||||||
value={stepSize}
|
value={stepSizeMagnitude}
|
||||||
onChange={(e) => handleValueChange(Number(e.target.value), 'stepSize')}
|
disabled={stepSizeReadOnly}
|
||||||
|
onChange={(e) => handleValueChange(Number(e.target.value), 'step_size')}
|
||||||
/>
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import logging
|
import logging
|
||||||
from typing import Any, Literal
|
from typing import Any
|
||||||
|
|
||||||
from pydase.data_service.data_service import DataService
|
from pydase.data_service.data_service import DataService
|
||||||
|
|
||||||
@ -21,15 +21,60 @@ class NumberSlider(DataService):
|
|||||||
The maximum value of the slider. Defaults to 100.
|
The maximum value of the slider. Defaults to 100.
|
||||||
step_size (float, optional):
|
step_size (float, optional):
|
||||||
The increment/decrement step size of the slider. Defaults to 1.0.
|
The increment/decrement step size of the slider. Defaults to 1.0.
|
||||||
type (Literal["int", "float"], optional):
|
|
||||||
The type of the slider value. Determines if the value is an integer or float.
|
|
||||||
Defaults to "float".
|
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
--------
|
--------
|
||||||
```python
|
```python
|
||||||
class MyService(DataService):
|
class MySlider(pydase.components.NumberSlider):
|
||||||
voltage = NumberSlider(1, 0, 10, 0.1, "int")
|
def __init__(
|
||||||
|
self,
|
||||||
|
value: float = 0.0,
|
||||||
|
min_: float = 0.0,
|
||||||
|
max_: float = 100.0,
|
||||||
|
step_size: float = 1.0,
|
||||||
|
) -> None:
|
||||||
|
super().__init__(value, min_, max_, step_size)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def min(self) -> float:
|
||||||
|
return self._min
|
||||||
|
|
||||||
|
@min.setter
|
||||||
|
def min(self, value: float) -> None:
|
||||||
|
self._min = value
|
||||||
|
|
||||||
|
@property
|
||||||
|
def max(self) -> float:
|
||||||
|
return self._max
|
||||||
|
|
||||||
|
@max.setter
|
||||||
|
def max(self, value: float) -> None:
|
||||||
|
self._max = value
|
||||||
|
|
||||||
|
@property
|
||||||
|
def step_size(self) -> float:
|
||||||
|
return self._step_size
|
||||||
|
|
||||||
|
@step_size.setter
|
||||||
|
def step_size(self, value: float) -> None:
|
||||||
|
self._step_size = value
|
||||||
|
|
||||||
|
@property
|
||||||
|
def value(self) -> float:
|
||||||
|
return self._value
|
||||||
|
|
||||||
|
@value.setter
|
||||||
|
def value(self, value: float) -> None:
|
||||||
|
if value < self._min or value > self._max:
|
||||||
|
raise ValueError(
|
||||||
|
"Value is either below allowed min or above max value."
|
||||||
|
)
|
||||||
|
|
||||||
|
self._value = value
|
||||||
|
|
||||||
|
class MyService(pydase.DataService):
|
||||||
|
def __init__(self) -> None:
|
||||||
|
self.voltage = MyService()
|
||||||
|
|
||||||
# Modifying or accessing the voltage value:
|
# Modifying or accessing the voltage value:
|
||||||
my_service = MyService()
|
my_service = MyService()
|
||||||
@ -38,29 +83,39 @@ class NumberSlider(DataService):
|
|||||||
```
|
```
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__( # noqa: PLR0913
|
def __init__(
|
||||||
self,
|
self,
|
||||||
value: float = 0,
|
value: Any = 0.0,
|
||||||
min_: float = 0.0,
|
min_: float = 0.0,
|
||||||
max_: float = 100.0,
|
max_: float = 100.0,
|
||||||
step_size: float = 1.0,
|
step_size: float = 1.0,
|
||||||
type_: Literal["int", "float"] = "float",
|
|
||||||
) -> None:
|
) -> None:
|
||||||
super().__init__()
|
super().__init__()
|
||||||
if type_ not in {"float", "int"}:
|
self._step_size = step_size
|
||||||
logger.error("Unknown type '%s'. Using 'float'.", type_)
|
self._value = value
|
||||||
type_ = "float"
|
self._min = min_
|
||||||
|
self._max = max_
|
||||||
|
|
||||||
self._type = type_
|
@property
|
||||||
self.step_size = step_size
|
def min(self) -> float:
|
||||||
self.value = value
|
"""The min property."""
|
||||||
self.min = min_
|
return self._min
|
||||||
self.max = max_
|
|
||||||
|
|
||||||
def __setattr__(self, name: str, value: Any) -> None:
|
@property
|
||||||
if name in ["value", "step_size"]:
|
def max(self) -> float:
|
||||||
value = int(value) if self._type == "int" else float(value)
|
"""The min property."""
|
||||||
elif not name.startswith("_"):
|
return self._max
|
||||||
value = float(value)
|
|
||||||
|
|
||||||
return super().__setattr__(name, value)
|
@property
|
||||||
|
def step_size(self) -> float:
|
||||||
|
"""The min property."""
|
||||||
|
return self._step_size
|
||||||
|
|
||||||
|
@property
|
||||||
|
def value(self) -> Any:
|
||||||
|
"""The value property."""
|
||||||
|
return self._value
|
||||||
|
|
||||||
|
@value.setter
|
||||||
|
def value(self, value: Any) -> None:
|
||||||
|
self._value = value
|
||||||
|
@ -1,13 +1,13 @@
|
|||||||
{
|
{
|
||||||
"files": {
|
"files": {
|
||||||
"main.css": "/static/css/main.2d8458eb.css",
|
"main.css": "/static/css/main.2d8458eb.css",
|
||||||
"main.js": "/static/js/main.7f907b0f.js",
|
"main.js": "/static/js/main.da9f921a.js",
|
||||||
"index.html": "/index.html",
|
"index.html": "/index.html",
|
||||||
"main.2d8458eb.css.map": "/static/css/main.2d8458eb.css.map",
|
"main.2d8458eb.css.map": "/static/css/main.2d8458eb.css.map",
|
||||||
"main.7f907b0f.js.map": "/static/js/main.7f907b0f.js.map"
|
"main.da9f921a.js.map": "/static/js/main.da9f921a.js.map"
|
||||||
},
|
},
|
||||||
"entrypoints": [
|
"entrypoints": [
|
||||||
"static/css/main.2d8458eb.css",
|
"static/css/main.2d8458eb.css",
|
||||||
"static/js/main.7f907b0f.js"
|
"static/js/main.da9f921a.js"
|
||||||
]
|
]
|
||||||
}
|
}
|
@ -1 +1 @@
|
|||||||
<!doctype html><html lang="en"><head><meta charset="utf-8"/><link rel="icon" href="/favicon.ico"/><meta name="viewport" content="width=device-width,initial-scale=1"/><meta name="theme-color" content="#000000"/><meta name="description" content="Web site displaying a pydase UI."/><link rel="apple-touch-icon" href="/logo192.png"/><link rel="manifest" href="/manifest.json"/><title>pydase App</title><script defer="defer" src="/static/js/main.7f907b0f.js"></script><link href="/static/css/main.2d8458eb.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"></div></body></html>
|
<!doctype html><html lang="en"><head><meta charset="utf-8"/><link rel="icon" href="/favicon.ico"/><meta name="viewport" content="width=device-width,initial-scale=1"/><meta name="theme-color" content="#000000"/><meta name="description" content="Web site displaying a pydase UI."/><link rel="apple-touch-icon" href="/logo192.png"/><link rel="manifest" href="/manifest.json"/><title>pydase App</title><script defer="defer" src="/static/js/main.da9f921a.js"></script><link href="/static/css/main.2d8458eb.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"></div></body></html>
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
1
src/pydase/frontend/static/js/main.da9f921a.js.map
Normal file
1
src/pydase/frontend/static/js/main.da9f921a.js.map
Normal file
File diff suppressed because one or more lines are too long
@ -1,46 +1,83 @@
|
|||||||
|
import logging
|
||||||
|
from collections.abc import Callable
|
||||||
|
|
||||||
from pydase.components.number_slider import NumberSlider
|
from pydase.components.number_slider import NumberSlider
|
||||||
from pydase.data_service.data_service import DataService
|
from pydase.data_service.data_service import DataService
|
||||||
from pydase.data_service.data_service_observer import DataServiceObserver
|
from pydase.data_service.data_service_observer import DataServiceObserver
|
||||||
from pydase.data_service.state_manager import StateManager
|
from pydase.data_service.state_manager import StateManager
|
||||||
from pytest import LogCaptureFixture
|
from pytest import LogCaptureFixture
|
||||||
|
|
||||||
|
from tests.utils.test_serializer import pytest
|
||||||
|
|
||||||
def test_NumberSlider(caplog: LogCaptureFixture) -> None:
|
logger = logging.getLogger(__name__)
|
||||||
class ServiceClass(DataService):
|
|
||||||
number_slider = NumberSlider(1, 0, 10, 1)
|
|
||||||
int_number_slider = NumberSlider(1, 0, 10, 1, "int")
|
|
||||||
|
|
||||||
service_instance = ServiceClass()
|
|
||||||
|
def test_number_slider(caplog: LogCaptureFixture) -> None:
|
||||||
|
class MySlider(NumberSlider):
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
value: float = 0,
|
||||||
|
min_: float = 0,
|
||||||
|
max_: float = 100,
|
||||||
|
step_size: float = 1,
|
||||||
|
callback: Callable[..., None] = lambda: None,
|
||||||
|
) -> None:
|
||||||
|
super().__init__(value, min_, max_, step_size)
|
||||||
|
self._callback = callback
|
||||||
|
|
||||||
|
@property
|
||||||
|
def value(self) -> float:
|
||||||
|
return self._value
|
||||||
|
|
||||||
|
@value.setter
|
||||||
|
def value(self, value: float) -> None:
|
||||||
|
self._callback(value)
|
||||||
|
self._value = value
|
||||||
|
|
||||||
|
@property
|
||||||
|
def max(self) -> float:
|
||||||
|
return self._max
|
||||||
|
|
||||||
|
@max.setter
|
||||||
|
def max(self, value: float) -> None:
|
||||||
|
self._max = value
|
||||||
|
|
||||||
|
@property
|
||||||
|
def step_size(self) -> float:
|
||||||
|
return self._step_size
|
||||||
|
|
||||||
|
@step_size.setter
|
||||||
|
def step_size(self, value: float) -> None:
|
||||||
|
self._step_size = value
|
||||||
|
|
||||||
|
class MyService(DataService):
|
||||||
|
def __init__(self) -> None:
|
||||||
|
super().__init__()
|
||||||
|
self.my_slider = MySlider(callback=self.some_method)
|
||||||
|
|
||||||
|
def some_method(self, slider_value: float) -> None:
|
||||||
|
logger.info("Slider changed to '%s'", slider_value)
|
||||||
|
|
||||||
|
service_instance = MyService()
|
||||||
state_manager = StateManager(service_instance)
|
state_manager = StateManager(service_instance)
|
||||||
DataServiceObserver(state_manager)
|
DataServiceObserver(state_manager)
|
||||||
|
|
||||||
assert service_instance.number_slider.value == 1
|
service_instance.my_slider.value = 10.0
|
||||||
assert isinstance(service_instance.number_slider.value, float)
|
|
||||||
assert service_instance.number_slider.min == 0
|
|
||||||
assert isinstance(service_instance.number_slider.min, float)
|
|
||||||
assert service_instance.number_slider.max == 10
|
|
||||||
assert isinstance(service_instance.number_slider.max, float)
|
|
||||||
assert service_instance.number_slider.step_size == 1
|
|
||||||
assert isinstance(service_instance.number_slider.step_size, float)
|
|
||||||
|
|
||||||
assert service_instance.int_number_slider.value == 1
|
assert "'my_slider.value' changed to '10.0'" in caplog.text
|
||||||
assert isinstance(service_instance.int_number_slider.value, int)
|
assert "Slider changed to '10.0'" in caplog.text
|
||||||
assert service_instance.int_number_slider.step_size == 1
|
|
||||||
assert isinstance(service_instance.int_number_slider.step_size, int)
|
|
||||||
|
|
||||||
service_instance.number_slider.value = 10.0
|
|
||||||
service_instance.int_number_slider.value = 10.1
|
|
||||||
|
|
||||||
assert "'number_slider.value' changed to '10.0'" in caplog.text
|
|
||||||
assert "'int_number_slider.value' changed to '10'" in caplog.text
|
|
||||||
caplog.clear()
|
caplog.clear()
|
||||||
|
|
||||||
service_instance.number_slider.min = 1.1
|
service_instance.my_slider.max = 12.0
|
||||||
|
|
||||||
assert "'number_slider.min' changed to '1.1'" in caplog.text
|
assert "'my_slider.max' changed to '12.0'" in caplog.text
|
||||||
|
caplog.clear()
|
||||||
|
|
||||||
|
service_instance.my_slider.step_size = 0.1
|
||||||
|
|
||||||
def test_init_error(caplog: LogCaptureFixture) -> None:
|
assert "'my_slider.step_size' changed to '0.1'" in caplog.text
|
||||||
number_slider = NumberSlider(type_="str") # type: ignore # noqa
|
caplog.clear()
|
||||||
|
|
||||||
assert "Unknown type 'str'. Using 'float'" in caplog.text
|
# by overriding the getter only you can make the property read-only
|
||||||
|
with pytest.raises(AttributeError):
|
||||||
|
service_instance.my_slider.min = 1.1 # type: ignore[reportGeneralTypeIssues, misc]
|
||||||
|
Loading…
x
Reference in New Issue
Block a user