Commit 6f26209d authored by Perceval Guillou's avatar Perceval Guillou
Browse files

Merge branch 'create-a-custom-timescan' into 'master'

Flint: Create a custom time curve plot

See merge request bliss/bliss!3777
parents 672f7d3f 0d900cac
Pipeline #48287 failed with stages
in 111 minutes and 27 seconds
......@@ -9,8 +9,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added
- Flint
- Create a `time-curve-plot` custom plot
### Changed
- Use `time-curve-plot` for the regulation plot
### Fixed
### Removed
......
......@@ -759,9 +759,6 @@ class Loop(SamplingCounterController):
self._wait_mode = self.WaitMode.DEADBAND # RAMP
self._history_size = 100
self.clear_history_data()
self.reg_plot = None
self.max_sampling_frequency = config.get("max_sampling_frequency", 5)
......@@ -942,54 +939,12 @@ class Loop(SamplingCounterController):
def is_in_idleband(self):
return self._x_is_in_idleband(self.input.read())
##--- DATA HISTORY METHODS
def clear_history_data(self):
self._history_start_time = time.time()
self.history_data = {"input": [], "output": [], "setpoint": [], "time": []}
self._history_counter = 0
def _store_history_data(self):
xval = time.time() - self._history_start_time
# xval = self._history_counter
self._history_counter += 1
self.history_data["time"].append(xval)
self.history_data["setpoint"].append(self._get_working_setpoint())
self.history_data["input"].append(self._get_last_input_value())
self.history_data["output"].append(self._get_last_output_value())
for data in self.history_data.values():
dx = len(data) - self._history_size
if dx > 0:
for i in range(dx):
data.pop(0)
def _get_last_input_value(self):
return self.input.read()
def _get_last_output_value(self):
return self.output.read()
@property
def history_size(self):
"""
Get the size of the buffer that stores the latest data (input_value, output_value, working_setpoint)
"""
log_debug(self, "Loop:get_history_size")
return self._history_size
@history_size.setter
def history_size(self, value):
"""
Set the size of the buffer that stores the latest data (input_value, output_value, working_setpoint)
"""
log_debug(self, "Loop:set_history_size: %s" % (value,))
self._history_size = value
##--- CTRL METHODS
@property
def setpoint(self):
......@@ -2010,16 +1965,14 @@ class RegPlot:
def create_plot(self):
# Declare a CurvePlot (see bliss.flint.client.plots)
# Declare and setup the plot
self.fig = get_flint().get_plot(
plot_class="Plot1D",
plot_class="TimeCurvePlot",
name=self.loop.name,
unique_name=f"regul_plot_{self.loop.name}",
closeable=True,
selected=True,
)
self.fig.submit("setGraphXLabel", "Time (s)")
self.fig.submit(
"setGraphYLabel",
f"Processed value ({self.loop.input.config.get('unit','')})",
......@@ -2031,13 +1984,14 @@ class RegPlot:
)
self.fig.submit("setGraphGrid", which=True)
self.fig.submit(
"setDataMargins",
xMinMargin=0.0,
xMaxMargin=0.0,
yMinMargin=0.1,
yMaxMargin=0.1,
# Define the plot content
self.fig.select_time_curve("setpoint", color="blue", linestyle="-", z=2)
self.fig.select_time_curve("input", color="red", linestyle="-", z=2)
self.fig.select_time_curve(
"output", color="green", linestyle="-", yaxis="right", z=2
)
self.fig.select_time_curve("deadband_high", color="blue", linestyle="--", z=2)
self.fig.select_time_curve("deadband_low", color="blue", linestyle="--", z=2)
def is_plot_active(self):
if self.fig is None:
......@@ -2050,7 +2004,6 @@ class RegPlot:
self.create_plot()
if not self.task:
self.loop.clear_history_data()
self._stop_event.clear()
self.task = gevent.spawn(self.run)
......@@ -2069,43 +2022,24 @@ class RegPlot:
except (gevent.timeout.Timeout, Exception) as e:
pass
# update data history
self.loop._store_history_data()
try:
self.fig.submit("setAutoReplot", False)
self.fig.add_data(self.loop.history_data["time"], field="time")
self.fig.add_data(self.loop.history_data["input"], field="Input")
self.fig.add_data(self.loop.history_data["output"], field="Output")
self.fig.add_data(self.loop.history_data["setpoint"], field="Setpoint")
dbp = [
x + self.loop.deadband for x in self.loop.history_data["setpoint"]
]
dbm = [
x - self.loop.deadband for x in self.loop.history_data["setpoint"]
]
self.fig.add_data(dbp, field="Deadband_high")
self.fig.add_data(dbm, field="Deadband_low")
loop = self.loop
data_time = time.time()
setpoint = loop._get_working_setpoint()
input_value = loop._get_last_input_value()
output_value = loop._get_last_output_value()
dbp = setpoint + loop.deadband
dbm = setpoint - loop.deadband
# Update curves plot (refreshes the plot widget)
# select_data takes all kwargs of the associated plot methode (e.g. silx => addCurve(kwargs) )
self.fig.select_data(
"time", "Setpoint", color="blue", linestyle="-", z=2
)
self.fig.select_data("time", "Input", color="red", linestyle="-", z=2)
self.fig.select_data(
"time", "Output", color="green", linestyle="-", yaxis="right", z=2
)
self.fig.select_data(
"time", "Deadband_high", color="blue", linestyle="--", z=2
self.fig.append_data(
time=[data_time],
input=[input_value],
output=[output_value],
setpoint=[setpoint],
deadband_high=[dbp],
deadband_low=[dbm],
)
self.fig.select_data(
"time", "Deadband_low", color="blue", linestyle="--", z=2
)
self.fig.submit("setAutoReplot", True)
except (gevent.timeout.Timeout, Exception):
log_debug(self, "Error while plotting the data", exc_info=True)
......
......@@ -622,6 +622,69 @@ class CurveStack(BasePlot):
)
class TimeCurvePlot(BasePlot):
# Name of the corresponding silx widget
WIDGET = "bliss.flint.custom_plots.time_curve_plot.TimeCurvePlot"
# Available name to identify this plot
ALIASES = ["timecurveplot"]
# Name of the method to add data to the plot
METHOD = "appendData"
# Single / Multiple data handling
MULTIPLE = False
# Data input number for a single representation
DATA_INPUT_NUMBER = 1
def select_x_axis(self, name: str):
"""
Select the x-axis to use
Arguments:
name: Name of the data to use as x-axis
"""
self.submit("setXName", name)
def select_x_duration(self, second: int):
"""
Select the x-axis duration in second
Arguments:
second: Amount of seconds displayed in the x-axis
"""
self.submit("setXDuration", second)
def select_time_curve(self, yname, **kwargs):
"""
Select a dedicated data to be displayed against the time.
Arguments:
name: Name of the data to use as y-axis
kwargs: Associated style (see `addCurve` from silx plot)
"""
self.submit("selectCurve", yname, **kwargs)
def set_data(self, **kwargs):
"""
Set the data displayed in this plot.
Arguments:
kwargs: Name of the data associated to the new numpy array to use
"""
self.submit("setData", **kwargs)
def append_data(self, **kwargs):
"""
Append the data displayed in this plot.
Arguments:
kwargs: Name of the data associated to the numpy array to append
"""
self.submit("appendData", **kwargs)
class ImageView(BasePlot):
# Name of the corresponding silx widget
......@@ -699,7 +762,15 @@ class LiveOneDimPlot(Plot1D):
ALIASES = ["onedim"]
CUSTOM_CLASSES = [Plot1D, Plot2D, ScatterView, ImageView, StackView, CurveStack]
CUSTOM_CLASSES = [
Plot1D,
Plot2D,
ScatterView,
ImageView,
StackView,
CurveStack,
TimeCurvePlot,
]
LIVE_CLASSES = [
LiveCurvePlot,
......
......@@ -48,8 +48,8 @@ class CurveStack(qt.QWidget):
def setGraphXLabel(self, label: str):
self.__plot.setGraphXLabel(label)
def setGraphYLabel(self, label: str):
self.__plot.setGraphYLabel(label)
def setGraphYLabel(self, label: str, axis="left"):
self.__plot.setGraphYLabel(label, axis=axis)
def getPlotWidget(self):
return self.__plot
......
# -*- coding: utf-8 -*-
#
# This file is part of the bliss project
#
# Copyright (c) 2015-2020 Beamline Control Unit, ESRF
# Distributed under the GNU LGPLv3. See LICENSE for more info.
from __future__ import annotations
import logging
import numpy
from silx.gui import qt
from silx.gui import icons
from silx.gui.plot import Plot1D
from silx.gui.plot.items import axis as axis_mdl
_logger = logging.getLogger(__name__)
class DurationAction(qt.QAction):
valueChanged = qt.Signal(float)
def __init__(self):
super(DurationAction, self).__init__()
self.__duration = None
self.__durations = {}
self.__menu = qt.QMenu()
self.__menu.aboutToShow.connect(self.__menuAboutToShow)
self.setMenu(self.__menu)
def __menuAboutToShow(self):
menu = self.sender()
menu.clear()
currentDuration = self.__duration
currentWasFound = False
group = qt.QActionGroup(menu)
group.setExclusive(True)
for value, (label, icon) in self.__durations.items():
action = qt.QAction()
action.setText(label)
action.setData(value)
action.setIcon(icon)
action.setCheckable(True)
if currentDuration == value:
action.setChecked(True)
currentWasFound = True
group.addAction(action)
menu.addAction(action)
if currentDuration is not None and not currentWasFound:
menu.addSeparator()
action = qt.QAction()
action.setText(f"{currentDuration}s")
action.setData(currentDuration)
action.setCheckable(True)
action.setChecked(True)
currentWasFound = True
group.addAction(action)
menu.addAction(action)
group.triggered.connect(self.__actionSelected)
def __actionSelected(self, action):
duration = action.data()
self.setDuration(duration)
def setDuration(self, duration):
if self.__duration == duration:
return
self.__duration = duration
self.__updateLookAndFeel()
self.valueChanged.emit(duration)
def addDuration(self, label, value, icon):
if isinstance(icon, str):
icon = icons.getQIcon(icon)
self.__durations[value] = label, icon
def duration(self):
"""Return a duration in second"""
return self.__duration
def __updateLookAndFeel(self):
duration = self.__duration
label, icon = self.__durations.get(duration, (None, None))
if icon is None:
icon = icons.getQIcon("flint:icons/duration-x")
if label is None:
label = f"{duration}s"
self.setToolTip(f"Duration of {label} selected")
self.setIcon(icon)
class TimeCurvePlot(qt.QWidget):
"""Curve plot which handle data following the time
- The X is supposed to be the epoch time
- The data can be appended
- The user can choose the amount of time to watch
"""
def __init__(self, parent=None):
super(TimeCurvePlot, self).__init__(parent=parent)
self.__data = {}
self.__description = {}
self.__xAxisName = "time"
self.__plot = Plot1D(self)
layout = qt.QVBoxLayout(self)
layout.addWidget(self.__plot)
self.__duration = 60 * 2
self.__durationAction = DurationAction()
self.__durationAction.setCheckable(True)
self.__durationAction.setChecked(True)
self.__durationAction.addDuration("1h", 60 * 60, "flint:icons/duration-1h")
self.__durationAction.addDuration("30m", 30 * 60, "flint:icons/duration-30m")
self.__durationAction.addDuration("10m", 10 * 60, "flint:icons/duration-10m")
self.__durationAction.addDuration("5m", 5 * 60, "flint:icons/duration-5m")
self.__durationAction.addDuration("2m", 2 * 60, "flint:icons/duration-2m")
self.__durationAction.addDuration("1m", 1 * 60, "flint:icons/duration-1m")
self.__durationAction.addDuration("30s", 30, "flint:icons/duration-30s")
self.__durationAction.setDuration(self.__duration)
self.__durationAction.valueChanged.connect(self.__durationChanged)
self.__plot.setGraphXLabel("Time")
xAxis = self.__plot.getXAxis()
xAxis.setTickMode(axis_mdl.TickMode.TIME_SERIES)
xAxis.setTimeZone(None)
self.__plot.setDataMargins(
xMinMargin=0.0, xMaxMargin=0.0, yMinMargin=0.1, yMaxMargin=0.1
)
# FIXME: The toolbar have to be recreated, not updated
toolbar = self.__plot.toolBar()
xAutoAction = self.__plot.getXAxisAutoScaleAction()
toolbar.insertAction(xAutoAction, self.__durationAction)
xAutoAction.setVisible(False)
xLogAction = self.__plot.getXAxisLogarithmicAction()
xLogAction.setVisible(False)
self.clear()
def __durationChanged(self, duration):
self.setXDuration(duration)
def setXDuration(self, duration):
self.__durationAction.setDuration(duration)
self.__duration = duration
self.__dropOldData()
self.__safeUpdatePlot()
def __dropOldData(self):
xData = self.__data.get(self.__xAxisName)
if xData is None:
return
if len(xData) == 0:
return
duration = xData[-1] - xData[0]
if duration <= self.__duration:
return
# FIXME: most of the time only last items with be removed
# There is maybe no need to recompute the whole array
distFromLastValueOfView = self.__duration - numpy.abs(
xData[-1] - self.__duration - xData
)
index = numpy.argmax(distFromLastValueOfView)
if index >= 1:
index = index - 1
if index == 0:
# early skip
return
for name, data in self.__data.items():
data = data[index:]
self.__data[name] = data
def getDataRange(self):
r = self.__plot.getDataRange()
if r is None:
return None
return r[0], r[1]
def setGraphGrid(self, which):
self.__plot.setGraphGrid(which)
def setGraphTitle(self, title: str):
self.__plot.setGraphTitle(title)
def setGraphXLabel(self, label: str):
self.__plot.setGraphXLabel(label)
def setGraphYLabel(self, label: str, axis="left"):
self.__plot.setGraphYLabel(label, axis=axis)
def getPlotWidget(self):
return self.__plot
def clear(self):
self.__data = {}
self.__plot.clear()
def __appendData(self, name, newData):
if name in self.__data:
data = self.__data[name]
data = numpy.concatenate((data, newData))
else:
data = newData
self.__data[name] = data
def selectCurve(self, yName, **kwargs):
"""Update the plot description"""
self.__description[yName] = kwargs
self.__safeUpdatePlot()
def setXName(self, name):
"""Update the name used as X axis"""
self.__xAxisName = name
self.__safeUpdatePlot()
def setData(self, **kwargs):
self.__data = dict(kwargs)
self.__safeUpdatePlot()
def appendData(self, **kwargs):
"""Update the current data with extra data"""
for name, data in kwargs.items():
self.__appendData(name, data)
self.__dropOldData()
self.__safeUpdatePlot()
def resetZoom(self):
if self.__durationAction.isChecked():
self.__plot.resetZoom()
xData = self.__data.get(self.__xAxisName)
if xData is not None and len(xData) > 0:
xmax = xData[-1]
xmin = xmax - self.__duration
xAxis = self.__plot.getXAxis()
xAxis.setLimits(xmin, xmax)
def __safeUpdatePlot(self):
try:
self.__updatePlot()
except Exception:
_logger.critical("Error while updating the plot", exc_info=True)
def __updatePlot(self):
self.__plot.clear()
xData = self.__data.get(self.__xAxisName)
if xData is None:
return
for name, style in self.__description.items():
yData = self.__data.get(name)
if yData is None:
continue
if "legend" not in style:
style["legend"] = name
style["resetzoom"] = False
self.__plot.addCurve(xData, yData, **style)
self.resetZoom()
<?xml version="1.0" encoding="UTF-8"?>
<svg id="svg8295" version="1.1" viewBox="0 0 32 32" xml:space="preserve" xmlns="http://www.w3.org/2000/svg" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"><metadata id="metadata8301"><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/></cc:Work></rdf:RDF></metadata><g id="text8305" transform="scale(.8179 1.2226)" stroke-width=".39147" aria-label="10m"><path id="path4743" d="m6.7421 12.593h1.965v-5.6885q0-0.35171 0.022938-0.734l-1.3227 1.1392q-0.12998 0.10704-0.25996 0.12998-0.12998 0.022938-0.24467 0-0.11469-0.022938-0.20644-0.076459-0.084105-0.061167-0.12998-0.12233l-0.58109-0.79517 3.0507-2.6455h1.5139v8.7928h1.7356v1.3763h-5.5433z"/><path id="path4745" d="m20.505 8.8852q0 1.3304-0.2829 2.3091-0.27525 0.97867-0.77224 1.6209-0.48934 0.64225-1.1622 0.95574-0.66519 0.30584-1.4451 0.30584t-1.4451-0.30584q-0.65755-0.31348-1.1469-0.95574-0.48934-0.64225-0.76459-1.6209-0.27525-0.97867-0.27525-2.3091 0-1.3304 0.27525-2.3014 0.27525-0.97867 0.76459-1.6133 0.48934-0.64225 1.1469-0.95574 0.66519-0.31348 1.4451-0.31348t1.4451 0.31348q0.67284 0.31348 1.1622 0.95574 0.49698 0.63461 0.77224 1.6133 0.2829 0.97103 0.2829 2.3014zm-1.9038 0q0-1.0857-0.15292-1.7891-0.14527-0.71107-0.38994-1.1239-0.24467-0.42052-0.5658-0.58109-0.31348-0.16821-0.6499-0.16821t-0.6499 0.16821q-0.30584 0.16056-0.5505 0.58109-0.23702 0.41288-0.38229 1.1239-0.14527 0.70342-0.14527 1.7891 0 1.0934 0.14527 1.8044 0.14527 0.70342 0.38229 1.1239 0.24467 0.41288 0.5505 0.58109 0.31348 0.16056 0.6499 0.16056t0.6499-0.16056q0.32113-0.16821 0.5658-0.58109 0.24467-0.42052 0.38994-1.1239 0.15292-0.71107 0.15292-1.8044z"/><path id="path4747" d="m21.82 13.97v-7.5924h1.1928q0.1835 0 0.30584 0.084105 0.12998 0.084105 0.16821 0.25996l0.12998 0.53521q0.20644-0.21408 0.42052-0.39759 0.22173-0.1835 0.47404-0.31348 0.25231-0.13763 0.53521-0.21408 0.29054-0.076459 0.63461-0.076459 0.72636 0 1.1928 0.38994 0.47404 0.38229 0.70342 1.0169 0.1835-0.37465 0.45111-0.64225 0.27525-0.26761 0.60402-0.43582 0.32877-0.16821 0.68813-0.24467 0.367-0.084105 0.734-0.084105 0.64225 0 1.1392 0.19879 0.50463 0.19115 0.84105 0.5658 0.34406 0.367 0.51992 0.90221 0.17586 0.53521 0.17586 1.2157v4.8322h-1.9421v-4.8322q0-1.3686-1.2233-1.3686-0.27525 0-0.51227 0.091751-0.23702 0.084105-0.42052 0.25996-0.17586 0.16821-0.2829 0.42817-0.0994 0.25231-0.0994 0.58873v4.8322h-1.9421v-4.8322q0-0.734-0.30584-1.0475-0.30584-0.32113-0.88692-0.32113-0.38994 0-0.72636 0.17586-0.33642 0.17586-0.62696 0.49698v5.528z"/></g>
<line id="line8287" x1="25.712" x2="6.7701" y1="22.685" y2="22.685" fill="none" stroke="#00a14b" stroke-miterlimit="10" stroke-width="1.5"/><polygon id="polygon8289" transform="translate(-.22991)" points="25.47 24.417 25.463 20.953 28.467 22.68" fill="#00a14b" stroke="#00a14b" stroke-miterlimit="10" stroke-width="1.5"/><polygon id="polygon8291" transform="translate(-.22991)" points="6.988 24.417 3.993 22.676 7 20.953" fill="#00a14b" stroke="#00a14b" stroke-miterlimit="10" stroke-width="1.5"/><line id="line8287-2" x1="2.1686" x2="2.1686" y1="26.413" y2="18.957" fill="none" stroke="#00a14b" stroke-miterlimit="10" stroke-width="1.5"/><line id="line8287-2-1" x1="29.831" x2="29.831" y1="26.413" y2="18.957" fill="none" stroke="#00a14b" stroke-miterlimit="10" stroke-width="1.5"/></svg>
<?xml version="1.0" encoding="UTF-8"?>
<svg id="svg8295" version="1.1" viewBox="0 0 32 32" xml:space="preserve" xmlns="http://www.w3.org/2000/svg" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"><metadata id="metadata8301"><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/></cc:Work></rdf:RDF></metadata><text id="text8305" x="15.536421" y="17.979942" fill="#000000" font-family="sans-serif" font-size="19.145px" letter-spacing="0px" stroke-width=".47863" text-align="center" text-anchor="middle" word-spacing="0px" style="line-height:1.25" xml:space="preserve"><tspan id="tspan8303" x="15.53642" y="17.979942" font-family="Carlito" font-weight="bold" stroke-width=".47863" text-align="center" text-anchor="middle">1h</tspan></text>
<line id="line8287" x1="25.712" x2="6.7701" y1="22.685" y2="22.685" fill="none" stroke="#00a14b" stroke-miterlimit="10" stroke-width="1.5"/><polygon id="polygon8289" transform="translate(-.22991)" points="25.47 24.417 25.463 20.953 28.467 22.68" fill="#00a14b" stroke="#00a14b" stroke-miterlimit="10" stroke-width="1.5"/><polygon id="polygon8291" transform="translate(-.22991)" points="6.988 24.417 3.993 22.676 7 20.953" fill="#00a14b" stroke="#00a14b" stroke-miterlimit="10" stroke-width="1.5"/><line id="line8287-2" x1="2.1686" x2="2.1686" y1="26.413" y2="18.957" fill="none" stroke="#00a14b" stroke-miterlimit="10" stroke-width="1.5"/><line id="line8287-2-1" x1="29.831" x2="29.831" y1="26.413" y2="18.957" fill="none" stroke="#00a14b" stroke-miterlimit="10" stroke-width="1.5"/></svg>
<?xml version="1.0" encoding="UTF-8"?>
<svg id="svg8295" version="1.1" viewBox="0 0 32 32" xml:space="preserve" xmlns="http://www.w3.org/2000/svg" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"><metadata id="metadata8301"><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/></cc:Work></rdf:RDF></metadata><g id="text8305" transform="scale(.89393 1.1187)" stroke-width=".42786" aria-label="1m"><path id="path4738" d="m8.0338 13.688h2.1477v-6.2173q0-0.38441 0.02507-0.80224l-1.4457 1.2451q-0.14206 0.11699-0.28413 0.14206-0.14206 0.02507-0.26741 0-0.12535-0.02507-0.22563-0.083566-0.091923-0.066853-0.14206-0.13371l-0.6351-0.86909 3.3343-2.8914h1.6546v9.6101h1.897v1.5042h-6.0586z"/><path id="path4740" d="m15.822 15.192v-8.2981h1.3036q0.20056 0 0.33427 0.091923 0.14206 0.091923 0.18385 0.28413l0.14206 0.58497q0.22563-0.23399 0.45962-0.43455 0.24234-0.20056 0.51811-0.34262 0.27577-0.15042 0.58496-0.23399 0.31755-0.083566 0.6936-0.083566 0.79388 0 1.3036 0.42619 0.51811 0.41783 0.76881 1.1114 0.20056-0.40948 0.49304-0.70196 0.30084-0.29248 0.66018-0.47633 0.35934-0.18385 0.7521-0.26741 0.40112-0.091923 0.80224-0.091923 0.70196 0 1.2451 0.21727 0.55154 0.20892 0.91923 0.61839 0.37605 0.40112 0.56825 0.98608 0.1922 0.58497 0.1922 1.3287v5.2814h-2.1226v-5.2814q0-1.4958-1.3371-1.4958-0.30084 0-0.5599 0.10028-0.25906 0.091923-0.45962 0.28413-0.1922 0.18385-0.3092 0.46797-0.10864 0.27577-0.10864 0.64346v5.2814h-2.1226v-5.2814q0-0.80224-0.33426-1.1449-0.33427-0.35098-0.96937-0.35098-0.42619 0-0.79388 0.1922-0.36769 0.1922-0.68524 0.54318v6.0419z"/></g>
<line id="line8287" x1="25.712" x2="6.7701" y1="22.685" y2="22.685" fill="none" stroke="#00a14b" stroke-miterlimit="10" stroke-width="1.5"/><polygon id="polygon8289" transform="translate(-.22991)" points="25.47 24.417 25.463 20.953 28.467 22.68" fill="#00a14b" stroke="#00a14b" stroke-miterlimit="10" stroke-width="1.5"/><polygon id="polygon8291" transform="translate(-.22991)" points="6.988 24.417 3.993 22.676 7 20.953" fill="#00a14b" stroke="#00a14b" stroke-miterlimit="10" stroke-width="1.5"/><line id="line8287-2" x1="2.1686" x2="2.1686" y1="26.413" y2="18.957" fill="none" stroke="#00a14b" stroke-miterlimit="10" stroke-width="1.5"/><line id="line8287-2-1" x1="29.831" x2="29.831" y1="26.413" y2="18.957" fill="none" stroke="#00a14b" stroke-miterlimit="10" stroke-width="1.5"/></svg>
<?xml version="1.0" encoding="UTF-8"?>
<svg id="svg8295" version="1.1" viewBox="0 0 32 32" xml:space="preserve" xmlns="http://www.w3.org/2000/svg" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"><metadata id="metadata8301"><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/></cc:Work></rdf:RDF></metadata><g id="text8305" transform="scale(.89393 1.1187)" stroke-width=".42786" aria-label="2m"><path id="path818" d="m7.344 15.192m3.9193-11.223q0.76881 0 1.3956 0.23399 0.6351 0.22563 1.078 0.64346 0.45126 0.41783 0.6936 1.0112 0.2507 0.58497 0.2507 1.2953 0 0.61004-0.17549 1.1365-0.17549 0.51811-0.46797 0.99444-0.29248 0.46797-0.68524 0.91923-0.39276 0.4429-0.82731 0.89416l-2.3566 2.4652q0.37605-0.11699 0.7521-0.17549 0.37605-0.06685 0.70196-0.06685h2.5154q0.31755 0 0.50976 0.18385 0.20056 0.18385 0.20056 0.48469v1.2034h-7.5043v-0.67689q0-0.1922 0.07521-0.40948 0.083566-0.22563 0.28413-0.41783l3.2257-3.3176q0.40948-0.42619 0.71867-0.81059 0.31755-0.38441 0.52647-0.76045 0.21727-0.38441 0.32591-0.76881 0.10864-0.39276 0.10864-0.81895 0-0.76881-0.38441-1.1616-0.3844-0.39276-1.0864-0.39276-0.30084 0-0.55154 0.091923-0.2507 0.09192-0.45126 0.2507-0.20056 0.15878-0.34262 0.37605-0.14206 0.21727-0.21727 0.46797-0.13371 0.38441-0.36769 0.5014-0.22563 0.11699-0.62675 0.05014l-1.0697-0.18385q0.12535-0.81059 0.45126-1.4123 0.32591-0.61004 0.81059-1.0112 0.49304-0.40948 1.1281-0.61004 0.6351-0.20892 1.3621-0.20892z"/><path id="path820" d="m16.528 15.192v-8.2981h1.3036q0.20056 0 0.33427 0.091923 0.14206 0.091923 0.18385 0.28413l0.14206 0.58497q0.22563-0.23399 0.45962-0.43455 0.24234-0.20056 0.51811-0.34262 0.27577-0.15042 0.58496-0.23399 0.31755-0.083566 0.6936-0.083566 0.79388 0 1.3036 0.42619 0.51811 0.41783 0.76881 1.1114 0.20056-0.40948 0.49304-0.70196 0.30084-0.29248 0.66018-0.47633 0.35934-0.18385 0.7521-0.26741 0.40112-0.091923 0.80224-0.091923 0.70196 0 1.2451 0.21727 0.55154 0.20892 0.91923 0.61839 0.37605 0.40112 0.56825 0.98608 0.1922 0.58497 0.1922 1.3287v5.2814h-2.1226v-5.2814q0-1.4958-1.3371-1.4958-0.30084 0-0.5599 0.10028-0.25906 0.091923-0.45962 0.28413-0.1922 0.18385-0.3092 0.46797-0.10864 0.27577-0.10864 0.64346v5.2814h-2.1226v-5.2814q0-0.80224-0.33427-1.1449-0.33427-0.35098-0.96937-0.35098-0.42619 0-0.79388 0.1922t-0.68524 0.54318v6.0419z"/></g>
<line id="line8287" x1="25.712" x2="6.7701" y1="22.685" y2="22.685" fill="none" stroke="#00a14b" stroke-miterlimit="10" stroke-width="1.5"/><polygon id="polygon8289" transform="translate(-.22991)" points="25.47 24.417 25.463 20.953 28.467 22.68" fill="#00a14b" stroke="#00a14b" stroke-miterlimit="10" stroke-width="1.5"/><polygon id="polygon8291" transform="translate(-.22991)" points="6.988 24.417 3.993 22.676 7 20.953" fill="#00a14b" stroke="#00a14b" stroke-miterlimit="10" stroke-width="1.5"/><line id="line8287-2" x1="2.1686" x2="2.1686" y1="26.413" y2="18.957" fill="none" stroke="#00a14b" stroke-miterlimit="10" stroke-width="1.5"/><line id="line8287-2-1" x1="29.831" x2="29.831" y1="26.413" y2="18.957" fill="none" stroke="#00a14b" stroke-miterlimit="10" stroke-width="1.5"/></svg>