Meh. I wouldn’t do expensive interpolation. When each element of the heat map is larger than one pixel, I would just paint the larger element with the single color. If too sharp to use directly, I’d apply a cheap gaussian blur.
I tried to do the Gateway & Client Setup, but somehow Ignition Script Console is not reading the easysvg and troubleshooting the gatewayscript without something to check is quite hard since I will need to update it.
../lib/core/common
and the /user-lib/pylib/
I tried putting the file here but it doesn’t seem to recognize it… I have also converted it from zip to .modl file to work but it doesn’t seem to be able
Code
import easysvg
import base64
import jnumeric.PyMultiarray as np
class heatPalette:
'''
Calculate the RGB value of a point on a gradient.
Usage: heatPalette(minval, maxval, [colors])
minval - minimun value to scale to
maxval - maximum value to scale to
colors - A list of RGB colors delineating a series of
adjacent linear color gradients between each pair.
Example: scale the values between 20 and 35 to a three color gradient.
p = heatPalette(20, 35, ['#0000FF', '#FFFF00', '#FF0000'])
After the instance is created, call it with a value to get the scaled color
p(20) returns '#0000FF'
p(25) returns '#AAAA55'
'''
def __init__(self, minval, maxval, colors):
self.minval = minval
self.maxval = maxval
self.colors = []
for color in colors:
if type(color).__name__ == 'str':
c = self.hex_to_rgb(color)
elif type(color).__name__ == 'list':
c = tuple(color)
elif type(color).__name__ == 'tuple':
c = color
self.colors.append(c)
def __call__(self, value):
return self.calc(value)
def calc(self, value):
import sys
# Smallest possible difference.
epsilon = sys.float_info.epsilon
# Keep value within limits
if value < self.minval:
value = self.minval
if value > self.maxval:
value = self.maxval
'''
Determine where the given value falls proportionality within
the range from minval->maxval and scale that fractional value
by the total number in the "colors" pallette.
'''
i_f = float(value-self.minval) / float(self.maxval-self.minval) * (len(self.colors)-1)
'''
Determine the lower index of the pair of color indices this
value corresponds and its fractional distance between the lower
and the upper colors.
'''
# Split into integer & fractional parts.
i, f = int(i_f // 1), i_f % 1
# Does it fall exactly on one of the color points?
if f < epsilon:
return self.rgb_to_hex(self.colors[i])
# Otherwise return a color within the range between them.
else:
(r1, g1, b1), (r2, g2, b2) = self.colors[i], self.colors[i+1]
print (r1, g1, b1), (r2, g2, b2)
print int(r1 + f*(r2-r1)), int(g1 + f*(g2-g1)), int(b1 + f*(b2-b1))
return self.rgb_to_hex((int(r1 + f*(r2-r1)), int(g1 + f*(g2-g1)), int(b1 + f*(b2-b1))))
def hex_to_rgb(self, value):
'''
Convert a hex RGB value to a tuple of 0-255 values
'''
value = value.lstrip('#')
return tuple(int(value[i:i+2], 16) for i in (0, 2, 4))
def rgb_to_hex(self, rgb):
'''
Convert an RGB tuple to a hex string
'''
return '#%02x%02x%02x' % rgb
# Convert to pyDataSet as easier to iterate
pyData = system.dataset.toPyDataSet(Row0.value.getValue())
sensornumber_row = 32
sensornumber_col = 32
new_row_size = 94
new_col_size = 94
row_pixel_spread = 2
col_pixel_spread = 2
resize=np.zeros((94,94), 'f')
imageid= 1
for m in range( 0, sensornumber_row):
for n in range (0, new_col_size):
count = n % (col_pixel_spread + 1)
if (count ==0):
resize[int((m*(row_pixel_spread + 1)))][n] = pyData[m][int((n/(col_pixel_spread+1)))]
if count!=0:
GRADIENT = pyData[m][int((n/(col_pixel_spread+1))+1)] - pyData[m][int((n/(col_pixel_spread+1)))]
GRADIENT = GRADIENT/(col_pixel_spread+1)
resize[int((m*(row_pixel_spread + 1)))][n] = (pyData[m][int((n/(col_pixel_spread+1)))] + GRADIENT*count)
for n in range( 0, new_col_size):
for m in range (0, new_row_size):
count = m % (row_pixel_spread + 1)
if (count == 0):
resize[m][n] = resize[m][n]
if (count != 0):
m1 = int(int(m/(row_pixel_spread+1))*(row_pixel_spread+1)+(row_pixel_spread+1))
GRADIENT = resize[m1][n] - resize[int(int(m/(row_pixel_spread+1))*(row_pixel_spread+1))][n]
GRADIENT = GRADIENT/(row_pixel_spread+1)
resize[m][n] = (resize[int(int(m/(row_pixel_spread+1))*(row_pixel_spread+1)),n] + GRADIENT*count)
#Define palette for heatmap
palette = heatPalette(20, 30, ['#0000FF', '#FFFF00', '#FF0000'])
# Width, Height of each cell in heatmap
cellSize = (50, 50)
# Define padding area aroung the heatmap
padding = 50
# Create SVG
svg = easysvg.SvgGenerator()
svg.begin(cellSize[0] * (new_col_size - 2) + 2 * padding, cellSize[1] * new_row_size + 2* padding)
for i, row in enumerate(resize):
for j, col in enumerate(list(row)[2:]):
color = palette(col)
svg.rect(j*cellSize[0] + padding, i * cellSize[1] + padding, cellSize[0], cellSize[1], color)
svg.end()
svg_string = svg.get_svg()
b64 = base64.b64encode(svg_string)
system.db.runPrepQuery("INSERT INTO 'svgtable' (`ID`, `converttime`, `base64svg`)", [NULL, current_timestamp(), b64])
The retrieving portion for the Client is quite clear, I might need to do some modification to the string to remove “/u003d/u003d” just in case
It should to go into /user-lib/pylib/site-packages
. You may also need to restart the gateway to get it to update.
Hi,
Just wondering where the SVG will live after creation.
Thanks
I'd recommend as a blob in a database table.
Sorry, I was not very clear with my question. When you assigned the SVG to the image component as follows:
self.getSibling('Image').props.source = 'data:image/svg+xml;base64,' + b64
Where does the generated SVG file live? Is this SVG file generated by the script only a one-time thing if I do not choose to store it in the DB?
Yes, that particular example is creating a data URI; the SVG is that base64 string, and the user agent (browser, mobile device, whatever) knows how to parse that encoded string back into an actual useful image. Useful for testing, but can be a performance cliff.
I see that it can cause performance issue when the data points exceed a certain number. Do you have other recommendations as to how to handle it? This method does have better performance than using the native Perspective table or chart.
It depends on how dynamic your data is. There's a lot of range between "dynamically calculate the image every time" and "cache the image forever".
If you do need it to be dynamic, you might want to take a look at the Webdev module (not free) or @pturmel's Blob Server module (free). With Phil's module, you could pre-calculate your image on some trigger with scripting and store it to the DB, then use Phil's module to 'serve' it up for as many clients as needed. With Webdev, you can run a script every time the image is requested, allowing you to implement any caching strategy you want.
Is this available in Maker?
Looks like it is.
The data is very dynamic. The client wanted to choose the time range, query the data, and build the heat map right then and there. So it does not make sense to pre-calculate the image and store it to the DB in this case. If I choose to go with Webdev, will I just utilize the script that @JordanCClark provided to generate the data URL in the Webdev environment and then return it to the image component?
You would skip this part. You would return the binary image data directly with the appropriate content type.
That makes sense. I am guessing the following code does not give me the binary image data:
svg = easysvg.SvgGenerator()
system.perspective.print(cellSize[0] * (data.columnCount) + 2 * padding)
system.perspective.print(cellSize[1] * data.rowCount + 2* padding)
svg.begin(cellSize[0] * (data.columnCount) + 2 * padding, cellSize[1] * data.rowCount + 2* padding)
for i, row in enumerate(data):
for j, col in enumerate(list(row)[2:]):
color = palette(col)
svg.rect(j*cellSize[0] + padding, i * cellSize[1] + padding, cellSize[0], cellSize[1], color)
svg.end()
svg_string = svg.get_svg()
How would I get the binary data from this?
Thanks
Actually, for that, just return the svg_string
and let WebDev encode it for you.
It is not working for me. Not sure if it is because the dataset is too big. I got an error that the localhost is unable to handle this request. Should I not have done the computing in the Web Dev? I am trying to create a heat map image with at least 500 columns X 200 rows of data.
code in Web Dev:
#query data from database
startTime = '2023-04-07 03:58:16.093'
endTime = '2023-04-07 04:40:51'
parameter = {'startDate': startTime, 'endDate': endTime}
ds = system.db.runNamedQuery('SlitRollReport','masterRollData', parameter)
colsRemove = ['Accumulated_Length','t_stamp','Scan_Count','Specification_Maximum','Specification_Minimum']
colsKeep = list(ds.columnNames)
for col in colsRemove: colsKeep.remove(col)
ds = system.dataset.filterColumns(ds, colsKeep)
data = system.dataset.toPyDataSet(ds)
print data
#_________________________________________________________________________________
import easysvg
import base64
class heatPalette:
'''
Calculate the RGB value of a point on a gradient.
Usage: heatPalette(minval, maxval, [colors])
minval - minimun value to scale to
maxval - maximum value to scale to
colors - A list of RGB colors delineating a series of
adjacent linear color gradients between each pair.
Example: scale the values between 20 and 35 to a three color gradient.
p = heatPalette(20, 35, ['#0000FF', '#FFFF00', '#FF0000'])
After the instance is created, call it with a value to get the scaled color
p(20) returns '#0000FF'
p(25) returns '#AAAA55'
'''
def __init__(self, minval, maxval, colors):
self.minval = minval
self.maxval = maxval
self.colors = []
for color in colors:
if type(color).__name__ == 'str':
c = self.hex_to_rgb(color)
elif type(color).__name__ == 'list':
c = tuple(color)
elif type(color).__name__ == 'tuple':
c = color
self.colors.append(c)
def __call__(self, value):
return self.calc(value)
def calc(self, value):
import sys
# Smallest possible difference.
epsilon = sys.float_info.epsilon
# Keep value within limits
if value < self.minval:
value = self.minval
if value > self.maxval:
value = self.maxval
'''
Determine where the given value falls proportionality within
the range from minval->maxval and scale that fractional value
by the total number in the "colors" pallette.
'''
i_f = float(value-self.minval) / float(self.maxval-self.minval) * (len(self.colors)-1)
'''
Determine the lower index of the pair of color indices this
value corresponds and its fractional distance between the lower
and the upper colors.
'''
# Split into integer & fractional parts.
i, f = int(i_f // 1), i_f % 1
# Does it fall exactly on one of the color points?
if f < epsilon:
return self.rgb_to_hex(self.colors[i])
# Otherwise return a color within the range between them.
else:
(r1, g1, b1), (r2, g2, b2) = self.colors[i], self.colors[i+1]
#print (r1, g1, b1), (r2, g2, b2)
#print int(r1 + f*(r2-r1)), int(g1 + f*(g2-g1)), int(b1 + f*(b2-b1))
return self.rgb_to_hex((int(r1 + f*(r2-r1)), int(g1 + f*(g2-g1)), int(b1 + f*(b2-b1))))
def hex_to_rgb(self, value):
'''
Convert a hex RGB value to a tuple of 0-255 values
'''
value = value.lstrip('#')
return tuple(int(value[i:i+2], 16) for i in (0, 2, 4))
def rgb_to_hex(self, rgb):
'''
Convert an RGB tuple to a hex string
'''
#Update the heat Map
#_________________________________#
# #
# INITIAL SETTINGS #
#_________________________________#
Min = 29
Max = 31
# Define palette for heatmap
palette = heatPalette(Min, Max, ['#FF0000', '#00FF00', '#00D900', '#FFFF00'])
# Width, Height of each cell in heatmap
cellSize = (30, 30)
# Define padding area aroung the heatmap
padding = 20
#this is the original script that creates a 32 x 32 pixels image
# Create SVG
svg = easysvg.SvgGenerator()
svg.begin(cellSize[0] * (data.columnCount) + 2 * padding, cellSize[1] * data.rowCount + 2* padding)
for i, row in enumerate(data):
for j, col in enumerate(list(row)[2:]):
color = palette(col)
svg.rect(j*cellSize[0] + padding, i * cellSize[1] + padding, cellSize[0], cellSize[1], color)
svg.end()
svg_string = svg.get_svg()
return {'bytes': svg_string}
Check your gateway logs to see the actual error.
Error from the gateway logs:
INFO | jvm 4 | 2023/04/28 16:05:43 | I [p.ClientSession ] [20:05:43]: WebSocket disconnected from session. session-project=SlitRollReport
INFO | jvm 4 | 2023/04/28 16:05:46 | <PyDataset rows:217 cols:512>
INFO | jvm 4 | 2023/04/28 16:05:46 | after svg begin
INFO | jvm 4 | 2023/04/28 16:05:47 | end svg
INFO | jvm 4 | 2023/04/28 16:05:47 | Bad Base64 input character at 0: 60(decimal)
it appears that the error occurred because the last line of code
return{'bytes': svg_string}
You shouldn't need any base64 with WebDev, as Phil alluded to.
base64 was not used in my code for WebDev. In the example, I returned the svg_string as bytes unless I misunderstood something here.