From b83b1f410a67dd16f914960de87111ab3fc9784c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zden=C4=9Bk=20Dole=C5=BEal?= Date: Fri, 27 Mar 2020 18:07:27 +0100 Subject: [PATCH 01/24] from stash init --- data_vis/__init__.py | 5 ++- data_vis/general.py | 11 +++--- data_vis/operators/data_load.py | 5 +++ data_vis/operators/line_chart.py | 2 +- data_vis/operators/point_chart.py | 2 + data_vis/utils/data_utils.py | 9 +---- src/data_manager.py | 64 +++++++++++++++++++++++++++++++ 7 files changed, 83 insertions(+), 15 deletions(-) create mode 100644 src/data_manager.py diff --git a/data_vis/__init__.py b/data_vis/__init__.py index 0e7f2a0..e2586f9 100644 --- a/data_vis/__init__.py +++ b/data_vis/__init__.py @@ -42,6 +42,7 @@ def draw(self, context): row.label(text='Data', icon='WORLD_DATA') row = layout.row() + row.operator(OBJECT_OT_test.bl_idname) row.operator('ui.dv_load_data') layout.label(text='Axis settings') @@ -107,7 +108,7 @@ def draw(self, context): preview_collections = {} -data_loaded = 0 +data_manager = DataManager() def chart_ops(self, context): @@ -142,6 +143,7 @@ def register(): bpy.utils.register_class(OBJECT_OT_pie_chart) bpy.utils.register_class(OBJECT_OT_line_chart) bpy.utils.register_class(OBJECT_OT_point_chart) + bpy.utils.register_class(OBJECT_OT_test) bpy.utils.register_class(FILE_OT_DVLoadFile) bpy.utils.register_class(DV_AddonPanel) bpy.utils.register_class(OBJECT_MT_AddChart) @@ -161,6 +163,7 @@ def unregister(): bpy.utils.unregister_class(OBJECT_OT_line_chart) bpy.utils.unregister_class(OBJECT_OT_point_chart) bpy.utils.unregister_class(FILE_OT_DVLoadFile) + bpy.utils.unregister_class(OBJECT_OT_test) bpy.utils.unregister_class(DV_LabelPropertyGroup) bpy.types.VIEW3D_MT_add.remove(chart_ops) diff --git a/data_vis/general.py b/data_vis/general.py index fc22476..c3cf598 100644 --- a/data_vis/general.py +++ b/data_vis/general.py @@ -315,23 +315,24 @@ def new_mat(self, color, alpha, name='Mat'): return mat def init_data(self, data_type): - data = list(bpy.data.scenes[0].dv_props.data) + data_manager = DataManager() + # data = list(bpy.data.scenes[0].dv_props.data) if hasattr(self, 'label_settings'): - self.init_labels(data) + self.init_labels(data_manager) try: - self.data = get_data_as_ll(data, data_type) + data = data_manager.get_parsed_data(data_type) except Exception as e: self.report({'ERROR'}, 'Data should be in X, Y, Z format (2 or 3 dimensions are currently supported).\nData should be in format according to chart type!' + str(e)) return False + self.data = data return True - def init_labels(self, data): + def init_labels(self, labels): if not self.label_settings.create: self.labels = [None, None, None] return if self.label_settings.from_data: - first_line = data.pop(0).value.split(',') length = len(first_line) if length == 2: self.labels = (first_line[0], '', first_line[1]) diff --git a/data_vis/operators/data_load.py b/data_vis/operators/data_load.py index 0021233..61e9466 100644 --- a/data_vis/operators/data_load.py +++ b/data_vis/operators/data_load.py @@ -1,4 +1,5 @@ import bpy +from src.data_manager import DataManager class FILE_OT_DVLoadFile(bpy.types.Operator): @@ -20,12 +21,16 @@ def invoke(self, context, event): def execute(self, context): bpy.data.scenes[0].dv_props.data.clear() + data_manager = DataManager() + data_manager.load_data(self.filepath) + with open(self.filepath, 'r') as file: line_n = 0 for row in file: line_n += 1 row_prop = bpy.data.scenes[0].dv_props.data.add() row_prop.value = row + self.report({'INFO'}, f'File: {self.filepath}, loaded {line_n} lines!') return {'FINISHED'} diff --git a/data_vis/operators/line_chart.py b/data_vis/operators/line_chart.py index 9a1cf79..da1482e 100644 --- a/data_vis/operators/line_chart.py +++ b/data_vis/operators/line_chart.py @@ -91,10 +91,10 @@ def __init__(self): def draw(self, context): super().draw(context) + layout = self.layout if self.bevel_edges: row = layout.row() row.prop(self, 'rounded') - layout = self.layout row = layout.row() row.prop(self, 'bevel_edges') if self.bevel_edges: diff --git a/data_vis/operators/point_chart.py b/data_vis/operators/point_chart.py index 1aa5d65..b68bcd6 100644 --- a/data_vis/operators/point_chart.py +++ b/data_vis/operators/point_chart.py @@ -1,4 +1,6 @@ import bpy +from mathutils import Vector +import math from data_vis.general import OBJECT_OT_generic_chart, DV_LabelPropertyGroup from data_vis.operators.features.axis import AxisFactory diff --git a/data_vis/utils/data_utils.py b/data_vis/utils/data_utils.py index bee5754..7729a85 100644 --- a/data_vis/utils/data_utils.py +++ b/data_vis/utils/data_utils.py @@ -1,11 +1,4 @@ -from enum import Enum - - -class DataType(Enum): - Numerical = 0 - Categorical = 1 - Categorical_3D = 2 - +from src.data_manager import DataType def get_row_list(row_data, data_type, separator): if data_type == DataType.Categorical: diff --git a/src/data_manager.py b/src/data_manager.py new file mode 100644 index 0000000..118c3d0 --- /dev/null +++ b/src/data_manager.py @@ -0,0 +1,64 @@ +from enum import Enum + + +class DataType(Enum): + Numerical = 0 + Categorical = 1 + + +class DataManager: + """ + Singleton that manages data access across the addon + pattern design from https://python-3-patterns-idioms-test.readthedocs.io/en/latest/Singleton.html + """ + class __DataManager: + def __init__(self): + self.raw_data = None + self.parsed_data = None + self.dimensions = 0 + + def set_data(self, data): + self.raw_data = data + + def get_raw_data(self): + return self.raw_data + + def load_data(self, filepath, separator=','): + self.parsed_data = None + with open(filepath, 'r') as file: + self.raw_data = [line.split(separator) for line in file] + self.dimensions = len(self.raw_data[0]) + + def get_parsed_data(self, data_type): + if self.raw_data is None: + print('No data has been loaded!') + return [[]] + + self.parsed_data = [] + data = self.raw_data + for row in data: + self.parsed_data.append(self.__get_row_list(row, data_type)) + + return self.parsed_data, first_line + + def __get_row_list(self, row, data_type): + if data_type == DataType.Categorical: + return [str(row[0]), float(row[1])] + elif data_type == DataType.Numerical: + return [float(x) for x in row] + + def get_data_range(self, col): + ... + + instance = None + + def __new__(cls): + if not DataManager.instance: + DataManager.instance = DataManager.__DataManager() + return DataManager.instance + + def __getattr__(self, name): + return getattr(self.instance, name) + + def __setattr__(self, name): + return setattr(self.instance, name) From 6e3cfa22a07b531e89289bac2b1a355a30150733 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zden=C4=9Bk=20Dole=C5=BEal?= Date: Mon, 30 Mar 2020 00:10:37 +0200 Subject: [PATCH 02/24] data manager implemented into charts --- data_vis/__init__.py | 13 +-- data_vis/data_manager.py | 130 ++++++++++++++++++++++++++++++ data_vis/general.py | 39 ++++----- data_vis/operators/bar_chart.py | 11 ++- data_vis/operators/data_load.py | 25 +++--- data_vis/operators/line_chart.py | 18 +++-- data_vis/operators/pie_chart.py | 11 ++- data_vis/operators/point_chart.py | 10 ++- data_vis/utils/data_utils.py | 2 +- src/data_manager.py | 64 --------------- 10 files changed, 206 insertions(+), 117 deletions(-) create mode 100644 data_vis/data_manager.py delete mode 100644 src/data_manager.py diff --git a/data_vis/__init__.py b/data_vis/__init__.py index e2586f9..4f49800 100644 --- a/data_vis/__init__.py +++ b/data_vis/__init__.py @@ -19,6 +19,7 @@ from .operators.pie_chart import OBJECT_OT_pie_chart from .operators.point_chart import OBJECT_OT_point_chart from .general import DV_LabelPropertyGroup +from .data_manager import DataManager class DV_AddonPanel(bpy.types.Panel): @@ -35,16 +36,18 @@ def draw(self, context): layout = self.layout data_storage = bpy.data.scenes[0] - - layout.label(text='Chart settings') - + row = layout.row() row.label(text='Data', icon='WORLD_DATA') row = layout.row() - row.operator(OBJECT_OT_test.bl_idname) row.operator('ui.dv_load_data') + box = layout.box() + box.label(text='Dims: ' + str(data_manager.dimensions)) + box.label(text='Labels: ' + str(data_manager.has_labels)) + box.label(text='Type: ' + str(data_manager.predicted_data_type)) + layout.label(text='Axis settings') row = layout.row() @@ -143,7 +146,6 @@ def register(): bpy.utils.register_class(OBJECT_OT_pie_chart) bpy.utils.register_class(OBJECT_OT_line_chart) bpy.utils.register_class(OBJECT_OT_point_chart) - bpy.utils.register_class(OBJECT_OT_test) bpy.utils.register_class(FILE_OT_DVLoadFile) bpy.utils.register_class(DV_AddonPanel) bpy.utils.register_class(OBJECT_MT_AddChart) @@ -163,7 +165,6 @@ def unregister(): bpy.utils.unregister_class(OBJECT_OT_line_chart) bpy.utils.unregister_class(OBJECT_OT_point_chart) bpy.utils.unregister_class(FILE_OT_DVLoadFile) - bpy.utils.unregister_class(OBJECT_OT_test) bpy.utils.unregister_class(DV_LabelPropertyGroup) bpy.types.VIEW3D_MT_add.remove(chart_ops) diff --git a/data_vis/data_manager.py b/data_vis/data_manager.py new file mode 100644 index 0000000..43d8c58 --- /dev/null +++ b/data_vis/data_manager.py @@ -0,0 +1,130 @@ +from enum import Enum +import bpy + + +class DataType(Enum): + Numerical = 0 + Categorical = 1 + Invalid = 2 + + +class DataManager: + """ + Singleton that manages data access across the addon + pattern design from https://python-3-patterns-idioms-test.readthedocs.io/en/latest/Singleton.html + """ + class __DataManager: + + def __init__(self): + self.raw_data = None + self.parsed_data = None + self.predicted_data_type = None + self.has_labels = False + self.labels = () + self.dimensions = 0 + + def set_data(self, data): + self.raw_data = data + + def get_raw_data(self): + return self.raw_data + + def load_data(self, filepath, separator=','): + self.__init__() + try: + with open(filepath, 'r') as file: + self.raw_data = [line.split(separator) for line in file] + self.analyse_data() + except UnicodeDecodeError as e: + self.predicted_data_type = DataType.Invalid + return 0 + + return len(self.raw_data) + + def analyse_data(self): + total = 0 + for i, col in enumerate(self.raw_data[0]): + try: + row = float(col) + print(row) + except Exception as e: + print(e) + total += 1 + + if total == len(self.raw_data[0]): + self.has_labels = True + + prev_row_info = {} + for i in range(1, len(self.raw_data)): + row_info = {'floats': 0, 'strings': 0, 'first_string': False} + row = self.raw_data[1] + for j, col in enumerate(row): + try: + float(col) + row_info['floats'] += 1 + except Exception as e: + if j == 0: + row_info['first_string'] = True + row_info['strings'] += 1 + + if prev_row_info and prev_row_info != row_info: + print('Invalid entry: {}: {}'.format(i), row) + self.predicted_data_type = DataType.Invalid + prev_row_info = row_info + + if self.predicted_data_type != DataType.Invalid: + if row_info['first_string'] and row_info['strings'] == 1 and row_info['floats'] > 0: + self.predicted_data_type = DataType.Categorical + elif row_info['strings'] == 0 and row_info['floats'] >= 2: + self.predicted_data_type = DataType.Numerical + else: + self.predicted_data_type = DataType.Invalid + + self.parse_data() + + self.dimensions = len(self.raw_data[0]) + + def parse_data(self): + if self.raw_data is None: + print('No data has been loaded!') + self.parsed_data = [[]] + return + + self.parsed_data = [] + data = self.raw_data + + if self.has_labels: + self.labels = tuple(str(x) for x in self.raw_data[0]) + start_idx = 1 + else: + start_idx = 0 + for i in range(start_idx, len(data)): + self.parsed_data.append(self.__get_row_list(self.raw_data[i])) + + def get_parsed_data(self): + return self.parsed_data + + def get_labels(self): + return self.labels + + def is_type(self, data_type, dims): + return data_type == self.predicted_data_type and self.dimensions <= dims and dims > 1 and dims <= 3 + + def __get_row_list(self, row): + if self.predicted_data_type == DataType.Categorical: + return [str(row[0]), float(row[1])] + elif self.predicted_data_type == DataType.Numerical: + return [float(x) for x in row] + + instance = None + + def __new__(cls): + if not DataManager.instance: + DataManager.instance = DataManager.__DataManager() + return DataManager.instance + + def __getattr__(self, name): + return getattr(self.instance, name) + + def __setattr__(self, name): + return setattr(self.instance, name) diff --git a/data_vis/general.py b/data_vis/general.py index c3cf598..35f9b10 100644 --- a/data_vis/general.py +++ b/data_vis/general.py @@ -4,6 +4,7 @@ from mathutils import Vector from data_vis.utils.data_utils import get_data_as_ll, DataType +from data_vis.data_manager import DataManager class CONST: @@ -123,6 +124,12 @@ def __init__(self): self.container_object = None self.labels = [] self.chart_origin = (0, 0, 0) + self.dm = DataManager() + if hasattr(self, 'dimensions'): + self.dimensions = str(self.dm.dimensions) + + if hasattr(self, 'data_type'): + self.data_type = '0' if self.dm.predicted_data_type == DataType.Numerical else '1' def draw(self, context): layout = self.layout @@ -130,7 +137,7 @@ def draw(self, context): row = layout.row() row.prop(self, 'data_type') - only_2d = hasattr(self, 'only_2d') + only_2d = hasattr(self, 'dimensions') numerical = True if hasattr(self, 'data_type'): if self.data_type == '1': @@ -138,7 +145,7 @@ def draw(self, context): only_2d = only_2d or not numerical - if not only_2d: + if hasattr(self, 'dimensions') and self.dm.predicted_data_type != DataType.Categorical: row = layout.row() row.prop(self, 'dimensions') @@ -150,7 +157,7 @@ def draw(self, context): if not self.auto_ranges: row = layout.row() row.prop(self, 'x_axis_range') - if not only_2d and self.dimensions == '3': + if self.dm.dimensions == 3: row = layout.row() row.prop(self, 'y_axis_range') @@ -161,7 +168,7 @@ def draw(self, context): row = layout.row() if numerical: row.prop(self, 'x_axis_step', text='x') - if not only_2d and self.dimensions == '3': + if self.dm.dimensions == 3: row.prop(self, 'y_axis_step', text='y') row.prop(self, 'z_axis_step', text='z') @@ -178,14 +185,14 @@ def draw(self, context): if not self.label_settings.from_data: row = layout.row() row.prop(self.label_settings, 'x_label') - if not only_2d and self.dimensions == '3': + if self.dm.dimensions == 3: row.prop(self.label_settings, 'y_label') row.prop(self.label_settings, 'z_label') @classmethod def poll(cls, context): '''Default behavior for every chart poll method (when data is not available, cannot create chart)''' - return len(bpy.data.scenes[0].dv_props.data) > 0 + return self.dm.parsed_data is not None def execute(self, context): raise NotImplementedError('Execute method should be implemented in every chart operator!') @@ -221,6 +228,7 @@ def data_type_as_enum(self): elif self.data_type == '1': return DataType.Categorical + def create_y_axis(self, min_val, max_val, offset, padding): bpy.ops.object.empty_add() axis_cont = bpy.context.object @@ -314,25 +322,20 @@ def new_mat(self, color, alpha, name='Mat'): mat.diffuse_color = (*color, alpha) return mat - def init_data(self, data_type): - data_manager = DataManager() - # data = list(bpy.data.scenes[0].dv_props.data) + def init_data(self): if hasattr(self, 'label_settings'): - self.init_labels(data_manager) - try: - data = data_manager.get_parsed_data(data_type) - except Exception as e: - self.report({'ERROR'}, 'Data should be in X, Y, Z format (2 or 3 dimensions are currently supported).\nData should be in format according to chart type!' + str(e)) - return False + self.init_labels() + data = self.dm.get_parsed_data() self.data = data return True - def init_labels(self, labels): + def init_labels(self): if not self.label_settings.create: - self.labels = [None, None, None] + self.labels = (None, None, None) return - if self.label_settings.from_data: + if self.dm.has_labels: + first_line = self.dm.get_labels() length = len(first_line) if length == 2: self.labels = (first_line[0], '', first_line[1]) diff --git a/data_vis/operators/bar_chart.py b/data_vis/operators/bar_chart.py index 6ce13b5..0b43857 100644 --- a/data_vis/operators/bar_chart.py +++ b/data_vis/operators/bar_chart.py @@ -3,10 +3,11 @@ from mathutils import Vector -from data_vis.utils.data_utils import get_data_as_ll, find_data_range, normalize_value, find_axis_range, DataType +from data_vis.utils.data_utils import get_data_as_ll, find_data_range, normalize_value, find_axis_range from data_vis.utils.color_utils import ColorGen from data_vis.general import OBJECT_OT_generic_chart, CONST, Properties, DV_LabelPropertyGroup from data_vis.operators.features.axis import AxisFactory +from data_vis.data_manager import DataManager, DataType class OBJECT_OT_bar_chart(OBJECT_OT_generic_chart): @@ -91,6 +92,11 @@ class OBJECT_OT_bar_chart(OBJECT_OT_generic_chart): type=DV_LabelPropertyGroup ) + @classmethod + def poll(cls, context): + dm = DataManager() + return dm.is_type(DataType.Numerical, 3) or dm.is_type(DataType.Categorical, 2) + def draw(self, context): super().draw(context) layout = self.layout @@ -111,8 +117,7 @@ def data_type_as_enum(self): return DataType.Categorical def execute(self, context): - if not self.init_data(self.data_type_as_enum()): - return {'CANCELLED'} + self.init_data() if self.data_type_as_enum() == DataType.Numerical: if self.auto_ranges: self.init_range(self.data) diff --git a/data_vis/operators/data_load.py b/data_vis/operators/data_load.py index 61e9466..d8b452d 100644 --- a/data_vis/operators/data_load.py +++ b/data_vis/operators/data_load.py @@ -1,5 +1,5 @@ import bpy -from src.data_manager import DataManager +from data_vis.data_manager import DataManager class FILE_OT_DVLoadFile(bpy.types.Operator): @@ -22,15 +22,18 @@ def invoke(self, context, event): def execute(self, context): bpy.data.scenes[0].dv_props.data.clear() data_manager = DataManager() - data_manager.load_data(self.filepath) - - with open(self.filepath, 'r') as file: - line_n = 0 - for row in file: - line_n += 1 - row_prop = bpy.data.scenes[0].dv_props.data.add() - row_prop.value = row - - self.report({'INFO'}, f'File: {self.filepath}, loaded {line_n} lines!') + line_n = data_manager.load_data(self.filepath) + + # with open(self.filepath, 'r') as file: + # line_n = 0 + # for row in file: + # line_n += 1 + # row_prop = bpy.data.scenes[0].dv_props.data.add() + # row_prop.value = row + + report_type = {'INFO'} + if line_n == 0: + report_type = {'WARNING'} + self.report(report_type, f'File: {self.filepath}, loaded {line_n} lines!') return {'FINISHED'} diff --git a/data_vis/operators/line_chart.py b/data_vis/operators/line_chart.py index da1482e..6b8eb39 100644 --- a/data_vis/operators/line_chart.py +++ b/data_vis/operators/line_chart.py @@ -1,13 +1,12 @@ import bpy import math -from itertools import zip_longest from mathutils import Vector -from data_vis.utils.data_utils import get_data_as_ll, find_data_range, find_axis_range, normalize_value, get_data_in_range, DataType +from data_vis.utils.data_utils import find_data_range, find_axis_range, normalize_value, get_data_in_range from data_vis.operators.features.axis import AxisFactory from data_vis.general import OBJECT_OT_generic_chart, DV_LabelPropertyGroup -from data_vis.general import CONST +from data_vis.data_manager import DataManager, DataType class OBJECT_OT_line_chart(OBJECT_OT_generic_chart): @@ -73,6 +72,7 @@ class OBJECT_OT_line_chart(OBJECT_OT_generic_chart): ) def __init__(self): + super().__init__() self.only_2d = True self.x_delta = 0.2 self.bevel_obj_size = (0.01, 0.01, 0.01) @@ -89,6 +89,11 @@ def __init__(self): }, } + @classmethod + def poll(cls, context): + dm = DataManager() + return dm.is_type(DataType.Numerical, 2) or dm.is_type(DataType.Categorical, 2) + def draw(self, context): super().draw(context) layout = self.layout @@ -102,11 +107,8 @@ def draw(self, context): row.prop(self, 'rounded') def execute(self, context): - if not self.init_data(self.data_type_as_enum()): - return {'CANCELLED'} - if len(self.data[0]) > 2: - self.report({'ERROR'}, 'Line chart supports X Y values only') - return {'CANCELLED'} + self.init_data() + self.create_container() if self.data_type_as_enum() == DataType.Numerical: diff --git a/data_vis/operators/pie_chart.py b/data_vis/operators/pie_chart.py index 096755e..fad1493 100644 --- a/data_vis/operators/pie_chart.py +++ b/data_vis/operators/pie_chart.py @@ -2,9 +2,10 @@ import math from mathutils import Matrix, Vector -from data_vis.utils.data_utils import get_data_as_ll, find_data_range, DataType +from data_vis.utils.data_utils import get_data_as_ll, find_data_range from data_vis.utils.color_utils import sat_col_gen, ColorGen from data_vis.general import OBJECT_OT_generic_chart, CONST +from data_vis.data_manager import DataManager, DataType class OBJECT_OT_pie_chart(OBJECT_OT_generic_chart): @@ -27,6 +28,11 @@ class OBJECT_OT_pie_chart(OBJECT_OT_generic_chart): max=1.0 ) + @classmethod + def poll(cls, context): + dm = DataManager() + return not dm.has_labels and dm.is_type(DataType.Categorical, 2) + def draw(self, context): layout = self.layout row = layout.row() @@ -37,8 +43,7 @@ def draw(self, context): def execute(self, context): self.slices = [] self.materials = [] - if not self.init_data(DataType.Categorical): - return {'CANCELLED'} + self.init_data() data_min = min(self.data, key=lambda entry: entry[1])[1] if data_min <= 0: diff --git a/data_vis/operators/point_chart.py b/data_vis/operators/point_chart.py index b68bcd6..8eb4021 100644 --- a/data_vis/operators/point_chart.py +++ b/data_vis/operators/point_chart.py @@ -4,8 +4,9 @@ from data_vis.general import OBJECT_OT_generic_chart, DV_LabelPropertyGroup from data_vis.operators.features.axis import AxisFactory -from data_vis.utils.data_utils import get_data_as_ll, find_data_range, normalize_value, find_axis_range, DataType +from data_vis.utils.data_utils import get_data_as_ll, find_data_range, normalize_value, find_axis_range from data_vis.utils.color_utils import sat_col_gen, color_to_triplet, reverse_iterator, ColorGen +from data_vis.data_manager import DataManager, DataType from mathutils import Vector import math @@ -85,6 +86,10 @@ class OBJECT_OT_point_chart(OBJECT_OT_generic_chart): type=DV_LabelPropertyGroup ) + @classmethod + def poll(cls, context): + return DataManager().is_type(DataType.Numerical, 3) + def draw(self, context): super().draw(context) layout = self.layout @@ -99,8 +104,7 @@ def init_range(self, data): self.y_axis_range = find_axis_range(data, 1) def execute(self, context): - if not self.init_data(DataType.Numerical): - return {'CANCELLED'} + self.init_data() if self.auto_ranges: self.init_range(self.data) diff --git a/data_vis/utils/data_utils.py b/data_vis/utils/data_utils.py index 7729a85..612954f 100644 --- a/data_vis/utils/data_utils.py +++ b/data_vis/utils/data_utils.py @@ -1,4 +1,4 @@ -from src.data_manager import DataType +from data_vis.data_manager import DataType def get_row_list(row_data, data_type, separator): if data_type == DataType.Categorical: diff --git a/src/data_manager.py b/src/data_manager.py deleted file mode 100644 index 118c3d0..0000000 --- a/src/data_manager.py +++ /dev/null @@ -1,64 +0,0 @@ -from enum import Enum - - -class DataType(Enum): - Numerical = 0 - Categorical = 1 - - -class DataManager: - """ - Singleton that manages data access across the addon - pattern design from https://python-3-patterns-idioms-test.readthedocs.io/en/latest/Singleton.html - """ - class __DataManager: - def __init__(self): - self.raw_data = None - self.parsed_data = None - self.dimensions = 0 - - def set_data(self, data): - self.raw_data = data - - def get_raw_data(self): - return self.raw_data - - def load_data(self, filepath, separator=','): - self.parsed_data = None - with open(filepath, 'r') as file: - self.raw_data = [line.split(separator) for line in file] - self.dimensions = len(self.raw_data[0]) - - def get_parsed_data(self, data_type): - if self.raw_data is None: - print('No data has been loaded!') - return [[]] - - self.parsed_data = [] - data = self.raw_data - for row in data: - self.parsed_data.append(self.__get_row_list(row, data_type)) - - return self.parsed_data, first_line - - def __get_row_list(self, row, data_type): - if data_type == DataType.Categorical: - return [str(row[0]), float(row[1])] - elif data_type == DataType.Numerical: - return [float(x) for x in row] - - def get_data_range(self, col): - ... - - instance = None - - def __new__(cls): - if not DataManager.instance: - DataManager.instance = DataManager.__DataManager() - return DataManager.instance - - def __getattr__(self, name): - return getattr(self.instance, name) - - def __setattr__(self, name): - return setattr(self.instance, name) From eb1625f1c3537d1d08a060d8552146dd4c31e902 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zden=C4=9Bk=20Dole=C5=BEal?= Date: Mon, 30 Mar 2020 18:37:14 +0200 Subject: [PATCH 03/24] basic shader for bar chart --- data_vis/colors.py | 51 +++++++++++++++++++++++++++++++++ data_vis/operators/bar_chart.py | 8 ++++-- 2 files changed, 57 insertions(+), 2 deletions(-) create mode 100644 data_vis/colors.py diff --git a/data_vis/colors.py b/data_vis/colors.py new file mode 100644 index 0000000..828f996 --- /dev/null +++ b/data_vis/colors.py @@ -0,0 +1,51 @@ +import bpy + + +class NodeShader: + # Constant + # Contrast + # Gradient / Ramp color + def __init__(self, base_color): + self.base_color = self.__add_alpha(base_color, 1) + self.material = self.create_color_ramp() + + def create_color_ramp(self): + material = bpy.data.materials.new(name='ChartMat') + material.use_nodes = True + + nodes = material.node_tree.nodes + + bsdf_node = nodes.get('Principled BSDF') + + cr_node = nodes.new('ShaderNodeValToRGB') + cr_node.location = (-300, 0) + + cr_node.color_ramp.elements[0].color = (1, 1, 1, 1) + cr_node.color_ramp.elements[1].color = self.base_color + + math_node = nodes.new('ShaderNodeMath') + math_node.location = (-500, 0) + + # normalize + math_node.operation = 'MULTIPLY' + math_node.inputs[1].default_value = 2.0 + + xyz_sep_node = nodes.new('ShaderNodeSeparateXYZ') + xyz_sep_node.location = (-700, 0) + + oi_node = nodes.new('ShaderNodeObjectInfo') + oi_node.location = (-900, 0) + + links = material.node_tree.links + links.new(oi_node.outputs[0], xyz_sep_node.inputs[0]) + links.new(xyz_sep_node.outputs[2], math_node.inputs[0]) + links.new(math_node.outputs[0], cr_node.inputs[0]) + links.new(cr_node.outputs[0], bsdf_node.inputs[0]) + + return material + + def __add_alpha(self, color, alpha): + return (color[0], color[1], color[2], alpha) + + + diff --git a/data_vis/operators/bar_chart.py b/data_vis/operators/bar_chart.py index 0b43857..5f8ce06 100644 --- a/data_vis/operators/bar_chart.py +++ b/data_vis/operators/bar_chart.py @@ -8,6 +8,7 @@ from data_vis.general import OBJECT_OT_generic_chart, CONST, Properties, DV_LabelPropertyGroup from data_vis.operators.features.axis import AxisFactory from data_vis.data_manager import DataManager, DataType +from data_vis.colors import NodeShader class OBJECT_OT_bar_chart(OBJECT_OT_generic_chart): @@ -137,7 +138,8 @@ def execute(self, context): data_min = min(self.data, key=lambda val: val[1])[1] data_max = max(self.data, key=lambda val: val[1])[1] - color_gen = ColorGen(self.color_shade, (data_min, data_max)) + #color_gen = ColorGen(self.color_shade, (data_min, data_max)) + shader = NodeShader(self.color_shade) if self.dimensions == '2': value_index = 1 @@ -167,7 +169,9 @@ def execute(self, context): y_norm = normalize_value(entry[1], self.y_axis_range[0], self.y_axis_range[1]) bar_obj.scale = (self.bar_size[0], self.bar_size[1], z_norm * 0.5) bar_obj.location = (x_norm, y_norm, z_norm * 0.5) - bar_obj.active_material = self.new_mat(color_gen.next(entry[value_index]), 1) + + bar_obj.data.materials.append(shader.material) + bar_obj.active_material = shader.material # self.new_mat(color_gen.next(entry[value_index]), 1) bar_obj.parent = self.container_object AxisFactory.create( From e00b5353ea2cc0d2123e75a3f143806dbd77cf1d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zden=C4=9Bk=20Dole=C5=BEal?= Date: Tue, 31 Mar 2020 00:57:19 +0200 Subject: [PATCH 04/24] Pseudorandom shader --- data_vis/colors.py | 65 ++++++++++++++++++++++++++++----- data_vis/general.py | 1 - data_vis/operators/bar_chart.py | 13 ++++++- 3 files changed, 68 insertions(+), 11 deletions(-) diff --git a/data_vis/colors.py b/data_vis/colors.py index 828f996..c1c38a7 100644 --- a/data_vis/colors.py +++ b/data_vis/colors.py @@ -1,15 +1,31 @@ import bpy +from enum import Enum class NodeShader: - # Constant - # Contrast - # Gradient / Ramp color - def __init__(self, base_color): + class Type(Enum): + Constant = 0 + Random = 1 + Gradient = 2 + + def str_to_type(value): + if str(value) == '0' or value == 'Constant': + return NodeShader.Type.Constant + if str(value) == '1' or value == 'Random': + return NodeShader.Type.Random + if str(value) == '2' or value == 'Gradient': + return NodeShader.Type.Gradient + + def __init__(self, base_color, shader_type=Type.Constant): self.base_color = self.__add_alpha(base_color, 1) - self.material = self.create_color_ramp() + self.shader_type = shader_type - def create_color_ramp(self): + if self.shader_type == NodeShader.Type.Random: + self.material = self.create_random_shader() + else: + self.material = self.create_shader() + + def create_shader(self): material = bpy.data.materials.new(name='ChartMat') material.use_nodes = True @@ -20,8 +36,12 @@ def create_color_ramp(self): cr_node = nodes.new('ShaderNodeValToRGB') cr_node.location = (-300, 0) - cr_node.color_ramp.elements[0].color = (1, 1, 1, 1) - cr_node.color_ramp.elements[1].color = self.base_color + if self.shader_type == NodeShader.Type.Constant: + cr_node.color_ramp.elements[0].color = self.base_color + cr_node.color_ramp.elements[1].color = self.base_color + elif self.shader_type == NodeShader.Type.Gradient: + cr_node.color_ramp.elements[0].color = (1, 1, 1, 1) + cr_node.color_ramp.elements[1].color = self.base_color math_node = nodes.new('ShaderNodeMath') math_node.location = (-500, 0) @@ -33,7 +53,7 @@ def create_color_ramp(self): xyz_sep_node = nodes.new('ShaderNodeSeparateXYZ') xyz_sep_node.location = (-700, 0) - oi_node = nodes.new('ShaderNodeObjectInfo') + oi_node = nodes.new('ShaderNodeObjectInfo') oi_node.location = (-900, 0) links = material.node_tree.links @@ -44,6 +64,33 @@ def create_color_ramp(self): return material + def create_random_shader(self): + material = bpy.data.materials.new(name='ChartMat') + material.use_nodes = True + + nodes = material.node_tree.nodes + + bsdf_node = nodes.get('Principled BSDF') + + cr_node = nodes.new('ShaderNodeValToRGB') + cr_node.location = (-300, 0) + + cr_node.color_ramp.elements.new(position=0.5) + cr_node.color_ramp.elements[0].color = (1, 0, 0, 1) + cr_node.color_ramp.elements[0].position = 0.1 + cr_node.color_ramp.elements[1].color = (0, 1, 0, 1) + cr_node.color_ramp.elements[2].color = (0, 0, 1, 1) + cr_node.color_ramp.elements[2].position = 0.9 + + oi_node = nodes.new('ShaderNodeObjectInfo') + oi_node.location = (-900, 0) + + links = material.node_tree.links + links.new(oi_node.outputs[4], cr_node.inputs[0]) + links.new(cr_node.outputs[0], bsdf_node.inputs[0]) + + return material + def __add_alpha(self, color, alpha): return (color[0], color[1], color[2], alpha) diff --git a/data_vis/general.py b/data_vis/general.py index 35f9b10..739df2f 100644 --- a/data_vis/general.py +++ b/data_vis/general.py @@ -228,7 +228,6 @@ def data_type_as_enum(self): elif self.data_type == '1': return DataType.Categorical - def create_y_axis(self, min_val, max_val, offset, padding): bpy.ops.object.empty_add() axis_cont = bpy.context.object diff --git a/data_vis/operators/bar_chart.py b/data_vis/operators/bar_chart.py index 5f8ce06..7ece383 100644 --- a/data_vis/operators/bar_chart.py +++ b/data_vis/operators/bar_chart.py @@ -81,6 +81,16 @@ class OBJECT_OT_bar_chart(OBJECT_OT_generic_chart): default=0.1 ) + color_type: bpy.props.EnumProperty( + name='Shader color type', + items=( + ('0', 'Constant', 'One color'), + ('1', 'Random', 'Random colors'), + ('2', 'Gradient', 'Gradient') + ), + default='2' + ) + color_shade: bpy.props.FloatVectorProperty( name='Color', subtype='COLOR', @@ -105,6 +115,7 @@ def draw(self, context): row.prop(self, 'bar_size') row = layout.row() + row.prop(self, 'color_type') row.prop(self, 'color_shade') def init_range(self, data): @@ -139,7 +150,7 @@ def execute(self, context): data_max = max(self.data, key=lambda val: val[1])[1] #color_gen = ColorGen(self.color_shade, (data_min, data_max)) - shader = NodeShader(self.color_shade) + shader = NodeShader(self.color_shade, NodeShader.Type.str_to_type(self.color_type)) if self.dimensions == '2': value_index = 1 From a7aaa769d4e37e8e37da8e7353d1d2465a1af24d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zden=C4=9Bk=20Dole=C5=BEal?= Date: Tue, 31 Mar 2020 01:39:42 +0200 Subject: [PATCH 05/24] basic draw for color properties in general chart --- data_vis/__init__.py | 24 +++------ data_vis/colors.py | 93 ++++++++++++++++++++++++--------- data_vis/general.py | 43 ++++++++++++++- data_vis/operators/bar_chart.py | 26 ++------- data_vis/operators/data_load.py | 1 - 5 files changed, 118 insertions(+), 69 deletions(-) diff --git a/data_vis/__init__.py b/data_vis/__init__.py index 4f49800..92f0c05 100644 --- a/data_vis/__init__.py +++ b/data_vis/__init__.py @@ -1,5 +1,5 @@ bl_info = { - 'name': 'Data Visualisation', + 'name': 'DataVis', 'author': 'Zdenek Dolezal', 'description': '', 'blender': (2, 80, 0), @@ -18,7 +18,7 @@ from .operators.line_chart import OBJECT_OT_line_chart from .operators.pie_chart import OBJECT_OT_pie_chart from .operators.point_chart import OBJECT_OT_point_chart -from .general import DV_LabelPropertyGroup +from .general import DV_LabelPropertyGroup, DV_ColorPropertyGroup from .data_manager import DataManager @@ -26,11 +26,11 @@ class DV_AddonPanel(bpy.types.Panel): ''' Menu panel used for loading data and managing addon settings ''' - bl_label = 'Data visualisation' + bl_label = 'DataVis' bl_idname = 'OBJECT_PT_dv' bl_space_type = 'VIEW_3D' bl_region_type = 'UI' - bl_category = 'Data Visualisation' + bl_category = 'DataVis' def draw(self, context): layout = self.layout @@ -60,22 +60,10 @@ def draw(self, context): row.prop(data_storage.dv_props, 'axis_tick_mark_height') -class DV_RowProp(bpy.types.PropertyGroup): - ''' - One row of loaded data as string - ''' - value: bpy.props.StringProperty() - - class DV_PropertyGroup(bpy.types.PropertyGroup): ''' General addon settings and data are stored in this property group. ''' - data: bpy.props.CollectionProperty( - name='Data', - type=DV_RowProp - ) - text_size: bpy.props.FloatProperty( name='Text size', default=0.05, @@ -139,9 +127,9 @@ def remove_icons(): def register(): load_icons() - bpy.utils.register_class(DV_RowProp) bpy.utils.register_class(DV_PropertyGroup) bpy.utils.register_class(DV_LabelPropertyGroup) + bpy.utils.register_class(DV_ColorPropertyGroup) bpy.utils.register_class(OBJECT_OT_bar_chart) bpy.utils.register_class(OBJECT_OT_pie_chart) bpy.utils.register_class(OBJECT_OT_line_chart) @@ -157,7 +145,6 @@ def register(): def unregister(): remove_icons() bpy.utils.unregister_class(DV_PropertyGroup) - bpy.utils.unregister_class(DV_RowProp) bpy.utils.unregister_class(OBJECT_MT_AddChart) bpy.utils.unregister_class(DV_AddonPanel) bpy.utils.unregister_class(OBJECT_OT_bar_chart) @@ -166,6 +153,7 @@ def unregister(): bpy.utils.unregister_class(OBJECT_OT_point_chart) bpy.utils.unregister_class(FILE_OT_DVLoadFile) bpy.utils.unregister_class(DV_LabelPropertyGroup) + bpy.utils.unregister_class(DV_ColorPropertyGroup) bpy.types.VIEW3D_MT_add.remove(chart_ops) diff --git a/data_vis/colors.py b/data_vis/colors.py index c1c38a7..b7c09be 100644 --- a/data_vis/colors.py +++ b/data_vis/colors.py @@ -16,16 +16,21 @@ def str_to_type(value): if str(value) == '2' or value == 'Gradient': return NodeShader.Type.Gradient - def __init__(self, base_color, shader_type=Type.Constant): + def __init__(self, base_color, shader_type=Type.Constant, scale=1.0, location_z=0): self.base_color = self.__add_alpha(base_color, 1) self.shader_type = shader_type + self.scale = scale if self.shader_type == NodeShader.Type.Random: self.material = self.create_random_shader() + elif self.shader_type == NodeShader.Type.Constant: + self.material = self.create_const_shader() + elif self.shader_type == NodeShader.Type.Gradient: + self.material = self.create_gradient_shader(location_z) else: - self.material = self.create_shader() + raise AttributeError('Unsupported shader type!') - def create_shader(self): + def create_random_shader(self): material = bpy.data.materials.new(name='ChartMat') material.use_nodes = True @@ -36,19 +41,42 @@ def create_shader(self): cr_node = nodes.new('ShaderNodeValToRGB') cr_node.location = (-300, 0) - if self.shader_type == NodeShader.Type.Constant: - cr_node.color_ramp.elements[0].color = self.base_color - cr_node.color_ramp.elements[1].color = self.base_color - elif self.shader_type == NodeShader.Type.Gradient: - cr_node.color_ramp.elements[0].color = (1, 1, 1, 1) - cr_node.color_ramp.elements[1].color = self.base_color + cr_node.color_ramp.elements.new(position=0.5) + cr_node.color_ramp.elements[0].color = (1, 0, 0, 1) + cr_node.color_ramp.elements[0].position = 0.1 + cr_node.color_ramp.elements[1].color = (0, 1, 0, 1) + cr_node.color_ramp.elements[2].color = (0, 0, 1, 1) + cr_node.color_ramp.elements[2].position = 0.9 - math_node = nodes.new('ShaderNodeMath') - math_node.location = (-500, 0) + oi_node = nodes.new('ShaderNodeObjectInfo') + oi_node.location = (-900, 0) + + links = material.node_tree.links + links.new(oi_node.outputs[4], cr_node.inputs[0]) + links.new(cr_node.outputs[0], bsdf_node.inputs[0]) + + return material + + def create_const_shader(self): + material = bpy.data.materials.new(name='ChartMat') + material.use_nodes = True + + nodes = material.node_tree.nodes + + bsdf_node = nodes.get('Principled BSDF') + + cr_node = nodes.new('ShaderNodeValToRGB') + cr_node.location = (-300, 0) + + cr_node.color_ramp.elements[0].color = self.base_color + cr_node.color_ramp.elements[1].color = self.base_color + + mul_node = nodes.new('ShaderNodeMath') + mul_node.location = (-500, 0) # normalize - math_node.operation = 'MULTIPLY' - math_node.inputs[1].default_value = 2.0 + mul_node.operation = 'MULTIPLY' + mul_node.inputs[1].default_value = self.scale xyz_sep_node = nodes.new('ShaderNodeSeparateXYZ') xyz_sep_node.location = (-700, 0) @@ -58,13 +86,13 @@ def create_shader(self): links = material.node_tree.links links.new(oi_node.outputs[0], xyz_sep_node.inputs[0]) - links.new(xyz_sep_node.outputs[2], math_node.inputs[0]) - links.new(math_node.outputs[0], cr_node.inputs[0]) + links.new(xyz_sep_node.outputs[2], mul_node.inputs[0]) + links.new(mul_node.outputs[0], cr_node.inputs[0]) links.new(cr_node.outputs[0], bsdf_node.inputs[0]) return material - def create_random_shader(self): + def create_gradient_shader(self, location_z): material = bpy.data.materials.new(name='ChartMat') material.use_nodes = True @@ -74,19 +102,34 @@ def create_random_shader(self): cr_node = nodes.new('ShaderNodeValToRGB') cr_node.location = (-300, 0) + + cr_node.color_ramp.elements[0].color = (1, 1, 1, 1) + cr_node.color_ramp.elements[1].color = self.base_color - cr_node.color_ramp.elements.new(position=0.5) - cr_node.color_ramp.elements[0].color = (1, 0, 0, 1) - cr_node.color_ramp.elements[0].position = 0.1 - cr_node.color_ramp.elements[1].color = (0, 1, 0, 1) - cr_node.color_ramp.elements[2].color = (0, 0, 1, 1) - cr_node.color_ramp.elements[2].position = 0.9 + mul_node = nodes.new('ShaderNodeMath') + mul_node.location = (-500, 0) + + # normalize + mul_node.operation = 'MULTIPLY' + mul_node.inputs[1].default_value = self.scale + + # Normalize the position when creating shader + sub_node = nodes.new('ShaderNodeMath') + sub_node.location = (-700, 0) + sub_node.operation = 'SUBTRACT' + sub_node.inputs[1].default_value = location_z + + xyz_sep_node = nodes.new('ShaderNodeSeparateXYZ') + xyz_sep_node.location = (-900, 0) oi_node = nodes.new('ShaderNodeObjectInfo') - oi_node.location = (-900, 0) + oi_node.location = (-1100, 0) links = material.node_tree.links - links.new(oi_node.outputs[4], cr_node.inputs[0]) + links.new(oi_node.outputs[0], xyz_sep_node.inputs[0]) + links.new(xyz_sep_node.outputs[2], sub_node.inputs[0]) + links.new(sub_node.outputs[0], mul_node.inputs[0]) + links.new(mul_node.outputs[0], cr_node.inputs[0]) links.new(cr_node.outputs[0], bsdf_node.inputs[0]) return material @@ -94,5 +137,3 @@ def create_random_shader(self): def __add_alpha(self, color, alpha): return (color[0], color[1], color[2], alpha) - - diff --git a/data_vis/general.py b/data_vis/general.py index 739df2f..6d60f3d 100644 --- a/data_vis/general.py +++ b/data_vis/general.py @@ -5,6 +5,7 @@ from mathutils import Vector from data_vis.utils.data_utils import get_data_as_ll, DataType from data_vis.data_manager import DataManager +from data_vis.colors import NodeShader class CONST: @@ -90,6 +91,31 @@ class DV_LabelPropertyGroup(bpy.types.PropertyGroup): ) +class DV_ColorPropertyGroup(bpy.types.PropertyGroup): + use_shader: bpy.props.BoolProperty( + name='Use Shader', + default=True + ) + + color_type: bpy.props.EnumProperty( + name='Shader Type', + items=( + ('0', 'Constant', 'One color'), + ('1', 'Random', 'Random colors'), + ('2', 'Gradient', 'Gradient based on value') + ), + default='2' + ) + + color_shade: bpy.props.FloatVectorProperty( + name='Base Color', + subtype='COLOR', + default=(0.0, 0.0, 1.0), + min=0.0, + max=1.0 + ) + + class Properties: ''' Access to Blender properties related to addon, which are not in specific chart operators @@ -175,10 +201,13 @@ def draw(self, context): row = layout.row() row.prop(self, 'padding') - row = layout.row() - row.label(text='Label settings:') + self.draw_label_settings(layout) + self.draw_color_settings(layout) + def draw_label_settings(self, layout): if hasattr(self, 'label_settings'): + row = layout.row() + row.label(text='Label settings:') row.prop(self.label_settings, 'create') if self.label_settings.create: row.prop(self.label_settings, 'from_data') @@ -188,6 +217,16 @@ def draw(self, context): if self.dm.dimensions == 3: row.prop(self.label_settings, 'y_label') row.prop(self.label_settings, 'z_label') + + def draw_color_settings(self, layout): + if hasattr(self, 'color_settings'): + box = layout.box() + box.label(text='Color settings') + box.prop(self.color_settings, 'use_shader') + if self.color_settings.use_shader: + box.prop(self.color_settings, 'color_type') + if not NodeShader.Type.str_to_type(self.color_settings.color_type) == NodeShader.Type.Random: + box.prop(self.color_settings, 'color_shade') @classmethod def poll(cls, context): diff --git a/data_vis/operators/bar_chart.py b/data_vis/operators/bar_chart.py index 7ece383..a3a6ef7 100644 --- a/data_vis/operators/bar_chart.py +++ b/data_vis/operators/bar_chart.py @@ -5,7 +5,7 @@ from data_vis.utils.data_utils import get_data_as_ll, find_data_range, normalize_value, find_axis_range from data_vis.utils.color_utils import ColorGen -from data_vis.general import OBJECT_OT_generic_chart, CONST, Properties, DV_LabelPropertyGroup +from data_vis.general import OBJECT_OT_generic_chart, CONST, Properties, DV_LabelPropertyGroup, DV_ColorPropertyGroup from data_vis.operators.features.axis import AxisFactory from data_vis.data_manager import DataManager, DataType from data_vis.colors import NodeShader @@ -81,22 +81,8 @@ class OBJECT_OT_bar_chart(OBJECT_OT_generic_chart): default=0.1 ) - color_type: bpy.props.EnumProperty( - name='Shader color type', - items=( - ('0', 'Constant', 'One color'), - ('1', 'Random', 'Random colors'), - ('2', 'Gradient', 'Gradient') - ), - default='2' - ) - - color_shade: bpy.props.FloatVectorProperty( - name='Color', - subtype='COLOR', - default=(0.0, 0.0, 1.0), - min=0.0, - max=1.0 + color_settings: bpy.props.PointerProperty( + type=DV_ColorPropertyGroup ) label_settings: bpy.props.PointerProperty( @@ -114,10 +100,6 @@ def draw(self, context): row = layout.row() row.prop(self, 'bar_size') - row = layout.row() - row.prop(self, 'color_type') - row.prop(self, 'color_shade') - def init_range(self, data): self.x_axis_range = find_axis_range(data, 0) self.y_axis_range = find_axis_range(data, 1) @@ -150,7 +132,7 @@ def execute(self, context): data_max = max(self.data, key=lambda val: val[1])[1] #color_gen = ColorGen(self.color_shade, (data_min, data_max)) - shader = NodeShader(self.color_shade, NodeShader.Type.str_to_type(self.color_type)) + shader = NodeShader(self.color_settings.color_shade, NodeShader.Type.str_to_type(self.color_settings.color_type), 2.0, self.chart_origin[2]) if self.dimensions == '2': value_index = 1 diff --git a/data_vis/operators/data_load.py b/data_vis/operators/data_load.py index d8b452d..31cfd5f 100644 --- a/data_vis/operators/data_load.py +++ b/data_vis/operators/data_load.py @@ -20,7 +20,6 @@ def invoke(self, context, event): return {'RUNNING_MODAL'} def execute(self, context): - bpy.data.scenes[0].dv_props.data.clear() data_manager = DataManager() line_n = data_manager.load_data(self.filepath) From 7aba02b89c33fae9412323be711350b5a2f35a18 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zden=C4=9Bk=20Dole=C5=BEal?= Date: Tue, 31 Mar 2020 01:48:25 +0200 Subject: [PATCH 06/24] Naming + removed unused methods --- data_vis/__init__.py | 32 ++++---- data_vis/general.py | 120 +++--------------------------- data_vis/operators/bar_chart.py | 4 +- data_vis/operators/line_chart.py | 4 +- data_vis/operators/pie_chart.py | 4 +- data_vis/operators/point_chart.py | 7 +- 6 files changed, 35 insertions(+), 136 deletions(-) diff --git a/data_vis/__init__.py b/data_vis/__init__.py index 92f0c05..4cce512 100644 --- a/data_vis/__init__.py +++ b/data_vis/__init__.py @@ -14,10 +14,10 @@ import os from .operators.data_load import FILE_OT_DVLoadFile -from .operators.bar_chart import OBJECT_OT_bar_chart -from .operators.line_chart import OBJECT_OT_line_chart -from .operators.pie_chart import OBJECT_OT_pie_chart -from .operators.point_chart import OBJECT_OT_point_chart +from .operators.bar_chart import OBJECT_OT_BarChart +from .operators.line_chart import OBJECT_OT_LineChart +from .operators.pie_chart import OBJECT_OT_PieChart +from .operators.point_chart import OBJECT_OT_PointChart from .general import DV_LabelPropertyGroup, DV_ColorPropertyGroup from .data_manager import DataManager @@ -92,10 +92,10 @@ class OBJECT_MT_AddChart(bpy.types.Menu): def draw(self, context): layout = self.layout main_icons = preview_collections['main'] - layout.operator(OBJECT_OT_bar_chart.bl_idname, icon_value=main_icons['bar_chart'].icon_id) - layout.operator(OBJECT_OT_line_chart.bl_idname, icon_value=main_icons['line_chart'].icon_id) - layout.operator(OBJECT_OT_pie_chart.bl_idname, icon_value=main_icons['pie_chart'].icon_id) - layout.operator(OBJECT_OT_point_chart.bl_idname, icon_value=main_icons['point_chart'].icon_id) + layout.operator(OBJECT_OT_BarChart.bl_idname, icon_value=main_icons['bar_chart'].icon_id) + layout.operator(OBJECT_OT_LineChart.bl_idname, icon_value=main_icons['line_chart'].icon_id) + layout.operator(OBJECT_OT_PieChart.bl_idname, icon_value=main_icons['pie_chart'].icon_id) + layout.operator(OBJECT_OT_PointChart.bl_idname, icon_value=main_icons['point_chart'].icon_id) preview_collections = {} @@ -130,10 +130,10 @@ def register(): bpy.utils.register_class(DV_PropertyGroup) bpy.utils.register_class(DV_LabelPropertyGroup) bpy.utils.register_class(DV_ColorPropertyGroup) - bpy.utils.register_class(OBJECT_OT_bar_chart) - bpy.utils.register_class(OBJECT_OT_pie_chart) - bpy.utils.register_class(OBJECT_OT_line_chart) - bpy.utils.register_class(OBJECT_OT_point_chart) + bpy.utils.register_class(OBJECT_OT_BarChart) + bpy.utils.register_class(OBJECT_OT_PieChart) + bpy.utils.register_class(OBJECT_OT_LineChart) + bpy.utils.register_class(OBJECT_OT_PointChart) bpy.utils.register_class(FILE_OT_DVLoadFile) bpy.utils.register_class(DV_AddonPanel) bpy.utils.register_class(OBJECT_MT_AddChart) @@ -147,10 +147,10 @@ def unregister(): bpy.utils.unregister_class(DV_PropertyGroup) bpy.utils.unregister_class(OBJECT_MT_AddChart) bpy.utils.unregister_class(DV_AddonPanel) - bpy.utils.unregister_class(OBJECT_OT_bar_chart) - bpy.utils.unregister_class(OBJECT_OT_pie_chart) - bpy.utils.unregister_class(OBJECT_OT_line_chart) - bpy.utils.unregister_class(OBJECT_OT_point_chart) + bpy.utils.unregister_class(OBJECT_OT_BarChart) + bpy.utils.unregister_class(OBJECT_OT_PieChart) + bpy.utils.unregister_class(OBJECT_OT_LineChart) + bpy.utils.unregister_class(OBJECT_OT_PointChart) bpy.utils.unregister_class(FILE_OT_DVLoadFile) bpy.utils.unregister_class(DV_LabelPropertyGroup) bpy.utils.unregister_class(DV_ColorPropertyGroup) diff --git a/data_vis/general.py b/data_vis/general.py index 6d60f3d..0d70980 100644 --- a/data_vis/general.py +++ b/data_vis/general.py @@ -3,8 +3,7 @@ import math from mathutils import Vector -from data_vis.utils.data_utils import get_data_as_ll, DataType -from data_vis.data_manager import DataManager +from data_vis.data_manager import DataManager, DataType from data_vis.colors import NodeShader @@ -13,10 +12,14 @@ class CONST: HALF_PI = math.pi * 0.5 -# for future use class DV_AxisPropertyGroup(bpy.types.PropertyGroup): + create: bpy.props.BoolProperty( + name='Create Axis', + default=True + ) + auto_ranges: bpy.props.BoolProperty( - name='Automatic axis ranges', + name='Automatic Axis Ranges', default=True ) @@ -31,7 +34,7 @@ class DV_AxisPropertyGroup(bpy.types.PropertyGroup): default=(0.0, 1.0) ) - _step: bpy.props.FloatProperty( + y_step: bpy.props.FloatProperty( name='Step of y axis', default=1.0 ) @@ -48,13 +51,13 @@ class DV_AxisPropertyGroup(bpy.types.PropertyGroup): ) thickness: bpy.props.FloatProperty( - name='Axis thickness', + name='Axis Thickness', default=0.01, description='How thick is the axis object' ) tick_mark_height: bpy.props.FloatProperty( - name='Axis tick mark height', + name='Axis Tick Mark Height', default=0.03 ) @@ -120,10 +123,6 @@ class Properties: ''' Access to Blender properties related to addon, which are not in specific chart operators ''' - @staticmethod - def get_data(): - return bpy.data.scenes[0].dv_props.data - @staticmethod def get_text_size(): return bpy.data.scenes[0].dv_props.text_size @@ -137,7 +136,7 @@ def get_axis_tick_mark_height(): return bpy.data.scenes[0].dv_props.axis_tick_mark_height -class OBJECT_OT_generic_chart(bpy.types.Operator): +class OBJECT_OT_GenericChart(bpy.types.Operator): '''Creates chart''' bl_idname = 'object.create_chart' bl_label = 'Generic chart operator' @@ -248,15 +247,6 @@ def create_container(self): # set default location for parent object self.container_object.location = self.chart_origin - def create_axis(self, spacing, x_vals, y_max=None, y_min=0, z_vals=None, padding=(0, 0, 0), offset=(0, 0, 0)): - self.axis_mat = self.new_mat((1, 1, 1), 1, name='Axis_Mat') - length = self.create_one_axis(spacing, x_vals, offset[0], padding[0]) - if y_max: - cont = self.create_y_axis(y_min, y_max, offset[1], padding[1]) - if z_vals: - cont.location.x += 2 * length - if z_vals: - self.create_one_axis(spacing, z_vals, offset[2], padding[2], dim='z') def data_type_as_enum(self): if not hasattr(self, 'data_type'): @@ -267,94 +257,6 @@ def data_type_as_enum(self): elif self.data_type == '1': return DataType.Categorical - def create_y_axis(self, min_val, max_val, offset, padding): - bpy.ops.object.empty_add() - axis_cont = bpy.context.object - axis_cont.name = 'Axis_Container' - axis_cont.location = (0, 0, 0) - axis_cont.parent = self.container_object - - bpy.ops.mesh.primitive_cube_add() - line_obj = bpy.context.active_object - line_obj.location = (0, 0, 0) - - line_obj.scale = (CONST.GRAPH_Z_SCALE + padding + offset * 0.5, 0.005, 0.005) - line_obj.location.x += CONST.GRAPH_Z_SCALE + padding + offset * 0.5 - line_obj.parent = axis_cont - - line_obj.active_material = self.axis_mat - - spacing = 0.2 * CONST.GRAPH_Z_SCALE - val_inc = (abs(min_val) + max_val) * 0.1 - val = min_val - for i in range(0, 11): - bpy.ops.mesh.primitive_cube_add() - obj = bpy.context.active_object - obj.scale = (0.005, 0.005, 0.02) - obj.location = (0, 0, 0) - obj.location.x += i * spacing + offset - obj.parent = axis_cont - obj.active_material = self.axis_mat - - self.create_text_object(axis_cont, '{0:.3}'.format(float(val)), (i * spacing + offset, 0, 0.07), (CONST.HALF_PI, CONST.HALF_PI, 0)) - val += val_inc - - axis_cont.location += Vector((-padding, 0, -padding)) - axis_cont.rotation_euler.y -= CONST.HALF_PI - return axis_cont - - def create_one_axis(self, spacing, vals, offset, padding, dim='x'): - bpy.ops.object.empty_add() - axis_cont = bpy.context.object - axis_cont.name = 'Axis_Container' - axis_cont.location = (0, 0, 0) - axis_cont.parent = self.container_object - # TODO WHAT self.axis_containers.append(axis_cont) - - v_len = ((len(vals) - 1) * spacing) * 0.5 + padding + offset * 0.5 - bpy.ops.mesh.primitive_cube_add() - line_obj = bpy.context.active_object - line_obj.location = (0, 0, 0) - - line_obj.scale = (v_len, 0.005, 0.005) - line_obj.location.x += v_len - line_obj.parent = axis_cont - line_obj.active_material = self.axis_mat - - for i in range(0, len(vals)): - bpy.ops.mesh.primitive_cube_add() - obj = bpy.context.active_object - obj.scale = (0.005, 0.005, 0.02) - obj.location = (0, 0, 0) - obj.location.x += i * spacing + offset - obj.parent = axis_cont - obj.active_material = self.axis_mat - - to_loc = (i * spacing + offset, 0, -0.07) - to_rot = (CONST.HALF_PI, 0, 0) - if dim == 'z': - to_rot = (CONST.HALF_PI, 0, math.pi) - self.create_text_object(axis_cont, vals[i], to_loc, to_rot) - - axis_cont.location += Vector((-padding, 0, -padding)) - if dim == 'z': - axis_cont.rotation_euler.z += CONST.HALF_PI - - return v_len - - def create_text_object(self, parent, text, location_offset, rotation_offset): - bpy.ops.object.text_add() - to = bpy.context.object - to.data.body = str(text) - to.data.align_x = 'CENTER' - to.scale *= 0.05 - to.location = parent.location - to.location += Vector(location_offset) - to.rotation_euler.x += rotation_offset[0] - to.rotation_euler.y += rotation_offset[1] - to.rotation_euler.z += rotation_offset[2] - to.parent = parent - def new_mat(self, color, alpha, name='Mat'): mat = bpy.data.materials.new(name=name) mat.diffuse_color = (*color, alpha) diff --git a/data_vis/operators/bar_chart.py b/data_vis/operators/bar_chart.py index a3a6ef7..8fbba1d 100644 --- a/data_vis/operators/bar_chart.py +++ b/data_vis/operators/bar_chart.py @@ -5,13 +5,13 @@ from data_vis.utils.data_utils import get_data_as_ll, find_data_range, normalize_value, find_axis_range from data_vis.utils.color_utils import ColorGen -from data_vis.general import OBJECT_OT_generic_chart, CONST, Properties, DV_LabelPropertyGroup, DV_ColorPropertyGroup +from data_vis.general import OBJECT_OT_GenericChart, DV_LabelPropertyGroup, DV_ColorPropertyGroup from data_vis.operators.features.axis import AxisFactory from data_vis.data_manager import DataManager, DataType from data_vis.colors import NodeShader -class OBJECT_OT_bar_chart(OBJECT_OT_generic_chart): +class OBJECT_OT_BarChart(OBJECT_OT_GenericChart): '''Creates (3D or 2D) bar chart from data''' bl_idname = 'object.create_bar_chart' bl_label = 'Bar Chart' diff --git a/data_vis/operators/line_chart.py b/data_vis/operators/line_chart.py index 6b8eb39..532f213 100644 --- a/data_vis/operators/line_chart.py +++ b/data_vis/operators/line_chart.py @@ -5,11 +5,11 @@ from data_vis.utils.data_utils import find_data_range, find_axis_range, normalize_value, get_data_in_range from data_vis.operators.features.axis import AxisFactory -from data_vis.general import OBJECT_OT_generic_chart, DV_LabelPropertyGroup +from data_vis.general import OBJECT_OT_GenericChart, DV_LabelPropertyGroup from data_vis.data_manager import DataManager, DataType -class OBJECT_OT_line_chart(OBJECT_OT_generic_chart): +class OBJECT_OT_LineChart(OBJECT_OT_GenericChart): '''Creates line chart as a line or as curve''' bl_idname = 'object.create_line_chart' bl_label = 'Line Chart' diff --git a/data_vis/operators/pie_chart.py b/data_vis/operators/pie_chart.py index fad1493..b38ee66 100644 --- a/data_vis/operators/pie_chart.py +++ b/data_vis/operators/pie_chart.py @@ -4,11 +4,11 @@ from data_vis.utils.data_utils import get_data_as_ll, find_data_range from data_vis.utils.color_utils import sat_col_gen, ColorGen -from data_vis.general import OBJECT_OT_generic_chart, CONST +from data_vis.general import OBJECT_OT_GenericChart from data_vis.data_manager import DataManager, DataType -class OBJECT_OT_pie_chart(OBJECT_OT_generic_chart): +class OBJECT_OT_PieChart(OBJECT_OT_GenericChart): '''Creates pie chart''' bl_idname = 'object.create_pie_chart' bl_label = 'Pie Chart' diff --git a/data_vis/operators/point_chart.py b/data_vis/operators/point_chart.py index 8eb4021..cafcf88 100644 --- a/data_vis/operators/point_chart.py +++ b/data_vis/operators/point_chart.py @@ -2,17 +2,14 @@ from mathutils import Vector import math -from data_vis.general import OBJECT_OT_generic_chart, DV_LabelPropertyGroup +from data_vis.general import OBJECT_OT_GenericChart, DV_LabelPropertyGroup from data_vis.operators.features.axis import AxisFactory from data_vis.utils.data_utils import get_data_as_ll, find_data_range, normalize_value, find_axis_range from data_vis.utils.color_utils import sat_col_gen, color_to_triplet, reverse_iterator, ColorGen from data_vis.data_manager import DataManager, DataType -from mathutils import Vector -import math - -class OBJECT_OT_point_chart(OBJECT_OT_generic_chart): +class OBJECT_OT_PointChart(OBJECT_OT_GenericChart): '''Creates point chart''' bl_idname = 'object.create_point_chart' bl_label = 'Point Chart' From a909aef4c7df916713c3907212576ba872cf2686 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zden=C4=9Bk=20Dole=C5=BEal?= Date: Tue, 31 Mar 2020 01:52:41 +0200 Subject: [PATCH 07/24] Updated descriptions for charts --- data_vis/general.py | 1 - data_vis/operators/bar_chart.py | 2 +- data_vis/operators/line_chart.py | 2 +- data_vis/operators/pie_chart.py | 2 +- data_vis/operators/point_chart.py | 2 +- 5 files changed, 4 insertions(+), 5 deletions(-) diff --git a/data_vis/general.py b/data_vis/general.py index 0d70980..f52f659 100644 --- a/data_vis/general.py +++ b/data_vis/general.py @@ -247,7 +247,6 @@ def create_container(self): # set default location for parent object self.container_object.location = self.chart_origin - def data_type_as_enum(self): if not hasattr(self, 'data_type'): return DataType.Numerical diff --git a/data_vis/operators/bar_chart.py b/data_vis/operators/bar_chart.py index 8fbba1d..12df8a1 100644 --- a/data_vis/operators/bar_chart.py +++ b/data_vis/operators/bar_chart.py @@ -12,7 +12,7 @@ class OBJECT_OT_BarChart(OBJECT_OT_GenericChart): - '''Creates (3D or 2D) bar chart from data''' + '''Creates Bar Chart, supports 2D and 3D Numerical Data and 2D categorical data with or w/o labels''' bl_idname = 'object.create_bar_chart' bl_label = 'Bar Chart' bl_options = {'REGISTER', 'UNDO'} diff --git a/data_vis/operators/line_chart.py b/data_vis/operators/line_chart.py index 532f213..08bd143 100644 --- a/data_vis/operators/line_chart.py +++ b/data_vis/operators/line_chart.py @@ -10,7 +10,7 @@ class OBJECT_OT_LineChart(OBJECT_OT_GenericChart): - '''Creates line chart as a line or as curve''' + '''Creates Line Chart, supports 2D Numerical or Categorical values with or w/o labels''' bl_idname = 'object.create_line_chart' bl_label = 'Line Chart' bl_options = {'REGISTER', 'UNDO'} diff --git a/data_vis/operators/pie_chart.py b/data_vis/operators/pie_chart.py index b38ee66..d117d94 100644 --- a/data_vis/operators/pie_chart.py +++ b/data_vis/operators/pie_chart.py @@ -9,7 +9,7 @@ class OBJECT_OT_PieChart(OBJECT_OT_GenericChart): - '''Creates pie chart''' + '''Creates Pie Chart, supports 2D Categorical values without labels''' bl_idname = 'object.create_pie_chart' bl_label = 'Pie Chart' bl_options = {'REGISTER', 'UNDO'} diff --git a/data_vis/operators/point_chart.py b/data_vis/operators/point_chart.py index cafcf88..f9aff4d 100644 --- a/data_vis/operators/point_chart.py +++ b/data_vis/operators/point_chart.py @@ -10,7 +10,7 @@ class OBJECT_OT_PointChart(OBJECT_OT_GenericChart): - '''Creates point chart''' + '''Creates Point Chart, supports 2D and 3D Numerical values with or w/o labels''' bl_idname = 'object.create_point_chart' bl_label = 'Point Chart' bl_options = {'REGISTER', 'UNDO'} From 59241da1aa882cc93ced1b2223dcccee2f8a5291 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zden=C4=9Bk=20Dole=C5=BEal?= Date: Wed, 1 Apr 2020 01:22:13 +0200 Subject: [PATCH 08/24] Axis UI changed to fit new data manager --- data_vis/__init__.py | 4 +- data_vis/general.py | 140 ++++++++++++++++++++++---------- data_vis/operators/bar_chart.py | 75 +++++------------ data_vis/utils/data_utils.py | 3 + 4 files changed, 122 insertions(+), 100 deletions(-) diff --git a/data_vis/__init__.py b/data_vis/__init__.py index 4cce512..5a52b4d 100644 --- a/data_vis/__init__.py +++ b/data_vis/__init__.py @@ -18,7 +18,7 @@ from .operators.line_chart import OBJECT_OT_LineChart from .operators.pie_chart import OBJECT_OT_PieChart from .operators.point_chart import OBJECT_OT_PointChart -from .general import DV_LabelPropertyGroup, DV_ColorPropertyGroup +from .general import DV_LabelPropertyGroup, DV_ColorPropertyGroup, DV_AxisPropertyGroup from .data_manager import DataManager @@ -130,6 +130,7 @@ def register(): bpy.utils.register_class(DV_PropertyGroup) bpy.utils.register_class(DV_LabelPropertyGroup) bpy.utils.register_class(DV_ColorPropertyGroup) + bpy.utils.register_class(DV_AxisPropertyGroup) bpy.utils.register_class(OBJECT_OT_BarChart) bpy.utils.register_class(OBJECT_OT_PieChart) bpy.utils.register_class(OBJECT_OT_LineChart) @@ -154,6 +155,7 @@ def unregister(): bpy.utils.unregister_class(FILE_OT_DVLoadFile) bpy.utils.unregister_class(DV_LabelPropertyGroup) bpy.utils.unregister_class(DV_ColorPropertyGroup) + bpy.utils.unregister_class(DV_AxisPropertyGroup) bpy.types.VIEW3D_MT_add.remove(chart_ops) diff --git a/data_vis/general.py b/data_vis/general.py index f52f659..9fbe619 100644 --- a/data_vis/general.py +++ b/data_vis/general.py @@ -13,57 +13,77 @@ class CONST: class DV_AxisPropertyGroup(bpy.types.PropertyGroup): + def range_updated(self, context): + if self.x_range[0] == self.x_range[1]: + self.x_range[1] += 1.0 + if self.y_range[0] == self.y_range[1]: + self.y_range[1] += 1.0 + create: bpy.props.BoolProperty( name='Create Axis', - default=True + default=True, ) auto_ranges: bpy.props.BoolProperty( - name='Automatic Axis Ranges', - default=True + name='Automatic Ranges', + default=True, + description='Automatically displays all data' + ) + + auto_steps: bpy.props.BoolProperty( + name='Automatic Steps', + default=True, + description='Automatically calculates stepsize to display 10 marks' ) x_step: bpy.props.FloatProperty( name='Step of x axis', - default=1.0 + default=1.0, + min=0.05 ) x_range: bpy.props.FloatVectorProperty( name='Range of x axis', size=2, - default=(0.0, 1.0) + default=(0.0, 1.0), + update=range_updated ) y_step: bpy.props.FloatProperty( name='Step of y axis', - default=1.0 + default=1.0, + min=0.05 ) y_range: bpy.props.FloatVectorProperty( name='Range of y axis', size=2, - default=(0.0, 1.0) + default=(0.0, 1.0), + update=range_updated ) z_step: bpy.props.FloatProperty( name='Step of z axis', - default=1.0 + default=1.0, + min=0.05 ) thickness: bpy.props.FloatProperty( - name='Axis Thickness', + name='Thickness', default=0.01, description='How thick is the axis object' ) tick_mark_height: bpy.props.FloatProperty( - name='Axis Tick Mark Height', - default=0.03 + name='Tick Mark Height', + default=0.03, + description='Thickness of axis mark objects' ) padding: bpy.props.FloatProperty( name='Padding', - default=0.1 + default=0.1, + description='Axis distance from chart origin' ) @@ -174,49 +194,23 @@ def draw(self, context): row = layout.row() row.prop(self, 'dimensions') - if numerical: - row = layout.row() - row.label(text='Axis ranges:') - row.prop(self, 'auto_ranges') - - if not self.auto_ranges: - row = layout.row() - row.prop(self, 'x_axis_range') - if self.dm.dimensions == 3: - row = layout.row() - row.prop(self, 'y_axis_range') - - row = layout.row() - row.label(text='Axis steps:') - row.prop(self, 'auto_steps') - if not self.auto_steps: - row = layout.row() - if numerical: - row.prop(self, 'x_axis_step', text='x') - if self.dm.dimensions == 3: - row.prop(self, 'y_axis_step', text='y') - row.prop(self, 'z_axis_step', text='z') - - row = layout.row() - row.prop(self, 'padding') - - self.draw_label_settings(layout) + self.draw_axis_settings(layout, numerical) self.draw_color_settings(layout) - def draw_label_settings(self, layout): + def draw_label_settings(self, box): if hasattr(self, 'label_settings'): - row = layout.row() - row.label(text='Label settings:') + row = box.row() + row.label(text='Label Settings:') row.prop(self.label_settings, 'create') if self.label_settings.create: - row.prop(self.label_settings, 'from_data') + box.prop(self.label_settings, 'from_data') if not self.label_settings.from_data: - row = layout.row() + row = box.row() row.prop(self.label_settings, 'x_label') if self.dm.dimensions == 3: row.prop(self.label_settings, 'y_label') row.prop(self.label_settings, 'z_label') - + def draw_color_settings(self, layout): if hasattr(self, 'color_settings'): box = layout.box() @@ -226,6 +220,42 @@ def draw_color_settings(self, layout): box.prop(self.color_settings, 'color_type') if not NodeShader.Type.str_to_type(self.color_settings.color_type) == NodeShader.Type.Random: box.prop(self.color_settings, 'color_shade') + + def draw_axis_settings(self, layout, numerical): + if not hasattr(self, 'axis_settings'): + return + + box = layout.box() + row = box.row() + row.label(text='Axis Settings:') + row.prop(self.axis_settings, 'create') + if not self.axis_settings.create: + return + + if numerical: + box.prop(self.axis_settings, 'auto_ranges') + if not self.axis_settings.auto_ranges: + row = box.row() + row.prop(self.axis_settings, 'x_range', text='x') + if self.dimensions == '3': + row = box.row() + row.prop(self.axis_settings, 'y_range', text='y') + box.prop(self.axis_settings, 'auto_steps') + + if not self.axis_settings.auto_steps: + row = box.row() + if numerical: + row.prop(self.axis_settings, 'x_step', text='x') + if self.dimensions == '3': + row.prop(self.axis_settings, 'y_step', text='y') + row.prop(self.axis_settings, 'z_step', text='z') + + row = box.row() + row.prop(self.axis_settings, 'padding') + row.prop(self.axis_settings, 'thickness') + row.prop(self.axis_settings, 'tick_mark_height') + box.separator() + self.draw_label_settings(box) @classmethod def poll(cls, context): @@ -303,3 +333,23 @@ def in_axis_range_bounds(self, entry): return False return True + + def in_axis_range_bounds_new(self, entry): + ''' + Checks whether the entry point defined as [x, y, z] is within user selected axis range + returns False if not in range, else True + ''' + entry_dims = len(entry) + if entry_dims == 2 or entry_dims == 3: + if hasattr(self, 'data_type') and self.data_type != '0': + return True + + if entry[0] < self.axis_settings.x_range[0] or entry[0] > self.axis_settings.x_range[1]: + return False + + if entry_dims == 3: + if entry[1] < self.axis_settings.y_range[0] or entry[1] > self.axis_settings.y_range[1]: + return False + + return True + diff --git a/data_vis/operators/bar_chart.py b/data_vis/operators/bar_chart.py index 12df8a1..4054451 100644 --- a/data_vis/operators/bar_chart.py +++ b/data_vis/operators/bar_chart.py @@ -5,7 +5,7 @@ from data_vis.utils.data_utils import get_data_as_ll, find_data_range, normalize_value, find_axis_range from data_vis.utils.color_utils import ColorGen -from data_vis.general import OBJECT_OT_GenericChart, DV_LabelPropertyGroup, DV_ColorPropertyGroup +from data_vis.general import OBJECT_OT_GenericChart, DV_LabelPropertyGroup, DV_ColorPropertyGroup, DV_AxisPropertyGroup from data_vis.operators.features.axis import AxisFactory from data_vis.data_manager import DataManager, DataType from data_vis.colors import NodeShader @@ -39,46 +39,8 @@ class OBJECT_OT_BarChart(OBJECT_OT_GenericChart): default=(0.05, 0.05) ) - auto_ranges: bpy.props.BoolProperty( - name='Automatic axis ranges', - default=True - ) - - auto_steps: bpy.props.BoolProperty( - name='Automatic axis steps', - default=True - ) - - x_axis_step: bpy.props.FloatProperty( - name='Step of x axis', - default=1.0 - ) - - x_axis_range: bpy.props.FloatVectorProperty( - name='Range of x axis', - size=2, - default=(0.0, 1.0) - ) - - y_axis_step: bpy.props.FloatProperty( - name='Step of y axis', - default=1.0 - ) - - y_axis_range: bpy.props.FloatVectorProperty( - name='Range of y axis', - size=2, - default=(0.0, 1.0) - ) - - z_axis_step: bpy.props.FloatProperty( - name='Step of z axis', - default=1.0 - ) - - padding: bpy.props.FloatProperty( - name='Padding', - default=0.1 + axis_settings: bpy.props.PointerProperty( + type=DV_AxisPropertyGroup ) color_settings: bpy.props.PointerProperty( @@ -91,6 +53,7 @@ class OBJECT_OT_BarChart(OBJECT_OT_GenericChart): @classmethod def poll(cls, context): + return True dm = DataManager() return dm.is_type(DataType.Numerical, 3) or dm.is_type(DataType.Categorical, 2) @@ -101,8 +64,8 @@ def draw(self, context): row.prop(self, 'bar_size') def init_range(self, data): - self.x_axis_range = find_axis_range(data, 0) - self.y_axis_range = find_axis_range(data, 1) + self.axis_settings.x_range = find_axis_range(data, 0) + self.axis_settings.y_range = find_axis_range(data, 1) def data_type_as_enum(self): if self.data_type == '0': @@ -113,12 +76,12 @@ def data_type_as_enum(self): def execute(self, context): self.init_data() if self.data_type_as_enum() == DataType.Numerical: - if self.auto_ranges: + if self.axis_settings.auto_ranges: self.init_range(self.data) else: self.dimensions = '2' - self.x_axis_range[0] = 0 - self.x_axis_range[1] = len(self.data) - 1 + self.axis_settings.x_range[0] = 0 + self.axis_settings.x_range[1] = len(self.data) - 1 if self.dimensions == '3' and len(self.data[0]) != 3: self.report({'ERROR'}, 'Data are only 2D!') @@ -126,7 +89,11 @@ def execute(self, context): tick_labels = [] self.create_container() if self.data_type_as_enum() == DataType.Numerical: - data_min, data_max = find_data_range(self.data, self.x_axis_range, self.y_axis_range if self.dimensions == '3' else None) + try: + data_min, data_max = find_data_range(self.data, self.axis_settings.x_range, self.axis_settings.y_range if self.dimensions == '3' else None) + except Exception as e: + self.report({'ERROR'}, 'Cannot find data in this range!') + return {'CANCELLED'} else: data_min = min(self.data, key=lambda val: val[1])[1] data_max = max(self.data, key=lambda val: val[1])[1] @@ -140,7 +107,7 @@ def execute(self, context): value_index = 2 for i, entry in enumerate(self.data): - if not self.in_axis_range_bounds(entry): + if not self.in_axis_range_bounds_new(entry): continue bpy.ops.mesh.primitive_cube_add() @@ -150,7 +117,7 @@ def execute(self, context): else: tick_labels.append(entry[0]) x_value = i - x_norm = normalize_value(x_value, self.x_axis_range[0], self.x_axis_range[1]) + x_norm = normalize_value(x_value, self.axis_settings.x_range[0], self.axis_settings.x_range[1]) z_norm = normalize_value(entry[value_index], data_min, data_max) if z_norm >= 0.0 and z_norm <= 0.0001: @@ -159,7 +126,7 @@ def execute(self, context): bar_obj.scale = (self.bar_size[0], self.bar_size[1], z_norm * 0.5) bar_obj.location = (x_norm, 0.0, z_norm * 0.5) else: - y_norm = normalize_value(entry[1], self.y_axis_range[0], self.y_axis_range[1]) + y_norm = normalize_value(entry[1], self.axis_settings.y_range[0], self.axis_settings.y_range[1]) bar_obj.scale = (self.bar_size[0], self.bar_size[1], z_norm * 0.5) bar_obj.location = (x_norm, y_norm, z_norm * 0.5) @@ -169,13 +136,13 @@ def execute(self, context): AxisFactory.create( self.container_object, - (self.x_axis_step, self.y_axis_step, self.z_axis_step), - (self.x_axis_range, self.y_axis_range, (data_min, data_max)), + (self.axis_settings.x_step, self.axis_settings.y_step, self.axis_settings.z_step), + (self.axis_settings.x_range, self.axis_settings.y_range, (data_min, data_max)), int(self.dimensions), tick_labels=(tick_labels, [], []), labels=self.labels, - padding=self.padding, - auto_steps=self.auto_steps, + padding=self.axis_settings.padding, + auto_steps=self.axis_settings.auto_steps, offset=0.0 ) diff --git a/data_vis/utils/data_utils.py b/data_vis/utils/data_utils.py index 612954f..a348fa3 100644 --- a/data_vis/utils/data_utils.py +++ b/data_vis/utils/data_utils.py @@ -144,4 +144,7 @@ def normalize_value(value, minimum, maximum): Normalizes value into <0, 1> interval, range of data where value is included is specified by minimum and maximum ''' + if maximum - minimum == 0: + print('Division by zero in normalize value!') + return 1.0 return (value - minimum) / (maximum - minimum) From e01382e9157ac04c41511c5a4dff44e19f982f9a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zden=C4=9Bk=20Dole=C5=BEal?= Date: Wed, 1 Apr 2020 02:12:13 +0200 Subject: [PATCH 09/24] Axis property rework implemented to other charts --- data_vis/colors.py | 36 ++++++++++++----------- data_vis/general.py | 9 +++--- data_vis/operators/bar_chart.py | 28 +++++++++--------- data_vis/operators/data_load.py | 8 ----- data_vis/operators/line_chart.py | 45 +++++++++++++++------------- data_vis/operators/point_chart.py | 49 ++++++++++++++++++------------- 6 files changed, 91 insertions(+), 84 deletions(-) diff --git a/data_vis/colors.py b/data_vis/colors.py index b7c09be..eef0400 100644 --- a/data_vis/colors.py +++ b/data_vis/colors.py @@ -2,30 +2,32 @@ from enum import Enum +class ColorType(Enum): + Constant = 0 + Random = 1 + Gradient = 2 + + def str_to_type(value): + if str(value) == '0' or value == 'Constant': + return ColorType.Constant + if str(value) == '1' or value == 'Random': + return ColorType.Random + if str(value) == '2' or value == 'Gradient': + return ColorType.Gradient + + class NodeShader: - class Type(Enum): - Constant = 0 - Random = 1 - Gradient = 2 - - def str_to_type(value): - if str(value) == '0' or value == 'Constant': - return NodeShader.Type.Constant - if str(value) == '1' or value == 'Random': - return NodeShader.Type.Random - if str(value) == '2' or value == 'Gradient': - return NodeShader.Type.Gradient - - def __init__(self, base_color, shader_type=Type.Constant, scale=1.0, location_z=0): + + def __init__(self, base_color, shader_type=ColorType.Constant, scale=1.0, location_z=0): self.base_color = self.__add_alpha(base_color, 1) self.shader_type = shader_type self.scale = scale - if self.shader_type == NodeShader.Type.Random: + if self.shader_type == ColorType.Random: self.material = self.create_random_shader() - elif self.shader_type == NodeShader.Type.Constant: + elif self.shader_type == ColorType.Constant: self.material = self.create_const_shader() - elif self.shader_type == NodeShader.Type.Gradient: + elif self.shader_type == ColorType.Gradient: self.material = self.create_gradient_shader(location_z) else: raise AttributeError('Unsupported shader type!') diff --git a/data_vis/general.py b/data_vis/general.py index 9fbe619..82183ec 100644 --- a/data_vis/general.py +++ b/data_vis/general.py @@ -4,7 +4,7 @@ from mathutils import Vector from data_vis.data_manager import DataManager, DataType -from data_vis.colors import NodeShader +from data_vis.colors import ColorType class CONST: @@ -83,6 +83,7 @@ def range_updated(self, context): padding: bpy.props.FloatProperty( name='Padding', default=0.1, + min=0, description='Axis distance from chart origin' ) @@ -218,7 +219,7 @@ def draw_color_settings(self, layout): box.prop(self.color_settings, 'use_shader') if self.color_settings.use_shader: box.prop(self.color_settings, 'color_type') - if not NodeShader.Type.str_to_type(self.color_settings.color_type) == NodeShader.Type.Random: + if not ColorType.str_to_type(self.color_settings.color_type) == ColorType.Random: box.prop(self.color_settings, 'color_shade') def draw_axis_settings(self, layout, numerical): @@ -237,7 +238,7 @@ def draw_axis_settings(self, layout, numerical): if not self.axis_settings.auto_ranges: row = box.row() row.prop(self.axis_settings, 'x_range', text='x') - if self.dimensions == '3': + if hasattr(self, 'dimensions') and self.dimensions == '3': row = box.row() row.prop(self.axis_settings, 'y_range', text='y') box.prop(self.axis_settings, 'auto_steps') @@ -246,7 +247,7 @@ def draw_axis_settings(self, layout, numerical): row = box.row() if numerical: row.prop(self.axis_settings, 'x_step', text='x') - if self.dimensions == '3': + if hasattr(self, 'dimensions') and self.dimensions == '3': row.prop(self.axis_settings, 'y_step', text='y') row.prop(self.axis_settings, 'z_step', text='z') diff --git a/data_vis/operators/bar_chart.py b/data_vis/operators/bar_chart.py index 4054451..c011013 100644 --- a/data_vis/operators/bar_chart.py +++ b/data_vis/operators/bar_chart.py @@ -8,7 +8,7 @@ from data_vis.general import OBJECT_OT_GenericChart, DV_LabelPropertyGroup, DV_ColorPropertyGroup, DV_AxisPropertyGroup from data_vis.operators.features.axis import AxisFactory from data_vis.data_manager import DataManager, DataType -from data_vis.colors import NodeShader +from data_vis.colors import NodeShader, ColorType class OBJECT_OT_BarChart(OBJECT_OT_GenericChart): @@ -53,7 +53,6 @@ class OBJECT_OT_BarChart(OBJECT_OT_GenericChart): @classmethod def poll(cls, context): - return True dm = DataManager() return dm.is_type(DataType.Numerical, 3) or dm.is_type(DataType.Categorical, 2) @@ -99,7 +98,7 @@ def execute(self, context): data_max = max(self.data, key=lambda val: val[1])[1] #color_gen = ColorGen(self.color_shade, (data_min, data_max)) - shader = NodeShader(self.color_settings.color_shade, NodeShader.Type.str_to_type(self.color_settings.color_type), 2.0, self.chart_origin[2]) + shader = NodeShader(self.color_settings.color_shade, ColorType.str_to_type(self.color_settings.color_type), 2.0, self.chart_origin[2]) if self.dimensions == '2': value_index = 1 @@ -134,16 +133,17 @@ def execute(self, context): bar_obj.active_material = shader.material # self.new_mat(color_gen.next(entry[value_index]), 1) bar_obj.parent = self.container_object - AxisFactory.create( - self.container_object, - (self.axis_settings.x_step, self.axis_settings.y_step, self.axis_settings.z_step), - (self.axis_settings.x_range, self.axis_settings.y_range, (data_min, data_max)), - int(self.dimensions), - tick_labels=(tick_labels, [], []), - labels=self.labels, - padding=self.axis_settings.padding, - auto_steps=self.axis_settings.auto_steps, - offset=0.0 - ) + if self.axis_settings.create: + AxisFactory.create( + self.container_object, + (self.axis_settings.x_step, self.axis_settings.y_step, self.axis_settings.z_step), + (self.axis_settings.x_range, self.axis_settings.y_range, (data_min, data_max)), + int(self.dimensions), + tick_labels=(tick_labels, [], []), + labels=self.labels, + padding=self.axis_settings.padding, + auto_steps=self.axis_settings.auto_steps, + offset=0.0 + ) return {'FINISHED'} diff --git a/data_vis/operators/data_load.py b/data_vis/operators/data_load.py index 31cfd5f..8a605ae 100644 --- a/data_vis/operators/data_load.py +++ b/data_vis/operators/data_load.py @@ -23,16 +23,8 @@ def execute(self, context): data_manager = DataManager() line_n = data_manager.load_data(self.filepath) - # with open(self.filepath, 'r') as file: - # line_n = 0 - # for row in file: - # line_n += 1 - # row_prop = bpy.data.scenes[0].dv_props.data.add() - # row_prop.value = row - report_type = {'INFO'} if line_n == 0: report_type = {'WARNING'} self.report(report_type, f'File: {self.filepath}, loaded {line_n} lines!') return {'FINISHED'} - diff --git a/data_vis/operators/line_chart.py b/data_vis/operators/line_chart.py index 08bd143..9143b66 100644 --- a/data_vis/operators/line_chart.py +++ b/data_vis/operators/line_chart.py @@ -5,7 +5,7 @@ from data_vis.utils.data_utils import find_data_range, find_axis_range, normalize_value, get_data_in_range from data_vis.operators.features.axis import AxisFactory -from data_vis.general import OBJECT_OT_GenericChart, DV_LabelPropertyGroup +from data_vis.general import OBJECT_OT_GenericChart, DV_LabelPropertyGroup, DV_AxisPropertyGroup from data_vis.data_manager import DataManager, DataType @@ -71,6 +71,10 @@ class OBJECT_OT_LineChart(OBJECT_OT_GenericChart): type=DV_LabelPropertyGroup ) + axis_settings: bpy.props.PointerProperty( + type=DV_AxisPropertyGroup + ) + def __init__(self): super().__init__() self.only_2d = True @@ -112,23 +116,23 @@ def execute(self, context): self.create_container() if self.data_type_as_enum() == DataType.Numerical: - if self.auto_ranges: - self.x_axis_range = find_axis_range(self.data, 0) - data_min, data_max = find_data_range(self.data, self.x_axis_range) - self.data = get_data_in_range(self.data, self.x_axis_range) + if self.axis_settings.auto_ranges: + self.axis_settings.x_range = find_axis_range(self.data, 0) + data_min, data_max = find_data_range(self.data, self.axis_settings.x_range) + self.data = get_data_in_range(self.data, self.axis_settings.x_range) sorted_data = sorted(self.data, key=lambda x: x[0]) else: - self.x_axis_range[0] = 0 - self.x_axis_range[1] = len(self.data) - 1 + self.axis_settings.x_range[0] = 0 + self.axis_settings.x_range[1] = len(self.data) - 1 data_min = min(self.data, key=lambda val: val[1])[1] data_max = max(self.data, key=lambda val: val[1])[1] sorted_data = self.data tick_labels = [] if self.data_type_as_enum() == DataType.Numerical: - normalized_vert_list = [(normalize_value(entry[0], self.x_axis_range[0], self.x_axis_range[1]), 0.0, normalize_value(entry[1], data_min, data_max)) for entry in sorted_data] + normalized_vert_list = [(normalize_value(entry[0], self.axis_settings.x_range[0], self.axis_settings.x_range[1]), 0.0, normalize_value(entry[1], data_min, data_max)) for entry in sorted_data] else: - normalized_vert_list = [(normalize_value(i, self.x_axis_range[0], self.x_axis_range[1]), 0.0, normalize_value(entry[1], data_min, data_max)) for i, entry in enumerate(sorted_data)] + normalized_vert_list = [(normalize_value(i, self.axis_settings.x_range[0], self.axis_settings.x_range[1]), 0.0, normalize_value(entry[1], data_min, data_max)) for i, entry in enumerate(sorted_data)] tick_labels = list(zip(*sorted_data))[0] edges = [[i - 1, i] for i in range(1, len(normalized_vert_list))] @@ -136,17 +140,18 @@ def execute(self, context): self.create_curve(normalized_vert_list, edges) self.add_bevel_obj() - AxisFactory.create( - self.container_object, - (self.x_axis_step, 0, self.z_axis_step), - (self.x_axis_range, [], (data_min, data_max)), - 2, - tick_labels=(tick_labels, [], []), - labels=self.labels, - padding=self.padding, - auto_steps=self.auto_steps, - offset=0.0 - ) + if self.axis_settings.create: + AxisFactory.create( + self.container_object, + (self.axis_settings.x_step, 0, self.axis_settings.z_step), + (self.axis_settings.x_range, [], (data_min, data_max)), + 2, + tick_labels=(tick_labels, [], []), + labels=self.labels, + padding=self.axis_settings.padding, + auto_steps=self.axis_settings.auto_steps, + offset=0.0 + ) return {'FINISHED'} def create_curve(self, verts, edges): diff --git a/data_vis/operators/point_chart.py b/data_vis/operators/point_chart.py index f9aff4d..0b364e5 100644 --- a/data_vis/operators/point_chart.py +++ b/data_vis/operators/point_chart.py @@ -2,7 +2,7 @@ from mathutils import Vector import math -from data_vis.general import OBJECT_OT_GenericChart, DV_LabelPropertyGroup +from data_vis.general import OBJECT_OT_GenericChart, DV_LabelPropertyGroup, DV_AxisPropertyGroup, DV_ColorPropertyGroup from data_vis.operators.features.axis import AxisFactory from data_vis.utils.data_utils import get_data_as_ll, find_data_range, normalize_value, find_axis_range from data_vis.utils.color_utils import sat_col_gen, color_to_triplet, reverse_iterator, ColorGen @@ -83,6 +83,14 @@ class OBJECT_OT_PointChart(OBJECT_OT_GenericChart): type=DV_LabelPropertyGroup ) + axis_settings: bpy.props.PointerProperty( + type=DV_AxisPropertyGroup + ) + + # color_settings: bpy.props.PointerProperty( + # type=DV_ColorPropertyGroup + # ) + @classmethod def poll(cls, context): return DataManager().is_type(DataType.Numerical, 3) @@ -97,12 +105,12 @@ def draw(self, context): row.prop(self, 'color_shade') def init_range(self, data): - self.x_axis_range = find_axis_range(data, 0) - self.y_axis_range = find_axis_range(data, 1) + self.axis_settings.x_range = find_axis_range(data, 0) + self.axis_settings.y_range = find_axis_range(data, 1) def execute(self, context): self.init_data() - if self.auto_ranges: + if self.axis_settings.auto_ranges: self.init_range(self.data) if self.dimensions == '2': @@ -114,41 +122,40 @@ def execute(self, context): value_index = 2 self.create_container() - # fix length of data to parse - data_min, data_max = find_data_range(self.data, self.x_axis_range, self.y_axis_range if self.dimensions == '3' else None) + data_min, data_max = find_data_range(self.data, self.axis_settings.x_range, self.axis_settings.y_range if self.dimensions == '3' else None) color_gen = ColorGen(self.color_shade, (data_min, data_max)) for i, entry in enumerate(self.data): # skip values outside defined axis range - if not self.in_axis_range_bounds(entry): + if not self.in_axis_range_bounds_new(entry): continue - bpy.ops.mesh.primitive_ico_sphere_add() + bpy.ops.mesh.primitive_uv_sphere_add() point_obj = context.active_object point_obj.scale = Vector((self.point_scale, self.point_scale, self.point_scale)) point_obj.active_material = self.new_mat(color_gen.next(entry[value_index]), 1) # normalize height - - x_norm = normalize_value(entry[0], self.x_axis_range[0], self.x_axis_range[1]) + x_norm = normalize_value(entry[0], self.axis_settings.x_range[0], self.axis_settings.x_range[1]) z_norm = normalize_value(entry[value_index], data_min, data_max) if self.dimensions == '2': point_obj.location = (x_norm, 0.0, z_norm) else: - y_norm = normalize_value(entry[1], self.y_axis_range[0], self.y_axis_range[1]) + y_norm = normalize_value(entry[1], self.axis_settings.y_range[0], self.axis_settings.y_range[1]) point_obj.location = (x_norm, y_norm, z_norm) point_obj.parent = self.container_object - AxisFactory.create( - self.container_object, - (self.x_axis_step, self.y_axis_step, self.z_axis_step), - (self.x_axis_range, self.y_axis_range, (data_min, data_max)), - int(self.dimensions), - labels=self.labels, - padding=self.padding, - auto_steps=self.auto_steps, - offset=0.0 - ) + if self.axis_settings.create: + AxisFactory.create( + self.container_object, + (self.axis_settings.x_step, self.axis_settings.y_step, self.axis_settings.z_step), + (self.axis_settings.x_range, self.axis_settings.y_range, (data_min, data_max)), + int(self.dimensions), + labels=self.labels, + padding=self.axis_settings.padding, + auto_steps=self.axis_settings.auto_steps, + offset=0.0 + ) return {'FINISHED'} From 1aefab2c77c3c18177d31c82a627bfe2dd3a862f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zden=C4=9Bk=20Dole=C5=BEal?= Date: Wed, 1 Apr 2020 02:53:33 +0200 Subject: [PATCH 10/24] ColorFactory --- data_vis/colors.py | 44 +++++++++++++++++++++++++++++-- data_vis/general.py | 15 ++++++----- data_vis/operators/bar_chart.py | 13 ++++----- data_vis/operators/point_chart.py | 9 ++++--- 4 files changed, 62 insertions(+), 19 deletions(-) diff --git a/data_vis/colors.py b/data_vis/colors.py index eef0400..d511e50 100644 --- a/data_vis/colors.py +++ b/data_vis/colors.py @@ -1,5 +1,7 @@ import bpy +import random from enum import Enum +from colorsys import rgb_to_hsv, hsv_to_rgb class ColorType(Enum): @@ -17,8 +19,7 @@ def str_to_type(value): class NodeShader: - - def __init__(self, base_color, shader_type=ColorType.Constant, scale=1.0, location_z=0): + def __init__(self, base_color, shader_type, scale=1.0, location_z=0): self.base_color = self.__add_alpha(base_color, 1) self.shader_type = shader_type self.scale = scale @@ -136,6 +137,45 @@ def create_gradient_shader(self, location_z): return material + def get_material(self, *args): + return self.material + def __add_alpha(self, color, alpha): return (color[0], color[1], color[2], alpha) + +class ColorGen: + def __init__(self, base_color, color_type, value_range): + self.base_color = rgb_to_hsv(*base_color) + self.value_range = value_range + self.color_type = color_type + if self.color_type == ColorType.Constant: + self.material = bpy.data.materials.new(name='ChartMat') + self.material.diffuse_color = (*base_color, 1.0) + + def get_material(self, value=1.0): + if self.color_type == ColorType.Constant: + return self.material + elif self.color_type == ColorType.Gradient: + material = bpy.data.materials.new(name='ChartMat') + norm = (value - self.value_range[0]) / (self.value_range[1] - self.value_range[0]) + color = hsv_to_rgb(self.base_color[0], self.base_color[1] * norm, self.base_color[2]) + material.diffuse_color = (*color, 1.0) + return material + elif self.color_type == ColorType.Random: + material = bpy.data.materials.new(name='ChartMat') + material.diffuse_color = (random.random(), random.random(), random.random(), 1.0) + return material + + +class ColoringFactory: + def __init__(self, base_color, color_type, use_shader): + self.base_color = base_color + self.color_type = color_type + self.use_shader = use_shader + + def create(self, value_range=(0, 1), scale=1.0, location_z=0): + if self.use_shader: + return NodeShader(self.base_color, self.color_type, scale, location_z) + else: + return ColorGen(self.base_color, self.color_type, value_range) diff --git a/data_vis/general.py b/data_vis/general.py index 82183ec..857d78e 100644 --- a/data_vis/general.py +++ b/data_vis/general.py @@ -118,17 +118,19 @@ class DV_LabelPropertyGroup(bpy.types.PropertyGroup): class DV_ColorPropertyGroup(bpy.types.PropertyGroup): use_shader: bpy.props.BoolProperty( name='Use Shader', - default=True + default=True, + description='Uses Node Shading to color created objects. Not using this option may create material for every chart object when not using constant color type' ) color_type: bpy.props.EnumProperty( - name='Shader Type', + name='Coloring Type', items=( ('0', 'Constant', 'One color'), ('1', 'Random', 'Random colors'), ('2', 'Gradient', 'Gradient based on value') ), - default='2' + default='2', + description='Type of coloring for chart' ) color_shade: bpy.props.FloatVectorProperty( @@ -136,7 +138,8 @@ class DV_ColorPropertyGroup(bpy.types.PropertyGroup): subtype='COLOR', default=(0.0, 0.0, 1.0), min=0.0, - max=1.0 + max=1.0, + description='Base color shade to work with' ) @@ -217,8 +220,7 @@ def draw_color_settings(self, layout): box = layout.box() box.label(text='Color settings') box.prop(self.color_settings, 'use_shader') - if self.color_settings.use_shader: - box.prop(self.color_settings, 'color_type') + box.prop(self.color_settings, 'color_type') if not ColorType.str_to_type(self.color_settings.color_type) == ColorType.Random: box.prop(self.color_settings, 'color_shade') @@ -275,7 +277,6 @@ def create_container(self): self.container_object = bpy.context.object self.container_object.empty_display_type = 'PLAIN_AXES' self.container_object.name = 'Chart_Container' - # set default location for parent object self.container_object.location = self.chart_origin def data_type_as_enum(self): diff --git a/data_vis/operators/bar_chart.py b/data_vis/operators/bar_chart.py index c011013..78ea2b2 100644 --- a/data_vis/operators/bar_chart.py +++ b/data_vis/operators/bar_chart.py @@ -8,7 +8,7 @@ from data_vis.general import OBJECT_OT_GenericChart, DV_LabelPropertyGroup, DV_ColorPropertyGroup, DV_AxisPropertyGroup from data_vis.operators.features.axis import AxisFactory from data_vis.data_manager import DataManager, DataType -from data_vis.colors import NodeShader, ColorType +from data_vis.colors import ColoringFactory, ColorType class OBJECT_OT_BarChart(OBJECT_OT_GenericChart): @@ -97,8 +97,8 @@ def execute(self, context): data_min = min(self.data, key=lambda val: val[1])[1] data_max = max(self.data, key=lambda val: val[1])[1] - #color_gen = ColorGen(self.color_shade, (data_min, data_max)) - shader = NodeShader(self.color_settings.color_shade, ColorType.str_to_type(self.color_settings.color_type), 2.0, self.chart_origin[2]) + color_factory = ColoringFactory(self.color_settings.color_shade, ColorType.str_to_type(self.color_settings.color_type), self.color_settings.use_shader) + color_gen = color_factory.create((data_min, data_max), 2.0, self.chart_origin[2]) if self.dimensions == '2': value_index = 1 @@ -128,9 +128,10 @@ def execute(self, context): y_norm = normalize_value(entry[1], self.axis_settings.y_range[0], self.axis_settings.y_range[1]) bar_obj.scale = (self.bar_size[0], self.bar_size[1], z_norm * 0.5) bar_obj.location = (x_norm, y_norm, z_norm * 0.5) - - bar_obj.data.materials.append(shader.material) - bar_obj.active_material = shader.material # self.new_mat(color_gen.next(entry[value_index]), 1) + + mat = color_gen.get_material(entry[value_index]) + bar_obj.data.materials.append(mat) + bar_obj.active_material = mat bar_obj.parent = self.container_object if self.axis_settings.create: diff --git a/data_vis/operators/point_chart.py b/data_vis/operators/point_chart.py index 0b364e5..4688aff 100644 --- a/data_vis/operators/point_chart.py +++ b/data_vis/operators/point_chart.py @@ -6,6 +6,7 @@ from data_vis.operators.features.axis import AxisFactory from data_vis.utils.data_utils import get_data_as_ll, find_data_range, normalize_value, find_axis_range from data_vis.utils.color_utils import sat_col_gen, color_to_triplet, reverse_iterator, ColorGen +from data_vis.colors import ColoringFactory from data_vis.data_manager import DataManager, DataType @@ -87,9 +88,9 @@ class OBJECT_OT_PointChart(OBJECT_OT_GenericChart): type=DV_AxisPropertyGroup ) - # color_settings: bpy.props.PointerProperty( - # type=DV_ColorPropertyGroup - # ) + color_settings: bpy.props.PointerProperty( + type=DV_ColorPropertyGroup + ) @classmethod def poll(cls, context): @@ -124,7 +125,7 @@ def execute(self, context): self.create_container() data_min, data_max = find_data_range(self.data, self.axis_settings.x_range, self.axis_settings.y_range if self.dimensions == '3' else None) - color_gen = ColorGen(self.color_shade, (data_min, data_max)) + color_gen = ColorGen(self.color_settings.color_shade, (data_min, data_max), ColorType.str_to_type(self.color_settings.color_type)) for i, entry in enumerate(self.data): # skip values outside defined axis range From 34550072a7146839ee4f03c5bea865ec3385c43c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zden=C4=9Bk=20Dole=C5=BEal?= Date: Wed, 1 Apr 2020 03:36:02 +0200 Subject: [PATCH 11/24] Axis materials added, additional ax set --- data_vis/__init__.py | 27 ++---------- data_vis/colors.py | 14 +++--- data_vis/data_manager.py | 3 +- data_vis/general.py | 12 +---- data_vis/operators/bar_chart.py | 7 +-- data_vis/operators/features/axis.py | 26 ++++++++--- data_vis/operators/line_chart.py | 2 + data_vis/operators/pie_chart.py | 4 +- data_vis/operators/point_chart.py | 68 +++++------------------------ 9 files changed, 51 insertions(+), 112 deletions(-) diff --git a/data_vis/__init__.py b/data_vis/__init__.py index 5a52b4d..afe3a25 100644 --- a/data_vis/__init__.py +++ b/data_vis/__init__.py @@ -34,9 +34,7 @@ class DV_AddonPanel(bpy.types.Panel): def draw(self, context): layout = self.layout - - data_storage = bpy.data.scenes[0] - + row = layout.row() row.label(text='Data', icon='WORLD_DATA') @@ -48,16 +46,10 @@ def draw(self, context): box.label(text='Labels: ' + str(data_manager.has_labels)) box.label(text='Type: ' + str(data_manager.predicted_data_type)) - layout.label(text='Axis settings') - - row = layout.row() - row.prop(data_storage.dv_props, 'text_size') - - row = layout.row() - row.prop(data_storage.dv_props, 'axis_thickness') + layout.label(text='General Settings') row = layout.row() - row.prop(data_storage.dv_props, 'axis_tick_mark_height') + row.prop(bpy.data.scenes[0].dv_props, 'text_size') class DV_PropertyGroup(bpy.types.PropertyGroup): @@ -67,18 +59,7 @@ class DV_PropertyGroup(bpy.types.PropertyGroup): text_size: bpy.props.FloatProperty( name='Text size', default=0.05, - description='Size of addon generated text' - ) - - axis_thickness: bpy.props.FloatProperty( - name='Axis thickness', - default=0.01, - description='How thick is the axis object' - ) - - axis_tick_mark_height: bpy.props.FloatProperty( - name='Axis tick mark height', - default=0.03 + description='Size of text generated by addon' ) diff --git a/data_vis/colors.py b/data_vis/colors.py index d511e50..53c4f4b 100644 --- a/data_vis/colors.py +++ b/data_vis/colors.py @@ -34,7 +34,7 @@ def __init__(self, base_color, shader_type, scale=1.0, location_z=0): raise AttributeError('Unsupported shader type!') def create_random_shader(self): - material = bpy.data.materials.new(name='ChartMat') + material = bpy.data.materials.new(name='DV_ChartMat') material.use_nodes = True nodes = material.node_tree.nodes @@ -61,7 +61,7 @@ def create_random_shader(self): return material def create_const_shader(self): - material = bpy.data.materials.new(name='ChartMat') + material = bpy.data.materials.new(name='DV_ChartMat') material.use_nodes = True nodes = material.node_tree.nodes @@ -96,7 +96,7 @@ def create_const_shader(self): return material def create_gradient_shader(self, location_z): - material = bpy.data.materials.new(name='ChartMat') + material = bpy.data.materials.new(name='DV_ChartMat') material.use_nodes = True nodes = material.node_tree.nodes @@ -150,21 +150,21 @@ def __init__(self, base_color, color_type, value_range): self.value_range = value_range self.color_type = color_type if self.color_type == ColorType.Constant: - self.material = bpy.data.materials.new(name='ChartMat') + self.material = bpy.data.materials.new(name='DV_ChartMat') self.material.diffuse_color = (*base_color, 1.0) def get_material(self, value=1.0): if self.color_type == ColorType.Constant: return self.material elif self.color_type == ColorType.Gradient: - material = bpy.data.materials.new(name='ChartMat') + material = bpy.data.materials.new(name='DV_ChartMat') norm = (value - self.value_range[0]) / (self.value_range[1] - self.value_range[0]) color = hsv_to_rgb(self.base_color[0], self.base_color[1] * norm, self.base_color[2]) material.diffuse_color = (*color, 1.0) return material elif self.color_type == ColorType.Random: - material = bpy.data.materials.new(name='ChartMat') - material.diffuse_color = (random.random(), random.random(), random.random(), 1.0) + material = bpy.data.materials.new(name='DV_ChartMat') + material.diffuse_color = (*hsv_to_rgb(random.random(), 1.0, 1.0), 1.0) return material diff --git a/data_vis/data_manager.py b/data_vis/data_manager.py index 43d8c58..e7cd3f4 100644 --- a/data_vis/data_manager.py +++ b/data_vis/data_manager.py @@ -46,9 +46,8 @@ def analyse_data(self): for i, col in enumerate(self.raw_data[0]): try: row = float(col) - print(row) except Exception as e: - print(e) + print('Labels analysis: ', e) total += 1 if total == len(self.raw_data[0]): diff --git a/data_vis/general.py b/data_vis/general.py index 857d78e..ffe167d 100644 --- a/data_vis/general.py +++ b/data_vis/general.py @@ -151,14 +151,6 @@ class Properties: def get_text_size(): return bpy.data.scenes[0].dv_props.text_size - @staticmethod - def get_axis_thickness(): - return bpy.data.scenes[0].dv_props.axis_thickness - - @staticmethod - def get_axis_tick_mark_height(): - return bpy.data.scenes[0].dv_props.axis_tick_mark_height - class OBJECT_OT_GenericChart(bpy.types.Operator): '''Creates chart''' @@ -172,7 +164,6 @@ class OBJECT_OT_GenericChart(bpy.types.Operator): def __init__(self): self.container_object = None self.labels = [] - self.chart_origin = (0, 0, 0) self.dm = DataManager() if hasattr(self, 'dimensions'): self.dimensions = str(self.dm.dimensions) @@ -269,7 +260,6 @@ def execute(self, context): raise NotImplementedError('Execute method should be implemented in every chart operator!') def invoke(self, context, event): - self.chart_origin = context.scene.cursor.location return context.window_manager.invoke_props_dialog(self) def create_container(self): @@ -277,7 +267,7 @@ def create_container(self): self.container_object = bpy.context.object self.container_object.empty_display_type = 'PLAIN_AXES' self.container_object.name = 'Chart_Container' - self.container_object.location = self.chart_origin + self.container_object.location = bpy.context.scene.cursor.location def data_type_as_enum(self): if not hasattr(self, 'data_type'): diff --git a/data_vis/operators/bar_chart.py b/data_vis/operators/bar_chart.py index 78ea2b2..bae2b19 100644 --- a/data_vis/operators/bar_chart.py +++ b/data_vis/operators/bar_chart.py @@ -4,7 +4,6 @@ from data_vis.utils.data_utils import get_data_as_ll, find_data_range, normalize_value, find_axis_range -from data_vis.utils.color_utils import ColorGen from data_vis.general import OBJECT_OT_GenericChart, DV_LabelPropertyGroup, DV_ColorPropertyGroup, DV_AxisPropertyGroup from data_vis.operators.features.axis import AxisFactory from data_vis.data_manager import DataManager, DataType @@ -86,7 +85,6 @@ def execute(self, context): self.report({'ERROR'}, 'Data are only 2D!') return {'CANCELLED'} tick_labels = [] - self.create_container() if self.data_type_as_enum() == DataType.Numerical: try: data_min, data_max = find_data_range(self.data, self.axis_settings.x_range, self.axis_settings.y_range if self.dimensions == '3' else None) @@ -97,8 +95,9 @@ def execute(self, context): data_min = min(self.data, key=lambda val: val[1])[1] data_max = max(self.data, key=lambda val: val[1])[1] + self.create_container() color_factory = ColoringFactory(self.color_settings.color_shade, ColorType.str_to_type(self.color_settings.color_type), self.color_settings.use_shader) - color_gen = color_factory.create((data_min, data_max), 2.0, self.chart_origin[2]) + color_gen = color_factory.create((data_min, data_max), 2.0, self.container_object.location[2]) if self.dimensions == '2': value_index = 1 @@ -140,6 +139,8 @@ def execute(self, context): (self.axis_settings.x_step, self.axis_settings.y_step, self.axis_settings.z_step), (self.axis_settings.x_range, self.axis_settings.y_range, (data_min, data_max)), int(self.dimensions), + self.axis_settings.thickness, + self.axis_settings.tick_mark_height, tick_labels=(tick_labels, [], []), labels=self.labels, padding=self.axis_settings.padding, diff --git a/data_vis/operators/features/axis.py b/data_vis/operators/features/axis.py index 42015dd..a71e8a1 100644 --- a/data_vis/operators/features/axis.py +++ b/data_vis/operators/features/axis.py @@ -15,7 +15,7 @@ class AxisDir(Enum): class AxisFactory: @staticmethod - def create(parent, axis_steps, axis_ranges, dim, labels=[], tick_labels=([], [], []), auto_steps=False, padding=0.0, offset=0.0): + def create(parent, axis_steps, axis_ranges, dim, thickness, tick_height, labels=[], tick_labels=([], [], []), auto_steps=False, padding=0.0, offset=0.0): ''' Factory method that creates all axis with all values specified by parameters parent - parent object for axis containers @@ -43,9 +43,9 @@ def create(parent, axis_steps, axis_ranges, dim, labels=[], tick_labels=([], [], dir_idx = i if dim == 2 and i == 1: dir_idx = 2 - axis = Axis(parent, axis_steps[dir_idx], axis_ranges[dir_idx], direction, tick_labels[dir_idx], auto_steps) + axis = Axis(parent, axis_steps[dir_idx], axis_ranges[dir_idx], direction, tick_labels[dir_idx], thickness, tick_height, auto_steps) else: - axis = Axis(parent, axis_steps[dir_idx], axis_ranges[dir_idx], direction, tick_labels[dir_idx], auto_steps) + axis = Axis(parent, axis_steps[dir_idx], axis_ranges[dir_idx], direction, tick_labels[dir_idx], thickness, tick_height, auto_steps) axis.create(padding, offset, labels[dir_idx], True if dim == 2 else False) @@ -58,15 +58,15 @@ class Axis: dir - direction of axis specified by AxisDir class hm - height multiplier to normalize chart height ''' - def __init__(self, parent, step, ax_range, ax_dir, labels, auto_step=False): + def __init__(self, parent, step, ax_range, ax_dir, labels, thickness, tick_height, auto_step=False): self.range = ax_range if not auto_step or len(labels) > 0: self.step = step else: self.step = (self.range[1] - self.range[0]) / 10 self.parent_object = parent - self.thickness = Properties.get_axis_thickness() - self.mark_height = Properties.get_axis_tick_mark_height() + self.thickness = thickness + self.mark_height = tick_height self.text_size = Properties.get_text_size() if isinstance(ax_dir, AxisDir): self.dir = ax_dir @@ -75,6 +75,16 @@ def __init__(self, parent, step, ax_range, ax_dir, labels, auto_step=False): self.axis_cont = None self.labels = labels + self.create_materials() + + def create_materials(self): + self.axis_mat = bpy.data.materials.get('DV_AxisMat') + if self.axis_mat is None: + self.axis_mat = bpy.data.materials.new(name='DV_AxisMat') + + self.tick_mat = bpy.data.materials.get('DV_TickMat') + if self.tick_mat is None: + self.tick_mat = bpy.data.materials.new(name='DV_TickMat') def create_container(self): ''' @@ -99,6 +109,8 @@ def create_axis_line(self, length): obj.scale = (length, self.thickness, self.thickness) obj.location.x += length + obj.data.materials.append(self.axis_mat) + obj.active_material = self.axis_mat return obj def create_tick_mark(self, x_location): @@ -113,6 +125,8 @@ def create_tick_mark(self, x_location): obj.location = (0, 0, 0) obj.location.x += x_location - self.thickness * 0.5 obj.parent = self.axis_cont + obj.data.materials.append(self.tick_mat) + obj.active_material = self.tick_mat def create_ticks(self, start_pos): ''' diff --git a/data_vis/operators/line_chart.py b/data_vis/operators/line_chart.py index 9143b66..3e66c77 100644 --- a/data_vis/operators/line_chart.py +++ b/data_vis/operators/line_chart.py @@ -146,6 +146,8 @@ def execute(self, context): (self.axis_settings.x_step, 0, self.axis_settings.z_step), (self.axis_settings.x_range, [], (data_min, data_max)), 2, + self.axis_settings.thickness, + self.axis_settings.tick_mark_height, tick_labels=(tick_labels, [], []), labels=self.labels, padding=self.axis_settings.padding, diff --git a/data_vis/operators/pie_chart.py b/data_vis/operators/pie_chart.py index d117d94..8b12463 100644 --- a/data_vis/operators/pie_chart.py +++ b/data_vis/operators/pie_chart.py @@ -3,9 +3,9 @@ from mathutils import Matrix, Vector from data_vis.utils.data_utils import get_data_as_ll, find_data_range -from data_vis.utils.color_utils import sat_col_gen, ColorGen from data_vis.general import OBJECT_OT_GenericChart from data_vis.data_manager import DataManager, DataType +from data_vis.colors import ColorGen class OBJECT_OT_PieChart(OBJECT_OT_GenericChart): @@ -83,7 +83,7 @@ def execute(self, context): raise Exception('Error occurred, try to increase number of vertices, i_from" {}, i_to: {}, inc: {}, val: {}'.format(prev_i, portion_end_i, increment, self.data[i][1])) break - slice_mat = self.new_mat(color_gen.next(data_len - i), 1) + slice_mat = color_gen.get_material(data_len - i) slice_obj.active_material = slice_mat slice_obj.parent = self.container_object label_rot_z = (((prev_i + portion_end_i) * 0.5) / self.vertices) * 2.0 * math.pi diff --git a/data_vis/operators/point_chart.py b/data_vis/operators/point_chart.py index 4688aff..84628aa 100644 --- a/data_vis/operators/point_chart.py +++ b/data_vis/operators/point_chart.py @@ -6,7 +6,7 @@ from data_vis.operators.features.axis import AxisFactory from data_vis.utils.data_utils import get_data_as_ll, find_data_range, normalize_value, find_axis_range from data_vis.utils.color_utils import sat_col_gen, color_to_triplet, reverse_iterator, ColorGen -from data_vis.colors import ColoringFactory +from data_vis.colors import ColoringFactory, ColorType from data_vis.data_manager import DataManager, DataType @@ -29,57 +29,6 @@ class OBJECT_OT_PointChart(OBJECT_OT_GenericChart): default=0.05 ) - auto_ranges: bpy.props.BoolProperty( - name='Automatic axis ranges', - default=True - ) - - auto_steps: bpy.props.BoolProperty( - name='Automatic axis steps', - default=True - ) - - x_axis_step: bpy.props.FloatProperty( - name='Step of x axis', - default=1.0 - ) - - x_axis_range: bpy.props.FloatVectorProperty( - name='Range of x axis', - size=2, - default=(0.0, 1.0) - ) - - y_axis_step: bpy.props.FloatProperty( - name='Step of y axis', - default=1.0 - ) - - y_axis_range: bpy.props.FloatVectorProperty( - name='Range of y axis', - size=2, - default=(0.0, 1.0) - ) - - z_axis_step: bpy.props.FloatProperty( - name='Step of z axis', - default=1.0 - ) - - color_shade: bpy.props.FloatVectorProperty( - name='Color', - subtype='COLOR', - default=(0.0, 0.0, 1.0), - min=0.0, - max=1.0 - ) - - padding: bpy.props.FloatProperty( - name='Padding', - default=0.1, - min=0.0 - ) - label_settings: bpy.props.PointerProperty( type=DV_LabelPropertyGroup ) @@ -102,9 +51,6 @@ def draw(self, context): row = layout.row() row.prop(self, 'point_scale') - row = layout.row() - row.prop(self, 'color_shade') - def init_range(self, data): self.axis_settings.x_range = find_axis_range(data, 0) self.axis_settings.y_range = find_axis_range(data, 1) @@ -124,8 +70,9 @@ def execute(self, context): self.create_container() data_min, data_max = find_data_range(self.data, self.axis_settings.x_range, self.axis_settings.y_range if self.dimensions == '3' else None) - - color_gen = ColorGen(self.color_settings.color_shade, (data_min, data_max), ColorType.str_to_type(self.color_settings.color_type)) + color_factory = ColoringFactory(self.color_settings.color_shade, ColorType.str_to_type(self.color_settings.color_type), self.color_settings.use_shader) + color_gen = color_factory.create((data_min, data_max), 1.0, self.container_object.location[2]) + for i, entry in enumerate(self.data): # skip values outside defined axis range @@ -135,7 +82,10 @@ def execute(self, context): bpy.ops.mesh.primitive_uv_sphere_add() point_obj = context.active_object point_obj.scale = Vector((self.point_scale, self.point_scale, self.point_scale)) - point_obj.active_material = self.new_mat(color_gen.next(entry[value_index]), 1) + + mat = color_gen.get_material(entry[value_index]) + point_obj.data.materials.append(mat) + point_obj.active_material = mat # normalize height x_norm = normalize_value(entry[0], self.axis_settings.x_range[0], self.axis_settings.x_range[1]) @@ -154,6 +104,8 @@ def execute(self, context): (self.axis_settings.x_step, self.axis_settings.y_step, self.axis_settings.z_step), (self.axis_settings.x_range, self.axis_settings.y_range, (data_min, data_max)), int(self.dimensions), + self.axis_settings.thickness, + self.axis_settings.tick_mark_height, labels=self.labels, padding=self.axis_settings.padding, auto_steps=self.axis_settings.auto_steps, From cd26de98b437448f197f52ae531d304ef01b6a65 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zden=C4=9Bk=20Dole=C5=BEal?= Date: Wed, 1 Apr 2020 17:17:35 +0200 Subject: [PATCH 12/24] Text material added --- data_vis/__init__.py | 2 +- data_vis/operators/features/axis.py | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/data_vis/__init__.py b/data_vis/__init__.py index afe3a25..41945e3 100644 --- a/data_vis/__init__.py +++ b/data_vis/__init__.py @@ -34,7 +34,7 @@ class DV_AddonPanel(bpy.types.Panel): def draw(self, context): layout = self.layout - + row = layout.row() row.label(text='Data', icon='WORLD_DATA') diff --git a/data_vis/operators/features/axis.py b/data_vis/operators/features/axis.py index a71e8a1..5321011 100644 --- a/data_vis/operators/features/axis.py +++ b/data_vis/operators/features/axis.py @@ -86,6 +86,10 @@ def create_materials(self): if self.tick_mat is None: self.tick_mat = bpy.data.materials.new(name='DV_TickMat') + self.text_mat = bpy.data.materials.get('DV_TextMat') + if self.text_mat is None: + self.text_mat = bpy.data.materials.new(name='DV_TextMat') + def create_container(self): ''' Creates container for axis, with default name 'Axis_Container_DIM' where DIM is X, Y or Z @@ -200,6 +204,8 @@ def create_text_object(self, value): obj.data.body = str(value) obj.data.align_x = 'CENTER' obj.scale *= self.text_size + obj.data.materials.append(self.text_mat) + obj.active_material = self.text_mat return obj def rotate_text_object(self, obj): From ea5ed253ee0bbfa90dcac8b0c4576cd0a7016c5b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zden=C4=9Bk=20Dole=C5=BEal?= Date: Thu, 2 Apr 2020 02:14:41 +0200 Subject: [PATCH 13/24] interpolation using scipy --- data_vis/__init__.py | 4 ++ data_vis/general.py | 5 ++ data_vis/operators/bar_chart.py | 6 +-- data_vis/operators/pie_chart.py | 2 +- data_vis/operators/point_chart.py | 6 +-- data_vis/operators/surface_chart.py | 84 +++++++++++++++++++++++++++++ data_vis/utils/color_utils.py | 9 ---- data_vis/utils/data_utils.py | 79 --------------------------- 8 files changed, 96 insertions(+), 99 deletions(-) create mode 100644 data_vis/operators/surface_chart.py diff --git a/data_vis/__init__.py b/data_vis/__init__.py index 41945e3..bb6be79 100644 --- a/data_vis/__init__.py +++ b/data_vis/__init__.py @@ -18,6 +18,7 @@ from .operators.line_chart import OBJECT_OT_LineChart from .operators.pie_chart import OBJECT_OT_PieChart from .operators.point_chart import OBJECT_OT_PointChart +from .operators.surface_chart import OBJECT_OT_SurfaceChart from .general import DV_LabelPropertyGroup, DV_ColorPropertyGroup, DV_AxisPropertyGroup from .data_manager import DataManager @@ -77,6 +78,7 @@ def draw(self, context): layout.operator(OBJECT_OT_LineChart.bl_idname, icon_value=main_icons['line_chart'].icon_id) layout.operator(OBJECT_OT_PieChart.bl_idname, icon_value=main_icons['pie_chart'].icon_id) layout.operator(OBJECT_OT_PointChart.bl_idname, icon_value=main_icons['point_chart'].icon_id) + layout.operator(OBJECT_OT_SurfaceChart.bl_idname) preview_collections = {} @@ -116,6 +118,7 @@ def register(): bpy.utils.register_class(OBJECT_OT_PieChart) bpy.utils.register_class(OBJECT_OT_LineChart) bpy.utils.register_class(OBJECT_OT_PointChart) + bpy.utils.register_class(OBJECT_OT_SurfaceChart) bpy.utils.register_class(FILE_OT_DVLoadFile) bpy.utils.register_class(DV_AddonPanel) bpy.utils.register_class(OBJECT_MT_AddChart) @@ -133,6 +136,7 @@ def unregister(): bpy.utils.unregister_class(OBJECT_OT_PieChart) bpy.utils.unregister_class(OBJECT_OT_LineChart) bpy.utils.unregister_class(OBJECT_OT_PointChart) + bpy.utils.unregister_class(OBJECT_OT_SurfaceChart) bpy.utils.unregister_class(FILE_OT_DVLoadFile) bpy.utils.unregister_class(DV_LabelPropertyGroup) bpy.utils.unregister_class(DV_ColorPropertyGroup) diff --git a/data_vis/general.py b/data_vis/general.py index ffe167d..9af9ea4 100644 --- a/data_vis/general.py +++ b/data_vis/general.py @@ -4,6 +4,7 @@ from mathutils import Vector from data_vis.data_manager import DataManager, DataType +from data_vis.utils.data_utils import find_axis_range from data_vis.colors import ColorType @@ -307,6 +308,10 @@ def init_labels(self): else: self.labels = [self.label_settings.x_label, self.label_settings.y_label, self.label_settings.z_label] + def init_range(self, data): + self.axis_settings.x_range = find_axis_range(data, 0) + self.axis_settings.y_range = find_axis_range(data, 1) + def in_axis_range_bounds(self, entry): ''' Checks whether the entry point defined as [x, y, z] is within user selected axis range diff --git a/data_vis/operators/bar_chart.py b/data_vis/operators/bar_chart.py index bae2b19..a0c9ed7 100644 --- a/data_vis/operators/bar_chart.py +++ b/data_vis/operators/bar_chart.py @@ -3,7 +3,7 @@ from mathutils import Vector -from data_vis.utils.data_utils import get_data_as_ll, find_data_range, normalize_value, find_axis_range +from data_vis.utils.data_utils import find_data_range, normalize_value, find_axis_range from data_vis.general import OBJECT_OT_GenericChart, DV_LabelPropertyGroup, DV_ColorPropertyGroup, DV_AxisPropertyGroup from data_vis.operators.features.axis import AxisFactory from data_vis.data_manager import DataManager, DataType @@ -61,10 +61,6 @@ def draw(self, context): row = layout.row() row.prop(self, 'bar_size') - def init_range(self, data): - self.axis_settings.x_range = find_axis_range(data, 0) - self.axis_settings.y_range = find_axis_range(data, 1) - def data_type_as_enum(self): if self.data_type == '0': return DataType.Numerical diff --git a/data_vis/operators/pie_chart.py b/data_vis/operators/pie_chart.py index 8b12463..1e1918b 100644 --- a/data_vis/operators/pie_chart.py +++ b/data_vis/operators/pie_chart.py @@ -2,7 +2,7 @@ import math from mathutils import Matrix, Vector -from data_vis.utils.data_utils import get_data_as_ll, find_data_range +from data_vis.utils.data_utils import find_data_range from data_vis.general import OBJECT_OT_GenericChart from data_vis.data_manager import DataManager, DataType from data_vis.colors import ColorGen diff --git a/data_vis/operators/point_chart.py b/data_vis/operators/point_chart.py index 84628aa..44de533 100644 --- a/data_vis/operators/point_chart.py +++ b/data_vis/operators/point_chart.py @@ -4,7 +4,7 @@ from data_vis.general import OBJECT_OT_GenericChart, DV_LabelPropertyGroup, DV_AxisPropertyGroup, DV_ColorPropertyGroup from data_vis.operators.features.axis import AxisFactory -from data_vis.utils.data_utils import get_data_as_ll, find_data_range, normalize_value, find_axis_range +from data_vis.utils.data_utils import find_data_range, normalize_value, find_axis_range from data_vis.utils.color_utils import sat_col_gen, color_to_triplet, reverse_iterator, ColorGen from data_vis.colors import ColoringFactory, ColorType from data_vis.data_manager import DataManager, DataType @@ -51,10 +51,6 @@ def draw(self, context): row = layout.row() row.prop(self, 'point_scale') - def init_range(self, data): - self.axis_settings.x_range = find_axis_range(data, 0) - self.axis_settings.y_range = find_axis_range(data, 1) - def execute(self, context): self.init_data() if self.axis_settings.auto_ranges: diff --git a/data_vis/operators/surface_chart.py b/data_vis/operators/surface_chart.py new file mode 100644 index 0000000..4ec0b31 --- /dev/null +++ b/data_vis/operators/surface_chart.py @@ -0,0 +1,84 @@ +import bpy +from scipy.interpolate import griddata +import numpy as np +import math + +from data_vis.general import OBJECT_OT_GenericChart, DV_AxisPropertyGroup +from data_vis.utils.data_utils import find_data_range, normalize_value + + +class OBJECT_OT_SurfaceChart(OBJECT_OT_GenericChart): + '''Creates Surface Chart''' + bl_idname = 'object.create_surface_chart' + bl_label = 'Surface Chart' + bl_options = {'REGISTER', 'UNDO'} + + density: bpy.props.IntProperty( + name='Density of grid', + min=1, + default=50, + ) + + axis_settings: bpy.props.PointerProperty( + type=DV_AxisPropertyGroup + ) + + @classmethod + def poll(cls, context): + return True + + def draw(self, context): + super().draw(context) + layout = self.layout + row = layout.row() + row.prop(self, 'density') + + def face(self, column, row): + return (column* self.density + row, + (column + 1) * self.density + row, + (column + 1) * self.density + 1 + row, + column * self.density + 1 + row) + + def execute(self, context): + self.init_data() + if self.axis_settings.auto_ranges: + self.init_range(self.data) + + self.create_container() + + x = np.linspace(0, 1, self.density) + y = np.linspace(0, 1, self.density) + X, Y = np.meshgrid(x, y) + + data_min, data_max = find_data_range(self.data, self.axis_settings.x_range, self.axis_settings.y_range) + + px = [entry[0] for entry in self.data] + py = [entry[1] for entry in self.data] + f = [entry[2] for entry in self.data] + + res = griddata((px, py), f, (X, Y)) + + faces = [] + verts = [] + for x in range(self.density): + for y in range(self.density): + x_norm = x / self.density + y_norm = y / self.density + value = res[x][y] + if math.isnan(value): + value = 0.0 + z_norm = normalize_value(value, data_min, data_max) + # bpy.ops.mesh.primitive_uv_sphere_add() + # obj = context.active_object + # obj.scale = (0.02, 0.02, 0.02) + # obj.location = (, y / nof_points, res[x][y]) + verts.append((x_norm, y_norm, z_norm)) + + mesh = bpy.data.meshes.new('DV_SurfaceChart_Mesh') + mesh.from_pydata(verts, [], faces) + + obj = bpy.data.objects.new('SurfaceChart', mesh) + bpy.context.scene.collection.objects.link(obj) + + return {'FINISHED'} + diff --git a/data_vis/utils/color_utils.py b/data_vis/utils/color_utils.py index 612b766..87c1b3b 100644 --- a/data_vis/utils/color_utils.py +++ b/data_vis/utils/color_utils.py @@ -1,15 +1,6 @@ from mathutils import Vector from colorsys import hsv_to_rgb, rgb_to_hsv - -def random_col_gen(): - ... - - -def color_variety_gen(): - ... - - def rgb_col_gen(length, r, g, b): base_color = Vector((r, g, b)) step_size = 1.0 / length diff --git a/data_vis/utils/data_utils.py b/data_vis/utils/data_utils.py index a348fa3..60a2a5a 100644 --- a/data_vis/utils/data_utils.py +++ b/data_vis/utils/data_utils.py @@ -1,19 +1,5 @@ from data_vis.data_manager import DataType -def get_row_list(row_data, data_type, separator): - if data_type == DataType.Categorical: - row = row_data.value.split(separator) - return [str(row[0]), float(row[1])] - elif data_type == DataType.Numerical: - return [float(x) for x in row_data.value.split(separator)] - - -def get_data_as_ll(data, data_type, separator=','): - mat = [] - for row in data: - mat.append(get_row_list(row, data_type, separator)) - return mat - def find_axis_range(data, val_idx): return (min(data, key=lambda x: x[val_idx])[val_idx], max(data, key=lambda x: x[val_idx])[val_idx]) @@ -40,71 +26,6 @@ def find_data_range(data, range_x, range_y=None): return (min(filtered_data, key=lambda x: x[top_index])[top_index], max(filtered_data, key=lambda x: x[top_index])[top_index]) -def get_col_float(row_data, col, separator=','): - ''' - Tries to convert row_data at column position into float value, uses separator to split the row into columns. - row_data - row of data to parse - col - column to retrieve from row_data - - returns - tuple of (value, return_code) - value of col and True if succesfull, else 1.0 and False - ''' - try: - return (float(row_data.value.split(separator)[col].replace('"', '')), True) - except Exception as e: - print('data[{}] - conversion to float error: {}'.format(col, e)) - return (1.0, False) - - -def get_col_str(row_data, col, separator=','): - return str(row_data.value.split(separator)[col]) - - -def col_values_min(data, col, start=0, nof=0): - ''' - Finds min value in column in given data and returns it - ''' - if start >= len(data) or start + nof > len(data): - raise IndexError('Out of data bounds!') - if nof == 0: - nof = len(data) - - found_min, res = get_col_float(min(data[start:start + nof], key=lambda x: get_col_float(x, col)[0]), col) - return (found_min, res) - - -def col_values_max(data, col, start=0, nof=0): - ''' - Finds max value in column in given data and returns it - ''' - if start >= len(data) or start + nof > len(data): - raise IndexError('NOF > Length of data!') - if nof == 0: - nof = len(data) - found_max, res = get_col_float(max(data[start:start + nof], key=lambda x: get_col_float(x, col)[0]), col) - return (found_max, res) - - -def col_values_min_max(data, col, start=0, nof=0): - ''' - Finds min and max value in column in given data and returns it - ''' - found_max, _ = col_values_max(data, col, start, nof) - found_min, _ = col_values_min(data, col, start, nof) - return (found_min, found_max) - - -def col_values_sum(data, col): - ''' - Counts sum of given data values and returns it - ''' - total = 0 - for i in range(0, len(data)): - value, res = get_col_float(data[i], col) - total += value - - return total - - def float_data_gen(data, col, label_col, separator=','): ''' Creates generator from data entry saved in blender data From d90e19bdda06fdc88b4c110588b51d69237e3f3c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zden=C4=9Bk=20Dole=C5=BEal?= Date: Thu, 2 Apr 2020 14:23:27 +0200 Subject: [PATCH 14/24] Mesh Creation --- data_vis/operators/features/axis.py | 2 +- data_vis/operators/surface_chart.py | 78 +++++++++++++++++++++-------- 2 files changed, 58 insertions(+), 22 deletions(-) diff --git a/data_vis/operators/features/axis.py b/data_vis/operators/features/axis.py index 5321011..c4bdc63 100644 --- a/data_vis/operators/features/axis.py +++ b/data_vis/operators/features/axis.py @@ -15,7 +15,7 @@ class AxisDir(Enum): class AxisFactory: @staticmethod - def create(parent, axis_steps, axis_ranges, dim, thickness, tick_height, labels=[], tick_labels=([], [], []), auto_steps=False, padding=0.0, offset=0.0): + def create(parent, axis_steps, axis_ranges, dim, thickness, tick_height, labels=(None, None, None), tick_labels=([], [], []), auto_steps=False, padding=0.0, offset=0.0): ''' Factory method that creates all axis with all values specified by parameters parent - parent object for axis containers diff --git a/data_vis/operators/surface_chart.py b/data_vis/operators/surface_chart.py index 4ec0b31..8355aff 100644 --- a/data_vis/operators/surface_chart.py +++ b/data_vis/operators/surface_chart.py @@ -3,8 +3,10 @@ import numpy as np import math -from data_vis.general import OBJECT_OT_GenericChart, DV_AxisPropertyGroup +from data_vis.general import OBJECT_OT_GenericChart, DV_AxisPropertyGroup, DV_LabelPropertyGroup, DV_ColorPropertyGroup from data_vis.utils.data_utils import find_data_range, normalize_value +from data_vis.colors import ColoringFactory, ColorType +from data_vis.operators.features.axis import AxisFactory class OBJECT_OT_SurfaceChart(OBJECT_OT_GenericChart): @@ -16,13 +18,30 @@ class OBJECT_OT_SurfaceChart(OBJECT_OT_GenericChart): density: bpy.props.IntProperty( name='Density of grid', min=1, - default=50, + default=10, + ) + + interpolation_method: bpy.props.EnumProperty( + name='Interpolation method', + items=( + ('nearest', 'Nearest', 'nearest'), + ('linear', 'Linear', 'linear'), + ('cubic', 'Cubic', 'cubic'), + ) ) axis_settings: bpy.props.PointerProperty( type=DV_AxisPropertyGroup ) + label_settings: bpy.props.PointerProperty( + type=DV_LabelPropertyGroup + ) + + color_settings: bpy.props.PointerProperty( + type=DV_ColorPropertyGroup + ) + @classmethod def poll(cls, context): return True @@ -31,13 +50,15 @@ def draw(self, context): super().draw(context) layout = self.layout row = layout.row() + row.prop(self, 'interpolation_method') + row = layout.row() row.prop(self, 'density') def face(self, column, row): - return (column* self.density + row, - (column + 1) * self.density + row, - (column + 1) * self.density + 1 + row, - column * self.density + 1 + row) + return (column * self.density + row, + (column + 1) * self.density + row, + (column + 1) * self.density + 1 + row, + column * self.density + 1 + row) def execute(self, context): self.init_data() @@ -54,31 +75,46 @@ def execute(self, context): px = [entry[0] for entry in self.data] py = [entry[1] for entry in self.data] - f = [entry[2] for entry in self.data] - - res = griddata((px, py), f, (X, Y)) + f = [normalize_value(entry[2], data_min, data_max) for entry in self.data] + res = griddata((px, py), f, (X, Y), self.interpolation_method, 0.0 ) faces = [] verts = [] - for x in range(self.density): - for y in range(self.density): - x_norm = x / self.density - y_norm = y / self.density - value = res[x][y] - if math.isnan(value): - value = 0.0 + for row in range(self.density): + for col in range(self.density): + x_norm = row / self.density + y_norm = col / self.density + value = res[row][col] z_norm = normalize_value(value, data_min, data_max) - # bpy.ops.mesh.primitive_uv_sphere_add() - # obj = context.active_object - # obj.scale = (0.02, 0.02, 0.02) - # obj.location = (, y / nof_points, res[x][y]) verts.append((x_norm, y_norm, z_norm)) + if row < self.density - 1 and col < self.density - 1: + fac = self.face(col, row) + faces.append(fac) mesh = bpy.data.meshes.new('DV_SurfaceChart_Mesh') mesh.from_pydata(verts, [], faces) - obj = bpy.data.objects.new('SurfaceChart', mesh) + obj = bpy.data.objects.new('SurfaceChart_Mesh_Obj', mesh) bpy.context.scene.collection.objects.link(obj) + obj.parent = self.container_object + + cf = ColoringFactory(self.color_settings.color_shade, ColorType.str_to_type(self.color_settings.color_type), self.color_settings.use_shader) + mat = cf.create((data_min, data_max)).get_material(1.0) + obj.data.materials.append(mat) + obj.active_material = mat + + if self.axis_settings.create: + AxisFactory.create( + self.container_object, + (self.axis_settings.x_step, self.axis_settings.y_step, self.axis_settings.z_step), + (self.axis_settings.x_range, self.axis_settings.y_range, (data_min, data_max)), + 3, + self.axis_settings.thickness, + self.axis_settings.tick_mark_height, + padding=self.axis_settings.padding, + auto_steps=self.axis_settings.auto_steps, + offset=0.0 + ) return {'FINISHED'} From 7109385dda7b0f118afb3111772212baba977343 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zden=C4=9Bk=20Dole=C5=BEal?= Date: Thu, 2 Apr 2020 14:36:37 +0200 Subject: [PATCH 15/24] Geometry based shader, position fix --- data_vis/colors.py | 48 +++++++++++++++++++++++++---- data_vis/operators/surface_chart.py | 26 ++++++++++------ 2 files changed, 59 insertions(+), 15 deletions(-) diff --git a/data_vis/colors.py b/data_vis/colors.py index 53c4f4b..8a3e9f1 100644 --- a/data_vis/colors.py +++ b/data_vis/colors.py @@ -7,7 +7,8 @@ class ColorType(Enum): Constant = 0 Random = 1 - Gradient = 2 + Gradient = 2, + Custom = 3 def str_to_type(value): if str(value) == '0' or value == 'Constant': @@ -19,18 +20,19 @@ def str_to_type(value): class NodeShader: - def __init__(self, base_color, shader_type, scale=1.0, location_z=0): + def __init__(self, base_color, shader_type=ColorType.Custom, scale=1.0, location_z=0): self.base_color = self.__add_alpha(base_color, 1) self.shader_type = shader_type self.scale = scale + self.location_z = location_z if self.shader_type == ColorType.Random: self.material = self.create_random_shader() elif self.shader_type == ColorType.Constant: self.material = self.create_const_shader() elif self.shader_type == ColorType.Gradient: - self.material = self.create_gradient_shader(location_z) - else: + self.material = self.create_gradient_shader() + elif self.shader_type != ColorType.Custom: raise AttributeError('Unsupported shader type!') def create_random_shader(self): @@ -95,7 +97,7 @@ def create_const_shader(self): return material - def create_gradient_shader(self, location_z): + def create_gradient_shader(self): material = bpy.data.materials.new(name='DV_ChartMat') material.use_nodes = True @@ -120,7 +122,7 @@ def create_gradient_shader(self, location_z): sub_node = nodes.new('ShaderNodeMath') sub_node.location = (-700, 0) sub_node.operation = 'SUBTRACT' - sub_node.inputs[1].default_value = location_z + sub_node.inputs[1].default_value = self.location_z xyz_sep_node = nodes.new('ShaderNodeSeparateXYZ') xyz_sep_node.location = (-900, 0) @@ -137,6 +139,40 @@ def create_gradient_shader(self, location_z): return material + def create_geometry_shader(self): + material = bpy.data.materials.new(name='DV_ChartMat') + material.use_nodes = True + + nodes = material.node_tree.nodes + + bsdf_node = nodes.get('Principled BSDF') + + cr_node = nodes.new('ShaderNodeValToRGB') + cr_node.location = (-300, 0) + + cr_node.color_ramp.elements[0].color = (1, 1, 1, 1) + cr_node.color_ramp.elements[1].color = self.base_color + + # Normalize the position when creating shader + sub_node = nodes.new('ShaderNodeMath') + sub_node.location = (-500, 0) + sub_node.operation = 'SUBTRACT' + sub_node.inputs[1].default_value = self.location_z + + xyz_sep_node = nodes.new('ShaderNodeSeparateXYZ') + xyz_sep_node.location = (-700, 0) + + geometry_node = nodes.new('ShaderNodeNewGeometry') + geometry_node.location = (-900, 0) + + links = material.node_tree.links + links.new(geometry_node.outputs[0], xyz_sep_node.inputs[0]) + links.new(xyz_sep_node.outputs[2], sub_node.inputs[0]) + links.new(sub_node.outputs[0], cr_node.inputs[0]) + links.new(cr_node.outputs[0], bsdf_node.inputs[0]) + + return material + def get_material(self, *args): return self.material diff --git a/data_vis/operators/surface_chart.py b/data_vis/operators/surface_chart.py index 8355aff..7765f1a 100644 --- a/data_vis/operators/surface_chart.py +++ b/data_vis/operators/surface_chart.py @@ -5,8 +5,8 @@ from data_vis.general import OBJECT_OT_GenericChart, DV_AxisPropertyGroup, DV_LabelPropertyGroup, DV_ColorPropertyGroup from data_vis.utils.data_utils import find_data_range, normalize_value -from data_vis.colors import ColoringFactory, ColorType -from data_vis.operators.features.axis import AxisFactory +from data_vis.colors import NodeShader +from data_vis.operators.features.axis import AxisFactory class OBJECT_OT_SurfaceChart(OBJECT_OT_GenericChart): @@ -38,8 +38,13 @@ class OBJECT_OT_SurfaceChart(OBJECT_OT_GenericChart): type=DV_LabelPropertyGroup ) - color_settings: bpy.props.PointerProperty( - type=DV_ColorPropertyGroup + color_shade: bpy.props.FloatVectorProperty( + name='Base Color', + subtype='COLOR', + default=(0.0, 0.0, 1.0), + min=0.0, + max=1.0, + description='Base color shade to work with' ) @classmethod @@ -49,11 +54,16 @@ def poll(cls, context): def draw(self, context): super().draw(context) layout = self.layout + row = layout.row() row.prop(self, 'interpolation_method') + row = layout.row() row.prop(self, 'density') + row = layout.row() + row.prop(self, 'color_shade') + def face(self, column, row): return (column * self.density + row, (column + 1) * self.density + row, @@ -76,7 +86,7 @@ def execute(self, context): px = [entry[0] for entry in self.data] py = [entry[1] for entry in self.data] f = [normalize_value(entry[2], data_min, data_max) for entry in self.data] - res = griddata((px, py), f, (X, Y), self.interpolation_method, 0.0 ) + res = griddata((px, py), f, (X, Y), self.interpolation_method, 0.0) faces = [] verts = [] @@ -84,8 +94,7 @@ def execute(self, context): for col in range(self.density): x_norm = row / self.density y_norm = col / self.density - value = res[row][col] - z_norm = normalize_value(value, data_min, data_max) + z_norm = res[row][col] verts.append((x_norm, y_norm, z_norm)) if row < self.density - 1 and col < self.density - 1: fac = self.face(col, row) @@ -98,8 +107,7 @@ def execute(self, context): bpy.context.scene.collection.objects.link(obj) obj.parent = self.container_object - cf = ColoringFactory(self.color_settings.color_shade, ColorType.str_to_type(self.color_settings.color_type), self.color_settings.use_shader) - mat = cf.create((data_min, data_max)).get_material(1.0) + mat = NodeShader(self.color_shade, location_z=self.container_object.location[2]).create_geometry_shader() obj.data.materials.append(mat) obj.active_material = mat From 1fe220fe19d81b9ba08f4f8a06d85aeb83ebdda1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zden=C4=9Bk=20Dole=C5=BEal?= Date: Thu, 2 Apr 2020 21:14:49 +0200 Subject: [PATCH 16/24] Icon, RBF Interpolation --- data_vis/__init__.py | 2 +- data_vis/icons/surface_chart.png | Bin 0 -> 16648 bytes data_vis/operators/surface_chart.py | 45 +++++++++++++++++++--------- 3 files changed, 32 insertions(+), 15 deletions(-) create mode 100644 data_vis/icons/surface_chart.png diff --git a/data_vis/__init__.py b/data_vis/__init__.py index bb6be79..252e893 100644 --- a/data_vis/__init__.py +++ b/data_vis/__init__.py @@ -78,7 +78,7 @@ def draw(self, context): layout.operator(OBJECT_OT_LineChart.bl_idname, icon_value=main_icons['line_chart'].icon_id) layout.operator(OBJECT_OT_PieChart.bl_idname, icon_value=main_icons['pie_chart'].icon_id) layout.operator(OBJECT_OT_PointChart.bl_idname, icon_value=main_icons['point_chart'].icon_id) - layout.operator(OBJECT_OT_SurfaceChart.bl_idname) + layout.operator(OBJECT_OT_SurfaceChart.bl_idname, icon_value=main_icons['surface_chart'].icon_id) preview_collections = {} diff --git a/data_vis/icons/surface_chart.png b/data_vis/icons/surface_chart.png new file mode 100644 index 0000000000000000000000000000000000000000..c5289aaae06af259b2378a2059037872e15dbaac GIT binary patch literal 16648 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 Date: Sun, 5 Apr 2020 02:28:23 +0200 Subject: [PATCH 17/24] Python modules importing --- data_vis/__init__.py | 87 ++++++++++++++++++++--------- data_vis/operators/surface_chart.py | 15 +++-- 2 files changed, 70 insertions(+), 32 deletions(-) diff --git a/data_vis/__init__.py b/data_vis/__init__.py index 252e893..257c6f8 100644 --- a/data_vis/__init__.py +++ b/data_vis/__init__.py @@ -3,7 +3,7 @@ 'author': 'Zdenek Dolezal', 'description': '', 'blender': (2, 80, 0), - 'version': (1, 0, 0), + 'version': (1, 1, 0), 'location': 'Object -> Add Mesh', 'warning': '', 'category': 'Generic' @@ -12,6 +12,8 @@ import bpy import bpy.utils.previews import os +import subprocess +import sys from .operators.data_load import FILE_OT_DVLoadFile from .operators.bar_chart import OBJECT_OT_BarChart @@ -23,6 +25,38 @@ from .data_manager import DataManager +class DV_Preferences(bpy.types.AddonPreferences): + ''' + Preferences for data visualisation addon + ''' + bl_idname = 'data_vis' + + def draw(self, context): + layout = self.layout + row = layout.row() + row.scale_y = 2.0 + row.operator('object.install_modules') + + +class OBJECT_OT_InstallModules(bpy.types.Operator): + ''' + Operator that tries to install scipy and numpy using pip into blender python + ''' + bl_label = "Install Python Dependencies" + bl_idname = "object.install_modules" + bl_options = {'REGISTER'} + + def execute(self, context): + version = '{}.{}'.format(bpy.app.version[0], bpy.app.version[1]) + python_path = os.path.join(cwd, version, 'python', 'bin', 'python') + self.install(python_path) + + return {'FINISHED'} + + def install(self, python_path): + subprocess.check_call([python_path, '-m', 'pip', 'install', 'scipy']) + + class DV_AddonPanel(bpy.types.Panel): ''' Menu panel used for loading data and managing addon settings @@ -64,7 +98,7 @@ class DV_PropertyGroup(bpy.types.PropertyGroup): ) -class OBJECT_MT_AddChart(bpy.types.Menu): +class OBJECT_OT_AddChart(bpy.types.Menu): ''' Menu panel grouping chart related operators in Blender AddObject panel ''' @@ -87,7 +121,7 @@ def draw(self, context): def chart_ops(self, context): icon = preview_collections['main']['addon_icon'] - self.layout.menu(OBJECT_MT_AddChart.bl_idname, icon_value=icon.icon_id) + self.layout.menu(OBJECT_OT_AddChart.bl_idname, icon_value=icon.icon_id) def load_icons(): @@ -108,20 +142,29 @@ def remove_icons(): preview_collections.clear() +classes = [ + DV_Preferences, + OBJECT_OT_InstallModules, + DV_PropertyGroup, + DV_LabelPropertyGroup, + DV_ColorPropertyGroup, + DV_AxisPropertyGroup, + OBJECT_OT_AddChart, + OBJECT_OT_BarChart, + OBJECT_OT_PieChart, + OBJECT_OT_PointChart, + OBJECT_OT_LineChart, + OBJECT_OT_SurfaceChart, + FILE_OT_DVLoadFile, + DV_AddonPanel, +] + + def register(): load_icons() - bpy.utils.register_class(DV_PropertyGroup) - bpy.utils.register_class(DV_LabelPropertyGroup) - bpy.utils.register_class(DV_ColorPropertyGroup) - bpy.utils.register_class(DV_AxisPropertyGroup) - bpy.utils.register_class(OBJECT_OT_BarChart) - bpy.utils.register_class(OBJECT_OT_PieChart) - bpy.utils.register_class(OBJECT_OT_LineChart) - bpy.utils.register_class(OBJECT_OT_PointChart) - bpy.utils.register_class(OBJECT_OT_SurfaceChart) - bpy.utils.register_class(FILE_OT_DVLoadFile) - bpy.utils.register_class(DV_AddonPanel) - bpy.utils.register_class(OBJECT_MT_AddChart) + for c in classes: + bpy.utils.register_class(c) + bpy.types.VIEW3D_MT_add.append(chart_ops) bpy.types.Scene.dv_props = bpy.props.PointerProperty(type=DV_PropertyGroup) @@ -129,18 +172,8 @@ def register(): def unregister(): remove_icons() - bpy.utils.unregister_class(DV_PropertyGroup) - bpy.utils.unregister_class(OBJECT_MT_AddChart) - bpy.utils.unregister_class(DV_AddonPanel) - bpy.utils.unregister_class(OBJECT_OT_BarChart) - bpy.utils.unregister_class(OBJECT_OT_PieChart) - bpy.utils.unregister_class(OBJECT_OT_LineChart) - bpy.utils.unregister_class(OBJECT_OT_PointChart) - bpy.utils.unregister_class(OBJECT_OT_SurfaceChart) - bpy.utils.unregister_class(FILE_OT_DVLoadFile) - bpy.utils.unregister_class(DV_LabelPropertyGroup) - bpy.utils.unregister_class(DV_ColorPropertyGroup) - bpy.utils.unregister_class(DV_AxisPropertyGroup) + for c in reversed(classes): + bpy.utils.unregister_class(c) bpy.types.VIEW3D_MT_add.remove(chart_ops) diff --git a/data_vis/operators/surface_chart.py b/data_vis/operators/surface_chart.py index 20eb156..1c862d3 100644 --- a/data_vis/operators/surface_chart.py +++ b/data_vis/operators/surface_chart.py @@ -1,12 +1,19 @@ import bpy -from scipy import interpolate -import numpy as np import math from data_vis.general import OBJECT_OT_GenericChart, DV_AxisPropertyGroup, DV_LabelPropertyGroup, DV_ColorPropertyGroup from data_vis.utils.data_utils import find_data_range, normalize_value from data_vis.colors import NodeShader from data_vis.operators.features.axis import AxisFactory +from data_vis.data_manager import DataManager, DataType + +try: + import numpy as np + from scipy import interpolate + modules_available = True +except ImportError as e: + print('Warning: Modules not installed in blender python: numpy, scipy') + modules_available = False class OBJECT_OT_SurfaceChart(OBJECT_OT_GenericChart): @@ -62,7 +69,7 @@ class OBJECT_OT_SurfaceChart(OBJECT_OT_GenericChart): @classmethod def poll(cls, context): - return True + return modules_available and DataManager().is_type(DataType.Numerical, 3) def draw(self, context): super().draw(context) @@ -103,8 +110,6 @@ def execute(self, context): rbfi = interpolate.Rbf(px, py, f, function=self.rbf_function) res = rbfi(X, Y) -# res = griddata((px, py), f, (X, Y), self.interpolation_method, 0.0) - faces = [] verts = [] for row in range(self.density): From 61b3650ca9100d6f52dcd1061d227aa8935e9d1b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zden=C4=9Bk=20Dole=C5=BEal?= Date: Sun, 5 Apr 2020 22:43:13 +0200 Subject: [PATCH 18/24] Operator in addon settings to install modules --- data_vis.zip | Bin 48337 -> 104397 bytes data_vis/__init__.py | 50 ++++++++++++++++++++-------- data_vis/operators/surface_chart.py | 4 +-- 3 files changed, 38 insertions(+), 16 deletions(-) diff --git a/data_vis.zip b/data_vis.zip index e36f56466b091375d012ee16af31609f93e093ad..8bf069258b437770e195188fc68317b5f070c7b6 100644 GIT binary patch literal 104397 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øvFVK?1djo@ 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 literal 48337 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 Date: Mon, 6 Apr 2020 02:51:10 +0200 Subject: [PATCH 19/24] Comments, data loading skips newlines, ui labelfix --- data_vis/data_manager.py | 7 ++++++- data_vis/general.py | 12 +++++------ data_vis/operators/features/axis.py | 29 ++++++++++++++++++--------- data_vis/operators/features/legend.py | 2 -- data_vis/operators/point_chart.py | 2 +- data_vis/operators/surface_chart.py | 3 ++- 6 files changed, 33 insertions(+), 22 deletions(-) delete mode 100644 data_vis/operators/features/legend.py diff --git a/data_vis/data_manager.py b/data_vis/data_manager.py index e7cd3f4..db6881b 100644 --- a/data_vis/data_manager.py +++ b/data_vis/data_manager.py @@ -33,7 +33,12 @@ def load_data(self, filepath, separator=','): self.__init__() try: with open(filepath, 'r') as file: - self.raw_data = [line.split(separator) for line in file] + self.raw_data = [] + for line in file: + if line == '\n': + continue + self.raw_data.append(line.split(separator)) + self.analyse_data() except UnicodeDecodeError as e: self.predicted_data_type = DataType.Invalid diff --git a/data_vis/general.py b/data_vis/general.py index 9af9ea4..330d7c9 100644 --- a/data_vis/general.py +++ b/data_vis/general.py @@ -207,9 +207,8 @@ def draw_label_settings(self, box): row.prop(self.label_settings, 'y_label') row.prop(self.label_settings, 'z_label') - def draw_color_settings(self, layout): + def draw_color_settings(self, box): if hasattr(self, 'color_settings'): - box = layout.box() box.label(text='Color settings') box.prop(self.color_settings, 'use_shader') box.prop(self.color_settings, 'color_type') @@ -224,11 +223,8 @@ def draw_axis_settings(self, layout, numerical): row = box.row() row.label(text='Axis Settings:') row.prop(self.axis_settings, 'create') - if not self.axis_settings.create: - return - if numerical: - box.prop(self.axis_settings, 'auto_ranges') + box.prop(self.axis_settings, 'auto_ranges') if not self.axis_settings.auto_ranges: row = box.row() row.prop(self.axis_settings, 'x_range', text='x') @@ -245,6 +241,8 @@ def draw_axis_settings(self, layout, numerical): row.prop(self.axis_settings, 'y_step', text='y') row.prop(self.axis_settings, 'z_step', text='z') + if not self.axis_settings.create: + return row = box.row() row.prop(self.axis_settings, 'padding') row.prop(self.axis_settings, 'thickness') @@ -296,7 +294,7 @@ def init_labels(self): if not self.label_settings.create: self.labels = (None, None, None) return - if self.dm.has_labels: + if self.dm.has_labels and self.label_settings.from_data: first_line = self.dm.get_labels() length = len(first_line) if length == 2: diff --git a/data_vis/operators/features/axis.py b/data_vis/operators/features/axis.py index c4bdc63..f90dc0f 100644 --- a/data_vis/operators/features/axis.py +++ b/data_vis/operators/features/axis.py @@ -22,7 +22,12 @@ def create(parent, axis_steps, axis_ranges, dim, thickness, tick_height, labels= axis_steps - list of axis step sizes (x_step_size, y_step_size, z_step_size) axis_ranges - list of axis ranges ((x_min, x_max), (...), (...)) dim - number of dimensions (2 or 3) in which to create axis - labels - array of labels for each axis [x, y, z] + tick_height - height of tick + labels - tuple of labels for each axis (x, y, z) + tick_labels - tuple of lists containing values to display next to ticks on axis + auto_steps - whether to create steps automatically and prevent axis being too dense + padding - space between chart and axis + offset - offset of start of ticks ''' if dim not in [2, 3]: raise AttributeError('Only 2 or 3 dim axis supported. {} is invalid number'.format(dim)) @@ -56,11 +61,13 @@ class Axis: step - space between axis ticks range - tuple/list of from..to values dir - direction of axis specified by AxisDir class - hm - height multiplier to normalize chart height + labels - custom labels for axis ticks + tick-height - height of tick mark + auto_step - creates 10 uniform steps across axis ''' - def __init__(self, parent, step, ax_range, ax_dir, labels, thickness, tick_height, auto_step=False): + def __init__(self, parent, step, ax_range, ax_dir, tick_labels, thickness, tick_height, auto_step=False): self.range = ax_range - if not auto_step or len(labels) > 0: + if not auto_step or (len(tick_labels) <= 10 and len(tick_labels) > 0): self.step = step else: self.step = (self.range[1] - self.range[0]) / 10 @@ -74,10 +81,11 @@ def __init__(self, parent, step, ax_range, ax_dir, labels, thickness, tick_heigh raise AttributeError('Use AxisDir enumeration as ax_range param') self.axis_cont = None - self.labels = labels + self.tick_labels = tick_labels self.create_materials() def create_materials(self): + '''Creates materials for axis, ticks and text''' self.axis_mat = bpy.data.materials.get('DV_AxisMat') if self.axis_mat is None: self.axis_mat = bpy.data.materials.new(name='DV_AxisMat') @@ -91,9 +99,7 @@ def create_materials(self): self.text_mat = bpy.data.materials.new(name='DV_TextMat') def create_container(self): - ''' - Creates container for axis, with default name 'Axis_Container_DIM' where DIM is X, Y or Z - ''' + '''Creates container for axis, with default name 'Axis_Container_DIM' where DIM is X, Y or Z''' bpy.ops.object.empty_add() self.axis_cont = bpy.context.object self.axis_cont.name = 'Axis_Container_' + str(self.dir) @@ -140,10 +146,10 @@ def create_ticks(self, start_pos): for value in float_range(self.range[0], self.range[1], self.step): tick_location = start_pos + (value - self.range[0]) / (self.range[1] - self.range[0]) self.create_tick_mark(tick_location) - if len(self.labels) == 0: + if len(self.tick_labels) == 0: self.create_tick_label(value, tick_location) else: - self.create_tick_label(self.labels[int(value)], tick_location, rotate=True) + self.create_tick_label(self.tick_labels[int(value)], tick_location, rotate=True) def create(self, padding, offset, label, only_2d=False): ''' @@ -179,12 +185,14 @@ def create(self, padding, offset, label, only_2d=False): self.axis_cont.location.y -= padding def create_label(self, value): + '''Creates axis label (description) with value''' obj = self.create_text_object(value) obj.parent = self.axis_cont obj.location = (1.3, 0, 0) self.rotate_text_object(obj) def create_tick_label(self, value, x_location, rotate=False): + '''Creates tick label with value on x_location offset''' obj = self.create_text_object(value) if rotate: obj.rotation_euler.y = math.radians(45) @@ -209,6 +217,7 @@ def create_text_object(self, value): return obj def rotate_text_object(self, obj): + '''Rotates text object 45 degrees to fit more text''' if self.dir == AxisDir.Z: obj.rotation_euler.y = math.radians(90) diff --git a/data_vis/operators/features/legend.py b/data_vis/operators/features/legend.py deleted file mode 100644 index e9129a4..0000000 --- a/data_vis/operators/features/legend.py +++ /dev/null @@ -1,2 +0,0 @@ -class Legend: - ... diff --git a/data_vis/operators/point_chart.py b/data_vis/operators/point_chart.py index 44de533..475408a 100644 --- a/data_vis/operators/point_chart.py +++ b/data_vis/operators/point_chart.py @@ -75,7 +75,7 @@ def execute(self, context): if not self.in_axis_range_bounds_new(entry): continue - bpy.ops.mesh.primitive_uv_sphere_add() + bpy.ops.mesh.primitive_uv_sphere_add(segments=16, ring_count=8) point_obj = context.active_object point_obj.scale = Vector((self.point_scale, self.point_scale, self.point_scale)) diff --git a/data_vis/operators/surface_chart.py b/data_vis/operators/surface_chart.py index 8e41adc..451e8b2 100644 --- a/data_vis/operators/surface_chart.py +++ b/data_vis/operators/surface_chart.py @@ -17,7 +17,7 @@ class OBJECT_OT_SurfaceChart(OBJECT_OT_GenericChart): - '''Creates Surface Chart''' + '''Creates Surface Chart (needs scipy in Blender python)''' bl_idname = 'object.create_surface_chart' bl_label = 'Surface Chart' bl_options = {'REGISTER', 'UNDO'} @@ -141,6 +141,7 @@ def execute(self, context): 3, self.axis_settings.thickness, self.axis_settings.tick_mark_height, + labels=self.labels, padding=self.axis_settings.padding, auto_steps=self.axis_settings.auto_steps, offset=0.0 From a3ae91c6f67ac7146adfcbb9fb772e4080043414 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zden=C4=9Bk=20Dole=C5=BEal?= Date: Mon, 6 Apr 2020 15:30:46 +0200 Subject: [PATCH 20/24] Ranges fix for line_chart --- data_vis/data_manager.py | 34 +++++++++++++++++++-- data_vis/general.py | 25 ++++++++++++--- data_vis/operators/line_chart.py | 52 +++++++------------------------- 3 files changed, 64 insertions(+), 47 deletions(-) diff --git a/data_vis/data_manager.py b/data_vis/data_manager.py index db6881b..226f654 100644 --- a/data_vis/data_manager.py +++ b/data_vis/data_manager.py @@ -22,6 +22,7 @@ def __init__(self): self.has_labels = False self.labels = () self.dimensions = 0 + self.ranges = {} def set_data(self, data): self.raw_data = data @@ -54,7 +55,7 @@ def analyse_data(self): except Exception as e: print('Labels analysis: ', e) total += 1 - + if total == len(self.raw_data[0]): self.has_labels = True @@ -102,8 +103,31 @@ def parse_data(self): start_idx = 1 else: start_idx = 0 + + min_max = [] for i in range(start_idx, len(data)): - self.parsed_data.append(self.__get_row_list(self.raw_data[i])) + row_list = self.__get_row_list(self.raw_data[i]) + if i == start_idx: + min_max = [[val, val] for val in row_list] + else: + for j, val in enumerate(row_list): + if val < min_max[j][0]: + min_max[j][0] = val + + if val > min_max[j][1]: + min_max[j][1] = val + + self.parsed_data.append(row_list) + + self.ranges['x'] = min_max[0] + if len(min_max) == 2: + self.ranges['z'] = min_max[1] + if len(min_max) > 2: + self.ranges['y'] = min_max[1] + self.ranges['z'] = min_max[2] + + if self.predicted_data_type == DataType.Categorical: + self.ranges['x'] = (0, len(self.parsed_data) - 1) def get_parsed_data(self): return self.parsed_data @@ -111,6 +135,12 @@ def get_parsed_data(self): def get_labels(self): return self.labels + def get_range(self, axis): + if axis in self.ranges: + return tuple(self.ranges[axis]) + else: + return (0.0, 1.0) + def is_type(self, data_type, dims): return data_type == self.predicted_data_type and self.dimensions <= dims and dims > 1 and dims <= 3 diff --git a/data_vis/general.py b/data_vis/general.py index 330d7c9..7f75b0a 100644 --- a/data_vis/general.py +++ b/data_vis/general.py @@ -19,6 +19,8 @@ def range_updated(self, context): self.x_range[1] += 1.0 if self.y_range[0] == self.y_range[1]: self.y_range[1] += 1.0 + if self.z_range[0] == self.z_range[1]: + self.z_range += 1 create: bpy.props.BoolProperty( name='Create Axis', @@ -46,8 +48,8 @@ def range_updated(self, context): x_range: bpy.props.FloatVectorProperty( name='Range of x axis', size=2, - default=(0.0, 1.0), - update=range_updated + update=range_updated, + default=DataManager().get_range('x') ) y_step: bpy.props.FloatProperty( @@ -59,8 +61,15 @@ def range_updated(self, context): y_range: bpy.props.FloatVectorProperty( name='Range of y axis', size=2, - default=(0.0, 1.0), - update=range_updated + update=range_updated, + default=DataManager().get_range('y') + ) + + z_range: bpy.props.FloatVectorProperty( + name='Range of y axis', + size=2, + update=range_updated, + default=DataManager().get_range('z'), ) z_step: bpy.props.FloatProperty( @@ -258,7 +267,15 @@ def poll(cls, context): def execute(self, context): raise NotImplementedError('Execute method should be implemented in every chart operator!') + def init_ranges(self): + self.axis_settings.x_range = self.dm.get_range('x') + self.axis_settings.y_range = self.dm.get_range('y') + self.axis_settings.z_range = self.dm.get_range('z') + def invoke(self, context, event): + if hasattr(self, 'axis_settings'): + self.init_ranges() + return context.window_manager.invoke_props_dialog(self) def create_container(self): diff --git a/data_vis/operators/line_chart.py b/data_vis/operators/line_chart.py index 3e66c77..c675944 100644 --- a/data_vis/operators/line_chart.py +++ b/data_vis/operators/line_chart.py @@ -35,50 +35,23 @@ class OBJECT_OT_LineChart(OBJECT_OT_GenericChart): ) ) - auto_steps: bpy.props.BoolProperty( - name='Automatic axis steps', + z_auto: bpy.props.BoolProperty( + name='Z', default=True ) - auto_ranges: bpy.props.BoolProperty( - name='Automatic axis ranges', - default=True - ) - - x_axis_step: bpy.props.FloatProperty( - name='Step of x axis', - default=1.0 - ) - - x_axis_range: bpy.props.FloatVectorProperty( - name='Range of x axis', - size=2, - default=(0.0, 1.0) - ) - - z_axis_step: bpy.props.FloatProperty( - name='Step of z axis', - default=1.0 - ) - - padding: bpy.props.FloatProperty( - name='Padding', - default=0.1, - min=0.0 - ) - label_settings: bpy.props.PointerProperty( type=DV_LabelPropertyGroup ) axis_settings: bpy.props.PointerProperty( - type=DV_AxisPropertyGroup + type=DV_AxisPropertyGroup, + options={'SKIP_SAVE'} ) def __init__(self): super().__init__() self.only_2d = True - self.x_delta = 0.2 self.bevel_obj_size = (0.01, 0.01, 0.01) self.bevel_settings = { 'rounded': { @@ -101,6 +74,10 @@ def poll(cls, context): def draw(self, context): super().draw(context) layout = self.layout + row = layout.row() + row.prop(self, 'z_auto') + if not self.z_auto: + row.prop(self.axis_settings, 'z_range') if self.bevel_edges: row = layout.row() row.prop(self, 'rounded') @@ -116,23 +93,16 @@ def execute(self, context): self.create_container() if self.data_type_as_enum() == DataType.Numerical: - if self.axis_settings.auto_ranges: - self.axis_settings.x_range = find_axis_range(self.data, 0) - data_min, data_max = find_data_range(self.data, self.axis_settings.x_range) self.data = get_data_in_range(self.data, self.axis_settings.x_range) sorted_data = sorted(self.data, key=lambda x: x[0]) else: - self.axis_settings.x_range[0] = 0 - self.axis_settings.x_range[1] = len(self.data) - 1 - data_min = min(self.data, key=lambda val: val[1])[1] - data_max = max(self.data, key=lambda val: val[1])[1] sorted_data = self.data tick_labels = [] if self.data_type_as_enum() == DataType.Numerical: - normalized_vert_list = [(normalize_value(entry[0], self.axis_settings.x_range[0], self.axis_settings.x_range[1]), 0.0, normalize_value(entry[1], data_min, data_max)) for entry in sorted_data] + normalized_vert_list = [(normalize_value(entry[0], self.axis_settings.x_range[0], self.axis_settings.x_range[1]), 0.0, normalize_value(entry[1], self.axis_settings.z_range[0], self.axis_settings.z_range[1])) for entry in sorted_data] else: - normalized_vert_list = [(normalize_value(i, self.axis_settings.x_range[0], self.axis_settings.x_range[1]), 0.0, normalize_value(entry[1], data_min, data_max)) for i, entry in enumerate(sorted_data)] + normalized_vert_list = [(normalize_value(i, self.axis_settings.x_range[0], self.axis_settings.x_range[1]), 0.0, normalize_value(entry[1], self.axis_settings.z_range[0], self.axis_settings.z_range[1])) for i, entry in enumerate(sorted_data)] tick_labels = list(zip(*sorted_data))[0] edges = [[i - 1, i] for i in range(1, len(normalized_vert_list))] @@ -144,7 +114,7 @@ def execute(self, context): AxisFactory.create( self.container_object, (self.axis_settings.x_step, 0, self.axis_settings.z_step), - (self.axis_settings.x_range, [], (data_min, data_max)), + (self.axis_settings.x_range, [], self.axis_settings.z_range), 2, self.axis_settings.thickness, self.axis_settings.tick_mark_height, From 39b2289d6b5c4f2eed0da1c586c2efe939f58b13 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zden=C4=9Bk=20Dole=C5=BEal?= Date: Mon, 6 Apr 2020 18:01:52 +0200 Subject: [PATCH 21/24] Ranges fix, dimensions poll fix --- data_vis/__init__.py | 1 + data_vis/data_manager.py | 13 ++++++++-- data_vis/general.py | 29 ++++++++++++---------- data_vis/operators/bar_chart.py | 38 ++++++----------------------- data_vis/operators/line_chart.py | 11 +-------- data_vis/operators/pie_chart.py | 2 +- data_vis/operators/point_chart.py | 2 +- data_vis/operators/surface_chart.py | 2 +- 8 files changed, 40 insertions(+), 58 deletions(-) diff --git a/data_vis/__init__.py b/data_vis/__init__.py index 3902629..3cf5898 100644 --- a/data_vis/__init__.py +++ b/data_vis/__init__.py @@ -45,6 +45,7 @@ def draw(self, context): row = layout.row() row.label(text='Blender has to be restarted after this process!') + class OBJECT_OT_InstallModules(bpy.types.Operator): '''Operator that tries to install scipy and numpy using pip into blender python''' bl_label = 'Install addon dependencies' diff --git a/data_vis/data_manager.py b/data_vis/data_manager.py index 226f654..0978189 100644 --- a/data_vis/data_manager.py +++ b/data_vis/data_manager.py @@ -53,7 +53,6 @@ def analyse_data(self): try: row = float(col) except Exception as e: - print('Labels analysis: ', e) total += 1 if total == len(self.raw_data[0]): @@ -140,9 +139,19 @@ def get_range(self, axis): return tuple(self.ranges[axis]) else: return (0.0, 1.0) + + def override(self, data_type, dims): + if data_type != self.predicted_data_type or dims != self.dimensions: + if data_type == DataType.Categorical: + self.ranges['x'] = (0, len(self.parsed_data) - 1) + else: + self.parse_data() + return True + else: + return False def is_type(self, data_type, dims): - return data_type == self.predicted_data_type and self.dimensions <= dims and dims > 1 and dims <= 3 + return data_type == self.predicted_data_type and self.dimensions in dims def __get_row_list(self, row): if self.predicted_data_type == DataType.Categorical: diff --git a/data_vis/general.py b/data_vis/general.py index 7f75b0a..2602d77 100644 --- a/data_vis/general.py +++ b/data_vis/general.py @@ -23,7 +23,7 @@ def range_updated(self, context): self.z_range += 1 create: bpy.props.BoolProperty( - name='Create Axis', + name='Create Axis Object', default=True, ) @@ -196,8 +196,11 @@ def draw(self, context): only_2d = only_2d or not numerical if hasattr(self, 'dimensions') and self.dm.predicted_data_type != DataType.Categorical: - row = layout.row() - row.prop(self, 'dimensions') + if numerical: + row = layout.row() + row.prop(self, 'dimensions') + else: + self.dimensions = '2' self.draw_axis_settings(layout, numerical) self.draw_color_settings(layout) @@ -229,17 +232,15 @@ def draw_axis_settings(self, layout, numerical): return box = layout.box() - row = box.row() - row.label(text='Axis Settings:') - row.prop(self.axis_settings, 'create') + box.label(text='Axis Settings:') - box.prop(self.axis_settings, 'auto_ranges') - if not self.axis_settings.auto_ranges: + row = box.row() + row.prop(self.axis_settings, 'x_range', text='x') + if hasattr(self, 'dimensions') and self.dimensions == '3': row = box.row() - row.prop(self.axis_settings, 'x_range', text='x') - if hasattr(self, 'dimensions') and self.dimensions == '3': - row = box.row() - row.prop(self.axis_settings, 'y_range', text='y') + row.prop(self.axis_settings, 'y_range', text='y') + row = box.row() + row.prop(self.axis_settings, 'z_range', text='z') box.prop(self.axis_settings, 'auto_steps') if not self.axis_settings.auto_steps: @@ -249,7 +250,9 @@ def draw_axis_settings(self, layout, numerical): if hasattr(self, 'dimensions') and self.dimensions == '3': row.prop(self.axis_settings, 'y_step', text='y') row.prop(self.axis_settings, 'z_step', text='z') - + + row = box.row() + row.prop(self.axis_settings, 'create') if not self.axis_settings.create: return row = box.row() diff --git a/data_vis/operators/bar_chart.py b/data_vis/operators/bar_chart.py index a0c9ed7..38aeb0f 100644 --- a/data_vis/operators/bar_chart.py +++ b/data_vis/operators/bar_chart.py @@ -29,7 +29,7 @@ class OBJECT_OT_BarChart(OBJECT_OT_GenericChart): items=( ('0', 'Numerical', 'X relative to Z or Y'), ('1', 'Categorical', 'Label and value'), - ) + ), ) bar_size: bpy.props.FloatVectorProperty( @@ -53,7 +53,7 @@ class OBJECT_OT_BarChart(OBJECT_OT_GenericChart): @classmethod def poll(cls, context): dm = DataManager() - return dm.is_type(DataType.Numerical, 3) or dm.is_type(DataType.Categorical, 2) + return dm.is_type(DataType.Numerical, [2, 3]) or dm.is_type(DataType.Categorical, [2]) def draw(self, context): super().draw(context) @@ -61,39 +61,17 @@ def draw(self, context): row = layout.row() row.prop(self, 'bar_size') - def data_type_as_enum(self): - if self.data_type == '0': - return DataType.Numerical - elif self.data_type == '1': - return DataType.Categorical - def execute(self, context): self.init_data() - if self.data_type_as_enum() == DataType.Numerical: - if self.axis_settings.auto_ranges: - self.init_range(self.data) - else: - self.dimensions = '2' - self.axis_settings.x_range[0] = 0 - self.axis_settings.x_range[1] = len(self.data) - 1 - if self.dimensions == '3' and len(self.data[0]) != 3: - self.report({'ERROR'}, 'Data are only 2D!') - return {'CANCELLED'} tick_labels = [] - if self.data_type_as_enum() == DataType.Numerical: - try: - data_min, data_max = find_data_range(self.data, self.axis_settings.x_range, self.axis_settings.y_range if self.dimensions == '3' else None) - except Exception as e: - self.report({'ERROR'}, 'Cannot find data in this range!') - return {'CANCELLED'} - else: - data_min = min(self.data, key=lambda val: val[1])[1] - data_max = max(self.data, key=lambda val: val[1])[1] + + if self.dm.override(self.data_type_as_enum(), int(self.dimensions)): + self.init_ranges() self.create_container() color_factory = ColoringFactory(self.color_settings.color_shade, ColorType.str_to_type(self.color_settings.color_type), self.color_settings.use_shader) - color_gen = color_factory.create((data_min, data_max), 2.0, self.container_object.location[2]) + color_gen = color_factory.create((self.axis_settings.z_range[0], self.axis_settings.z_range[1]), 2.0, self.container_object.location[2]) if self.dimensions == '2': value_index = 1 @@ -113,7 +91,7 @@ def execute(self, context): x_value = i x_norm = normalize_value(x_value, self.axis_settings.x_range[0], self.axis_settings.x_range[1]) - z_norm = normalize_value(entry[value_index], data_min, data_max) + z_norm = normalize_value(entry[value_index], self.axis_settings.z_range[0], self.axis_settings.z_range[1]) if z_norm >= 0.0 and z_norm <= 0.0001: z_norm = 0.0001 if self.dimensions == '2': @@ -133,7 +111,7 @@ def execute(self, context): AxisFactory.create( self.container_object, (self.axis_settings.x_step, self.axis_settings.y_step, self.axis_settings.z_step), - (self.axis_settings.x_range, self.axis_settings.y_range, (data_min, data_max)), + (self.axis_settings.x_range, self.axis_settings.y_range, self.axis_settings.z_range), int(self.dimensions), self.axis_settings.thickness, self.axis_settings.tick_mark_height, diff --git a/data_vis/operators/line_chart.py b/data_vis/operators/line_chart.py index c675944..1b40cf1 100644 --- a/data_vis/operators/line_chart.py +++ b/data_vis/operators/line_chart.py @@ -35,11 +35,6 @@ class OBJECT_OT_LineChart(OBJECT_OT_GenericChart): ) ) - z_auto: bpy.props.BoolProperty( - name='Z', - default=True - ) - label_settings: bpy.props.PointerProperty( type=DV_LabelPropertyGroup ) @@ -69,15 +64,11 @@ def __init__(self): @classmethod def poll(cls, context): dm = DataManager() - return dm.is_type(DataType.Numerical, 2) or dm.is_type(DataType.Categorical, 2) + return dm.is_type(DataType.Numerical, [2]) or dm.is_type(DataType.Categorical, [2]) def draw(self, context): super().draw(context) layout = self.layout - row = layout.row() - row.prop(self, 'z_auto') - if not self.z_auto: - row.prop(self.axis_settings, 'z_range') if self.bevel_edges: row = layout.row() row.prop(self, 'rounded') diff --git a/data_vis/operators/pie_chart.py b/data_vis/operators/pie_chart.py index 1e1918b..674f1e6 100644 --- a/data_vis/operators/pie_chart.py +++ b/data_vis/operators/pie_chart.py @@ -31,7 +31,7 @@ class OBJECT_OT_PieChart(OBJECT_OT_GenericChart): @classmethod def poll(cls, context): dm = DataManager() - return not dm.has_labels and dm.is_type(DataType.Categorical, 2) + return not dm.has_labels and dm.is_type(DataType.Categorical, [2]) def draw(self, context): layout = self.layout diff --git a/data_vis/operators/point_chart.py b/data_vis/operators/point_chart.py index 475408a..1e808be 100644 --- a/data_vis/operators/point_chart.py +++ b/data_vis/operators/point_chart.py @@ -43,7 +43,7 @@ class OBJECT_OT_PointChart(OBJECT_OT_GenericChart): @classmethod def poll(cls, context): - return DataManager().is_type(DataType.Numerical, 3) + return DataManager().is_type(DataType.Numerical, [3]) def draw(self, context): super().draw(context) diff --git a/data_vis/operators/surface_chart.py b/data_vis/operators/surface_chart.py index 451e8b2..5319d37 100644 --- a/data_vis/operators/surface_chart.py +++ b/data_vis/operators/surface_chart.py @@ -69,7 +69,7 @@ class OBJECT_OT_SurfaceChart(OBJECT_OT_GenericChart): @classmethod def poll(cls, context): - return modules_available and DataManager().is_type(DataType.Numerical, 3) + return modules_available and DataManager().is_type(DataType.Numerical, [3]) def draw(self, context): super().draw(context) From 4927e0090e12837aa2315abffbbbd6704a63f4ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zden=C4=9Bk=20Dole=C5=BEal?= Date: Mon, 6 Apr 2020 18:20:46 +0200 Subject: [PATCH 22/24] New range system implemented --- data_vis/operators/bar_chart.py | 2 +- data_vis/operators/point_chart.py | 11 ++++------- data_vis/operators/surface_chart.py | 8 ++------ 3 files changed, 7 insertions(+), 14 deletions(-) diff --git a/data_vis/operators/bar_chart.py b/data_vis/operators/bar_chart.py index 38aeb0f..231cf6a 100644 --- a/data_vis/operators/bar_chart.py +++ b/data_vis/operators/bar_chart.py @@ -71,7 +71,7 @@ def execute(self, context): self.create_container() color_factory = ColoringFactory(self.color_settings.color_shade, ColorType.str_to_type(self.color_settings.color_type), self.color_settings.use_shader) - color_gen = color_factory.create((self.axis_settings.z_range[0], self.axis_settings.z_range[1]), 2.0, self.container_object.location[2]) + color_gen = color_factory.create(self.axis_settings.z_range, 2.0, self.container_object.location[2]) if self.dimensions == '2': value_index = 1 diff --git a/data_vis/operators/point_chart.py b/data_vis/operators/point_chart.py index 1e808be..e78fb7b 100644 --- a/data_vis/operators/point_chart.py +++ b/data_vis/operators/point_chart.py @@ -43,7 +43,7 @@ class OBJECT_OT_PointChart(OBJECT_OT_GenericChart): @classmethod def poll(cls, context): - return DataManager().is_type(DataType.Numerical, [3]) + return DataManager().is_type(DataType.Numerical, [2, 3]) def draw(self, context): super().draw(context) @@ -53,8 +53,6 @@ def draw(self, context): def execute(self, context): self.init_data() - if self.axis_settings.auto_ranges: - self.init_range(self.data) if self.dimensions == '2': value_index = 1 @@ -65,9 +63,8 @@ def execute(self, context): value_index = 2 self.create_container() - data_min, data_max = find_data_range(self.data, self.axis_settings.x_range, self.axis_settings.y_range if self.dimensions == '3' else None) color_factory = ColoringFactory(self.color_settings.color_shade, ColorType.str_to_type(self.color_settings.color_type), self.color_settings.use_shader) - color_gen = color_factory.create((data_min, data_max), 1.0, self.container_object.location[2]) + color_gen = color_factory.create(self.axis_settings.z_range, 1.0, self.container_object.location[2]) for i, entry in enumerate(self.data): @@ -85,7 +82,7 @@ def execute(self, context): # normalize height x_norm = normalize_value(entry[0], self.axis_settings.x_range[0], self.axis_settings.x_range[1]) - z_norm = normalize_value(entry[value_index], data_min, data_max) + z_norm = normalize_value(entry[value_index], self.axis_settings.z_range[0], self.axis_settings.z_range[1]) if self.dimensions == '2': point_obj.location = (x_norm, 0.0, z_norm) else: @@ -98,7 +95,7 @@ def execute(self, context): AxisFactory.create( self.container_object, (self.axis_settings.x_step, self.axis_settings.y_step, self.axis_settings.z_step), - (self.axis_settings.x_range, self.axis_settings.y_range, (data_min, data_max)), + (self.axis_settings.x_range, self.axis_settings.y_range, self.axis_settings.z_range), int(self.dimensions), self.axis_settings.thickness, self.axis_settings.tick_mark_height, diff --git a/data_vis/operators/surface_chart.py b/data_vis/operators/surface_chart.py index 5319d37..d2cb1bb 100644 --- a/data_vis/operators/surface_chart.py +++ b/data_vis/operators/surface_chart.py @@ -92,8 +92,6 @@ def face(self, column, row): def execute(self, context): self.init_data() - if self.axis_settings.auto_ranges: - self.init_range(self.data) self.create_container() @@ -101,8 +99,6 @@ def execute(self, context): y = np.linspace(self.axis_settings.x_range[0], self.axis_settings.y_range[1], self.density) X, Y = np.meshgrid(x, y) - data_min, data_max = find_data_range(self.data, self.axis_settings.x_range, self.axis_settings.y_range) - px = [entry[0] for entry in self.data] py = [entry[1] for entry in self.data] f = [entry[2] for entry in self.data] @@ -116,7 +112,7 @@ def execute(self, context): for col in range(self.density): x_norm = row / self.density y_norm = col / self.density - z_norm = normalize_value(res[row][col], data_min, data_max) + z_norm = normalize_value(res[row][col], self.axis_settings.z_range[0], self.axis_settings.z_range[1]) verts.append((x_norm, y_norm, z_norm)) if row < self.density - 1 and col < self.density - 1: fac = self.face(col, row) @@ -137,7 +133,7 @@ def execute(self, context): AxisFactory.create( self.container_object, (self.axis_settings.x_step, self.axis_settings.y_step, self.axis_settings.z_step), - (self.axis_settings.x_range, self.axis_settings.y_range, (data_min, data_max)), + (self.axis_settings.x_range, self.axis_settings.y_range, self.axis_settings.z_range), 3, self.axis_settings.thickness, self.axis_settings.tick_mark_height, From 3875e3f76fcccb0a958dedb3c012614a53d14c70 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zden=C4=9Bk=20Dole=C5=BEal?= Date: Mon, 6 Apr 2020 18:56:16 +0200 Subject: [PATCH 23/24] PieChart hotfix --- data_vis/operators/pie_chart.py | 29 ++++++++++++++++++++++++----- 1 file changed, 24 insertions(+), 5 deletions(-) diff --git a/data_vis/operators/pie_chart.py b/data_vis/operators/pie_chart.py index 674f1e6..f19971e 100644 --- a/data_vis/operators/pie_chart.py +++ b/data_vis/operators/pie_chart.py @@ -5,7 +5,7 @@ from data_vis.utils.data_utils import find_data_range from data_vis.general import OBJECT_OT_GenericChart from data_vis.data_manager import DataManager, DataType -from data_vis.colors import ColorGen +from data_vis.colors import ColorGen, ColorType class OBJECT_OT_PieChart(OBJECT_OT_GenericChart): @@ -28,6 +28,17 @@ class OBJECT_OT_PieChart(OBJECT_OT_GenericChart): max=1.0 ) + color_type: bpy.props.EnumProperty( + name='Coloring Type', + items=( + ('0', 'Constant', 'One color'), + ('1', 'Random', 'Random colors'), + ('2', 'Gradient', 'Gradient based on value') + ), + default='2', + description='Type of coloring for chart' + ) + @classmethod def poll(cls, context): dm = DataManager() @@ -37,8 +48,9 @@ def draw(self, context): layout = self.layout row = layout.row() row.prop(self, 'vertices') - row = layout.row() - row.prop(self, 'color_shade') + box = layout.box() + box.prop(self, 'color_shade') + box.prop(self, 'color_type') def execute(self, context): self.slices = [] @@ -64,10 +76,10 @@ def execute(self, context): values_sum = sum(int(entry[1]) for entry in self.data) data_len = len(self.data) - color_gen = ColorGen(self.color_shade, (0, data_len)) + color_gen = ColorGen(self.color_shade, ColorType.str_to_type(self.color_type), (0, data_len)) prev_i = 0 - for i in range(len(self.data)): + for i in range(data_len): portion = self.data[i][1] / values_sum @@ -139,4 +151,11 @@ def add_value_label(self, location, rotation, label, portion, scale_multiplier, to.location.x *= -1 to.scale *= scale_multiplier to.parent = self.container_object + + mat = bpy.data.materials.get('DV_TextMat') + if mat is None: + mat = bpy.data.materials.new(name='DV_TextMat') + + to.data.materials.append(mat) + to.active_material = mat return to From 568ffed22e087131feca5e6e4786a3992e6f6622 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zden=C4=9Bk=20Dole=C5=BEal?= Date: Mon, 6 Apr 2020 19:48:38 +0200 Subject: [PATCH 24/24] Line chart color fix! --- data_vis/general.py | 2 +- data_vis/operators/line_chart.py | 36 +++++++++++++++++++++++++------- 2 files changed, 30 insertions(+), 8 deletions(-) diff --git a/data_vis/general.py b/data_vis/general.py index 2602d77..10a2171 100644 --- a/data_vis/general.py +++ b/data_vis/general.py @@ -127,7 +127,7 @@ class DV_LabelPropertyGroup(bpy.types.PropertyGroup): class DV_ColorPropertyGroup(bpy.types.PropertyGroup): use_shader: bpy.props.BoolProperty( - name='Use Shader', + name='Use Nodes', default=True, description='Uses Node Shading to color created objects. Not using this option may create material for every chart object when not using constant color type' ) diff --git a/data_vis/operators/line_chart.py b/data_vis/operators/line_chart.py index 1b40cf1..a445d26 100644 --- a/data_vis/operators/line_chart.py +++ b/data_vis/operators/line_chart.py @@ -7,6 +7,7 @@ from data_vis.operators.features.axis import AxisFactory from data_vis.general import OBJECT_OT_GenericChart, DV_LabelPropertyGroup, DV_AxisPropertyGroup from data_vis.data_manager import DataManager, DataType +from data_vis.colors import NodeShader, ColorGen, ColorType class OBJECT_OT_LineChart(OBJECT_OT_GenericChart): @@ -44,6 +45,20 @@ class OBJECT_OT_LineChart(OBJECT_OT_GenericChart): options={'SKIP_SAVE'} ) + color_shade: bpy.props.FloatVectorProperty( + name='Base Color', + subtype='COLOR', + default=(0.0, 0.0, 1.0), + min=0.0, + max=1.0, + description='Base color shade to work with' + ) + + use_shader: bpy.props.BoolProperty( + name='Use Nodes', + default=False, + ) + def __init__(self): super().__init__() self.only_2d = True @@ -69,14 +84,14 @@ def poll(cls, context): def draw(self, context): super().draw(context) layout = self.layout + box = layout.box() if self.bevel_edges: - row = layout.row() - row.prop(self, 'rounded') - row = layout.row() - row.prop(self, 'bevel_edges') - if self.bevel_edges: - row = layout.row() - row.prop(self, 'rounded') + box.prop(self, 'rounded') + box.prop(self, 'bevel_edges') + + box = layout.box() + box.prop(self, 'use_shader') + box.prop(self, 'color_shade') def execute(self, context): self.init_data() @@ -100,6 +115,13 @@ def execute(self, context): self.create_curve(normalized_vert_list, edges) self.add_bevel_obj() + if self.use_shader: + mat = NodeShader(self.color_shade, location_z=self.container_object.location[2]).create_geometry_shader() + else: + mat = ColorGen(self.color_shade, ColorType.Constant, self.axis_settings.z_range).get_material() + + self.curve_obj.data.materials.append(mat) + self.curve_obj.active_material = mat if self.axis_settings.create: AxisFactory.create(