Coverage for src/ipyvizzustory/storylib/story.py: 100%

79 statements  

« prev     ^ index     » next       coverage.py v7.2.2, created at 2023-03-25 14:02 +0100

1"""A module for working with ipyvizzu-story presentations.""" 

2 

3from typing import Optional, Union, List 

4import json 

5import uuid 

6 

7from ipyvizzu import RawJavaScriptEncoder, Data, Style, Config # , PlainAnimation 

8 

9from ipyvizzustory.storylib.animation import DataFilter 

10from ipyvizzustory.storylib.template import ( 

11 VIZZU_STORY, 

12 DISPLAY_TEMPLATE, 

13 DISPLAY_INDENT, 

14) 

15 

16 

17class Step(dict): 

18 """A class for representing a step of a slide.""" 

19 

20 def __init__( 

21 self, 

22 *animations: Union[Data, Style, Config], 

23 **anim_options: Optional[Union[str, int, float, dict]], 

24 ): 

25 super().__init__() 

26 if not animations: 

27 raise ValueError("No animation was set.") 

28 self._update(*animations) 

29 

30 if anim_options: 

31 # self["animOptions"] = PlainAnimation(**anim_options).build() 

32 raise NotImplementedError("Anim options are not supported.") 

33 

34 def _update(self, *animations: Union[Data, Style, Config]) -> None: 

35 for animation in animations: 

36 if not animation or type(animation) not in [ 

37 Data, 

38 Style, 

39 Config, 

40 ]: # pylint: disable=unidiomatic-typecheck 

41 raise TypeError("Type must be Data, Style or Config.") 

42 if type(animation) == Data: # pylint: disable=unidiomatic-typecheck 

43 animation = DataFilter(animation) 

44 

45 builded_animation = animation.build() 

46 common_keys = set(builded_animation).intersection(set(self)) 

47 if common_keys: 

48 raise ValueError(f"Animation is already merged: {common_keys}") 

49 self.update(builded_animation) 

50 

51 

52class Slide(list): 

53 """A class for representing a slide of a story.""" 

54 

55 def __init__(self, step: Optional[Step] = None): 

56 super().__init__() 

57 if step: 

58 self.add_step(step) 

59 

60 def add_step(self, step: Step) -> None: 

61 """A method for adding a step for the slide.""" 

62 

63 if not step or type(step) != Step: # pylint: disable=unidiomatic-typecheck 

64 raise TypeError("Type must be Step.") 

65 self.append(step) 

66 

67 

68class StorySize: 

69 """A class for representing a story's size.""" 

70 

71 def __init__(self, width: Optional[str] = None, height: Optional[str] = None): 

72 self._width = width 

73 self._height = height 

74 

75 self._style = "" 

76 if any([width is not None, height is not None]): 

77 width = "" if width is None else f"width: {width};" 

78 height = "" if height is None else f"height: {height};" 

79 self._style = f"vizzuPlayer.style.cssText = '{width}{height}'" 

80 

81 @property 

82 def width(self) -> Optional[str]: 

83 """A property for storing story's width.""" 

84 

85 return self._width 

86 

87 @property 

88 def height(self) -> Optional[str]: 

89 """A property for storing story's height.""" 

90 

91 return self._height 

92 

93 @property 

94 def style(self) -> str: 

95 """A property for storing story's height.""" 

96 

97 return self._style 

98 

99 

100class Story(dict): 

101 """A class for representing a presentation story.""" 

102 

103 def __init__(self, data: Data, style: Optional[Style] = None): 

104 super().__init__() 

105 

106 self._size: StorySize = StorySize() 

107 

108 self._features: List[str] = [] 

109 self._events: List[str] = [] 

110 

111 if not data or type(data) != Data: # pylint: disable=unidiomatic-typecheck 

112 raise TypeError("Type must be Data.") 

113 self.update(data.build()) 

114 

115 if style: 

116 if type(style) != Style: # pylint: disable=unidiomatic-typecheck 

117 raise TypeError("Type must be Style.") 

118 self.update(style.build()) 

119 

120 self["slides"] = [] 

121 

122 def add_slide(self, slide: Slide) -> None: 

123 """A method for adding a slide for the story.""" 

124 

125 if not slide or type(slide) != Slide: # pylint: disable=unidiomatic-typecheck 

126 raise TypeError("Type must be Slide.") 

127 self["slides"].append(slide) 

128 

129 def set_feature(self, name: str, enabled: bool) -> None: 

130 """A method for turning on/off a feature of the story.""" 

131 self._features.append(f"chart.feature('{name}', {json.dumps(enabled)});") 

132 

133 def add_event(self, event: str, handler: str) -> None: 

134 """A method for creating and turning on an event handler.""" 

135 self._events.append( 

136 f"chart.on('{event}', event => {{{' '.join(handler.split())}}});" 

137 ) 

138 

139 def set_size( 

140 self, width: Optional[str] = None, height: Optional[str] = None 

141 ) -> None: 

142 """A method for setting width/height settings.""" 

143 

144 self._size = StorySize(width=width, height=height) 

145 

146 def to_html(self) -> str: 

147 """A method for assembling the html code.""" 

148 

149 vizzu_player_data = f"{json.dumps(self, cls=RawJavaScriptEncoder)}" 

150 return DISPLAY_TEMPLATE.format( 

151 id=uuid.uuid4().hex[:7], 

152 vizzu_story=VIZZU_STORY, 

153 vizzu_player_data=vizzu_player_data, 

154 chart_size=self._size.style, 

155 chart_features=f"\n{DISPLAY_INDENT * 3}".join(self._features), 

156 chart_events=f"\n{DISPLAY_INDENT * 3}".join(self._events), 

157 )