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

58 statements  

« prev     ^ index     » next       coverage.py v7.2.2, created at 2023-03-25 14:04 +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 Story(dict): 

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

70 

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

72 super().__init__() 

73 

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

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

76 

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

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

79 self.update(data.build()) 

80 

81 if style: 

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

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

84 self.update(style.build()) 

85 

86 self["slides"] = [] 

87 

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

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

90 

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

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

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

94 

95 def feature(self, name: str, enabled: bool) -> None: 

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

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

98 

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

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

101 self._events.append( 

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

103 ) 

104 

105 def to_html(self) -> str: 

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

107 

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

109 return DISPLAY_TEMPLATE.format( 

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

111 vizzu_story=VIZZU_STORY, 

112 vizzu_player_data=vizzu_player_data, 

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

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

115 )