├── .gitignore ├── Cargo.toml ├── LICENSE.md ├── README.md ├── assets └── img │ ├── area-chart.svg │ ├── composite-bar-and-scatter-chart.svg │ ├── gallery │ ├── letter-frequency.svg │ └── revenue-by-music-format.svg │ ├── horizontal-bar-chart.svg │ ├── line-chart.svg │ ├── scatter-chart-multiple-keys.svg │ ├── scatter-chart-two-datasets.svg │ ├── scatter-chart.svg │ ├── stacked-horizontal-bar-chart.svg │ ├── stacked-vertical-bar-chart-key-order.svg │ ├── stacked-vertical-bar-chart.svg │ ├── vertical-bar-chart-domain-order.svg │ └── vertical-bar-chart.svg ├── examples ├── area_series_chart.rs ├── composite_bar_and_scatter_chart.rs ├── horizontal_bar_chart.rs ├── line_series_chart.rs ├── scatter_chart.rs ├── scatter_chart_multiple_keys.rs ├── scatter_chart_two_datasets.rs ├── stacked_horizontal_bar_chart.rs ├── stacked_vertical_bar_chart.rs └── vertical_bar_chart.rs ├── gallery ├── letter_frequency.rs ├── revenue_by_music_format.rs └── sources │ ├── letter_frequency.txt │ └── music.csv └── src ├── axis.rs ├── chart.rs ├── colors └── mod.rs ├── components ├── area.rs ├── axis.rs ├── bar.rs ├── legend.rs ├── line.rs ├── mod.rs └── scatter.rs ├── legend.rs ├── lib.rs ├── scales ├── band.rs ├── linear.rs └── mod.rs └── views ├── area.rs ├── datum.rs ├── horizontal_bar.rs ├── line.rs ├── mod.rs ├── scatter.rs └── vertical_bar.rs /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /src/main.rs 3 | /.idea 4 | /src/.DS_Store 5 | .DS_Store 6 | Cargo.lock -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "charts" 3 | version = "0.3.0" 4 | authors = ["Iulian Gulea "] 5 | edition = "2018" 6 | license = "MIT OR Apache-2.0" 7 | description = "A pure Rust visualization library inspired by D3.js" 8 | keywords = ["chart", "plot", "graph", "visualization"] 9 | categories = ["visualization", "science"] 10 | repository = "https://github.com/askanium/rustplotlib" 11 | readme = "README.md" 12 | 13 | [dependencies] 14 | svg="0.7.1" 15 | format_num = "0.1.0" -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Iulian Gulea 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /assets/img/area-chart.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Area Chart 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 20 14 | 15 | 16 | 17 | 18 | 19 | 40 20 | 21 | 22 | 23 | 24 | 25 | 60 26 | 27 | 28 | 29 | 30 | 31 | 80 32 | 33 | 34 | 35 | 36 | 37 | 100 38 | 39 | 40 | 41 | 42 | 43 | 120 44 | 45 | 46 | 47 | 48 | 49 | 140 50 | 51 | 52 | 53 | 54 | 55 | 160 56 | 57 | 58 | 59 | 60 | 61 | 180 62 | 63 | 64 | 65 | Custom Y Axis Label 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 0 74 | 75 | 76 | 77 | 78 | 79 | 10 80 | 81 | 82 | 83 | 84 | 85 | 20 86 | 87 | 88 | 89 | 90 | 91 | 30 92 | 93 | 94 | 95 | 96 | 97 | 40 98 | 99 | 100 | 101 | 102 | 103 | 50 104 | 105 | 106 | 107 | 108 | 109 | 60 110 | 111 | 112 | 113 | 114 | 115 | 70 116 | 117 | 118 | 119 | 120 | 121 | 80 122 | 123 | 124 | 125 | 126 | 127 | 90 128 | 129 | 130 | 131 | 132 | 133 | 100 134 | 135 | 136 | 137 | Custom X Axis Label 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | (12, 54) 148 | 149 | 150 | 151 | 152 | 153 | (100, 40) 154 | 155 | 156 | 157 | 158 | 159 | (120, 50) 160 | 161 | 162 | 163 | 164 | 165 | (180, 70) 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | -------------------------------------------------------------------------------- /assets/img/composite-bar-and-scatter-chart.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Composite Bar + Scatter Chart 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | A 14 | 15 | 16 | 17 | 18 | 19 | B 20 | 21 | 22 | 23 | 24 | 25 | C 26 | 27 | 28 | 29 | Categories 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 0 38 | 39 | 40 | 41 | 42 | 43 | 10 44 | 45 | 46 | 47 | 48 | 49 | 20 50 | 51 | 52 | 53 | 54 | 55 | 30 56 | 57 | 58 | 59 | 60 | 61 | 40 62 | 63 | 64 | 65 | 66 | 67 | 50 68 | 69 | 70 | 71 | 72 | 73 | 60 74 | 75 | 76 | 77 | 78 | 79 | 70 80 | 81 | 82 | 83 | 84 | 85 | 80 86 | 87 | 88 | 89 | 90 | 91 | 90 92 | 93 | 94 | 95 | 96 | 97 | 100 98 | 99 | 100 | 101 | Units of Measurement 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 10 110 | 111 | 112 | 113 | 114 | 115 | 30 116 | 117 | 118 | 119 | 120 | 121 | 70 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | (A, 90.3) 130 | 131 | 132 | 133 | 134 | 135 | (B, 20.1) 136 | 137 | 138 | 139 | 140 | 141 | (C, 10.8) 142 | 143 | 144 | 145 | 146 | 147 | -------------------------------------------------------------------------------- /assets/img/horizontal-bar-chart.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Horizontal Bar Chart 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 0 14 | 15 | 16 | 17 | 18 | 19 | 10 20 | 21 | 22 | 23 | 24 | 25 | 20 26 | 27 | 28 | 29 | 30 | 31 | 30 32 | 33 | 34 | 35 | 36 | 37 | 40 38 | 39 | 40 | 41 | 42 | 43 | 50 44 | 45 | 46 | 47 | 48 | 49 | 60 50 | 51 | 52 | 53 | 54 | 55 | 70 56 | 57 | 58 | 59 | 60 | 61 | 80 62 | 63 | 64 | 65 | 66 | 67 | 90 68 | 69 | 70 | 71 | 72 | 73 | 100 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 0 83 | 84 | 85 | 86 | 87 | 88 | 10 89 | 90 | 91 | 92 | 93 | 94 | 20 95 | 96 | 97 | 98 | 99 | 100 | 30 101 | 102 | 103 | 104 | 105 | 106 | 40 107 | 108 | 109 | 110 | 111 | 112 | 50 113 | 114 | 115 | 116 | 117 | 118 | 60 119 | 120 | 121 | 122 | 123 | 124 | 70 125 | 126 | 127 | 128 | 129 | 130 | 80 131 | 132 | 133 | 134 | 135 | 136 | 90 137 | 138 | 139 | 140 | 141 | 142 | 100 143 | 144 | 145 | 146 | X Axis Custom Label 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | A 155 | 156 | 157 | 158 | 159 | 160 | B 161 | 162 | 163 | 164 | 165 | 166 | C 167 | 168 | 169 | 170 | Y Axis Custom Label 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 10 179 | 180 | 181 | 182 | 183 | 184 | 90 185 | 186 | 187 | 188 | 189 | 190 | 30 191 | 192 | 193 | 194 | 195 | 196 | -------------------------------------------------------------------------------- /assets/img/line-chart.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Line Chart 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 0 14 | 15 | 16 | 17 | 18 | 19 | 20 20 | 21 | 22 | 23 | 24 | 25 | 40 26 | 27 | 28 | 29 | 30 | 31 | 60 32 | 33 | 34 | 35 | 36 | 37 | 80 38 | 39 | 40 | 41 | 42 | 43 | 100 44 | 45 | 46 | 47 | 48 | 49 | 120 50 | 51 | 52 | 53 | 54 | 55 | 140 56 | 57 | 58 | 59 | 60 | 61 | 160 62 | 63 | 64 | 65 | 66 | 67 | 180 68 | 69 | 70 | 71 | 72 | 73 | 200 74 | 75 | 76 | 77 | Custom Y Axis Label 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 0 86 | 87 | 88 | 89 | 90 | 91 | 10 92 | 93 | 94 | 95 | 96 | 97 | 20 98 | 99 | 100 | 101 | 102 | 103 | 30 104 | 105 | 106 | 107 | 108 | 109 | 40 110 | 111 | 112 | 113 | 114 | 115 | 50 116 | 117 | 118 | 119 | 120 | 121 | 60 122 | 123 | 124 | 125 | 126 | 127 | 70 128 | 129 | 130 | 131 | 132 | 133 | 80 134 | 135 | 136 | 137 | 138 | 139 | 90 140 | 141 | 142 | 143 | 144 | 145 | 100 146 | 147 | 148 | 149 | Custom X Axis Label 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | (12, 54) 160 | 161 | 162 | 163 | 164 | 165 | (100, 40) 166 | 167 | 168 | 169 | 170 | 171 | (120, 50) 172 | 173 | 174 | 175 | 176 | 177 | (180, 70) 178 | 179 | 180 | 181 | 182 | 183 | 184 | -------------------------------------------------------------------------------- /assets/img/scatter-chart-multiple-keys.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Scatter Chart 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 0 14 | 15 | 16 | 17 | 18 | 19 | 20 20 | 21 | 22 | 23 | 24 | 25 | 40 26 | 27 | 28 | 29 | 30 | 31 | 60 32 | 33 | 34 | 35 | 36 | 37 | 80 38 | 39 | 40 | 41 | 42 | 43 | 100 44 | 45 | 46 | 47 | 48 | 49 | 120 50 | 51 | 52 | 53 | 54 | 55 | 140 56 | 57 | 58 | 59 | 60 | 61 | 160 62 | 63 | 64 | 65 | 66 | 67 | 180 68 | 69 | 70 | 71 | 72 | 73 | 200 74 | 75 | 76 | 77 | Custom Y Axis Label 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 0 86 | 87 | 88 | 89 | 90 | 91 | 10 92 | 93 | 94 | 95 | 96 | 97 | 20 98 | 99 | 100 | 101 | 102 | 103 | 30 104 | 105 | 106 | 107 | 108 | 109 | 40 110 | 111 | 112 | 113 | 114 | 115 | 50 116 | 117 | 118 | 119 | 120 | 121 | 60 122 | 123 | 124 | 125 | 126 | 127 | 70 128 | 129 | 130 | 131 | 132 | 133 | 80 134 | 135 | 136 | 137 | 138 | 139 | 90 140 | 141 | 142 | 143 | 144 | 145 | 100 146 | 147 | 148 | 149 | Custom X Axis Label 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | (120, 90) 158 | 159 | 160 | 161 | 162 | 163 | (12, 54) 164 | 165 | 166 | 167 | 168 | 169 | (100, 40) 170 | 171 | 172 | 173 | 174 | 175 | (180, 10) 176 | 177 | 178 | 179 | 180 | 181 | -------------------------------------------------------------------------------- /assets/img/scatter-chart-two-datasets.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Scatter Chart 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 0 14 | 15 | 16 | 17 | 18 | 19 | 20 20 | 21 | 22 | 23 | 24 | 25 | 40 26 | 27 | 28 | 29 | 30 | 31 | 60 32 | 33 | 34 | 35 | 36 | 37 | 80 38 | 39 | 40 | 41 | 42 | 43 | 100 44 | 45 | 46 | 47 | 48 | 49 | 120 50 | 51 | 52 | 53 | 54 | 55 | 140 56 | 57 | 58 | 59 | 60 | 61 | 160 62 | 63 | 64 | 65 | 66 | 67 | 180 68 | 69 | 70 | 71 | 72 | 73 | 200 74 | 75 | 76 | 77 | Custom X Axis Label 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 0 86 | 87 | 88 | 89 | 90 | 91 | 10 92 | 93 | 94 | 95 | 96 | 97 | 20 98 | 99 | 100 | 101 | 102 | 103 | 30 104 | 105 | 106 | 107 | 108 | 109 | 40 110 | 111 | 112 | 113 | 114 | 115 | 50 116 | 117 | 118 | 119 | 120 | 121 | 60 122 | 123 | 124 | 125 | 126 | 127 | 70 128 | 129 | 130 | 131 | 132 | 133 | 80 134 | 135 | 136 | 137 | 138 | 139 | 90 140 | 141 | 142 | 143 | 144 | 145 | 100 146 | 147 | 148 | 149 | Custom Y Axis Label 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | (20, 90) 158 | 159 | 160 | 161 | 162 | 163 | (12, 54) 164 | 165 | 166 | 167 | 168 | 169 | (25, 70) 170 | 171 | 172 | 173 | 174 | 175 | (33, 40) 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | (120, 10) 184 | 185 | 186 | 187 | 188 | 189 | (143, 34) 190 | 191 | 192 | 193 | 194 | 195 | (170, 14) 196 | 197 | 198 | 199 | 200 | 201 | (190, 13) 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | Apples 211 | 212 | 213 | 214 | 215 | 216 | Oranges 217 | 218 | 219 | 220 | 221 | -------------------------------------------------------------------------------- /assets/img/scatter-chart.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Scatter Chart 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 0 14 | 15 | 16 | 17 | 18 | 19 | 20 20 | 21 | 22 | 23 | 24 | 25 | 40 26 | 27 | 28 | 29 | 30 | 31 | 60 32 | 33 | 34 | 35 | 36 | 37 | 80 38 | 39 | 40 | 41 | 42 | 43 | 100 44 | 45 | 46 | 47 | 48 | 49 | 120 50 | 51 | 52 | 53 | 54 | 55 | 140 56 | 57 | 58 | 59 | 60 | 61 | 160 62 | 63 | 64 | 65 | 66 | 67 | 180 68 | 69 | 70 | 71 | 72 | 73 | 200 74 | 75 | 76 | 77 | Custom Y Axis Label 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 0 86 | 87 | 88 | 89 | 90 | 91 | 10 92 | 93 | 94 | 95 | 96 | 97 | 20 98 | 99 | 100 | 101 | 102 | 103 | 30 104 | 105 | 106 | 107 | 108 | 109 | 40 110 | 111 | 112 | 113 | 114 | 115 | 50 116 | 117 | 118 | 119 | 120 | 121 | 60 122 | 123 | 124 | 125 | 126 | 127 | 70 128 | 129 | 130 | 131 | 132 | 133 | 80 134 | 135 | 136 | 137 | 138 | 139 | 90 140 | 141 | 142 | 143 | 144 | 145 | 100 146 | 147 | 148 | 149 | Custom X Axis Label 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | (120, 90) 158 | 159 | 160 | 161 | 162 | 163 | (12, 54) 164 | 165 | 166 | 167 | 168 | 169 | (100, 40) 170 | 171 | 172 | 173 | 174 | 175 | (180, 10) 176 | 177 | 178 | 179 | 180 | 181 | -------------------------------------------------------------------------------- /assets/img/stacked-horizontal-bar-chart.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Horizontal Stacked Bar Chart 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 0 14 | 15 | 16 | 17 | 18 | 19 | 10 20 | 21 | 22 | 23 | 24 | 25 | 20 26 | 27 | 28 | 29 | 30 | 31 | 30 32 | 33 | 34 | 35 | 36 | 37 | 40 38 | 39 | 40 | 41 | 42 | 43 | 50 44 | 45 | 46 | 47 | 48 | 49 | 60 50 | 51 | 52 | 53 | 54 | 55 | 70 56 | 57 | 58 | 59 | 60 | 61 | 80 62 | 63 | 64 | 65 | 66 | 67 | 90 68 | 69 | 70 | 71 | 72 | 73 | 100 74 | 75 | 76 | 77 | X Axis Custom Label 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | A 86 | 87 | 88 | 89 | 90 | 91 | B 92 | 93 | 94 | 95 | 96 | 97 | C 98 | 99 | 100 | 101 | Y Axis Custom Label 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 10 110 | 111 | 112 | 113 | 114 | 115 | 70 116 | 117 | 118 | 119 | 20 120 | 121 | 122 | 123 | 5 124 | 125 | 126 | 127 | 128 | 129 | 30 130 | 131 | 132 | 133 | 134 | 135 | -------------------------------------------------------------------------------- /assets/img/stacked-vertical-bar-chart-key-order.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Stacked Bar Chart 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | A 14 | 15 | 16 | 17 | 18 | 19 | B 20 | 21 | 22 | 23 | 24 | 25 | C 26 | 27 | 28 | 29 | Categories 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 0 38 | 39 | 40 | 41 | 42 | 43 | 10 44 | 45 | 46 | 47 | 48 | 49 | 20 50 | 51 | 52 | 53 | 54 | 55 | 30 56 | 57 | 58 | 59 | 60 | 61 | 40 62 | 63 | 64 | 65 | 66 | 67 | 50 68 | 69 | 70 | 71 | 72 | 73 | 60 74 | 75 | 76 | 77 | 78 | 79 | 70 80 | 81 | 82 | 83 | 84 | 85 | 80 86 | 87 | 88 | 89 | 90 | 91 | 90 92 | 93 | 94 | 95 | 96 | 97 | 100 98 | 99 | 100 | 101 | Units of Measurement 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 10 110 | 111 | 112 | 113 | 114 | 115 | 30 116 | 117 | 118 | 119 | 120 | 121 | 70 122 | 123 | 124 | 125 | 5 126 | 127 | 128 | 129 | 20 130 | 131 | 132 | 133 | 134 | 135 | -------------------------------------------------------------------------------- /assets/img/stacked-vertical-bar-chart.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Stacked Bar Chart 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | A 14 | 15 | 16 | 17 | 18 | 19 | B 20 | 21 | 22 | 23 | 24 | 25 | C 26 | 27 | 28 | 29 | Categories 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 0 38 | 39 | 40 | 41 | 42 | 43 | 10 44 | 45 | 46 | 47 | 48 | 49 | 20 50 | 51 | 52 | 53 | 54 | 55 | 30 56 | 57 | 58 | 59 | 60 | 61 | 40 62 | 63 | 64 | 65 | 66 | 67 | 50 68 | 69 | 70 | 71 | 72 | 73 | 60 74 | 75 | 76 | 77 | 78 | 79 | 70 80 | 81 | 82 | 83 | 84 | 85 | 80 86 | 87 | 88 | 89 | 90 | 91 | 90 92 | 93 | 94 | 95 | 96 | 97 | 100 98 | 99 | 100 | 101 | Units of Measurement 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 30 110 | 111 | 112 | 113 | 114 | 115 | 70 116 | 117 | 118 | 119 | 20 120 | 121 | 122 | 123 | 5 124 | 125 | 126 | 127 | 128 | 129 | 10 130 | 131 | 132 | 133 | 134 | 135 | -------------------------------------------------------------------------------- /assets/img/vertical-bar-chart-domain-order.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Bar Chart 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | A 14 | 15 | 16 | 17 | 18 | 19 | C 20 | 21 | 22 | 23 | 24 | 25 | B 26 | 27 | 28 | 29 | Categories 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 0 38 | 39 | 40 | 41 | 42 | 43 | 10 44 | 45 | 46 | 47 | 48 | 49 | 20 50 | 51 | 52 | 53 | 54 | 55 | 30 56 | 57 | 58 | 59 | 60 | 61 | 40 62 | 63 | 64 | 65 | 66 | 67 | 50 68 | 69 | 70 | 71 | 72 | 73 | 60 74 | 75 | 76 | 77 | 78 | 79 | 70 80 | 81 | 82 | 83 | 84 | 85 | 80 86 | 87 | 88 | 89 | 90 | 91 | 90 92 | 93 | 94 | 95 | 96 | 97 | 100 98 | 99 | 100 | 101 | Units of Measurement 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 10 110 | 111 | 112 | 113 | 114 | 115 | 90 116 | 117 | 118 | 119 | 120 | 121 | 30 122 | 123 | 124 | 125 | 126 | 127 | -------------------------------------------------------------------------------- /assets/img/vertical-bar-chart.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Bar Chart 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | A 14 | 15 | 16 | 17 | 18 | 19 | B 20 | 21 | 22 | 23 | 24 | 25 | C 26 | 27 | 28 | 29 | Categories 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 0 38 | 39 | 40 | 41 | 42 | 43 | 10 44 | 45 | 46 | 47 | 48 | 49 | 20 50 | 51 | 52 | 53 | 54 | 55 | 30 56 | 57 | 58 | 59 | 60 | 61 | 40 62 | 63 | 64 | 65 | 66 | 67 | 50 68 | 69 | 70 | 71 | 72 | 73 | 60 74 | 75 | 76 | 77 | 78 | 79 | 70 80 | 81 | 82 | 83 | 84 | 85 | 80 86 | 87 | 88 | 89 | 90 | 91 | 90 92 | 93 | 94 | 95 | 96 | 97 | 100 98 | 99 | 100 | 101 | Units of Measurement 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 30 110 | 111 | 112 | 113 | 114 | 115 | 10 116 | 117 | 118 | 119 | 120 | 121 | 90 122 | 123 | 124 | 125 | 126 | 127 | -------------------------------------------------------------------------------- /examples/area_series_chart.rs: -------------------------------------------------------------------------------- 1 | use charts::{Chart, ScaleLinear, MarkerType, PointLabelPosition, AreaSeriesView}; 2 | 3 | fn main() { 4 | // Define chart related sizes. 5 | let width = 800; 6 | let height = 600; 7 | let (top, right, bottom, left) = (90, 40, 50, 60); 8 | 9 | // Create a band scale that will interpolate values in [0, 200] to values in the 10 | // [0, availableWidth] range (the width of the chart without the margins). 11 | let x = ScaleLinear::new() 12 | .set_domain(vec![12_f32, 180_f32]) 13 | .set_range(vec![0, width - left - right]); 14 | 15 | // Create a linear scale that will interpolate values in [0, 100] range to corresponding 16 | // values in [availableHeight, 0] range (the height of the chart without the margins). 17 | // The [availableHeight, 0] range is inverted because SVGs coordinate system's origin is 18 | // in top left corner, while chart's origin is in bottom left corner, hence we need to invert 19 | // the range on Y axis for the chart to display as though its origin is at bottom left. 20 | let y = ScaleLinear::new() 21 | .set_domain(vec![0_f32, 100_f32]) 22 | .set_range(vec![height - top - bottom, 0]); 23 | 24 | // You can use your own iterable as data as long as its items implement the `PointDatum` trait. 25 | let area_data = vec![(12, 54), (100, 40), (120, 50), (180, 70)]; 26 | 27 | // Create Area series view that is going to represent the data. 28 | let area_view = AreaSeriesView::new() 29 | .set_x_scale(&x) 30 | .set_y_scale(&y) 31 | .set_marker_type(MarkerType::Circle) 32 | .set_label_position(PointLabelPosition::N) 33 | .load_data(&area_data).unwrap(); 34 | 35 | // Generate and save the chart. 36 | Chart::new() 37 | .set_width(width) 38 | .set_height(height) 39 | .set_margins(top, right, bottom, left) 40 | .add_title(String::from("Area Chart")) 41 | .add_view(&area_view) 42 | .add_axis_bottom(&x) 43 | .add_axis_left(&y) 44 | .add_left_axis_label("Custom Y Axis Label") 45 | .add_bottom_axis_label("Custom X Axis Label") 46 | .save("area-chart.svg").unwrap(); 47 | } 48 | -------------------------------------------------------------------------------- /examples/composite_bar_and_scatter_chart.rs: -------------------------------------------------------------------------------- 1 | use charts::{Chart, VerticalBarView, ScaleBand, ScaleLinear, ScatterView, MarkerType, Color, PointLabelPosition}; 2 | 3 | fn main() { 4 | // Define chart related sizes. 5 | let width = 800; 6 | let height = 600; 7 | let (top, right, bottom, left) = (90, 40, 50, 60); 8 | 9 | // Create a band scale that maps ["A", "B", "C"] categories to values in the [0, availableWidth] 10 | // range (the width of the chart without the margins). 11 | let x = ScaleBand::new() 12 | .set_domain(vec![String::from("A"), String::from("B"), String::from("C")]) 13 | .set_range(vec![0, width - left - right]); 14 | 15 | // Create a linear scale that will interpolate values in [0, 100] range to corresponding 16 | // values in [availableHeight, 0] range (the height of the chart without the margins). 17 | // The [availableHeight, 0] range is inverted because SVGs coordinate system's origin is 18 | // in top left corner, while chart's origin is in bottom left corner, hence we need to invert 19 | // the range on Y axis for the chart to display as though its origin is at bottom left. 20 | let y = ScaleLinear::new() 21 | .set_domain(vec![0_f32, 100_f32]) 22 | .set_range(vec![height - top - bottom, 0]); 23 | 24 | // You can use your own iterable as data as long as its items implement the `BarDatum` trait. 25 | let bar_data = vec![("A", 70), ("B", 10), ("C", 30)]; 26 | 27 | // You can use your own iterable as data as long as its items implement the `PointDatum` trait. 28 | let scatter_data = vec![(String::from("A"), 90.3), (String::from("B"), 20.1), (String::from("C"), 10.8)]; 29 | 30 | // Create VerticalBar view that is going to represent the data as vertical bars. 31 | let bar_view = VerticalBarView::new() 32 | .set_x_scale(&x) 33 | .set_y_scale(&y) 34 | .load_data(&bar_data).unwrap(); 35 | 36 | // Create Scatter view that is going to represent the data as points. 37 | let scatter_view = ScatterView::new() 38 | .set_x_scale(&x) 39 | .set_y_scale(&y) 40 | .set_label_position(PointLabelPosition::NE) 41 | .set_marker_type(MarkerType::Circle) 42 | .set_colors(Color::from_vec_of_hex_strings(vec!["#FF4700"])) 43 | .load_data(&scatter_data).unwrap(); 44 | 45 | // Generate and save the chart. 46 | Chart::new() 47 | .set_width(width) 48 | .set_height(height) 49 | .set_margins(top, right, bottom, left) 50 | .add_title(String::from("Composite Bar + Scatter Chart")) 51 | .add_view(&bar_view) // <-- add bar view 52 | .add_view(&scatter_view) // <-- add scatter view 53 | .add_axis_bottom(&x) 54 | .add_axis_left(&y) 55 | .add_left_axis_label("Units of Measurement") 56 | .add_bottom_axis_label("Categories") 57 | .save("composite-bar-and-scatter-chart.svg").unwrap(); 58 | } -------------------------------------------------------------------------------- /examples/horizontal_bar_chart.rs: -------------------------------------------------------------------------------- 1 | use charts::{Chart, HorizontalBarView, ScaleBand, ScaleLinear}; 2 | 3 | fn main() { 4 | // Define chart related sizes. 5 | let width = 800; 6 | let height = 600; 7 | let (top, right, bottom, left) = (90, 40, 50, 60); 8 | 9 | // Create a linear scale that will interpolate values in [0, 100] range to corresponding 10 | // values in [0, availableWidth] range (the width of the chart without the margins). 11 | let x = ScaleLinear::new() 12 | .set_domain(vec![0_f32, 100_f32]) 13 | .set_range(vec![0, width - left - right]); 14 | 15 | // Create a band scale that maps ["A", "B", "C"] categories to values in the [0, availableHeight] 16 | // range (the height of the chart without the margins). 17 | let y = ScaleBand::new() 18 | .set_domain(vec![String::from("A"), String::from("B"), String::from("C")]) 19 | .set_range(vec![0, height - top - bottom]); 20 | 21 | // You can use your own iterable as data as long as its items implement the `BarDatum` trait. 22 | let data = vec![("A", 90), ("B", 10), ("C", 30)]; 23 | 24 | // Create HorizontalBar view that is going to represent the data as vertical bars. 25 | let view = HorizontalBarView::new() 26 | .set_x_scale(&x) 27 | .set_y_scale(&y) 28 | .load_data(&data).unwrap(); 29 | 30 | // Generate and save the chart. 31 | Chart::new() 32 | .set_width(width) 33 | .set_height(height) 34 | .set_margins(top, right, bottom, left) 35 | .add_title(String::from("Horizontal Bar Chart")) 36 | .add_view(&view) 37 | .add_axis_bottom(&x) 38 | .add_axis_top(&x) 39 | .add_axis_left(&y) 40 | .add_left_axis_label("Y Axis Custom Label") 41 | .add_bottom_axis_label("X Axis Custom Label") 42 | .save("horizontal-bar-chart.svg").unwrap(); 43 | } -------------------------------------------------------------------------------- /examples/line_series_chart.rs: -------------------------------------------------------------------------------- 1 | use charts::{Chart, ScaleLinear, MarkerType, PointLabelPosition, LineSeriesView}; 2 | 3 | fn main() { 4 | // Define chart related sizes. 5 | let width = 800; 6 | let height = 600; 7 | let (top, right, bottom, left) = (90, 40, 50, 60); 8 | 9 | // Create a band scale that will interpolate values in [0, 200] to values in the 10 | // [0, availableWidth] range (the width of the chart without the margins). 11 | let x = ScaleLinear::new() 12 | .set_domain(vec![0_f32, 200_f32]) 13 | .set_range(vec![0, width - left - right]); 14 | 15 | // Create a linear scale that will interpolate values in [0, 100] range to corresponding 16 | // values in [availableHeight, 0] range (the height of the chart without the margins). 17 | // The [availableHeight, 0] range is inverted because SVGs coordinate system's origin is 18 | // in top left corner, while chart's origin is in bottom left corner, hence we need to invert 19 | // the range on Y axis for the chart to display as though its origin is at bottom left. 20 | let y = ScaleLinear::new() 21 | .set_domain(vec![0_f32, 100_f32]) 22 | .set_range(vec![height - top - bottom, 0]); 23 | 24 | // You can use your own iterable as data as long as its items implement the `PointDatum` trait. 25 | let line_data = vec![(12, 54), (100, 40), (120, 50), (180, 70)]; 26 | 27 | // Create Line series view that is going to represent the data. 28 | let line_view = LineSeriesView::new() 29 | .set_x_scale(&x) 30 | .set_y_scale(&y) 31 | .set_marker_type(MarkerType::Circle) 32 | .set_label_position(PointLabelPosition::N) 33 | .load_data(&line_data).unwrap(); 34 | 35 | // Generate and save the chart. 36 | Chart::new() 37 | .set_width(width) 38 | .set_height(height) 39 | .set_margins(top, right, bottom, left) 40 | .add_title(String::from("Line Chart")) 41 | .add_view(&line_view) 42 | .add_axis_bottom(&x) 43 | .add_axis_left(&y) 44 | .add_left_axis_label("Custom Y Axis Label") 45 | .add_bottom_axis_label("Custom X Axis Label") 46 | .save("line-chart.svg").unwrap(); 47 | } 48 | -------------------------------------------------------------------------------- /examples/scatter_chart.rs: -------------------------------------------------------------------------------- 1 | use charts::{Chart, ScaleLinear, ScatterView, MarkerType, PointLabelPosition}; 2 | 3 | fn main() { 4 | // Define chart related sizes. 5 | let width = 800; 6 | let height = 600; 7 | let (top, right, bottom, left) = (90, 40, 50, 60); 8 | 9 | // Create a band scale that will interpolate values in [0, 200] to values in the 10 | // [0, availableWidth] range (the width of the chart without the margins). 11 | let x = ScaleLinear::new() 12 | .set_domain(vec![0_f32, 200_f32]) 13 | .set_range(vec![0, width - left - right]); 14 | 15 | // Create a linear scale that will interpolate values in [0, 100] range to corresponding 16 | // values in [availableHeight, 0] range (the height of the chart without the margins). 17 | // The [availableHeight, 0] range is inverted because SVGs coordinate system's origin is 18 | // in top left corner, while chart's origin is in bottom left corner, hence we need to invert 19 | // the range on Y axis for the chart to display as though its origin is at bottom left. 20 | let y = ScaleLinear::new() 21 | .set_domain(vec![0_f32, 100_f32]) 22 | .set_range(vec![height - top - bottom, 0]); 23 | 24 | // You can use your own iterable as data as long as its items implement the `PointDatum` trait. 25 | let scatter_data = vec![(120, 90), (12, 54), (100, 40), (180, 10)]; 26 | 27 | // Create Scatter view that is going to represent the data as points. 28 | let scatter_view = ScatterView::new() 29 | .set_x_scale(&x) 30 | .set_y_scale(&y) 31 | .set_label_position(PointLabelPosition::E) 32 | .set_marker_type(MarkerType::Square) 33 | .load_data(&scatter_data).unwrap(); 34 | 35 | // Generate and save the chart. 36 | Chart::new() 37 | .set_width(width) 38 | .set_height(height) 39 | .set_margins(top, right, bottom, left) 40 | .add_title(String::from("Scatter Chart")) 41 | .add_view(&scatter_view) 42 | .add_axis_bottom(&x) 43 | .add_axis_left(&y) 44 | .add_left_axis_label("Custom X Axis Label") 45 | .add_bottom_axis_label("Custom Y Axis Label") 46 | .save("scatter-chart.svg").unwrap(); 47 | } -------------------------------------------------------------------------------- /examples/scatter_chart_multiple_keys.rs: -------------------------------------------------------------------------------- 1 | use charts::{Chart, ScaleLinear, ScatterView, MarkerType, Color, PointLabelPosition}; 2 | 3 | fn main() { 4 | // Define chart related sizes. 5 | let width = 800; 6 | let height = 600; 7 | let (top, right, bottom, left) = (90, 40, 50, 60); 8 | 9 | // Create a band scale that will interpolate values in [0, 200] to values in the 10 | // [0, availableWidth] range (the width of the chart without the margins). 11 | let x = ScaleLinear::new() 12 | .set_domain(vec![0_f32, 200_f32]) 13 | .set_range(vec![0, width - left - right]); 14 | 15 | // Create a linear scale that will interpolate values in [0, 100] range to corresponding 16 | // values in [availableHeight, 0] range (the height of the chart without the margins). 17 | // The [availableHeight, 0] range is inverted because SVGs coordinate system's origin is 18 | // in top left corner, while chart's origin is in bottom left corner, hence we need to invert 19 | // the range on Y axis for the chart to display as though its origin is at bottom left. 20 | let y = ScaleLinear::new() 21 | .set_domain(vec![0_f32, 100_f32]) 22 | .set_range(vec![height - top - bottom, 0]); 23 | 24 | // You can use your own iterable as data as long as its items implement the `PointDatum` trait. 25 | let scatter_data = vec![(120, 90, "foo"), (12, 54, "foo"), (100, 40, "bar"), (180, 10, "baz")]; 26 | 27 | // Create Scatter view that is going to represent the data as points. 28 | let scatter_view = ScatterView::new() 29 | .set_x_scale(&x) 30 | .set_y_scale(&y) 31 | .set_label_position(PointLabelPosition::E) 32 | .set_marker_type(MarkerType::Circle) 33 | .set_colors(Color::color_scheme_dark()) 34 | .load_data(&scatter_data).unwrap(); 35 | 36 | // Generate and save the chart. 37 | Chart::new() 38 | .set_width(width) 39 | .set_height(height) 40 | .set_margins(top, right, bottom, left) 41 | .add_title(String::from("Scatter Chart")) 42 | .add_view(&scatter_view) 43 | .add_axis_bottom(&x) 44 | .add_axis_left(&y) 45 | .add_left_axis_label("Custom X Axis Label") 46 | .add_bottom_axis_label("Custom Y Axis Label") 47 | .save("scatter-chart-multiple-keys.svg").unwrap(); 48 | } -------------------------------------------------------------------------------- /examples/scatter_chart_two_datasets.rs: -------------------------------------------------------------------------------- 1 | use charts::{Chart, ScaleLinear, ScatterView, MarkerType, PointLabelPosition, Color, AxisPosition}; 2 | 3 | fn main() { 4 | // Define chart related sizes. 5 | let width = 800; 6 | let height = 600; 7 | let (top, right, bottom, left) = (90, 40, 80, 60); 8 | 9 | // Create a band scale that will interpolate values in [0, 200] to values in the 10 | // [0, availableWidth] range (the width of the chart without the margins). 11 | let x = ScaleLinear::new() 12 | .set_domain(vec![0_f32, 200_f32]) 13 | .set_range(vec![0, width - left - right]); 14 | 15 | // Create a linear scale that will interpolate values in [0, 100] range to corresponding 16 | // values in [availableHeight, 0] range (the height of the chart without the margins). 17 | // The [availableHeight, 0] range is inverted because SVGs coordinate system's origin is 18 | // in top left corner, while chart's origin is in bottom left corner, hence we need to invert 19 | // the range on Y axis for the chart to display as though its origin is at bottom left. 20 | let y = ScaleLinear::new() 21 | .set_domain(vec![0_f32, 100_f32]) 22 | .set_range(vec![height - top - bottom, 0]); 23 | 24 | // You can use your own iterable as data as long as its items implement the `PointDatum` trait. 25 | let scatter_data_1 = vec![(20, 90), (12, 54), (25, 70), (33, 40)]; 26 | let scatter_data_2 = vec![(120, 10), (143, 34), (170, 14), (190, 13)]; 27 | 28 | // Create Scatter view that is going to represent the data as points. 29 | let scatter_view_1 = ScatterView::new() 30 | .set_x_scale(&x) 31 | .set_y_scale(&y) 32 | .set_marker_type(MarkerType::Circle) 33 | .set_label_position(PointLabelPosition::N) 34 | .set_custom_data_label("Apples".to_owned()) 35 | .load_data(&scatter_data_1).unwrap(); 36 | 37 | // Create Scatter view that is going to represent the data as points. 38 | let scatter_view_2 = ScatterView::new() 39 | .set_x_scale(&x) 40 | .set_y_scale(&y) 41 | .set_marker_type(MarkerType::Square) 42 | .set_label_position(PointLabelPosition::N) 43 | .set_custom_data_label("Oranges".to_owned()) 44 | .set_colors(Color::from_vec_of_hex_strings(vec!["#aa0000"])) 45 | .load_data(&scatter_data_2).unwrap(); 46 | 47 | // Generate and save the chart. 48 | Chart::new() 49 | .set_width(width) 50 | .set_height(height) 51 | .set_margins(top, right, bottom, left) 52 | .add_title(String::from("Scatter Chart")) 53 | .add_view(&scatter_view_1) 54 | .add_view(&scatter_view_2) 55 | .add_axis_bottom(&x) 56 | .add_axis_left(&y) 57 | .add_left_axis_label("Custom X Axis Label") 58 | .add_bottom_axis_label("Custom Y Axis Label") 59 | .add_legend_at(AxisPosition::Bottom) 60 | .save("scatter-chart-two-datasets.svg").unwrap(); 61 | } -------------------------------------------------------------------------------- /examples/stacked_horizontal_bar_chart.rs: -------------------------------------------------------------------------------- 1 | use charts::{Chart, HorizontalBarView, ScaleBand, ScaleLinear, BarLabelPosition}; 2 | 3 | fn main() { 4 | // Define chart related sizes. 5 | let width = 800; 6 | let height = 600; 7 | let (top, right, bottom, left) = (90, 40, 50, 60); 8 | 9 | // Create a linear scale that will interpolate values in [0, 100] range to corresponding 10 | // values in [0, availableWidth] range (the width of the chart without the margins). 11 | let x = ScaleLinear::new() 12 | .set_domain(vec![0_f32, 100_f32]) 13 | .set_range(vec![0, width - left - right]); 14 | 15 | // Create a band scale that maps ["A", "B", "C"] categories to values in the [0, availableHeight] 16 | // range (the height of the chart without the margins). 17 | let y = ScaleBand::new() 18 | .set_domain(vec![String::from("A"), String::from("B"), String::from("C")]) 19 | .set_range(vec![0, height - top - bottom]); 20 | 21 | // You can use your own iterable as data as long as its items implement the `BarDatum` trait. 22 | let data = vec![("A", 70, "foo"), ("B", 10, "foo"), ("C", 30, "foo"), ("A", 20, "bar"), ("A", 5, "baz")]; 23 | 24 | // Create VerticalBar view that is going to represent the data as vertical bars. 25 | let view = HorizontalBarView::new() 26 | .set_x_scale(&x) 27 | .set_y_scale(&y) 28 | .set_label_position(BarLabelPosition::Center) 29 | .load_data(&data).unwrap(); 30 | 31 | // Generate and save the chart. 32 | Chart::new() 33 | .set_width(width) 34 | .set_height(height) 35 | .set_margins(top, right, bottom, left) 36 | .add_title(String::from("Horizontal Stacked Bar Chart")) 37 | .add_view(&view) 38 | .add_axis_bottom(&x) 39 | .add_axis_left(&y) 40 | .add_left_axis_label("Y Axis Custom Label") 41 | .add_bottom_axis_label("X Axis Custom Label") 42 | .save("stacked-horizontal-bar-chart.svg").unwrap(); 43 | } -------------------------------------------------------------------------------- /examples/stacked_vertical_bar_chart.rs: -------------------------------------------------------------------------------- 1 | use charts::{Chart, VerticalBarView, ScaleBand, ScaleLinear, BarLabelPosition}; 2 | 3 | fn main() { 4 | // Define chart related sizes. 5 | let width = 800; 6 | let height = 600; 7 | let (top, right, bottom, left) = (90, 40, 50, 60); 8 | 9 | // Create a band scale that maps ["A", "B", "C"] categories to values in [0, availableWidth] 10 | // range (the width of the chart without the margins). 11 | let x = ScaleBand::new() 12 | .set_domain(vec![String::from("A"), String::from("B"), String::from("C")]) 13 | .set_range(vec![0, width - left - right]); 14 | 15 | // Create a linear scale that will interpolate values in [0, 100] range to corresponding 16 | // values in [availableHeight, 0] range (the height of the chart without the margins). 17 | // The [availableHeight, 0] range is inverted because SVGs coordinate system's origin is 18 | // in the top left corner, while chart's origin is in bottom left corner, hence we need to 19 | // invert the range on Y axis for the chart to display as though its origin is at bottom left. 20 | let y = ScaleLinear::new() 21 | .set_domain(vec![0_f32, 100_f32]) 22 | .set_range(vec![height - top - bottom, 0]); 23 | 24 | // You can use your own iterable as data as long as its items implement the `BarDatum` trait. 25 | let data = vec![("A", 70, "foo"), ("B", 10, "foo"), ("C", 30, "foo"), ("A", 20, "bar"), ("A", 5, "baz")]; 26 | 27 | // Create VerticalBar view that is going to represent the data as vertical bars. 28 | let view = VerticalBarView::new() 29 | .set_x_scale(&x) 30 | .set_y_scale(&y) 31 | // .set_label_visibility(false) // <-- uncomment this line to hide bar value labels 32 | .set_label_position(BarLabelPosition::Center) 33 | .load_data(&data).unwrap(); 34 | 35 | // Generate and save the chart. 36 | Chart::new() 37 | .set_width(width) 38 | .set_height(height) 39 | .set_margins(top, right, bottom, left) 40 | .add_title(String::from("Stacked Bar Chart")) 41 | .add_view(&view) 42 | .add_axis_bottom(&x) 43 | .add_axis_left(&y) 44 | .add_left_axis_label("Units of Measurement") 45 | .add_bottom_axis_label("Categories") 46 | .save("stacked-vertical-bar-chart.svg").unwrap(); 47 | } -------------------------------------------------------------------------------- /examples/vertical_bar_chart.rs: -------------------------------------------------------------------------------- 1 | use charts::{Chart, VerticalBarView, ScaleBand, ScaleLinear}; 2 | 3 | fn main() { 4 | // Define chart related sizes. 5 | let width = 800; 6 | let height = 600; 7 | let (top, right, bottom, left) = (90, 40, 50, 60); 8 | 9 | // Create a band scale that maps ["A", "B", "C"] categories to values in the [0, availableWidth] 10 | // range (the width of the chart without the margins). 11 | let x = ScaleBand::new() 12 | .set_domain(vec![String::from("A"), String::from("B"), String::from("C")]) 13 | .set_range(vec![0, width - left - right]) 14 | .set_inner_padding(0.1) 15 | .set_outer_padding(0.1); 16 | 17 | // Create a linear scale that will interpolate values in [0, 100] range to corresponding 18 | // values in [availableHeight, 0] range (the height of the chart without the margins). 19 | // The [availableHeight, 0] range is inverted because SVGs coordinate system's origin is 20 | // in top left corner, while chart's origin is in bottom left corner, hence we need to invert 21 | // the range on Y axis for the chart to display as though its origin is at bottom left. 22 | let y = ScaleLinear::new() 23 | .set_domain(vec![0_f32, 100_f32]) 24 | .set_range(vec![height - top - bottom, 0]); 25 | 26 | // You can use your own iterable as data as long as its items implement the `BarDatum` trait. 27 | let data = vec![("A", 90), ("B", 10), ("C", 30)]; 28 | 29 | // Create VerticalBar view that is going to represent the data as vertical bars. 30 | let view = VerticalBarView::new() 31 | .set_x_scale(&x) 32 | .set_y_scale(&y) 33 | .load_data(&data).unwrap(); 34 | 35 | // Generate and save the chart. 36 | Chart::new() 37 | .set_width(width) 38 | .set_height(height) 39 | .set_margins(top, right, bottom, left) 40 | .add_title(String::from("Bar Chart")) 41 | .add_view(&view) 42 | .add_axis_bottom(&x) 43 | .add_axis_left(&y) 44 | .add_left_axis_label("Units of Measurement") 45 | .add_bottom_axis_label("Categories") 46 | .save("vertical-bar-chart.svg").unwrap(); 47 | } -------------------------------------------------------------------------------- /gallery/letter_frequency.rs: -------------------------------------------------------------------------------- 1 | use charts::{Chart, VerticalBarView, ScaleBand, ScaleLinear}; 2 | use std::fs::File; 3 | use std::io::Read; 4 | 5 | fn main() -> std::io::Result<()> { 6 | let mut file = File::open("./sources/letter_frequency.txt")?; 7 | let mut contents = String::new(); 8 | file.read_to_string(&mut contents)?; 9 | 10 | // Convert the data into a vec of (&str, f32) tuples for which the `BarDatum` 11 | // trait is implemented and which can be displayed as a bar chart. 12 | let mut data = contents.split("\n") 13 | .collect::>() 14 | .iter_mut() 15 | .map(|row| { 16 | let letter_freq_pair = row.split(",").collect::>(); 17 | (letter_freq_pair[0], letter_freq_pair[1].parse::().unwrap() * 100_f32) 18 | }) 19 | .collect::>(); 20 | 21 | data.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap()); 22 | 23 | let width = 800; 24 | let height = 600; 25 | let (top, right, bottom, left) = (70, 10, 50, 60); 26 | 27 | let x = ScaleBand::new() 28 | .set_domain(data.iter().map(|d| String::from(d.0)).collect()) 29 | .set_range(vec![0, width - left - right]); 30 | 31 | let y = ScaleLinear::new() 32 | .set_domain(vec![0_f32, data.iter().map(|d| d.1.ceil() as isize).max().unwrap() as f32]) 33 | .set_range(vec![height - top - bottom, 0]); 34 | 35 | let view = VerticalBarView::new() 36 | .set_x_scale(&x) 37 | .set_y_scale(&y) 38 | .set_label_rounding_precision(1) 39 | .load_data(&data).unwrap(); 40 | 41 | Chart::new() 42 | .set_width(width) 43 | .set_height(height) 44 | .set_margins(top, right, bottom, left) 45 | .add_title(String::from("Frequency of English Letters")) 46 | .add_view(&view) 47 | .add_axis_bottom(&x) 48 | .add_axis_left(&y) 49 | .add_left_axis_label("Frequency (%)") 50 | .save("letter-frequency.svg").unwrap(); 51 | 52 | Ok(()) 53 | } -------------------------------------------------------------------------------- /gallery/revenue_by_music_format.rs: -------------------------------------------------------------------------------- 1 | use charts::{Chart, VerticalBarView, ScaleBand, ScaleLinear, AxisPosition, Color}; 2 | use std::fs::File; 3 | use std::io::Read; 4 | 5 | fn main() -> std::io::Result<()> { 6 | // source [RIAA](https://www.riaa.com/u-s-sales-database/) 7 | let mut file = File::open("./sources/music.csv")?; 8 | let mut contents = String::new(); 9 | file.read_to_string(&mut contents)?; 10 | 11 | // Convert the data into a vec of (&str, f32, &str) tuples for which the `BarDatum` 12 | // trait is implemented and which can be displayed as a stacked bar chart. 13 | let data = contents.split("\n") 14 | .collect::>() 15 | .iter_mut() 16 | .enumerate() 17 | .filter_map(|(i, row)| { 18 | if i > 0 { 19 | let cells = row.split(",").collect::>(); 20 | Some((cells[1], cells[4].parse::().unwrap(), cells[0])) 21 | } else { 22 | None 23 | } 24 | }) 25 | .collect::>(); 26 | 27 | let width = 960; 28 | let height = 600; 29 | let (top, right, bottom, left) = (180, 10, 50, 60); 30 | 31 | let x = ScaleBand::new() 32 | .set_domain(data.iter().map(|d| String::from(d.0)).collect()) 33 | .set_range(vec![0, width - left - right]); 34 | 35 | let y = ScaleLinear::new() 36 | .set_domain(vec![0_f32, 22_000_000_000_f32]) 37 | .set_range(vec![height - top - bottom, 0]); 38 | 39 | let view = VerticalBarView::new() 40 | .set_x_scale(&x) 41 | .set_y_scale(&y) 42 | .set_label_visibility(false) 43 | .set_keys(vec![String::from("LP/EP"), String::from("Vinyl Single"), String::from("8 - Track"), String::from("Cassette"), String::from("Cassette Single"), String::from("Other Tapes"), String::from("Kiosk"), String::from("CD"), String::from("CD Single"), String::from("SACD"), String::from("DVD Audio"), String::from("Music Video (Physical)"), String::from("Download Album"), String::from("Download Single"), String::from("Ringtones and Ringbacks"), String::from("Download Music Video"), String::from("Other Digital"), String::from("Synchronization"), String::from("Paid Subscription"), String::from("On-Demand Streaming (Ad-Supported)"), String::from("Other Ad-Supported Streaming"), String::from("SoundExchange Distributions"), String::from("Limited Tier Paid Subscription")]) 44 | .set_colors(Color::from_vec_of_hex_strings(vec!["#2A5784", "#43719F", "#5B8DB8", "#7AAAD0", "#9BC7E4", "#BADDF1", "#E1575A", "#EE7423", "#F59D3D", "#FFC686", "#9D7760", "#F1CF63", "#7C4D79", "#9B6A97", "#BE89AC", "#D5A5C4", "#EFC9E6", "#BBB1AC", "#24693D", "#398949", "#61AA57", "#7DC470", "#B4E0A7"])) 45 | .load_data(&data).unwrap(); 46 | 47 | Chart::new() 48 | .set_width(width) 49 | .set_height(height) 50 | .set_margins(top, right, bottom, left) 51 | .add_view(&view) 52 | .add_axis_bottom(&x) 53 | .add_axis_left(&y) 54 | .add_legend_at(AxisPosition::Top) 55 | .set_bottom_axis_tick_label_rotation(-90) 56 | .set_left_axis_tick_label_format(".2s") 57 | .add_left_axis_label("Revenue ($)") 58 | .save("revenue-by-music-format.svg").unwrap(); 59 | 60 | Ok(()) 61 | } -------------------------------------------------------------------------------- /gallery/sources/letter_frequency.txt: -------------------------------------------------------------------------------- 1 | A,0.08167 2 | B,0.01492 3 | C,0.02782 4 | D,0.04253 5 | E,0.12702 6 | F,0.02288 7 | G,0.02015 8 | H,0.06094 9 | I,0.06966 10 | J,0.00153 11 | K,0.00772 12 | L,0.04025 13 | M,0.02406 14 | N,0.06749 15 | O,0.07507 16 | P,0.01929 17 | Q,0.00095 18 | R,0.05987 19 | S,0.06327 20 | T,0.09056 21 | U,0.02758 22 | V,0.00978 23 | W,0.0236 24 | X,0.0015 25 | Y,0.01974 26 | Z,0.00074 -------------------------------------------------------------------------------- /src/axis.rs: -------------------------------------------------------------------------------- 1 | use std::string::ToString; 2 | use svg::node::element::Group; 3 | use svg::parser::Error; 4 | use svg::Node; 5 | use svg::node::Text as TextNode; 6 | use svg::node::element::Text; 7 | use crate::{Scale, Chart}; 8 | use crate::components::axis::{AxisLine, AxisTick}; 9 | use crate::scales::ScaleType; 10 | 11 | /// Enum of possible axis positions on the chart. 12 | #[derive(Copy, Clone, PartialEq)] 13 | pub enum AxisPosition { 14 | Top, 15 | Right, 16 | Bottom, 17 | Left, 18 | } 19 | 20 | /// An axis struct that represents an axis along a dimension of the chart. 21 | pub struct Axis { 22 | ticks: Vec, 23 | axis_line: AxisLine, 24 | position: AxisPosition, 25 | label: String, 26 | label_rotation: isize, 27 | label_format: String, 28 | length: isize, 29 | } 30 | 31 | impl Axis { 32 | /// Create a new instance of an axis for a chart based on the provided scale and position. 33 | fn new<'a, T: ToString>(scale: &'a dyn Scale, position: AxisPosition, chart: &Chart<'a>) -> Self { 34 | Self { 35 | ticks: Self::generate_ticks(scale, position), 36 | position, 37 | axis_line: Self::get_axis_line(position, chart), 38 | label: String::new(), 39 | label_rotation: 0, 40 | label_format: String::new(), 41 | length: Self::get_axis_length(position, chart), 42 | } 43 | } 44 | 45 | /// Create a new axis at the top of the chart. 46 | pub fn new_top_axis<'a, T: ToString>(scale: &'a dyn Scale, chart: &Chart<'a>) -> Self { 47 | Self::new(scale, AxisPosition::Top, chart) 48 | } 49 | 50 | /// Create a new axis to the right of the chart. 51 | pub fn new_right_axis<'a, T: ToString>(scale: &'a dyn Scale, chart: &Chart<'a>) -> Self { 52 | Self::new(scale, AxisPosition::Right, chart) 53 | } 54 | 55 | /// Create a new axis at the bottom of the chart. 56 | pub fn new_bottom_axis<'a, T: ToString>(scale: &'a dyn Scale, chart: &Chart<'a>) -> Self { 57 | Self::new(scale, AxisPosition::Bottom, chart) 58 | } 59 | 60 | /// Create a new axis to the left of the chart. 61 | pub fn new_left_axis<'a, T: ToString>(scale: &'a dyn Scale, chart: &Chart<'a>) -> Self { 62 | Self::new(scale, AxisPosition::Left, chart) 63 | } 64 | 65 | /// Set axis label. 66 | pub fn set_axis_label(&mut self, label: String) { 67 | self.label = label; 68 | } 69 | 70 | /// Set tick label rotation. 71 | pub fn set_tick_label_rotation(&mut self, rotation: isize) { 72 | self.label_rotation = rotation; 73 | self.ticks.iter_mut().for_each(|tick| tick.set_label_rotation(rotation)); 74 | } 75 | 76 | /// Set the label format. 77 | pub fn set_tick_label_format(&mut self, format: &str) { 78 | self.label_format = String::from(format); 79 | let label_format = self.label_format.as_str(); 80 | self.ticks.iter_mut().for_each(|tick| tick.set_label_format(label_format)); 81 | } 82 | 83 | /// Return whether the axis has a label or not. 84 | pub fn has_label(&self) -> bool { 85 | self.label.len() > 0 86 | } 87 | 88 | /// Compute the length of the axis. 89 | fn get_axis_length<'a>(position: AxisPosition, chart: &Chart<'a>) -> isize { 90 | if position == AxisPosition::Top || position == AxisPosition::Bottom { 91 | chart.get_view_width() 92 | } else { 93 | chart.get_view_height() 94 | } 95 | } 96 | 97 | /// Generate svg for the axis. 98 | pub fn to_svg(&self) -> Result { 99 | let axis_class = match self.position { 100 | AxisPosition::Top => "x-axis", 101 | AxisPosition::Bottom => "x-axis", 102 | AxisPosition::Left => "y-axis", 103 | AxisPosition::Right => "y-axis", 104 | }; 105 | 106 | let mut group = Group::new() 107 | .set("class", axis_class) 108 | .add(self.axis_line.to_svg().unwrap()); 109 | 110 | for tick in self.ticks.iter() { 111 | group.append(tick.to_svg().unwrap()); 112 | } 113 | 114 | if self.label.len() > 0 { 115 | let (x, y, rotate) = match self.position { 116 | AxisPosition::Top => ((self.length / 2) as i32, -32, 0), 117 | AxisPosition::Bottom => ((self.length / 2) as i32, 42, 0), 118 | AxisPosition::Left => (-(self.length as i32 / 2), -42, -90), 119 | AxisPosition::Right => ((self.length as i32 / 2), -42, 90), 120 | }; 121 | let axis_label = Text::new() 122 | .set("x", x) 123 | .set("y", y) 124 | .set("text-anchor", "middle") 125 | .set("font-size", "14px") 126 | .set("font-family", "sans-serif") 127 | .set("fill", "#777") 128 | .set("transform", format!("rotate({})", rotate)) 129 | .add(TextNode::new(&self.label)); 130 | group.append(axis_label); 131 | } 132 | 133 | Ok(group) 134 | } 135 | 136 | /// Generate ticks for the axis based on the scale and position. 137 | fn generate_ticks<'a, T: ToString>(scale: &'a dyn Scale, position: AxisPosition) -> Vec { 138 | let mut ticks = Vec::new(); 139 | let label_offset = { 140 | if position == AxisPosition::Top || position == AxisPosition::Bottom { 141 | 16 142 | } else { 143 | 12 144 | } 145 | }; 146 | 147 | for tick in scale.get_ticks() { 148 | let tick_offset = match position { 149 | AxisPosition::Bottom if scale.get_type() == ScaleType::Band => scale.scale(&tick) + scale.bandwidth().unwrap() / 2_f32, 150 | AxisPosition::Bottom => scale.scale(&tick), 151 | AxisPosition::Left if scale.get_type() == ScaleType::Band => scale.scale(&tick) + scale.bandwidth().unwrap() / 2_f32, 152 | AxisPosition::Left => scale.scale(&tick), 153 | AxisPosition::Top if scale.get_type() == ScaleType::Band => scale.scale(&tick) + scale.bandwidth().unwrap() / 2_f32, 154 | AxisPosition::Top => scale.scale(&tick), 155 | AxisPosition::Right if scale.get_type() == ScaleType::Band => scale.scale(&tick) + scale.bandwidth().unwrap() / 2_f32, 156 | AxisPosition::Right => scale.scale(&tick), 157 | }; 158 | let axis_tick = AxisTick::new(tick_offset, label_offset, 0, tick.to_string(), position); 159 | ticks.push(axis_tick); 160 | } 161 | 162 | ticks 163 | } 164 | 165 | /// Generate the line that represents the axis. 166 | fn get_axis_line<'a>(position: AxisPosition, chart: &Chart<'a>) -> AxisLine { 167 | match position { 168 | AxisPosition::Top => AxisLine::new(0_f32, 0_f32, chart.get_view_width() as f32, 0_f32), 169 | AxisPosition::Right => AxisLine::new(0_f32, 0_f32, 0_f32, chart.get_view_height() as f32), 170 | AxisPosition::Bottom => AxisLine::new(0_f32, 0_f32, chart.get_view_width() as f32, 0_f32), 171 | AxisPosition::Left => AxisLine::new(0_f32, 0_f32, 0_f32, chart.get_view_height() as f32), 172 | } 173 | } 174 | } 175 | -------------------------------------------------------------------------------- /src/colors/mod.rs: -------------------------------------------------------------------------------- 1 | /// A struct that represents a color. 2 | #[derive(Debug)] 3 | pub struct Color { 4 | hex: String, 5 | } 6 | 7 | impl Color { 8 | /// Generate a color scheme from a string. 9 | /// Useful when displaying a single dataset that requires one color. 10 | pub fn from_vec_of_hex_strings(color_strings: Vec<&str>) -> Vec { 11 | let mut colors = Vec::new(); 12 | 13 | for color in color_strings.iter() { 14 | colors.push(Color { hex: String::from(*color) }) 15 | } 16 | 17 | colors 18 | } 19 | 20 | /// Generate a color scheme made of 10 colors. 21 | pub fn color_scheme_10() -> Vec { 22 | vec!( 23 | Color { hex: "#1f77b4".to_string() }, 24 | Color { hex: "#ff7f0e".to_string() }, 25 | Color { hex: "#2ca02c".to_string() }, 26 | Color { hex: "#d62728".to_string() }, 27 | Color { hex: "#9467bd".to_string() }, 28 | Color { hex: "#8c564b".to_string() }, 29 | Color { hex: "#e377c2".to_string() }, 30 | Color { hex: "#7f7f7f".to_string() }, 31 | Color { hex: "#bcbd22".to_string() }, 32 | Color { hex: "#17becf".to_string() }, 33 | ) 34 | } 35 | 36 | /// An array of ten categorical colors authored by Tableau as part of 37 | /// [Tableau 10](https://www.tableau.com/about/blog/2016/7/colors-upgrade-tableau-10-56782). 38 | pub fn color_scheme_tableau_10() -> Vec { 39 | vec!( 40 | Color { hex: "#4e79a7".to_string() }, 41 | Color { hex: "#f28e2c".to_string() }, 42 | Color { hex: "#e15759".to_string() }, 43 | Color { hex: "#76b7b2".to_string() }, 44 | Color { hex: "#59a14f".to_string() }, 45 | Color { hex: "#edc949".to_string() }, 46 | Color { hex: "#af7aa1".to_string() }, 47 | Color { hex: "#ff9da7".to_string() }, 48 | Color { hex: "#9c755f".to_string() }, 49 | Color { hex: "#bab0ab".to_string() }, 50 | ) 51 | } 52 | 53 | /// An array of eight categorical colors 54 | pub fn color_scheme_dark() -> Vec { 55 | vec!( 56 | Color { hex: "#1b9e77".to_string() }, 57 | Color { hex: "#d95f02".to_string() }, 58 | Color { hex: "#7570b3".to_string() }, 59 | Color { hex: "#e7298a".to_string() }, 60 | Color { hex: "#66a61e".to_string() }, 61 | Color { hex: "#e6ab02".to_string() }, 62 | Color { hex: "#a6761d".to_string() }, 63 | Color { hex: "#666666".to_string() }, 64 | ) 65 | } 66 | 67 | /// Represent a color as a hex string. 68 | pub fn as_hex(&self) -> String { 69 | String::from(&self.hex) 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/components/area.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::Display; 2 | use svg::node::element::{Group, Path}; 3 | use svg::node::element::path::Data; 4 | use svg::node::Node; 5 | use crate::components::DatumRepresentation; 6 | use crate::components::scatter::ScatterPoint; 7 | 8 | /// Represents a point in a scatter plot. 9 | #[derive(Debug)] 10 | pub struct AreaSeries { 11 | points: Vec>, 12 | color: String, 13 | } 14 | 15 | impl AreaSeries { 16 | pub fn new( 17 | points: Vec>, 18 | color: String 19 | ) -> Self { 20 | Self { 21 | points, 22 | color, 23 | } 24 | } 25 | } 26 | 27 | impl DatumRepresentation for AreaSeries { 28 | 29 | fn to_svg(&self) -> Result { 30 | let mut group = Group::new() 31 | .set("class", "line"); 32 | 33 | let mut data = Data::new(); 34 | 35 | for (i, point) in self.points.iter().enumerate() { 36 | if i == 0 { 37 | data = data.move_to((point.get_x(), point.get_y())); 38 | } else { 39 | data = data.line_to((point.get_x(), point.get_y())); 40 | } 41 | } 42 | 43 | data = data.close(); 44 | 45 | let area = Path::new() 46 | .set("fill", self.color.as_ref()) 47 | .set("stroke", self.color.as_ref()) 48 | .set("d", data); 49 | 50 | group.append(area); 51 | 52 | for point in self.points.iter() { 53 | group.append(point.to_svg()?); 54 | } 55 | 56 | Ok(group) 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/components/axis.rs: -------------------------------------------------------------------------------- 1 | use svg::node::element::{Group, Line}; 2 | use svg::node::Text as TextNode; 3 | use svg::node::element::Text; 4 | use svg::Node; 5 | use format_num::NumberFormat; 6 | use crate::axis::AxisPosition; 7 | 8 | /// A simple struct that represents an axis line. 9 | pub(crate) struct AxisLine { 10 | x1: f32, 11 | y1: f32, 12 | x2: f32, 13 | y2: f32, 14 | } 15 | 16 | impl AxisLine { 17 | /// Create a new instance of axis line. 18 | pub fn new(x1: f32, y1: f32, x2: f32, y2: f32) -> Self { 19 | Self { x1, y1, x2, y2 } 20 | } 21 | 22 | /// Render the axis line to svg. 23 | pub fn to_svg(&self) -> Result { 24 | let line = Line::new() 25 | .set("x1", self.x1) 26 | .set("y1", self.y1) 27 | .set("x2", self.x2) 28 | .set("y2", self.y2) 29 | .set("shape-rendering", "crispEdges") 30 | .set("stroke-width", 1) 31 | .set("stroke", "#bbbbbb"); 32 | 33 | Ok(line) 34 | } 35 | } 36 | 37 | /// A struct to represent an axis tick 38 | pub struct AxisTick { 39 | axis_position: AxisPosition, 40 | label_offset: usize, 41 | label_rotation: isize, 42 | tick_offset: f32, 43 | label: String, 44 | label_format: Option 45 | } 46 | 47 | impl AxisTick { 48 | /// Create a new instance of AxisTick. 49 | pub fn new(tick_offset: f32, label_offset: usize, label_rotation: isize, label: String, axis_position: AxisPosition) -> Self { 50 | Self { 51 | label_offset, 52 | tick_offset, 53 | label_rotation, 54 | label, 55 | axis_position, 56 | label_format: None, 57 | } 58 | } 59 | 60 | /// Set label rotation. 61 | pub fn set_label_rotation(&mut self, rotation: isize) { 62 | self.label_rotation = rotation; 63 | } 64 | 65 | /// Set label rotation. 66 | pub fn set_label_format(&mut self, format: &str) { 67 | self.label_format = Some(format.to_owned()); 68 | } 69 | 70 | /// Render the axis tick to svg. 71 | pub fn to_svg(&self) -> Result { 72 | let formatted_label = if self.label_format.is_some() { 73 | let formatter = NumberFormat::new(); 74 | formatter.format(self.label_format.as_ref().unwrap(), self.label.parse::().unwrap()).replace('G', "B") 75 | } else { 76 | self.label.to_owned() 77 | }; 78 | let offsets: (f32, f32); 79 | let tick_line_p2: (isize, isize); 80 | let tick_label_offset: (isize, isize); 81 | let tick_label_text_anchor: &str; 82 | 83 | match self.axis_position { 84 | AxisPosition::Left => { 85 | offsets = (0_f32, self.tick_offset); 86 | tick_line_p2 = (-6, 0); 87 | tick_label_offset = (-(self.label_offset as isize), 0); 88 | tick_label_text_anchor = "end"; 89 | }, 90 | AxisPosition::Bottom => { 91 | offsets = (self.tick_offset, 0_f32); 92 | tick_line_p2 = (0, 6); 93 | tick_label_offset = (0, self.label_offset as isize); 94 | tick_label_text_anchor = "middle"; 95 | }, 96 | AxisPosition::Right => { 97 | offsets = (0_f32, self.tick_offset); 98 | tick_line_p2 = (6, 0); 99 | tick_label_offset = (self.label_offset as isize, 0); 100 | tick_label_text_anchor = "start"; 101 | }, 102 | AxisPosition::Top => { 103 | offsets = (self.tick_offset, 0_f32); 104 | tick_line_p2 = (0, -6); 105 | tick_label_offset = (0, -(self.label_offset as isize)); 106 | tick_label_text_anchor = "middle"; 107 | }, 108 | }; 109 | 110 | let mut group = Group::new() 111 | .set("class", "tick") 112 | .set("transform", format!("translate({},{})", offsets.0, offsets.1)); 113 | 114 | let tick_line = Line::new() 115 | .set("x1", 0) 116 | .set("y1", 0) 117 | .set("x2", tick_line_p2.0) 118 | .set("y2", tick_line_p2.1) 119 | .set("shape-rendering", "crispEdges") 120 | .set("stroke", "#bbbbbb") 121 | .set("stroke-width", "1px"); 122 | 123 | let tick_label = Text::new() 124 | .set("transform", format!("rotate({},{},{})", self.label_rotation, tick_label_offset.0, tick_label_offset.1)) 125 | .set("x", tick_label_offset.0) 126 | .set("y", tick_label_offset.1) 127 | .set("dy", ".35em") 128 | .set("text-anchor", tick_label_text_anchor) 129 | .set("font-size", "12px") 130 | .set("font-family", "sans-serif") 131 | .set("fill", "#777") 132 | .add(TextNode::new(formatted_label)); 133 | 134 | group.append(tick_line); 135 | group.append(tick_label); 136 | 137 | Ok(group) 138 | } 139 | } -------------------------------------------------------------------------------- /src/components/bar.rs: -------------------------------------------------------------------------------- 1 | use svg::node::Node; 2 | use svg::node::element::Group; 3 | use svg::node::element::Rectangle; 4 | use svg::node::Text as TextNode; 5 | use svg::node::element::Text; 6 | use crate::components::DatumRepresentation; 7 | use crate::chart::Orientation; 8 | 9 | /// Set the position of a bar's label. 10 | #[derive(Copy, Clone, Debug)] 11 | pub enum BarLabelPosition { 12 | StartOutside, 13 | StartInside, 14 | Center, 15 | EndInside, 16 | EndOutside, 17 | } 18 | 19 | /// Represents a block within a bar. 20 | /// The first tuple element represents the starting position, the second 21 | /// one is the size of that block and the third one is the color. 22 | #[derive(Debug)] 23 | pub struct BarBlock(f32, f32, f32, String); 24 | 25 | impl BarBlock { 26 | pub fn new(start: f32, end: f32, size: f32, color: String) -> Self { 27 | Self(start, end, size, color) 28 | } 29 | } 30 | 31 | #[derive(Debug)] 32 | pub struct Bar { 33 | blocks: Vec, 34 | orientation: Orientation, 35 | label_position: BarLabelPosition, 36 | rounding_precision: Option, 37 | label_visible: bool, 38 | category: String, 39 | bar_width: f32, 40 | offset: f32, 41 | } 42 | 43 | impl Bar { 44 | pub fn new( 45 | blocks: Vec, 46 | orientation: Orientation, 47 | category: String, 48 | label_position: BarLabelPosition, 49 | label_visible: bool, 50 | rounding_precision: Option, 51 | bar_width: f32, 52 | offset: f32 53 | ) -> Self { 54 | Self { 55 | blocks, 56 | orientation, 57 | label_position, 58 | rounding_precision, 59 | label_visible, 60 | category, 61 | bar_width, 62 | offset, 63 | } 64 | } 65 | } 66 | 67 | impl DatumRepresentation for Bar { 68 | 69 | fn to_svg(&self) -> Result { 70 | let (bar_group_offset_x, bar_group_offset_y) = { 71 | match self.orientation { 72 | Orientation::Vertical => (self.offset, 0_f32), 73 | Orientation::Horizontal => (0_f32, self.offset), 74 | } 75 | }; 76 | 77 | let mut group = Group::new() 78 | .set("transform", format!("translate({},{})", bar_group_offset_x, bar_group_offset_y)) 79 | .set("class", "bar"); 80 | 81 | let (x_attr, y_attr, width_attr, height_attr) = match self.orientation { 82 | Orientation::Horizontal => ("x", "y", "width", "height"), 83 | Orientation::Vertical => ("y", "x", "height", "width"), 84 | }; 85 | 86 | for block in self.blocks.iter() { 87 | let block_rect = Rectangle::new() 88 | .set(x_attr, block.0) 89 | .set(y_attr, 0) 90 | .set(width_attr, block.1 - block.0) 91 | .set(height_attr, self.bar_width) 92 | .set("shape-rendering", "crispEdges") 93 | .set("fill", block.3.as_ref()); 94 | 95 | group.append(block_rect); 96 | 97 | // Display labels if needed. 98 | if self.label_visible { 99 | let (label_x_attr_value, text_anchor) = match self.label_position { 100 | BarLabelPosition::StartOutside if self.orientation == Orientation::Horizontal => (block.0 - 12_f32, "end"), 101 | BarLabelPosition::StartOutside if self.orientation == Orientation::Vertical => (block.1 + 16_f32, "middle"), 102 | BarLabelPosition::StartInside if self.orientation == Orientation::Horizontal => (block.0 + 12_f32, "start"), 103 | BarLabelPosition::StartInside if self.orientation == Orientation::Vertical => (block.1 - 16_f32, "middle"), 104 | BarLabelPosition::Center if self.orientation == Orientation::Horizontal => (block.0 + (block.1 - block.0) / 2_f32, "middle"), 105 | BarLabelPosition::Center if self.orientation == Orientation::Vertical => (block.0 + (block.1 - block.0) / 2_f32, "middle"), 106 | BarLabelPosition::EndInside if self.orientation == Orientation::Horizontal => (block.1 - 12_f32, "end"), 107 | BarLabelPosition::EndInside if self.orientation == Orientation::Vertical => (block.0 + 16_f32, "middle"), 108 | BarLabelPosition::EndOutside if self.orientation == Orientation::Horizontal => (block.1 + 12_f32, "start"), 109 | BarLabelPosition::EndOutside if self.orientation == Orientation::Vertical => (block.0 - 16_f32, "middle"), 110 | _ => (0_f32, "middle"), // this is needed to get rid of compiler warning of exhaustively covering match pattern. 111 | }; 112 | 113 | let label_text = match &self.rounding_precision { 114 | None => block.2.to_string(), 115 | Some(nr_of_digits) => format!("{:.1$}", block.2.to_string().parse::().unwrap(), nr_of_digits) 116 | }; 117 | 118 | let label = Text::new() 119 | .set(x_attr, label_x_attr_value) 120 | .set(y_attr, self.bar_width / 2_f32) 121 | .set("text-anchor", text_anchor) 122 | .set("dy", ".35em") 123 | .set("font-family", "sans-serif") 124 | .set("fill", "#333") 125 | .set("font-size", "14px") 126 | .add(TextNode::new(label_text)); 127 | 128 | group.append(label); 129 | } 130 | } 131 | 132 | // svg::save("bar-vert.svg", &group).unwrap(); 133 | 134 | Ok(group) 135 | } 136 | } -------------------------------------------------------------------------------- /src/components/legend.rs: -------------------------------------------------------------------------------- 1 | use svg::node::element::{Group, Circle, Rectangle, Line}; 2 | use svg::Node; 3 | use svg::node::Text as TextNode; 4 | use svg::node::element::Text; 5 | use crate::MarkerType; 6 | 7 | /// Represents the possible marker types that a legend entry can have. 8 | pub enum LegendMarkerType { 9 | Circle, 10 | Square, 11 | X, 12 | Line, 13 | } 14 | 15 | impl From for LegendMarkerType { 16 | fn from(marker_type: MarkerType) -> Self { 17 | match marker_type { 18 | MarkerType::Circle => LegendMarkerType::Circle, 19 | MarkerType::Square => LegendMarkerType::Square, 20 | MarkerType::X => LegendMarkerType::X, 21 | } 22 | } 23 | } 24 | 25 | /// Represents an entry in the chart's legend. 26 | pub struct LegendEntry { 27 | marker_type: LegendMarkerType, 28 | marker_size: usize, 29 | marker_to_label_gap: usize, 30 | color: String, 31 | stroke_type: String, 32 | label: String, 33 | } 34 | 35 | impl LegendEntry { 36 | /// Create a new legend entry. 37 | pub fn new(marker_type: LegendMarkerType, color: String, stroke_type: String, label: String) -> Self { 38 | Self { 39 | marker_type, 40 | marker_size: 7, 41 | marker_to_label_gap: 6, 42 | color, 43 | stroke_type, 44 | label, 45 | } 46 | } 47 | 48 | /// Return legend entry width to compute the placement of legend entries on the chart. 49 | pub fn get_width(&self) -> usize { 50 | // TODO ideally, compute the length of the given `label` in the given font and size 51 | let avg_letter_width = 7; // this is for the default sans-serif 12px font + some buffer 52 | avg_letter_width * self.label.len() + self.marker_size * 2 + self.marker_to_label_gap 53 | } 54 | 55 | pub fn to_svg(&self) -> Result { 56 | let mut group = Group::new() 57 | .set("class", "legend-entry"); 58 | 59 | match self.marker_type { 60 | LegendMarkerType::Circle => group.append( 61 | Circle::new() 62 | .set("cx", self.marker_size) 63 | .set("cy", self.marker_size) 64 | .set("r", self.marker_size) 65 | .set("fill", self.color.as_ref()) 66 | .set("stroke", "none") 67 | ), 68 | LegendMarkerType::Square => group.append( 69 | Rectangle::new() 70 | .set("x", 0) 71 | .set("y", 0) 72 | .set("width", 2 * self.marker_size) 73 | .set("height", 2 * self.marker_size) 74 | .set("fill", self.color.as_ref()) 75 | .set("stroke", "none") 76 | ), 77 | LegendMarkerType::X => { 78 | group.append( 79 | Line::new() 80 | .set("x1", 0) 81 | .set("y1", 0) 82 | .set("x2", 2 * self.marker_size) 83 | .set("y2", 2 * self.marker_size) 84 | .set("stroke", self.color.as_ref()) 85 | .set("stroke-width", "2px") 86 | ); 87 | group.append( 88 | Line::new() 89 | .set("x1", 2 * self.marker_size) 90 | .set("y1", 0) 91 | .set("x2", 0) 92 | .set("y2", 2 * self.marker_size) 93 | .set("stroke", self.color.as_ref()) 94 | .set("stroke-width", "2px") 95 | ) 96 | }, 97 | LegendMarkerType::Line => group.append( 98 | Line::new() 99 | .set("x1", 0) 100 | .set("y1", self.marker_size) 101 | .set("x2", 2 * self.marker_size) 102 | .set("y2", self.marker_size) 103 | .set("stroke", self.color.as_ref()) 104 | .set("stroke-width", "2px") 105 | .set("stroke-dasharray", self.stroke_type.as_ref()) 106 | ), 107 | } 108 | 109 | group.append( 110 | Text::new() 111 | .set("x", 2 * self.marker_size + self.marker_to_label_gap) 112 | .set("y", self.marker_size) 113 | .set("dy", ".35em") 114 | .set("font-family", "sans-serif") 115 | .set("fill", "#777") 116 | .set("font-size", "12px") 117 | .add(TextNode::new(self.label.clone())) 118 | ); 119 | 120 | Ok(group) 121 | } 122 | } -------------------------------------------------------------------------------- /src/components/line.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::Display; 2 | use svg::node::element::{Group, Path}; 3 | use svg::node::element::path::Data; 4 | use svg::node::Node; 5 | use crate::components::DatumRepresentation; 6 | use crate::components::scatter::ScatterPoint; 7 | 8 | /// Represents a point in a scatter plot. 9 | #[derive(Debug)] 10 | pub struct LineSeries { 11 | points: Vec>, 12 | color: String, 13 | } 14 | 15 | impl LineSeries { 16 | pub fn new( 17 | points: Vec>, 18 | color: String 19 | ) -> Self { 20 | Self { 21 | points, 22 | color, 23 | } 24 | } 25 | } 26 | 27 | impl DatumRepresentation for LineSeries { 28 | 29 | fn to_svg(&self) -> Result { 30 | let mut group = Group::new() 31 | .set("class", "line"); 32 | 33 | let mut data = Data::new(); 34 | 35 | for (i, point) in self.points.iter().enumerate() { 36 | if i == 0 { 37 | data = data.move_to((point.get_x(), point.get_y())); 38 | } else { 39 | data = data.line_to((point.get_x(), point.get_y())); 40 | } 41 | } 42 | 43 | let line = Path::new() 44 | .set("fill", "none") 45 | .set("stroke", self.color.as_ref()) 46 | .set("stroke-width", 2) 47 | .set("d", data); 48 | 49 | group.append(line); 50 | 51 | for point in self.points.iter() { 52 | group.append(point.to_svg()?); 53 | } 54 | 55 | Ok(group) 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/components/mod.rs: -------------------------------------------------------------------------------- 1 | use svg::node::element::Group; 2 | 3 | pub(crate) mod bar; 4 | pub(crate) mod axis; 5 | pub(crate) mod scatter; 6 | pub(crate) mod line; 7 | pub(crate) mod legend; 8 | pub(crate) mod area; 9 | 10 | /// A trait that defines behavior of chart components. 11 | pub trait DatumRepresentation { 12 | fn to_svg(&self) -> Result; 13 | } -------------------------------------------------------------------------------- /src/components/scatter.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::Display; 2 | use svg::node::element::{Group, Circle, Rectangle, Line}; 3 | use svg::node::Node; 4 | use svg::node::Text as TextNode; 5 | use svg::node::element::Text; 6 | use crate::components::DatumRepresentation; 7 | 8 | /// Define the possible types of points in a scatter plot. 9 | #[derive(Debug, Copy, Clone)] 10 | pub enum MarkerType { 11 | Circle, 12 | Square, 13 | X, 14 | } 15 | 16 | /// Define the possible locations of a point's label. 17 | #[derive(Debug, Copy, Clone)] 18 | pub enum PointLabelPosition { 19 | N, 20 | NE, 21 | E, 22 | SE, 23 | S, 24 | SW, 25 | W, 26 | NW 27 | } 28 | 29 | /// Represents a point in a scatter plot. 30 | #[derive(Debug)] 31 | pub struct ScatterPoint { 32 | label_position: PointLabelPosition, 33 | label_visible: bool, 34 | point_visible: bool, 35 | marker_type: MarkerType, 36 | marker_size: usize, 37 | x: f32, 38 | y: f32, 39 | x_label: T, 40 | y_label: U, 41 | color: String, 42 | } 43 | 44 | impl ScatterPoint { 45 | pub fn new( 46 | x: f32, 47 | y: f32, 48 | marker_type: MarkerType, 49 | marker_size: usize, 50 | x_label: T, 51 | y_label: U, 52 | label_position: PointLabelPosition, 53 | label_visible: bool, 54 | point_visible: bool, 55 | color: String 56 | ) -> Self { 57 | Self { 58 | label_position, 59 | label_visible, 60 | point_visible, 61 | marker_type, 62 | marker_size, 63 | x, 64 | y, 65 | x_label, 66 | y_label, 67 | color, 68 | } 69 | } 70 | 71 | /// Return the x coordinate of the point. 72 | pub fn get_x(&self) -> f32 { 73 | self.x 74 | } 75 | 76 | /// Return the y coordinate of the point. 77 | pub fn get_y(&self) -> f32 { 78 | self.y 79 | } 80 | } 81 | 82 | impl DatumRepresentation for ScatterPoint { 83 | 84 | fn to_svg(&self) -> Result { 85 | let mut group = Group::new() 86 | .set("transform", format!("translate({},{})", self.x, self.y)) 87 | .set("class", "scatter-point"); 88 | 89 | match self.marker_type { 90 | MarkerType::Circle if self.point_visible => { 91 | group.append( 92 | Circle::new() 93 | .set("cx", 0) 94 | .set("cy", 0) 95 | .set("r", self.marker_size) 96 | .set("fill", self.color.as_ref()) 97 | ); 98 | }, 99 | MarkerType::Square if self.point_visible => { 100 | group.append( 101 | Rectangle::new() 102 | .set("x", -(self.marker_size as i32)) 103 | .set("y", -(self.marker_size as i32)) 104 | .set("width", 2 * self.marker_size) 105 | .set("height", 2 * self.marker_size) 106 | .set("fill", self.color.as_ref()) 107 | ); 108 | }, 109 | MarkerType::X if self.point_visible => { 110 | group.append( 111 | Group::new() 112 | .add( 113 | Line::new() 114 | .set("x1", -(self.marker_size as i32)) 115 | .set("y1", -(self.marker_size as i32)) 116 | .set("x2", self.marker_size) 117 | .set("y2", self.marker_size) 118 | .set("stroke-width", "2px") 119 | .set("stroke", self.color.as_ref()) 120 | ) 121 | .add( 122 | Line::new() 123 | .set("x1", self.marker_size) 124 | .set("y1", -(self.marker_size as i32)) 125 | .set("x2", -(self.marker_size as i32)) 126 | .set("y2", self.marker_size) 127 | .set("stroke-width", "2px") 128 | .set("stroke", self.color.as_ref()) 129 | ) 130 | ); 131 | }, 132 | _ => {}, 133 | }; 134 | 135 | if self.label_visible { 136 | let mut point_label = Text::new() 137 | .set("dy", ".35em") 138 | .set("font-family", "sans-serif") 139 | .set("fill", "#333") 140 | .set("font-size", "14px") 141 | .add(TextNode::new(format!("({}, {})", self.x_label, self.y_label))); 142 | 143 | let label_offset = self.marker_size as isize; 144 | match self.label_position { 145 | PointLabelPosition::N => { 146 | point_label.assign("x", 0); 147 | point_label.assign("y", -label_offset - 12); 148 | point_label.assign("text-anchor", "middle"); 149 | }, 150 | PointLabelPosition::NE => { 151 | point_label.assign("x", label_offset + 4); 152 | point_label.assign("y", -label_offset - 8); 153 | point_label.assign("text-anchor", "start"); 154 | }, 155 | PointLabelPosition::E => { 156 | point_label.assign("x", label_offset + 8); 157 | point_label.assign("y", 0); 158 | point_label.assign("text-anchor", "start"); 159 | }, 160 | PointLabelPosition::SE => { 161 | point_label.assign("x", label_offset + 4); 162 | point_label.assign("y", label_offset + 8); 163 | point_label.assign("text-anchor", "start"); 164 | }, 165 | PointLabelPosition::S => { 166 | point_label.assign("x", 0); 167 | point_label.assign("y", label_offset + 12); 168 | point_label.assign("text-anchor", "middle"); 169 | }, 170 | PointLabelPosition::SW => { 171 | point_label.assign("x", -label_offset - 4); 172 | point_label.assign("y", label_offset + 8); 173 | point_label.assign("text-anchor", "end"); 174 | }, 175 | PointLabelPosition::W => { 176 | point_label.assign("x", -label_offset - 8); 177 | point_label.assign("y", 0); 178 | point_label.assign("text-anchor", "end"); 179 | }, 180 | PointLabelPosition::NW => { 181 | point_label.assign("x", -label_offset - 4); 182 | point_label.assign("y", -label_offset - 8); 183 | point_label.assign("text-anchor", "end"); 184 | }, 185 | } 186 | group.append(point_label); 187 | } 188 | 189 | Ok(group) 190 | } 191 | } -------------------------------------------------------------------------------- /src/legend.rs: -------------------------------------------------------------------------------- 1 | use svg::node::element::Group; 2 | use svg::Node; 3 | use crate::components::legend::LegendEntry; 4 | 5 | pub(crate) struct Legend { 6 | width: usize, 7 | entries: Vec, 8 | } 9 | 10 | impl Legend { 11 | /// Create a new legend instance. 12 | pub fn new(entries: Vec, width: usize) -> Self { 13 | Self { 14 | entries, 15 | width, 16 | } 17 | } 18 | 19 | pub fn to_svg(&self) -> Result { 20 | let mut group = Group::new().set("class", "g-legend"); 21 | let max_entry_length = match self.entries.iter().map(|entry| entry.get_width()).max() { 22 | None => return Ok(group), 23 | Some(len) => len, 24 | }; 25 | let gap_between_legend_entries = 10; 26 | let legend_row_height = 20; 27 | let mut current_row_offset = 0; 28 | let mut acc_row_width = 0; 29 | 30 | for entry in self.entries.iter() { 31 | if acc_row_width + max_entry_length > self.width && acc_row_width > 0 { 32 | acc_row_width = 0; 33 | current_row_offset += 1; 34 | } 35 | 36 | let mut entry_group = entry.to_svg()?; 37 | entry_group.assign("transform", format!("translate({},{})", acc_row_width, current_row_offset * legend_row_height)); 38 | group.append(entry_group); 39 | 40 | acc_row_width += max_entry_length + gap_between_legend_entries; 41 | } 42 | 43 | Ok(group) 44 | } 45 | } -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! # rustplotlib 2 | //! 3 | //! A visualization library for Rust inspired by D3.js. 4 | //! 5 | //! ## Features 6 | //! 7 | //! This is a WIP, but so far the library supports the following chart types: 8 | //! 9 | //! 1. Bar Chart (horizontal and vertical) 10 | //! 2. Stacked Bar Chart (horizontal and vertical) 11 | //! 12 | //! ## Abstraction Layers 13 | //! 14 | //! There are several abstractions at the foundation of this visualization library: 15 | //! 16 | //! Page 17 | //! └- Grid 18 | //! └- Chart 19 | //! ├- Axes 20 | //! └- View 21 | //! └- Dataset 22 | //! 23 | //! TODO represent the structure visually 24 | 25 | mod chart; 26 | // mod view; 27 | mod scales; 28 | mod views; 29 | mod components; 30 | mod colors; 31 | mod axis; 32 | mod legend; 33 | 34 | pub use crate::chart::Chart; 35 | pub use crate::scales::band::ScaleBand; 36 | pub use crate::scales::linear::ScaleLinear; 37 | pub use crate::scales::Scale; 38 | pub use crate::views::vertical_bar::VerticalBarView; 39 | pub use crate::views::horizontal_bar::HorizontalBarView; 40 | pub use crate::views::scatter::ScatterView; 41 | pub use crate::views::line::LineSeriesView; 42 | pub use crate::views::area::AreaSeriesView; 43 | pub use crate::views::datum::{BarDatum, PointDatum}; 44 | pub use crate::axis::{Axis, AxisPosition}; 45 | pub use crate::components::bar::BarLabelPosition; 46 | pub use crate::components::line::LineSeries; 47 | pub use crate::components::scatter::{MarkerType, PointLabelPosition}; 48 | pub use crate::colors::Color; 49 | 50 | #[cfg(test)] 51 | mod tests { 52 | #[test] 53 | fn it_works() { 54 | assert_eq!(2 + 2, 4); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/scales/band.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | use std::collections::HashSet; 3 | use crate::scales::{Scale, ScaleType}; 4 | 5 | /// The scale to represent categorical data. 6 | #[derive(Debug)] 7 | pub struct ScaleBand { 8 | /// The domain limits of the dataset that the scale is going to represent. 9 | domain: Vec, 10 | /// The range limits of the drawable area on the chart. 11 | range: Vec, 12 | /// The offsets of each entry from domain. 13 | offsets: Vec, 14 | /// The hash map that maps domain keys with corresponding offset entries. 15 | index: HashMap, 16 | /// The distance between the start of the first bar and the start of the next one. 17 | step: f32, 18 | /// The width of a bar. 19 | bandwidth: f32, 20 | /// The distance between bars as a percentage of the step (between 0 and 1). 21 | padding_inner: f32, 22 | /// The distance from the beginning/end of the chart to the first/last bar (between 0 and 1). 23 | padding_outer: f32, 24 | /// The distribution of the outer padding between the first and last bars (between 0 and 1). 25 | /// An align value of 0.5 will distribute space evenly, while 0 will move all outer space to 26 | /// the right part, leaving no space on the left. 27 | align: f32, 28 | /// The start value of the range. 29 | r0: f32, 30 | /// The end value of the range. 31 | r1: f32, 32 | } 33 | 34 | impl ScaleBand { 35 | /// Create a new band scale with default values. 36 | pub fn new() -> Self { 37 | Self { 38 | domain: Vec::new(), 39 | range: vec![0, 1], 40 | offsets: Vec::new(), 41 | index: HashMap::new(), 42 | step: 1f32, 43 | bandwidth: 1f32, 44 | padding_inner: 0.1, 45 | padding_outer: 0.1, 46 | align: 0.5, 47 | r0: 0f32, 48 | r1: 0f32, 49 | } 50 | } 51 | 52 | /// Set the inner padding ratio. 53 | pub fn set_inner_padding(mut self, padding: f32) -> Self { 54 | self.padding_inner = padding; 55 | self.rescale(); 56 | self 57 | } 58 | 59 | /// Set the outer padding ratio. 60 | pub fn set_outer_padding(mut self, padding: f32) -> Self { 61 | self.padding_outer = padding; 62 | self.rescale(); 63 | self 64 | } 65 | 66 | /// Set the domain limits for the scale band. 67 | pub fn set_domain(mut self, range: Vec) -> Self { 68 | // Deduplicate the domain range and keep order of entries. 69 | let mut unique = Vec::new(); 70 | let mut set: HashSet = HashSet::new(); 71 | 72 | for el in range.into_iter() { 73 | let clone = el.clone(); 74 | if !set.contains(&clone) { 75 | set.insert(clone); 76 | unique.push(el); 77 | } 78 | } 79 | 80 | self.domain = unique; 81 | self.rescale(); 82 | self 83 | } 84 | 85 | /// Get the domain limits of the scale. 86 | pub fn domain(&self) -> &Vec { 87 | &self.domain 88 | } 89 | 90 | /// Set the range limits for the scale band. 91 | pub fn set_range(mut self, range: Vec) -> Self { 92 | self.range = range; 93 | self.rescale(); 94 | self 95 | } 96 | 97 | /// Get the range limits of the scale. 98 | pub fn range(&self) -> &Vec { 99 | &self.range 100 | } 101 | 102 | fn rescale(&mut self) { 103 | let n = self.domain.len(); 104 | let r0 = self.range[0]; 105 | let r1 = self.range[1]; 106 | let reverse = r1 < r0; 107 | let mut start = r0 as f32; 108 | let mut stop = r1 as f32; 109 | 110 | if reverse { 111 | self.range = vec![r1, r0]; 112 | start = r1 as f32; 113 | stop = r0 as f32; 114 | } 115 | 116 | let step_denominator = { 117 | let computed_step = n as f32 - self.padding_inner + self.padding_outer * 2f32; 118 | if computed_step > 1f32 { 119 | computed_step 120 | } else { 121 | 1f32 122 | } 123 | }; 124 | self.step = (stop - start) / step_denominator; 125 | 126 | // TODO implement rounding of step, start and bandwidth values if specified by user. 127 | 128 | start += (stop - start - self.step * (n as f32 - self.padding_inner)) * self.align; 129 | 130 | self.bandwidth = self.step * (1f32 - self.padding_inner); 131 | 132 | self.offsets.clear(); 133 | for i in 0..n { 134 | self.offsets.push(start + self.step * i as f32); 135 | } 136 | 137 | if reverse { 138 | self.offsets.reverse(); 139 | } 140 | 141 | self.index.clear(); 142 | let mut processed_domains = Vec::new(); 143 | for domain in self.domain.iter() { 144 | // Check for already existing keys to remove any duplicates in the domain vector. 145 | if !self.index.contains_key(domain) { 146 | self.index.insert(domain.clone(), processed_domains.len()); 147 | processed_domains.push(domain.clone()); 148 | } 149 | } 150 | // Re-assign domains with any duplicates removed. 151 | self.domain.clear(); 152 | self.domain = processed_domains; 153 | } 154 | } 155 | 156 | impl Scale for ScaleBand { 157 | /// Get the type of the scale. 158 | fn get_type(&self) -> ScaleType { 159 | ScaleType::Band 160 | } 161 | 162 | /// Get the range value for the given domain entry. 163 | fn scale(&self, domain: &String) -> f32 { 164 | self.offsets[*self.index.get(domain).unwrap()] 165 | } 166 | 167 | /// Get the bandwidth (if present). 168 | fn bandwidth(&self) -> Option { 169 | Some(self.bandwidth) 170 | } 171 | 172 | /// Get the start range value. 173 | fn range_start(&self) -> f32 { 174 | self.range[0] as f32 175 | } 176 | 177 | /// Get the end range value. 178 | fn range_end(&self) -> f32 { 179 | self.range[1] as f32 180 | } 181 | 182 | /// Get the list of ticks that represent the scale on a chart axis. 183 | fn get_ticks(&self) -> Vec { 184 | self.domain.clone() 185 | } 186 | } -------------------------------------------------------------------------------- /src/scales/linear.rs: -------------------------------------------------------------------------------- 1 | use std::cmp::{max, Ordering}; 2 | use crate::scales::{Scale, ScaleType}; 3 | 4 | /// The scale to represent categorical data. 5 | #[derive(Debug)] 6 | pub struct ScaleLinear { 7 | /// The domain limits of the dataset that the scale is going to represent. 8 | domain: Vec, 9 | /// The range limits of the drawable area on the chart. 10 | range: Vec, 11 | /// The amount of ticks to display. 12 | tick_count: usize, 13 | } 14 | 15 | impl ScaleLinear { 16 | /// Create a new linear scale with default values. 17 | pub fn new() -> Self { 18 | Self { 19 | domain: Vec::new(), 20 | range: vec![0, 1], 21 | tick_count: 10, 22 | } 23 | } 24 | 25 | /// Set the domain limits for the scale band. 26 | pub fn set_domain(mut self, range: Vec) -> Self { 27 | self.domain = range; 28 | self 29 | } 30 | 31 | /// Get the domain limits of the scale. 32 | pub fn domain(&self) -> &Vec { 33 | &self.domain 34 | } 35 | 36 | /// Set the range limits for the scale band. 37 | pub fn set_range(mut self, range: Vec) -> Self { 38 | self.range = range; 39 | self 40 | } 41 | 42 | /// Get the range limits of the scale. 43 | pub fn range(&self) -> &Vec { 44 | &self.range 45 | } 46 | 47 | /// Takes a value x in [a, b] and returns the corresponding value in [0, 1]. 48 | fn normalize(&self, a: f32, b: f32, x: f32) -> f32 { 49 | // If a == b then return 0.5 50 | if a == b { 51 | 0.5 52 | } else { 53 | let b = b - a; 54 | (x - a as f32) / b as f32 55 | } 56 | } 57 | 58 | /// Takes a value t in [0, 1] and returns the corresponding range in [a, b]. 59 | fn interpolate(&self, a: f32, b: f32, t: f32) -> f32 { 60 | (b - a) * t + a 61 | } 62 | 63 | /// Compute the distance between the ticks. 64 | fn tick_step(&self, start: f32, stop: f32) -> f32 { 65 | let e10 = 50_f32.sqrt(); 66 | let e5 = 10_f32.sqrt(); 67 | let e2 = 2_f32.sqrt(); 68 | let step = (stop - start) / max(0, self.tick_count) as f32; 69 | let power = (step.ln() / 10_f32.ln()).trunc() as i32; 70 | let error = step / 10_f32.powi(power); 71 | let dynamic = if error >= e10 { 72 | 10 73 | } else if error >= e5 { 74 | 5 75 | } else if error >= e2 { 76 | 2 77 | } else { 78 | 1 79 | }; 80 | 81 | let step = match power.cmp(&0) { 82 | Ordering::Less => -10_f32.powi(-power) / dynamic as f32, 83 | _ => dynamic as f32 * 10_f32.powi(power), 84 | }; 85 | 86 | step 87 | } 88 | } 89 | 90 | impl Scale for ScaleLinear { 91 | /// Get the type of the scale. 92 | fn get_type(&self) -> ScaleType { 93 | ScaleType::Linear 94 | } 95 | 96 | /// Get the range value for the given domain entry. 97 | fn scale(&self, domain: &f32) -> f32 { 98 | let a = self.domain[0]; 99 | let b = self.domain[1]; 100 | let normalized = self.normalize(a, b, *domain); 101 | let a = self.range[0] as f32; 102 | let b = self.range[1] as f32; 103 | let scaled = self.interpolate(a, b, normalized); 104 | 105 | scaled 106 | } 107 | 108 | /// Get the bandwidth (if present). 109 | fn bandwidth(&self) -> Option { 110 | Some(0_f32) 111 | } 112 | 113 | /// Get the start range value. 114 | fn range_start(&self) -> f32 { 115 | self.range[0] as f32 116 | } 117 | 118 | /// Get the end range value. 119 | fn range_end(&self) -> f32 { 120 | self.range[1] as f32 121 | } 122 | 123 | /// Get the list of ticks that represent the scale on a chart axis. 124 | fn get_ticks(&self) -> Vec { 125 | let mut ticks = Vec::new(); 126 | 127 | if self.domain[0] == self.domain[1] && self.tick_count > 0 { 128 | ticks.push(self.domain[0] as f32); 129 | return ticks; 130 | } 131 | 132 | let step = self.tick_step(self.domain[0] as f32, self.domain[1] as f32); 133 | let mut i = 0; 134 | if step > 0_f32 { 135 | let start = (self.domain[0] as f32 / step).ceil(); 136 | let stop = (self.domain[1] as f32 / step).floor(); 137 | let nr_of_ticks = (stop - start + 1_f32).ceil() as i32; 138 | while i < nr_of_ticks { 139 | ticks.push((start + i as f32) * step); 140 | i += 1; 141 | } 142 | } else { 143 | let start = (self.domain[0] as f32 * step).floor(); 144 | let stop = (self.domain[1] as f32 * step).ceil(); 145 | let nr_of_ticks = (start - stop + 1_f32).ceil() as i32; 146 | while i < nr_of_ticks { 147 | ticks.push((start - i as f32) / step); 148 | i += 1; 149 | } 150 | } 151 | 152 | ticks 153 | } 154 | } -------------------------------------------------------------------------------- /src/scales/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod band; 2 | pub mod linear; 3 | 4 | #[derive(PartialEq)] 5 | pub enum ScaleType { 6 | Band, 7 | Ordinal, 8 | Linear, 9 | } 10 | 11 | /// The Scale trait defines common operations on all scales. 12 | pub trait Scale { 13 | /// Get the type of the scale. 14 | fn get_type(&self) -> ScaleType; 15 | 16 | /// Get the range value for the given domain entry. 17 | fn scale(&self, domain: &T) -> f32; 18 | 19 | /// Get the bandwidth (if present). 20 | fn bandwidth(&self) -> Option; 21 | 22 | /// Get the start range value. 23 | fn range_start(&self) -> f32; 24 | 25 | /// Get the end range value. 26 | fn range_end(&self) -> f32; 27 | 28 | /// Check whether the range is in reversed order, meaning the start is greater than the end. 29 | fn is_range_reversed(&self) -> bool { 30 | self.range_start() > self.range_end() 31 | } 32 | 33 | /// Get the list of ticks that represent the scale on a chart axis. 34 | fn get_ticks(&self) -> Vec; 35 | } -------------------------------------------------------------------------------- /src/views/area.rs: -------------------------------------------------------------------------------- 1 | use svg::node::Node; 2 | use svg::node::element::Group; 3 | use crate::components::scatter::{ScatterPoint, MarkerType, PointLabelPosition}; 4 | use crate::colors::Color; 5 | use crate::Scale; 6 | use crate::views::datum::PointDatum; 7 | use crate::views::View; 8 | use crate::components::DatumRepresentation; 9 | use std::fmt::Display; 10 | use crate::components::legend::{LegendEntry, LegendMarkerType}; 11 | use crate::components::area::AreaSeries; 12 | 13 | /// A View that represents data as a scatter plot. 14 | pub struct AreaSeriesView<'a, T: Display + Clone, U: Display + Clone> { 15 | labels_visible: bool, 16 | label_position: PointLabelPosition, 17 | marker_type: MarkerType, 18 | entries: Vec>, 19 | colors: Vec, 20 | x_scale: Option<&'a dyn Scale>, 21 | y_scale: Option<&'a dyn Scale>, 22 | custom_data_label: String, 23 | } 24 | 25 | impl<'a, T: Display + Clone, U: Display + Clone> AreaSeriesView<'a, T, U> { 26 | /// Create a new empty instance of the view. 27 | pub fn new() -> Self { 28 | Self { 29 | labels_visible: true, 30 | label_position: PointLabelPosition::NW, 31 | marker_type: MarkerType::Circle, 32 | entries: Vec::new(), 33 | colors: Color::color_scheme_10(), 34 | x_scale: None, 35 | y_scale: None, 36 | custom_data_label: String::new(), 37 | } 38 | } 39 | 40 | /// Set the scale for the X dimension. 41 | pub fn set_x_scale(mut self, scale: &'a impl Scale) -> Self { 42 | self.x_scale = Some(scale); 43 | self 44 | } 45 | 46 | /// Set the scale for the Y dimension. 47 | pub fn set_y_scale(mut self, scale: &'a impl Scale) -> Self { 48 | self.y_scale = Some(scale); 49 | self 50 | } 51 | 52 | /// Set the positioning of the labels. 53 | pub fn set_label_position(mut self, label_position: PointLabelPosition) -> Self { 54 | self.label_position = label_position; 55 | self 56 | } 57 | 58 | /// Set the keys in case of a stacked bar chart. 59 | pub fn set_marker_type(mut self, marker_type: MarkerType) -> Self { 60 | self.marker_type = marker_type; 61 | self 62 | } 63 | 64 | /// Set the color palette of the view. 65 | pub fn set_colors(mut self, colors: Vec) -> Self { 66 | self.colors = colors; 67 | self 68 | } 69 | 70 | /// Set labels visibility. 71 | pub fn set_label_visibility(mut self, label_visibility: bool) -> Self { 72 | self.labels_visible = label_visibility; 73 | self 74 | } 75 | 76 | /// Set custom label for the dataset. 77 | /// This will work when the dataset represents only a single 78 | /// type of data (i.e. there are no different "keys" by which to 79 | /// differentiate data), otherwise, this will have no effect. 80 | pub fn set_custom_data_label(mut self, label: String) -> Self { 81 | self.custom_data_label = label; 82 | self 83 | } 84 | 85 | /// Load and process a dataset of BarDatum points. 86 | pub fn load_data(mut self, data: &Vec>) -> Result { 87 | match self.x_scale { 88 | Some(_) => {}, 89 | _ => return Err("Please provide a scale for the X dimension before loading data".to_string()), 90 | } 91 | match self.y_scale { 92 | Some(_) => {}, 93 | _ => return Err("Please provide a scale for the Y dimension before loading data".to_string()), 94 | } 95 | 96 | // Compute corresponding offsets to apply in case there is a non-zero bandwidth. 97 | let y_bandwidth_offset = { 98 | if self.y_scale.unwrap().is_range_reversed() { 99 | -self.y_scale.unwrap().bandwidth().unwrap() / 2_f32 100 | } else { 101 | self.y_scale.unwrap().bandwidth().unwrap() / 2_f32 102 | } 103 | }; 104 | let x_bandwidth_offset = { 105 | if self.x_scale.unwrap().is_range_reversed() { 106 | -self.x_scale.unwrap().bandwidth().unwrap() / 2_f32 107 | } else { 108 | self.x_scale.unwrap().bandwidth().unwrap() / 2_f32 109 | } 110 | }; 111 | 112 | let mut points = data.iter().map(|datum| { 113 | let scaled_x = self.x_scale.unwrap().scale(&datum.get_x()); 114 | let scaled_y = self.y_scale.unwrap().scale(&datum.get_y()); 115 | ScatterPoint::new(scaled_x + x_bandwidth_offset, scaled_y + y_bandwidth_offset, self.marker_type, 5, datum.get_x(), datum.get_y(), self.label_position, self.labels_visible, true, self.colors[0].as_hex()) 116 | }).collect::>>(); 117 | 118 | let y_origin = { 119 | if self.y_scale.unwrap().is_range_reversed() { 120 | self.y_scale.unwrap().range_start() 121 | } else { 122 | self.y_scale.unwrap().range_end() 123 | } 124 | }; 125 | let first = data.first().unwrap(); 126 | let last = data.last().unwrap(); 127 | points.push(ScatterPoint::new(self.x_scale.unwrap().scale(&last.get_x()) + x_bandwidth_offset, y_origin, self.marker_type, 5, data[0].get_x(), data[0].get_y(), self.label_position, false, false, "#fff".to_string())); 128 | points.push(ScatterPoint::new(self.x_scale.unwrap().scale(&first.get_x()) + x_bandwidth_offset, y_origin, self.marker_type, 5, data[0].get_x(), data[0].get_y(), self.label_position, false, false, "#fff".to_string())); 129 | 130 | self.entries.push(AreaSeries::new(points, self.colors[0].as_hex())); 131 | 132 | Ok(self) 133 | } 134 | } 135 | 136 | impl<'a, T: Display + Clone, U: Display + Clone> View<'a> for AreaSeriesView<'a, T, U> { 137 | /// Generate the SVG representation of the view. 138 | fn to_svg(&self) -> Result { 139 | let mut group = Group::new(); 140 | 141 | for entry in self.entries.iter() { 142 | let child_svg = entry.to_svg()?; 143 | group.append(child_svg); 144 | } 145 | 146 | Ok(group) 147 | } 148 | 149 | /// Return the legend entries that this view represents. 150 | fn get_legend_entries(&self) -> Vec { 151 | let mut entries = Vec::new(); 152 | 153 | // Area series currently does not support multiple keys per dataset, 154 | // hence when displaying a legend, it will display the custom data label 155 | // as the legend label. 156 | entries.push(LegendEntry::new(LegendMarkerType::Square, self.colors[0].as_hex(), String::from("none"), self.custom_data_label.clone())); 157 | 158 | entries 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /src/views/datum.rs: -------------------------------------------------------------------------------- 1 | /// A trait that defines interaction with a data point used in a bar chart. 2 | /// This provides greater flexibility in using different data sources as one 3 | /// can simply implement this trait and be able to use that data in a bar chart. 4 | pub trait BarDatum { 5 | /// Return the category of the datum. 6 | fn get_category(&self) -> String; 7 | 8 | /// Return the value of the datum. 9 | fn get_value(&self) -> f32; 10 | 11 | /// Return the key of the datum. This is optional in a simple bar chart 12 | /// (just return an empty string), but is required in a stacked bar chart 13 | /// as the stacked entries are differentiated by the key. 14 | fn get_key(&self) -> String; 15 | } 16 | 17 | /// A trait that defines interaction with a data point used in a scatter/line plots. 18 | pub trait PointDatum { 19 | /// Return the X value. 20 | fn get_x(&self) -> T; 21 | 22 | /// Return the Y value. 23 | fn get_y(&self) -> U; 24 | 25 | /// Return the key of the datum. This is optional in a scatter/line plot with 26 | /// only a single type of data (just return an empty string), but is required 27 | /// in a chart that represents multiple categories of points. 28 | fn get_key(&self) -> String; 29 | } 30 | 31 | impl BarDatum for (f32, &str) { 32 | fn get_category(&self) -> String { 33 | String::from(self.1) 34 | } 35 | 36 | fn get_value(&self) -> f32 { 37 | self.0 38 | } 39 | 40 | fn get_key(&self) -> String { 41 | String::new() 42 | } 43 | } 44 | 45 | impl BarDatum for (String, f32, String) { 46 | fn get_category(&self) -> String { 47 | String::from(&self.0) 48 | } 49 | 50 | fn get_value(&self) -> f32 { 51 | self.1 52 | } 53 | 54 | fn get_key(&self) -> String { 55 | String::from(&self.2) 56 | } 57 | } 58 | 59 | impl BarDatum for (&str, f32, &str) { 60 | fn get_category(&self) -> String { 61 | String::from(self.0) 62 | } 63 | 64 | fn get_value(&self) -> f32 { 65 | self.1 66 | } 67 | 68 | fn get_key(&self) -> String { 69 | String::from(self.2) 70 | } 71 | } 72 | 73 | impl BarDatum for (&str, isize, &str) { 74 | fn get_category(&self) -> String { 75 | String::from(self.0) 76 | } 77 | 78 | fn get_value(&self) -> f32 { 79 | self.1 as f32 80 | } 81 | 82 | fn get_key(&self) -> String { 83 | String::from(self.2) 84 | } 85 | } 86 | 87 | impl BarDatum for (String, f32) { 88 | fn get_category(&self) -> String { 89 | String::from(&self.0) 90 | } 91 | 92 | fn get_value(&self) -> f32 { 93 | self.1 94 | } 95 | 96 | fn get_key(&self) -> String { 97 | String::new() 98 | } 99 | } 100 | 101 | impl BarDatum for (&str, f32, String) { 102 | fn get_category(&self) -> String { 103 | String::from(self.0) 104 | } 105 | 106 | fn get_value(&self) -> f32 { 107 | self.1 108 | } 109 | 110 | fn get_key(&self) -> String { 111 | String::from(&self.2) 112 | } 113 | } 114 | 115 | impl BarDatum for (&str, f32) { 116 | fn get_category(&self) -> String { 117 | String::from(self.0) 118 | } 119 | 120 | fn get_value(&self) -> f32 { 121 | self.1 122 | } 123 | 124 | fn get_key(&self) -> String { 125 | String::new() 126 | } 127 | } 128 | 129 | impl BarDatum for (&str, i32, String) { 130 | fn get_category(&self) -> String { 131 | String::from(self.0) 132 | } 133 | 134 | fn get_value(&self) -> f32 { 135 | self.1 as f32 136 | } 137 | 138 | fn get_key(&self) -> String { 139 | String::from(&self.2) 140 | } 141 | } 142 | 143 | impl BarDatum for (&str, i32) { 144 | fn get_category(&self) -> String { 145 | String::from(self.0) 146 | } 147 | 148 | fn get_value(&self) -> f32 { 149 | self.1 as f32 150 | } 151 | 152 | fn get_key(&self) -> String { 153 | String::new() 154 | } 155 | } 156 | 157 | impl BarDatum for (&str, i32, &str) { 158 | fn get_category(&self) -> String { 159 | String::from(self.0) 160 | } 161 | 162 | fn get_value(&self) -> f32 { 163 | self.1 as f32 164 | } 165 | 166 | fn get_key(&self) -> String { 167 | String::from(self.2) 168 | } 169 | } 170 | 171 | impl PointDatum for (f32, f32) { 172 | fn get_x(&self) -> f32 { 173 | self.0 174 | } 175 | 176 | fn get_y(&self) -> f32 { 177 | self.1 178 | } 179 | 180 | fn get_key(&self) -> String { 181 | String::new() 182 | } 183 | } 184 | 185 | impl PointDatum for (isize, isize) { 186 | fn get_x(&self) -> f32 { 187 | self.0 as f32 188 | } 189 | 190 | fn get_y(&self) -> f32 { 191 | self.1 as f32 192 | } 193 | 194 | fn get_key(&self) -> String { 195 | String::new() 196 | } 197 | } 198 | 199 | impl PointDatum for (isize, isize, &str) { 200 | fn get_x(&self) -> f32 { 201 | self.0 as f32 202 | } 203 | 204 | fn get_y(&self) -> f32 { 205 | self.1 as f32 206 | } 207 | 208 | fn get_key(&self) -> String { 209 | String::from(self.2) 210 | } 211 | } 212 | 213 | impl PointDatum for (f32, f32, &str) { 214 | fn get_x(&self) -> f32 { 215 | self.0 216 | } 217 | 218 | fn get_y(&self) -> f32 { 219 | self.1 220 | } 221 | 222 | fn get_key(&self) -> String { 223 | String::from(self.2) 224 | } 225 | } 226 | 227 | impl PointDatum for (isize, isize, String) { 228 | fn get_x(&self) -> f32 { 229 | self.0 as f32 230 | } 231 | 232 | fn get_y(&self) -> f32 { 233 | self.1 as f32 234 | } 235 | 236 | fn get_key(&self) -> String { 237 | self.2.clone() 238 | } 239 | } 240 | 241 | impl PointDatum for (f32, f32, String) { 242 | fn get_x(&self) -> f32 { 243 | self.0 244 | } 245 | 246 | fn get_y(&self) -> f32 { 247 | self.1 248 | } 249 | 250 | fn get_key(&self) -> String { 251 | self.2.clone() 252 | } 253 | } 254 | 255 | impl PointDatum for (String, f32) { 256 | fn get_x(&self) -> String { 257 | self.0.clone() 258 | } 259 | 260 | fn get_y(&self) -> f32 { 261 | self.1 262 | } 263 | 264 | fn get_key(&self) -> String { 265 | String::new() 266 | } 267 | } 268 | 269 | impl PointDatum for (String, isize) { 270 | fn get_x(&self) -> String { 271 | self.0.clone() 272 | } 273 | 274 | fn get_y(&self) -> f32 { 275 | self.1 as f32 276 | } 277 | 278 | fn get_key(&self) -> String { 279 | String::new() 280 | } 281 | } 282 | -------------------------------------------------------------------------------- /src/views/horizontal_bar.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | use svg::node::Node; 3 | use svg::node::element::Group; 4 | use crate::components::bar::{Bar, BarBlock, BarLabelPosition}; 5 | use crate::colors::Color; 6 | use crate::{Scale, BarDatum}; 7 | use crate::scales::ScaleType; 8 | use crate::components::DatumRepresentation; 9 | use crate::views::View; 10 | use crate::chart::Orientation; 11 | use crate::components::legend::{LegendEntry, LegendMarkerType}; 12 | 13 | /// A View that represents data as horizontal bars. 14 | pub struct HorizontalBarView<'a> { 15 | label_position: BarLabelPosition, 16 | labels_visible: bool, 17 | rounding_precision: Option, 18 | entries: Vec, 19 | keys: Vec, 20 | colors: Vec, 21 | color_map: HashMap, 22 | x_scale: Option<&'a dyn Scale>, 23 | y_scale: Option<&'a dyn Scale>, 24 | custom_data_label: String, 25 | } 26 | 27 | impl<'a> HorizontalBarView<'a> { 28 | /// Create a new empty instance of the view. 29 | pub fn new() -> Self { 30 | Self { 31 | label_position: BarLabelPosition::EndOutside, 32 | labels_visible: true, 33 | rounding_precision: None, 34 | entries: Vec::new(), 35 | keys: Vec::new(), 36 | colors: Color::color_scheme_10(), 37 | color_map: HashMap::new(), 38 | x_scale: None, 39 | y_scale: None, 40 | custom_data_label: String::new(), 41 | } 42 | } 43 | 44 | /// Set the scale for the X dimension. 45 | pub fn set_x_scale(mut self, scale: &'a impl Scale) -> Self { 46 | self.x_scale = Some(scale); 47 | self 48 | } 49 | 50 | /// Set the scale for the Y dimension. 51 | pub fn set_y_scale(mut self, scale: &'a impl Scale) -> Self { 52 | self.y_scale = Some(scale); 53 | self 54 | } 55 | 56 | /// Set the keys in case of a stacked bar chart. 57 | pub fn set_keys(mut self, keys: Vec) -> Self { 58 | self.keys = keys; 59 | self 60 | } 61 | 62 | /// Set the positioning of the labels. 63 | pub fn set_label_position(mut self, label_position: BarLabelPosition) -> Self { 64 | self.label_position = label_position; 65 | self 66 | } 67 | 68 | /// Set the color palette of the view. 69 | pub fn set_colors(mut self, colors: Vec) -> Self { 70 | self.colors = colors; 71 | self 72 | } 73 | 74 | /// Set labels visibility. 75 | pub fn set_label_visibility(mut self, label_visibility: bool) -> Self { 76 | self.labels_visible = label_visibility; 77 | self 78 | } 79 | 80 | /// Set custom label for the dataset. 81 | /// This will work when the dataset represents only a single 82 | /// type of data (i.e. there are no different "keys" by which to 83 | /// differentiate data), otherwise, this will have no effect. 84 | pub fn set_custom_data_label(mut self, label: String) -> Self { 85 | self.custom_data_label = label; 86 | self 87 | } 88 | 89 | /// Set the precision to which value labels should be rounded. 90 | pub fn set_label_rounding_precision(mut self, nr_of_digits: usize) -> Self { 91 | self.rounding_precision = Some(nr_of_digits); 92 | self 93 | } 94 | 95 | /// Load and process a dataset of BarDatum points. 96 | pub fn load_data(mut self, data: &Vec) -> Result { 97 | match self.x_scale { 98 | Some(scale) if scale.get_type() == ScaleType::Linear => {}, 99 | _ => return Err("The X axis scale should be a Band scale.".to_string()), 100 | } 101 | match self.y_scale { 102 | Some(scale) if scale.get_type() == ScaleType::Band => {}, 103 | _ => return Err("The Y axis scale should be a Linear scale.".to_string()), 104 | } 105 | 106 | // If no keys were explicitly provided, extract the keys from the data. 107 | if self.keys.len() == 0 { 108 | self.keys = Self::extract_keys(&data); 109 | } 110 | 111 | // HashMap to group all data related to a category. This is needed when there 112 | // are many data entries under a single category as in a stacked bar chart. 113 | let mut categories: HashMap> = HashMap::new(); 114 | 115 | // Organize entries based on the order of the keys first, since displayed data 116 | // should keep the order defined in the `keys` attribute. 117 | for (i, key) in self.keys.iter_mut().enumerate() { 118 | // Map the key to the corresponding color. 119 | self.color_map.insert(key.clone(), self.colors[i % self.colors.len()].as_hex()); 120 | 121 | for entry in data.iter() { 122 | if entry.get_key() == *key { 123 | let entry_category = entry.get_category(); 124 | 125 | if !categories.contains_key(&entry_category) { 126 | categories.insert(entry.get_category(), Vec::new()); 127 | } 128 | if let Some(category_entries) = categories.get_mut(&entry_category) { 129 | category_entries.push((key, entry.get_value())); 130 | } 131 | } 132 | } 133 | } 134 | 135 | // Create a Bar entry for each category data that was grouped in the previous step. 136 | let mut bars = Vec::new(); 137 | let x_range_is_reversed = self.x_scale.unwrap().is_range_reversed(); 138 | 139 | for (category, key_value_pairs) in categories.iter_mut() { 140 | let mut value_acc = 0_f32; 141 | let mut bar_blocks = Vec::new(); 142 | let mut stacked_start = self.x_scale.unwrap().scale(&value_acc); 143 | let mut stacked_end = stacked_start; 144 | 145 | for (key, value) in key_value_pairs.iter() { 146 | value_acc += *value; 147 | 148 | if x_range_is_reversed { 149 | stacked_end = stacked_start; 150 | stacked_start = self.x_scale.unwrap().scale(&value_acc); 151 | } else { 152 | stacked_start = stacked_end; 153 | stacked_end = self.x_scale.unwrap().scale(&value_acc); 154 | } 155 | bar_blocks.push(BarBlock::new(stacked_start, stacked_end, *value, self.color_map.get(*key).unwrap().clone())); 156 | } 157 | 158 | let bar = Bar::new(bar_blocks, Orientation::Horizontal, category.to_string(), self.label_position, self.labels_visible, self.rounding_precision, self.y_scale.unwrap().bandwidth().unwrap(), self.y_scale.unwrap().scale(category)); 159 | bars.push(bar); 160 | } 161 | 162 | for bar in bars { 163 | self.add_bar(bar); 164 | } 165 | 166 | Ok(self) 167 | } 168 | 169 | /// Extract the list of keys to use when stacking and coloring the bars. 170 | fn extract_keys(data: &Vec) -> Vec { 171 | let mut keys = Vec::new(); 172 | let mut map = HashMap::new(); 173 | 174 | for datum in data.iter() { 175 | match map.insert(datum.get_key(), 0) { 176 | Some(_) => {}, 177 | None => keys.push(datum.get_key()), 178 | } 179 | } 180 | 181 | keys 182 | } 183 | 184 | /// Add a [Bar] entry to the dataset entries list. 185 | fn add_bar(&mut self, bar: Bar) { 186 | self.entries.push(bar); 187 | } 188 | } 189 | 190 | impl<'a> View<'a> for HorizontalBarView<'a> { 191 | /// Generate the SVG representation of the view. 192 | fn to_svg(&self) -> Result { 193 | let mut group = Group::new(); 194 | 195 | for entry in self.entries.iter() { 196 | let child_svg = entry.to_svg()?; 197 | group.append(child_svg); 198 | } 199 | 200 | Ok(group) 201 | } 202 | 203 | /// Return the legend entries that this view represents. 204 | fn get_legend_entries(&self) -> Vec { 205 | let mut entries = Vec::new(); 206 | 207 | // If there is a single key and it is an empty string (meaning 208 | // the dataset consists only of X and Y dimension values), return 209 | // the custom data label. 210 | if self.keys.len() == 1 && self.keys[0].len() == 0 { 211 | entries.push(LegendEntry::new(LegendMarkerType::Square, self.color_map.get(&self.keys[0]).unwrap().clone(), String::from("none"), self.custom_data_label.clone())); 212 | } else { 213 | for key in self.keys.iter() { 214 | entries.push(LegendEntry::new(LegendMarkerType::Square, self.color_map.get(key).unwrap().clone(), String::from("none"), key.clone())); 215 | } 216 | } 217 | 218 | entries 219 | } 220 | } 221 | -------------------------------------------------------------------------------- /src/views/line.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | use std::fmt::Display; 3 | use svg::node::Node; 4 | use svg::node::element::Group; 5 | use crate::components::scatter::{ScatterPoint, MarkerType, PointLabelPosition}; 6 | use crate::colors::Color; 7 | use crate::{Scale, LineSeries}; 8 | use crate::views::datum::PointDatum; 9 | use crate::views::View; 10 | use crate::components::DatumRepresentation; 11 | use crate::components::legend::{LegendEntry, LegendMarkerType}; 12 | 13 | /// A View that represents data as a scatter plot. 14 | pub struct LineSeriesView<'a, T: Display, U: Display> { 15 | labels_visible: bool, 16 | label_position: PointLabelPosition, 17 | marker_type: MarkerType, 18 | entries: Vec>, 19 | colors: Vec, 20 | keys: Vec, 21 | color_map: HashMap, 22 | x_scale: Option<&'a dyn Scale>, 23 | y_scale: Option<&'a dyn Scale>, 24 | custom_data_label: String, 25 | } 26 | 27 | impl<'a, T: Display, U: Display> LineSeriesView<'a, T, U> { 28 | /// Create a new empty instance of the view. 29 | pub fn new() -> Self { 30 | Self { 31 | labels_visible: true, 32 | label_position: PointLabelPosition::NW, 33 | marker_type: MarkerType::Circle, 34 | entries: Vec::new(), 35 | keys: Vec::new(), 36 | colors: Color::color_scheme_10(), 37 | color_map: HashMap::new(), 38 | x_scale: None, 39 | y_scale: None, 40 | custom_data_label: String::new(), 41 | } 42 | } 43 | 44 | /// Set the scale for the X dimension. 45 | pub fn set_x_scale(mut self, scale: &'a impl Scale) -> Self { 46 | self.x_scale = Some(scale); 47 | self 48 | } 49 | 50 | /// Set the scale for the Y dimension. 51 | pub fn set_y_scale(mut self, scale: &'a impl Scale) -> Self { 52 | self.y_scale = Some(scale); 53 | self 54 | } 55 | 56 | /// Set the keys in case of a stacked bar chart. 57 | pub fn set_keys(mut self, keys: Vec) -> Self { 58 | self.keys = keys; 59 | self 60 | } 61 | 62 | /// Set the positioning of the labels. 63 | pub fn set_label_position(mut self, label_position: PointLabelPosition) -> Self { 64 | self.label_position = label_position; 65 | self 66 | } 67 | 68 | /// Set the keys in case of a stacked bar chart. 69 | pub fn set_marker_type(mut self, marker_type: MarkerType) -> Self { 70 | self.marker_type = marker_type; 71 | self 72 | } 73 | 74 | /// Set the color palette of the view. 75 | pub fn set_colors(mut self, colors: Vec) -> Self { 76 | self.colors = colors; 77 | self 78 | } 79 | 80 | /// Set labels visibility. 81 | pub fn set_label_visibility(mut self, label_visibility: bool) -> Self { 82 | self.labels_visible = label_visibility; 83 | self 84 | } 85 | 86 | /// Set custom label for the dataset. 87 | /// This will work when the dataset represents only a single 88 | /// type of data (i.e. there are no different "keys" by which to 89 | /// differentiate data), otherwise, this will have no effect. 90 | pub fn set_custom_data_label(mut self, label: String) -> Self { 91 | self.custom_data_label = label; 92 | self 93 | } 94 | 95 | /// Load and process a dataset of BarDatum points. 96 | pub fn load_data(mut self, data: &Vec>) -> Result { 97 | match self.x_scale { 98 | Some(_) => {}, 99 | _ => return Err("Please provide a scale for the X dimension before loading data".to_string()), 100 | } 101 | match self.y_scale { 102 | Some(_) => {}, 103 | _ => return Err("Please provide a scale for the Y dimension before loading data".to_string()), 104 | } 105 | 106 | // If no keys were explicitly provided, extract the keys from the data. 107 | if self.keys.len() == 0 { 108 | self.keys = Self::extract_keys(&data); 109 | } 110 | 111 | // Organize entries based on the order of the keys first, since displayed data 112 | // should keep the order defined in the `keys` attribute. 113 | for (i, key) in self.keys.iter_mut().enumerate() { 114 | // Map the key to the corresponding color. 115 | self.color_map.insert(key.clone(), self.colors[i % self.colors.len()].as_hex()); 116 | } 117 | 118 | for key in self.keys.iter() { 119 | 120 | let points = data.iter().filter(|datum| &datum.get_key() == key).map(|datum| { 121 | let scaled_x = self.x_scale.unwrap().scale(&datum.get_x()); 122 | let scaled_y = self.y_scale.unwrap().scale(&datum.get_y()); 123 | let y_bandwidth_offset = { 124 | if self.y_scale.unwrap().is_range_reversed() { 125 | -self.y_scale.unwrap().bandwidth().unwrap() / 2_f32 126 | } else { 127 | self.y_scale.unwrap().bandwidth().unwrap() / 2_f32 128 | } 129 | }; 130 | let x_bandwidth_offset = { 131 | if self.x_scale.unwrap().is_range_reversed() { 132 | -self.x_scale.unwrap().bandwidth().unwrap() / 2_f32 133 | } else { 134 | self.x_scale.unwrap().bandwidth().unwrap() / 2_f32 135 | } 136 | }; 137 | ScatterPoint::new(scaled_x + x_bandwidth_offset, scaled_y + y_bandwidth_offset, self.marker_type, 5, datum.get_x(), datum.get_y(), self.label_position, self.labels_visible, true,self.color_map.get(&datum.get_key()).unwrap().clone()) 138 | }).collect::>>(); 139 | 140 | self.entries.push(LineSeries::new(points, self.color_map.get(key).unwrap().clone())); 141 | } 142 | 143 | Ok(self) 144 | } 145 | 146 | /// Extract the list of keys to use when stacking and coloring the bars. 147 | fn extract_keys(data: &Vec>) -> Vec { 148 | let mut keys = Vec::new(); 149 | let mut map = HashMap::new(); 150 | 151 | for datum in data.iter() { 152 | match map.insert(datum.get_key(), 0) { 153 | Some(_) => {}, 154 | None => keys.push(datum.get_key()), 155 | } 156 | } 157 | 158 | keys 159 | } 160 | 161 | } 162 | 163 | impl<'a, T: Display, U: Display> View<'a> for LineSeriesView<'a, T, U> { 164 | /// Generate the SVG representation of the view. 165 | fn to_svg(&self) -> Result { 166 | let mut group = Group::new(); 167 | 168 | for entry in self.entries.iter() { 169 | let child_svg = entry.to_svg()?; 170 | group.append(child_svg); 171 | } 172 | 173 | Ok(group) 174 | } 175 | 176 | /// Return the legend entries that this view represents. 177 | fn get_legend_entries(&self) -> Vec { 178 | let mut entries = Vec::new(); 179 | 180 | // If there is a single key and it is an empty string (meaning 181 | // the dataset consists only of X and Y dimension values), return 182 | // the custom data label. 183 | if self.keys.len() == 1 && self.keys[0].len() == 0 { 184 | entries.push(LegendEntry::new(LegendMarkerType::Line, self.color_map.get(&self.keys[0]).unwrap().clone(), String::from("none"), self.custom_data_label.clone())); 185 | } else { 186 | for key in self.keys.iter() { 187 | entries.push(LegendEntry::new(LegendMarkerType::Line, self.color_map.get(key).unwrap().clone(), String::from("none"), key.clone())); 188 | } 189 | } 190 | 191 | entries 192 | } 193 | } 194 | -------------------------------------------------------------------------------- /src/views/mod.rs: -------------------------------------------------------------------------------- 1 | use svg::node::element::Group; 2 | use crate::components::legend::LegendEntry; 3 | 4 | pub mod vertical_bar; 5 | pub mod horizontal_bar; 6 | pub mod scatter; 7 | pub mod datum; 8 | pub mod line; 9 | pub mod area; 10 | 11 | /// A trait that defines a View of a dataset that can be rendered within a chart. 12 | pub trait View<'a> { 13 | fn to_svg(&self) -> Result; 14 | 15 | fn get_legend_entries(&self) -> Vec; 16 | } 17 | -------------------------------------------------------------------------------- /src/views/scatter.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | use std::fmt::Display; 3 | use svg::node::Node; 4 | use svg::node::element::Group; 5 | use crate::components::scatter::{ScatterPoint, MarkerType, PointLabelPosition}; 6 | use crate::colors::Color; 7 | use crate::Scale; 8 | use crate::views::datum::PointDatum; 9 | use crate::views::View; 10 | use crate::components::DatumRepresentation; 11 | use crate::components::legend::{LegendEntry, LegendMarkerType}; 12 | 13 | /// A View that represents data as a scatter plot. 14 | pub struct ScatterView<'a, T: Display, U: Display> { 15 | labels_visible: bool, 16 | label_position: PointLabelPosition, 17 | marker_type: MarkerType, 18 | entries: Vec>, 19 | colors: Vec, 20 | keys: Vec, 21 | color_map: HashMap, 22 | x_scale: Option<&'a dyn Scale>, 23 | y_scale: Option<&'a dyn Scale>, 24 | custom_data_label: String, 25 | } 26 | 27 | impl<'a, T: Display, U: Display> ScatterView<'a, T, U> { 28 | /// Create a new empty instance of the view. 29 | pub fn new() -> Self { 30 | Self { 31 | labels_visible: true, 32 | label_position: PointLabelPosition::NW, 33 | marker_type: MarkerType::Circle, 34 | entries: Vec::new(), 35 | keys: Vec::new(), 36 | colors: Color::color_scheme_10(), 37 | color_map: HashMap::new(), 38 | x_scale: None, 39 | y_scale: None, 40 | custom_data_label: String::new(), 41 | } 42 | } 43 | 44 | /// Set the scale for the X dimension. 45 | pub fn set_x_scale(mut self, scale: &'a impl Scale) -> Self { 46 | self.x_scale = Some(scale); 47 | self 48 | } 49 | 50 | /// Set the scale for the Y dimension. 51 | pub fn set_y_scale(mut self, scale: &'a impl Scale) -> Self { 52 | self.y_scale = Some(scale); 53 | self 54 | } 55 | 56 | /// Set the keys in case of a stacked bar chart. 57 | pub fn set_keys(mut self, keys: Vec) -> Self { 58 | self.keys = keys; 59 | self 60 | } 61 | 62 | /// Set the positioning of the labels. 63 | pub fn set_label_position(mut self, label_position: PointLabelPosition) -> Self { 64 | self.label_position = label_position; 65 | self 66 | } 67 | 68 | /// Set the keys in case of a stacked bar chart. 69 | pub fn set_marker_type(mut self, marker_type: MarkerType) -> Self { 70 | self.marker_type = marker_type; 71 | self 72 | } 73 | 74 | /// Set the color palette of the view. 75 | pub fn set_colors(mut self, colors: Vec) -> Self { 76 | self.colors = colors; 77 | self 78 | } 79 | 80 | /// Set labels visibility. 81 | pub fn set_label_visibility(mut self, label_visibility: bool) -> Self { 82 | self.labels_visible = label_visibility; 83 | self 84 | } 85 | 86 | /// Set custom label for the dataset. 87 | /// This will work when the dataset represents only a single 88 | /// type of data (i.e. there are no different "keys" by which to 89 | /// differentiate data), otherwise, this will have no effect. 90 | pub fn set_custom_data_label(mut self, label: String) -> Self { 91 | self.custom_data_label = label; 92 | self 93 | } 94 | 95 | /// Load and process a dataset of BarDatum points. 96 | pub fn load_data(mut self, data: &Vec>) -> Result { 97 | match self.x_scale { 98 | Some(_) => {}, 99 | _ => return Err("Please provide a scale for the X dimension before loading data".to_string()), 100 | } 101 | match self.y_scale { 102 | Some(_) => {}, 103 | _ => return Err("Please provide a scale for the Y dimension before loading data".to_string()), 104 | } 105 | 106 | // If no keys were explicitly provided, extract the keys from the data. 107 | if self.keys.len() == 0 { 108 | self.keys = Self::extract_keys(&data); 109 | } 110 | 111 | // Organize entries based on the order of the keys first, since displayed data 112 | // should keep the order defined in the `keys` attribute. 113 | for (i, key) in self.keys.iter_mut().enumerate() { 114 | // Map the key to the corresponding color. 115 | self.color_map.insert(key.clone(), self.colors[i % self.colors.len()].as_hex()); 116 | } 117 | 118 | for datum in data.iter() { 119 | let scaled_x = self.x_scale.unwrap().scale(&datum.get_x()); 120 | let scaled_y = self.y_scale.unwrap().scale(&datum.get_y()); 121 | let y_bandwidth_offset = { 122 | if self.y_scale.unwrap().is_range_reversed() { 123 | -self.y_scale.unwrap().bandwidth().unwrap() / 2_f32 124 | } else { 125 | self.y_scale.unwrap().bandwidth().unwrap() / 2_f32 126 | } 127 | }; 128 | let x_bandwidth_offset = { 129 | if self.x_scale.unwrap().is_range_reversed() { 130 | -self.x_scale.unwrap().bandwidth().unwrap() / 2_f32 131 | } else { 132 | self.x_scale.unwrap().bandwidth().unwrap() / 2_f32 133 | } 134 | }; 135 | self.entries.push(ScatterPoint::new(scaled_x + x_bandwidth_offset, scaled_y + y_bandwidth_offset, self.marker_type, 5, datum.get_x(), datum.get_y(), self.label_position, self.labels_visible, true, self.color_map.get(&datum.get_key()).unwrap().clone())); 136 | } 137 | 138 | Ok(self) 139 | } 140 | 141 | /// Extract the list of keys to use when stacking and coloring the bars. 142 | fn extract_keys(data: &Vec>) -> Vec { 143 | let mut keys = Vec::new(); 144 | let mut map = HashMap::new(); 145 | 146 | for datum in data.iter() { 147 | match map.insert(datum.get_key(), 0) { 148 | Some(_) => {}, 149 | None => keys.push(datum.get_key()), 150 | } 151 | } 152 | 153 | keys 154 | } 155 | 156 | } 157 | 158 | impl<'a, T: Display, U: Display> View<'a> for ScatterView<'a, T, U> { 159 | /// Generate the SVG representation of the view. 160 | fn to_svg(&self) -> Result { 161 | let mut group = Group::new(); 162 | 163 | for entry in self.entries.iter() { 164 | let child_svg = entry.to_svg()?; 165 | group.append(child_svg); 166 | } 167 | 168 | Ok(group) 169 | } 170 | 171 | /// Return the legend entries that this view represents. 172 | fn get_legend_entries(&self) -> Vec { 173 | let mut entries = Vec::new(); 174 | 175 | // If there is a single key and it is an empty string (meaning 176 | // the dataset consists only of X and Y dimension values), return 177 | // the custom data label. 178 | if self.keys.len() == 1 && self.keys[0].len() == 0 { 179 | entries.push(LegendEntry::new(LegendMarkerType::from(self.marker_type), self.color_map.get(&self.keys[0]).unwrap().clone(), String::from("none"), self.custom_data_label.clone())); 180 | } else { 181 | for key in self.keys.iter() { 182 | entries.push(LegendEntry::new(LegendMarkerType::from(self.marker_type), self.color_map.get(key).unwrap().clone(), String::from("none"), key.clone())); 183 | } 184 | } 185 | 186 | entries 187 | } 188 | } 189 | -------------------------------------------------------------------------------- /src/views/vertical_bar.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | use svg::node::Node; 3 | use svg::node::element::Group; 4 | use crate::components::bar::{Bar, BarBlock, BarLabelPosition}; 5 | use crate::colors::Color; 6 | use crate::{Scale, BarDatum}; 7 | use crate::scales::ScaleType; 8 | use crate::components::DatumRepresentation; 9 | use crate::views::View; 10 | use crate::chart::Orientation; 11 | use crate::components::legend::{LegendEntry, LegendMarkerType}; 12 | 13 | /// A View that represents data as vertical bars. 14 | pub struct VerticalBarView<'a> { 15 | label_position: BarLabelPosition, 16 | labels_visible: bool, 17 | rounding_precision: Option, 18 | entries: Vec, 19 | keys: Vec, 20 | colors: Vec, 21 | color_map: HashMap, 22 | x_scale: Option<&'a dyn Scale>, 23 | y_scale: Option<&'a dyn Scale>, 24 | custom_data_label: String, 25 | } 26 | 27 | impl<'a> VerticalBarView<'a> { 28 | /// Create a new empty instance of the view. 29 | pub fn new() -> Self { 30 | Self { 31 | label_position: BarLabelPosition::EndOutside, 32 | labels_visible: true, 33 | rounding_precision: None, 34 | entries: Vec::new(), 35 | keys: Vec::new(), 36 | colors: Color::color_scheme_10(), 37 | color_map: HashMap::new(), 38 | x_scale: None, 39 | y_scale: None, 40 | custom_data_label: String::new(), 41 | } 42 | } 43 | 44 | /// Set the scale for the X dimension. 45 | pub fn set_x_scale(mut self, scale: &'a impl Scale) -> Self { 46 | self.x_scale = Some(scale); 47 | self 48 | } 49 | 50 | /// Set the scale for the Y dimension. 51 | pub fn set_y_scale(mut self, scale: &'a impl Scale) -> Self { 52 | self.y_scale = Some(scale); 53 | self 54 | } 55 | 56 | /// Set the keys in case of a stacked bar chart. 57 | pub fn set_keys(mut self, keys: Vec) -> Self { 58 | self.keys = keys; 59 | self 60 | } 61 | 62 | /// Set the positioning of the labels. 63 | pub fn set_label_position(mut self, label_position: BarLabelPosition) -> Self { 64 | self.label_position = label_position; 65 | self 66 | } 67 | 68 | /// Set the color palette of the view. 69 | pub fn set_colors(mut self, colors: Vec) -> Self { 70 | self.colors = colors; 71 | self 72 | } 73 | 74 | /// Set labels visibility. 75 | pub fn set_label_visibility(mut self, label_visibility: bool) -> Self { 76 | self.labels_visible = label_visibility; 77 | self 78 | } 79 | 80 | /// Set custom label for the dataset. 81 | /// This will work when the dataset represents only a single 82 | /// type of data (i.e. there are no different "keys" by which to 83 | /// differentiate data), otherwise, this will have no effect. 84 | pub fn set_custom_data_label(mut self, label: String) -> Self { 85 | self.custom_data_label = label; 86 | self 87 | } 88 | 89 | /// Set the precision to which value labels should be rounded. 90 | pub fn set_label_rounding_precision(mut self, nr_of_digits: usize) -> Self { 91 | self.rounding_precision = Some(nr_of_digits); 92 | self 93 | } 94 | 95 | /// Load and process a dataset of BarDatum points. 96 | pub fn load_data(mut self, data: &Vec) -> Result { 97 | match self.x_scale { 98 | Some(scale) if scale.get_type() == ScaleType::Band => {}, 99 | _ => return Err("The X axis scale should be a Band scale.".to_string()), 100 | } 101 | match self.y_scale { 102 | Some(scale) if scale.get_type() == ScaleType::Linear => {}, 103 | _ => return Err("The Y axis scale should be a Linear scale.".to_string()), 104 | } 105 | 106 | // If no keys were explicitly provided, extract the keys from the data. 107 | if self.keys.len() == 0 { 108 | self.keys = Self::extract_keys(&data); 109 | } 110 | 111 | // HashMap to group all data related to a category. This is needed when there 112 | // are many data entries under a single category as in a stacked bar chart. 113 | let mut categories: HashMap> = HashMap::new(); 114 | 115 | // Organize entries based on the order of the keys first, since displayed data 116 | // should keep the order defined in the `keys` attribute. 117 | for (i, key) in self.keys.iter_mut().enumerate() { 118 | // Map the key to the corresponding color. 119 | self.color_map.insert(key.clone(), self.colors[i % self.colors.len()].as_hex()); 120 | 121 | for entry in data.iter() { 122 | if entry.get_key() == *key { 123 | let entry_category = entry.get_category(); 124 | 125 | if !categories.contains_key(&entry_category) { 126 | categories.insert(entry.get_category(), Vec::new()); 127 | } 128 | if let Some(category_entries) = categories.get_mut(&entry_category) { 129 | category_entries.push((key, entry.get_value())); 130 | } 131 | } 132 | } 133 | } 134 | 135 | // Create a Bar entry for each category data that was grouped in the previous step. 136 | let mut bars = Vec::new(); 137 | let y_range_is_reversed = self.y_scale.unwrap().is_range_reversed(); 138 | 139 | for (category, key_value_pairs) in categories.iter_mut() { 140 | let mut value_acc = 0_f32; 141 | let mut bar_blocks = Vec::new(); 142 | let mut stacked_start = self.y_scale.unwrap().scale(&value_acc); 143 | let mut stacked_end = stacked_start; 144 | 145 | for (key, value) in key_value_pairs.iter() { 146 | value_acc += *value; 147 | // If Y axis' scale has the range in reversed order, then adjust the computation of 148 | // the start and end positions to account for SVG coordinate system origin. 149 | if y_range_is_reversed { 150 | stacked_end = stacked_start; 151 | stacked_start = self.y_scale.unwrap().scale(&value_acc); 152 | } else { 153 | stacked_start = stacked_end; 154 | stacked_end = self.y_scale.unwrap().scale(&value_acc); 155 | } 156 | bar_blocks.push(BarBlock::new(stacked_start, stacked_end, *value, self.color_map.get(*key).unwrap().clone())); 157 | } 158 | 159 | let bar = Bar::new(bar_blocks, Orientation::Vertical, category.to_string(), self.label_position, self.labels_visible, self.rounding_precision, self.x_scale.unwrap().bandwidth().unwrap(), self.x_scale.unwrap().scale(category)); 160 | bars.push(bar); 161 | } 162 | 163 | for bar in bars { 164 | self.add_bar(bar); 165 | } 166 | 167 | Ok(self) 168 | } 169 | 170 | /// Extract the list of keys to use when stacking and coloring the bars. 171 | fn extract_keys(data: &Vec) -> Vec { 172 | let mut keys = Vec::new(); 173 | let mut map = HashMap::new(); 174 | 175 | for datum in data.iter() { 176 | match map.insert(datum.get_key(), 0) { 177 | Some(_) => {}, 178 | None => keys.push(datum.get_key()), 179 | } 180 | } 181 | 182 | keys 183 | } 184 | 185 | /// Add a [Bar] entry to the dataset entries list. 186 | fn add_bar(&mut self, bar: Bar) { 187 | self.entries.push(bar); 188 | } 189 | 190 | } 191 | 192 | impl<'a> View<'a> for VerticalBarView<'a> { 193 | /// Generate the SVG representation of the view. 194 | fn to_svg(&self) -> Result { 195 | let mut group = Group::new(); 196 | 197 | for entry in self.entries.iter() { 198 | let child_svg = entry.to_svg()?; 199 | group.append(child_svg); 200 | } 201 | 202 | Ok(group) 203 | } 204 | 205 | /// Return the legend entries that this view represents. 206 | fn get_legend_entries(&self) -> Vec { 207 | let mut entries = Vec::new(); 208 | 209 | // If there is a single key and it is an empty string (meaning 210 | // the dataset consists only of X and Y dimension values), return 211 | // the custom data label. 212 | if self.keys.len() == 1 && self.keys[0].len() == 0 { 213 | entries.push(LegendEntry::new(LegendMarkerType::Square, self.color_map.get(&self.keys[0]).unwrap().clone(), String::from("none"), self.custom_data_label.clone())); 214 | } else { 215 | for key in self.keys.iter() { 216 | entries.push(LegendEntry::new(LegendMarkerType::Square, self.color_map.get(key).unwrap().clone(), String::from("none"), key.clone())); 217 | } 218 | } 219 | 220 | entries 221 | } 222 | } 223 | --------------------------------------------------------------------------------