plotlib-0.5.1/.github/PULL_REQUEST_TEMPLATE.md010064400017500000144000000010531363346402200165420ustar0000000000000000Thanks for submitting a PR, your contribution is really appreciated! Here's a quick checklist that should be present in PRs (you can delete this text from the final description, this is just a guideline): - [ ] Add an entry to the CHANGELOG file - [ ] Include documentation when adding new features. - [ ] Include new tests or update existing tests when applicable. If your work is still in progress, please [mark your PR as a draft](https://help.github.com/en/articles/about-pull-requests#draft-pull-requests) until you are ready for it to be merged. plotlib-0.5.1/.github/workflows/rust.yml010064400017500000144000000044301363763221700165270ustar0000000000000000name: Rust on: [push] jobs: build: runs-on: ubuntu-latest strategy: matrix: rust: - stable - nightly steps: - uses: actions/checkout@v2 - name: Install toolchain uses: actions-rs/toolchain@v1 with: toolchain: ${{ matrix.rust }} override: true profile: minimal - name: Build uses: actions-rs/cargo@v1 with: command: build args: --all-targets --verbose - name: Run tests uses: actions-rs/cargo@v1 with: command: test args: --verbose coverage: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - name: Install stable toolchain uses: actions-rs/toolchain@v1 with: toolchain: stable override: true profile: minimal - name: Run cargo-tarpaulin uses: actions-rs/tarpaulin@v0.1 - name: Upload to codecov.io uses: codecov/codecov-action@v1 with: token: ${{ secrets.CODECOV_TOKEN }} release: runs-on: ubuntu-latest needs: build if: startsWith(github.ref, 'refs/tags/') steps: - uses: actions/checkout@v1 - name: Get the changelog id: get_changes run: | changelog=$(sed '/^## '"${GITHUB_REF#refs/tags/}"'.*/,/^## .*/!d;//d' CHANGELOG) changelog="${changelog//'%'/'%25'}" changelog="${changelog//$'\n'/'%0A'}" changelog="${changelog//$'\r'/'%0D'}" echo "::set-output name=changelog::${changelog}" - name: Create Release id: create_release uses: actions/create-release@latest env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: tag_name: ${{ github.ref }} release_name: ${{ github.ref }} body: "${{ steps.get_changes.outputs.changelog }}" draft: false prerelease: true - name: Install stable toolchain uses: actions-rs/toolchain@v1 with: toolchain: stable override: true profile: minimal - name: Publish uses: actions-rs/cargo@v1 with: command: test args: --verbose env: CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_TOKEN }} plotlib-0.5.1/.gitignore010064400017500000144000000000541363346402200133710ustar0000000000000000target Cargo.lock *.bk .vscode .idea /*.svg plotlib-0.5.1/.travis.yml010064400017500000144000000001751363346402200135160ustar0000000000000000language: rust rust: - stable - nightly matrix: allow_failures: - rust: nightly fast_finish: true cache: cargo plotlib-0.5.1/CHANGELOG010064400017500000144000000033171363763163600126330ustar0000000000000000# Changelog ## Unreleased ## 0.5.1 - 2020-03-28 ### Fixed - Set default axis bounds for case where all values are equal (Issue #36) - Optimise tick generation to jump straight to range (Issue #42) - Fix text rendering of scatter plots ## 0.5.0 - 2020-03-14 ### Added - Legends for line plots ### Changed - Remove style traits and replace all style structs with 3 common ones in `style` - Group all representations under a `repr` module. - Add `linejoin` option to line style. - More Box and less & in the interface - Replace Line, Scatter and Function with Plot ## 0.4.0 - 2019-03-02 ### Added - Line charts. - Box plots. - Bar charts. - Introduce categorical representation, views and axes. - Add ability to set dimensions of plot (PR #8) - Added ability to display a histogram as densities - Add ability to display grids (PR #23) ### Changed - Change `create_axes`, `save`, `to_svg` and `to_text` to return `Result` indicating an error. - Make `representation` module public. - Rename `Scatter::from_vec` to `Scatter::from_slice`. - Rename `Histogram::from_vec` to `Histogram::from_slice`. - Rename `view::View` to `view::ContinuousView` and introduce `view::View` as a trait. - Change `svg_render` functions to take data slices rather than Representations. - `Histogram::from_slice` now takes either a bin count or a bin bound list as its second argument. ## 0.3.0 - 2018-03-01 ### Added - Axis labels - Function plotting - Histogram styling ### Changed - Rename `plot::Plot` to `page::Page` - Move `scatter::Marker` to `style::Marker` ## 0.2.0 - 2017-03-16 ### Added - SVG rendering ### Changed - Reorganise things to use traits for plot types ## 0.1.0 - 2017-03-09 ### Added - Initial release with histograms and scatter plots plotlib-0.5.1/Cargo.toml.orig010064400017500000144000000007041363763163600143050ustar0000000000000000[package] name = "plotlib" version = "0.5.1" authors = ["Matt Williams "] description = "Pure Rust plotting library" readme = "README.md" license = "MIT" edition = "2018" repository = "https://github.com/milliams/plotlib" categories = ["visualization", "science"] keywords = ["plotting", "plot", "graph", "chart", "histogram"] [badges] travis-ci = { repository = "milliams/plotlib" } [dependencies] svg = "0.7.1" failure = "0.1.7" plotlib-0.5.1/Cargo.toml0000644000000017511363763236700106240ustar00# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO # # When uploading crates to the registry Cargo will automatically # "normalize" Cargo.toml files for maximal compatibility # with all versions of Cargo and also rewrite `path` dependencies # to registry (e.g., crates.io) dependencies # # If you believe there's an error in this file please file an # issue against the rust-lang/cargo repository. If you're # editing this file be aware that the upstream Cargo.toml # will likely look very different (and much more reasonable) [package] edition = "2018" name = "plotlib" version = "0.5.1" authors = ["Matt Williams "] description = "Pure Rust plotting library" readme = "README.md" keywords = ["plotting", "plot", "graph", "chart", "histogram"] categories = ["visualization", "science"] license = "MIT" repository = "https://github.com/milliams/plotlib" [dependencies.failure] version = "0.1.7" [dependencies.svg] version = "0.7.1" [badges.travis-ci] repository = "milliams/plotlib" plotlib-0.5.1/LICENSE010064400017500000144000000020561363346402200124120ustar0000000000000000MIT License Copyright (c) 2018 Matt Williams Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. plotlib-0.5.1/README.md010064400017500000144000000041531363353747300126770ustar0000000000000000# plotlib ![Rust](https://github.com/milliams/plotlib/workflows/Rust/badge.svg) [![codecov](https://codecov.io/gh/milliams/plotlib/branch/master/graph/badge.svg)](https://codecov.io/gh/milliams/plotlib) [![Crates.io](https://img.shields.io/crates/v/plotlib.svg)](https://crates.io/crates/plotlib) ![MIT](https://img.shields.io/crates/l/plotlib.svg) [![Documentation](https://docs.rs/plotlib/badge.svg)](https://docs.rs/plotlib) `plotlib` is a generic data visualisation and plotting library for Rust. It is currently in the very early stages of development. It can currently produce: * histograms * scatter plots * line graphs from data or from function definitions * box plots * bar charts rendering them as either SVG or plain text. The API is still very much in flux and is subject to change. For example, code like: ```rust use plotlib::page::Page; use plotlib::repr::Plot; use plotlib::view::ContinuousView; use plotlib::style::{PointMarker, PointStyle}; fn main() { // Scatter plots expect a list of pairs let data1 = vec![ (-3.0, 2.3), (-1.6, 5.3), (0.3, 0.7), (4.3, -1.4), (6.4, 4.3), (8.5, 3.7), ]; // We create our scatter plot from the data let s1: Plot = Plot::new(data1).point_style( PointStyle::new() .marker(PointMarker::Square) // setting the marker to be a square .colour("#DD3355"), ); // and a custom colour // We can plot multiple data sets in the same view let data2 = vec![(-1.4, 2.5), (7.2, -0.3)]; let s2: Plot = Plot::new(data2).point_style( PointStyle::new() // uses the default marker .colour("#35C788"), ); // and a different colour // The 'view' describes what set of data is drawn let v = ContinuousView::new() .add(s1) .add(s2) .x_range(-5., 10.) .y_range(-2., 6.) .x_label("Some varying variable") .y_label("The response of something"); // A page with a single view is then saved to an SVG file Page::single(&v).save("scatter.svg").unwrap(); } ``` will produce output like: ![scatter plot](scatter.png) plotlib-0.5.1/examples/barchart_svg.rs010064400017500000144000000006541363352603400162420ustar0000000000000000use plotlib::page::Page; use plotlib::repr::BarChart; use plotlib::style::BoxStyle; use plotlib::view::CategoricalView; fn main() { let b1 = BarChart::new(5.3).label("1"); let b2 = BarChart::new(2.6) .label("2") .style(&BoxStyle::new().fill("darkolivegreen")); let v = CategoricalView::new().add(b1).add(b2).x_label("Experiment"); Page::single(&v).save("barchart.svg").expect("saving svg"); } plotlib-0.5.1/examples/boxplot_svg.rs010064400017500000144000000011561363352603400161410ustar0000000000000000use plotlib::page::Page; use plotlib::repr::BoxPlot; use plotlib::style::BoxStyle; use plotlib::view::CategoricalView; fn main() { let b1 = BoxPlot::from_slice(&[1.0, 4.0, 2.0, 3.5, 6.4, 2.5, 7.5, 1.8, 9.6]).label("1"); let b2 = BoxPlot::from_slice(&[3.0, 4.3, 2.0, 3.5, 6.9, 4.5, 7.5, 1.8, 10.6]) .label("2") .style(&BoxStyle::new().fill("darkolivegreen")); let v = CategoricalView::new() .add(b1) .add(b2) .x_label("Experiment") .y_label("y"); Page::single(&v) .dimensions(400, 300) .save("boxplot.svg") .expect("saving svg"); } plotlib-0.5.1/examples/function_svg.rs010064400017500000144000000011621363352603400162740ustar0000000000000000use plotlib::page::Page; use plotlib::repr::Plot; use plotlib::style::LineStyle; use plotlib::view::ContinuousView; fn main() { let f1 = Plot::from_function(|x| x * 5., 0., 10.).line_style(LineStyle::new().colour("burlywood")); let f2 = Plot::from_function(|x| x.powi(2), 0., 10.) .line_style(LineStyle::new().colour("darkolivegreen").width(2.)); let f3 = Plot::from_function(|x| x.sqrt() * 20., 0., 10.) .line_style(LineStyle::new().colour("brown").width(1.)); let v = ContinuousView::new().add(f1).add(f2).add(f3); Page::single(&v).save("function.svg").expect("saving svg"); } plotlib-0.5.1/examples/histogram_svg.rs010064400017500000144000000006721363352603400164510ustar0000000000000000use plotlib::page::Page; use plotlib::repr::{Histogram, HistogramBins}; use plotlib::style::BoxStyle; use plotlib::view::ContinuousView; fn main() { let data = [0.3, 0.5, 6.4, 5.3, 3.6, 3.6, 3.5, 7.5, 4.0]; let h = Histogram::from_slice(&data, HistogramBins::Count(10)) .style(&BoxStyle::new().fill("burlywood")); let v = ContinuousView::new().add(h); Page::single(&v).save("histogram.svg").expect("saving svg"); } plotlib-0.5.1/examples/histogram_text.rs010064400017500000144000000005641363352603400166360ustar0000000000000000use plotlib::page::Page; use plotlib::repr::{Histogram, HistogramBins}; use plotlib::view::ContinuousView; fn main() { let data = [0.3, 0.5, 6.4, 5.3, 3.6, 3.6, 3.5, 7.5, 4.0]; let h = Histogram::from_slice(&data, HistogramBins::Count(10)); let v = ContinuousView::new().add(h); println!("{}", Page::single(&v).dimensions(60, 15).to_text().unwrap()); } plotlib-0.5.1/examples/letter_counter.rs010064400017500000144000000015111363374705000166270ustar0000000000000000use std::collections::btree_map::BTreeMap; fn main() { let mut data = Vec::new(); let message: &str = "This is a long message"; let mut count = BTreeMap::new(); for c in message.trim().to_lowercase().chars() { if c.is_alphabetic() { *count.entry(c).or_insert(0) += 1 } } println!("Number of occurences per character"); for (ch, count) in &count { println!("{:?}: {}", ch, count); let count = *count as f64; data.push(plotlib::repr::BarChart::new(count).label(ch.to_string())); } // Add data to the view let v = data .into_iter() .fold(plotlib::view::CategoricalView::new(), |view, datum| { view.add(datum) }); plotlib::page::Page::single(&v) .save("barchart.svg") .expect("saving svg"); } plotlib-0.5.1/examples/line_and_point_svg.rs010064400017500000144000000007751363352603400174420ustar0000000000000000use plotlib::page::Page; use plotlib::repr::Plot; use plotlib::style::*; use plotlib::view::ContinuousView; fn main() { let l1 = Plot::new(vec![(0., 1.), (2., 1.5), (3., 1.2), (4., 1.1)]) .line_style( LineStyle::new() .colour("burlywood") .linejoin(LineJoin::Round), ) .point_style(PointStyle::new()); let v = ContinuousView::new().add(l1); Page::single(&v) .save("line_and_point.svg") .expect("saving svg"); } plotlib-0.5.1/examples/line_svg.rs010064400017500000144000000006631363377744600154220ustar0000000000000000use plotlib::page::Page; use plotlib::repr::Plot; use plotlib::style::{LineJoin, LineStyle}; use plotlib::view::ContinuousView; fn main() { let l1 = Plot::new(vec![(0., 1.), (2., 1.5), (3., 1.2), (4., 1.1)]).line_style( LineStyle::new() .colour("burlywood") .linejoin(LineJoin::Round), ); let v = ContinuousView::new().add(l1); Page::single(&v).save("line.svg").expect("saving svg"); } plotlib-0.5.1/examples/scatter_svg.rs010064400017500000144000000023511363352603400161150ustar0000000000000000use plotlib::page::Page; use plotlib::repr::Plot; use plotlib::style::{PointMarker, PointStyle}; use plotlib::view::ContinuousView; fn main() { // Scatter plots expect a list of pairs let data1 = vec![ (-3.0, 2.3), (-1.6, 5.3), (0.3, 0.7), (4.3, -1.4), (6.4, 4.3), (8.5, 3.7), ]; // We create our scatter plot from the data let s1: Plot = Plot::new(data1).point_style( PointStyle::new() .marker(PointMarker::Square) // setting the marker to be a square .colour("#DD3355"), ); // and a custom colour // We can plot multiple data sets in the same view let data2 = vec![(-1.4, 2.5), (7.2, -0.3)]; let s2: Plot = Plot::new(data2).point_style( PointStyle::new() // uses the default marker .colour("#35C788"), ); // and a different colour // The 'view' describes what set of data is drawn let v = ContinuousView::new() .add(s1) .add(s2) .x_range(-5., 10.) .y_range(-2., 6.) .x_label("Some varying variable") .y_label("The response of something"); // A page with a single view is then saved to an SVG file Page::single(&v).save("scatter.svg").unwrap(); } plotlib-0.5.1/examples/scatter_text.rs010064400017500000144000000014371363400470500163030ustar0000000000000000use plotlib::page::Page; use plotlib::repr::Plot; use plotlib::style::{PointMarker, PointStyle}; use plotlib::view::ContinuousView; fn main() { let data = vec![ (-3.0, 2.3), (-1.6, 5.3), (0.3, 0.7), (4.3, -1.4), (6.4, 4.3), (8.5, 3.7), ]; let s1 = Plot::new(data).point_style(PointStyle::new().marker(PointMarker::Circle)); let s2 = Plot::new(vec![(-1.4, 2.5), (7.2, -0.3)]) .point_style(PointStyle::new().marker(PointMarker::Square)); let v = ContinuousView::new() .add(s1) .add(s2) .x_range(-5., 10.) .y_range(-2., 6.) .x_label("Some varying variable") .y_label("The response of something"); println!("{}", Page::single(&v).dimensions(80, 30).to_text().unwrap()); } plotlib-0.5.1/examples/with_grid.rs010064400017500000144000000020621363352603400155500ustar0000000000000000use plotlib::grid::Grid; use plotlib::page::Page; use plotlib::repr::{BarChart, Plot}; use plotlib::style::{BoxStyle, LineStyle}; use plotlib::view::{CategoricalView, ContinuousView, View}; fn main() { render_line_chart("line_with_grid.svg"); render_barchart("barchart_with_grid.svg"); } fn render_line_chart(filename: S) where S: AsRef, { let l1 = Plot::new(vec![(0., 1.), (2., 1.5), (3., 1.2), (4., 1.1)]) .line_style(LineStyle::new().colour("burlywood")); let mut v = ContinuousView::new().add(l1); v.add_grid(Grid::new(3, 8)); Page::single(&v) .save(filename.as_ref()) .expect("saving svg"); } fn render_barchart(filename: S) where S: AsRef, { let b1 = BarChart::new(5.3).label("1"); let b2 = BarChart::new(2.6) .label("2") .style(&BoxStyle::new().fill("darkolivegreen")); let mut v = CategoricalView::new().add(b1).add(b2).x_label("Experiment"); v.add_grid(Grid::new(3, 8)); Page::single(&v) .save(filename.as_ref()) .expect("saving svg"); } plotlib-0.5.1/release.sh010075500017500000144000000013721363763233500133740ustar0000000000000000#!/usr/bin/env bash set -euo pipefail IFS=$'\n\t' [[ -n ${1+x} ]] || (echo "Usage: release.sh " && exit 1) version=$1 date=$(date --iso-8601) echo -e "\\n# Checking version number\\n" git fetch git tag --list | grep -q "${version}" && echo "Error: Git tag ${version} already exists" && exit 1 echo -e "\\n# Updating version number in files\\n" sed --in-place "s/version = \".*\"/version = \"${version}\"/" Cargo.toml sed --in-place "s/## Unreleased/## Unreleased\\n\\n## ${version} - ${date}/" CHANGELOG echo -e "\\n# Adding to Git\\n" git add Cargo.toml CHANGELOG git commit -m "Update version numbers for ${version}" echo -e "\\n# Pushing to Git\\n" git tag "${version}" git push origin git push origin --tags echo -e "\\n# Success ✓" plotlib-0.5.1/scatter.png010064400017500000144000000270131363346402200135600ustar0000000000000000PNG  IHDRXr5gAMA a cHRMz&u0`:pQ<bKGD pHYs+,IDATxy\e/; K-eiFTLԤLeͫf)1lSK-I4ISq\7\d<ΓiIr%|ޯ8_tu]=,˲x.lF f,lF f,lF f,lF f,y.n۷o_|!s=jԨq?\_%ooomܸQiii6]p3.55aӧ,߯|ř. ˲,EءTڱc [oq%ƒK_mۦ~[SMMq%šiڵ:u?\rXzQVʍ.8*0]*K6#`،`3X6#`،`3͸(iS.83RXz3_ ;.Y.D{i߾}n%֒%K4b.! X~VZ.) X:uBCCMܔKUxxnvӥ72"_]WS^pLVVk@=23XfҮ]ԪU+hӦM4hz-ӥ7aYe;TTTuzzy 0@~~~?rf .DؠA5hwVÆ /:\\, X?5|.! XѦKne6\)X6s%BEEZktn@=wx3#ӥ lF f, XׯWHH9bbb3g E3)xnZ=RUuuGyD ޽jF_9y7P5sL=c/ X^2D۷˲,|ץ7bdo߾zgս{wLӵ|rMz?)t #KK.ռyn:hBK.UNk.]V-[4ꅨ7.?aQ9JHHP\\$Z999JNN6:1***ݻw+$$syyy{P'F֪UB|000Pm۶%\dd+11Q׫d9ϱ wFΝ;o>+c`kVn믟wMNlrpeukРAf *cwrgh"uY35k, 5[ X0`:`;#Ҵe8p`MLO>YvUV) t_.e Xyyy;vN8CiŊsUUUڿMN.kjݺ&L7*++K<\ ԹsgyzrsyP]4p k4f5iDWTT<<S3#+??_O=֬Y~~XO6:1rrr4p@mygϞё#GLN}7q;t٣f͚ @yaRRڶmΝ;ĉz''xB^^^{P'F$;Zl֯_/ooo=JMM5:3vөr)55UIII*--MΌ`=zTIIImذtOHZzt}K2DءCYe. #3Xm۶ULLեK:M>tOHoѣGs+r;wjzM_?ݻmۦC~MCQQ7oH; @ X-[o. #KJJJҾ}4o<͟?_'NPrr~ԙG 2D c|Aj޼yZ|򔖖fpK W?vKe\u)!!AK\¢ Y^]jӥ\uA1Bޚ9s٣gyFƍ3%sqZP~FVNN 4͛7O= ոZ05j|9rDٺUZZ={(((tOR5mlT 2zꥢ"lRj֬:vÇsQf]njkte6 ׿2ʕ+knSk5.ڍ3)O ;ҳyzxᘾte$`p%puܸt j;go4_UjQ\0 xΈdl{VZGfK^xAk֬њ5k4vXpI\%´4 >\qqq*,,Tl2=~Svv.]{orONNV5h 5mTyyyP͞{Ph``+EEEW>}l3#+??_O>V\XF%a'ubdM.''Gz:wرcS^d6nܨ 2D&.1#{$i?~,RF4{l=Suz͉'jʔ) ֪UԣGUVVD<,˲La*+//OΝ;_Ԁ.5NzԆxO%%%$UWWDAAAKnCr87nխ[7e7ctp$5kjj4rHݻW .yf/++K}5"POX޽[ڵӢEUUU}Zg϶幆lr ֆ t7+!!ywjѢ9rQL-ZH QHH>Cܘ=X-Zui#GhϞ= lݺ9~pd())I%%%Eiҥ} 7ܠ۫D!!!pь`:u[4xxxGFL^tRK5n8EEEI&{P'FVFpBXB7t.9eff@{sΝ`O{` cf͚uA-[Tbblbuf$`رCӦMӲeTVVJ "a'ub$`m۶Mw}trOO 6LAAA*,,4:1u리,9}w,K͚53:1?*66Viii֭:wk׮OMattz_`!ue$`w:q´xbڵK{/7%J%''O>ї_~#FhժU[MΌkתSN3f2335n8͝;W'No'ubl{DD-Z䜹r82:1gϞzǴg5mT7n|||ԴiS=#3XZxZ-YDѢELΌ=0!!A?էO@Y"c*++KׯFo]?ԉM6o֭[(IZ~233UQQa'ub$`۷O=zXPPUZZj'ub$`%%%istQ =#{bbb4yduUJKK}3#㊎ցcy{{}5:3V\SjӦMJJJ2[ XzRppfΜT8ř @ Xyyyڼy6o|ι=#+%% \G*c? 6tX@=RStL]pOHӥ[3DvZ*.._m065n8XBJOOȑ#uw3:12i&mٲE[nUbb$izTQQa'ub$`۷O=zXPPUZZj'ub$`%%%is[pԬY3=#{bbb4yduUի4g3cGo]۷oڷo/???3ck׮]Zr%I_|$iҤI{P'FkV{ry.ܚf 8Pg6}@4X .pF>EpI\KjӦM|tM֭Yك=ݻwK#~,WrYVLLyuu\2u֜9sb pI.m>S8pgO>hCr;vu\2=` :T?d؃`:uw+8|vYVZZX" f,lFe襶zjZJ2d<=ɐr 5nXoƍg$\*`M:U~a-X@}:d,f\&`)//O{$SNڴiqURR&Mti͸&ԜuZ?u fVVk@=23X*//Wyyؑ#Gi4f\fG;wݻwkΝѣyf6c~e$wܡkÆ z饗ԤIe7Rk׮ںumۦ-Z(""tI Tܞ!%%t&w+ f.DD'+XaL+ ʾ/_yyK{.W  rs W߳]dùX2y2Y?sQiypW,~pT☼"}wY,_YRqj98ڍ `36ny1>^ҸRq!`3RZ*q/;2EA L+  _|AM)ƶeD\(w=-+N_߭rEhgX\4MЀ KA=!X6#`،`3X6#`،`3X6#`،`3X6#`،`3X6#`،`3X6#`،`3X6#`،`3X6#`،`3X6#`tکnʿʹ8q l\n5 0%BlF f,lF fh 8?0Ǚ.p{,'OxlF f,lF f,lF f,lm-YD7nTDD %7R3X'NԔ)SUVG4]p3.Mرc5k,Iʕ+M܌,Dj(((ti͸ $gr87nխ[7e7aYec2M&Iȑ#w^-\PM49O:?#++KԷo_ӗ [W2URRKlꃬ,g.7~`RVVVXv| ӧOkɨI$Ws\effjѢE WHHBBB. a֭wpӥ72+ @qqqp%B+ f,lFKhJ2iTf̘Ǐ;NOOWJJrss5k,yyyWvL7kF˂+((;C'xyT5`r-ˬ5AUUƏcǎ9TVV. .n׮]ձcG5o\}UQQ&x߃ ֠A4wܳ8PR~O7]j1 nݪ6mhKy74tP=Zyf;zM7uVnݚ=\6|nfG{q_vݫ+VS#G>3] ZIMMՖ-[L7c=. *;;[cǎ=xNNRRRy&oU}:N:rztjz:88XG1]Dnn ս{wpYtAmڴ9O 4ij;vtɵᏌ1¹'/^xA>>> ͘1LO~ RUUJy{*.UVV#7n|{aUU,rn^xkFՒPeggUV$tpqQQQ:|CE˂}Wh}ί:`5ltiBQF)S3fFnFe_g֨QTSS>@M7o[o&{0W^5j٣X;aZFȑ#[U^^ u]3f̐jjj4j([Nպ޼~^NC 40]\ܗ_~I&ic4i"##U]]ŋ+**tB"* t)p#'NPppR@ӥUWWHh)Z #\ djӦM7nzw}2}ڻwLb 駟วzJWpEQTTT W p;۶mS~GyD;wm|9^XX>ty~6nfź4qD1OOO-^Xt_W_6mhŊQ~t1}jӦ㝯lRqaUUUUVc7oV||sN}wjܸz!m5iڵk7X-:ZX? 8_?##zg-:x uZxYXO>XfD;Cwq=_zyf-X@[lы/;`M0uRR3r>ʢiӦЮ]tI=C)..VNNj19RJJJҜ9sl2yxxhʔ)9sl٢ߪt9&&F͚5$%$$(..NK,QllJKKvǼ\$5o\U-Լysm۞+5J֭{=-\POw͛uV9댎>ɇ?X?6oެ۷vׯ3˶iiiڸqE=P?AO?$RFι1ck92)Bjڴiڶm3gϞׯz-Ig9sY_H۶moVqqqjڴnF36&&F z5rHIr΀M6MzҚ5ktIsBjȐ!Zb͛#FԺΎ;O?%Iݻ;6**J0aݻwDӟԻwo_^'NceV\+W=s̑t擃t}jٲk瞫oK,<*,,Tjj|}}UVV{^s:tj%$${0jΜ95j^|EVZZyǏ=ZO7QϞ=եKuڵݫsI&JOOWAAZlY:5w\eddhĉٳu>\C$+==]]tQHHZjTݻxG^See}9رC:r䈞~iuѹ(yȑ#վ}{5lP>>>?ݿ&;nԩS*((P``6?vq9.VAA.1+jذan;l0tM뮻j?eddCNRHH٣_Ug``௺UByyN Range { Range { lower, upper } } pub(crate) fn is_valid(&self) -> bool { self.lower < self.upper } } #[derive(Debug)] pub struct ContinuousAxis { range: Range, ticks: Vec, label: String, } impl ContinuousAxis { /// Constructs a new ContinuousAxis pub fn new(lower: f64, upper: f64, max_ticks: usize) -> ContinuousAxis { ContinuousAxis { range: Range::new(lower, upper), ticks: calculate_ticks(lower, upper, max_ticks), label: "".into(), } } pub fn max(&self) -> f64 { self.range.upper } pub fn min(&self) -> f64 { self.range.lower } pub fn label(mut self, l: S) -> Self where S: Into, { self.label = l.into(); self } pub fn get_label(&self) -> &str { self.label.as_ref() } /// Get the positions of the ticks on the axis pub fn ticks(&self) -> &Vec { &self.ticks } } #[derive(Debug)] pub struct CategoricalAxis { ticks: Vec, label: String, } impl CategoricalAxis { /// Constructs a new ContinuousAxis pub fn new(ticks: &[String]) -> CategoricalAxis { CategoricalAxis { ticks: ticks.into(), label: "".into(), } } pub fn label(mut self, l: S) -> Self where S: Into, { self.label = l.into(); self } pub fn get_label(&self) -> &str { self.label.as_ref() } /// Get the positions of the ticks on the axis pub fn ticks(&self) -> &Vec { &self.ticks } } /// The base units for the step sizes /// They should be within one order of magnitude, e.g. [1,10) const BASE_STEPS: [u32; 4] = [1, 2, 4, 5]; #[derive(Debug, Clone)] struct TickSteps { next: f64, } impl TickSteps { fn start_at(start: f64) -> TickSteps { let start_options = TickSteps::scaled_steps(start); let overflow = start_options[0] * 10.0; let curr = start_options.iter().find(|&step| step >= &start); TickSteps { next: *curr.unwrap_or(&overflow), } } fn scaled_steps(curr: f64) -> Vec { let power = curr.log10().floor(); let base_step_scale = 10f64.powf(power); BASE_STEPS .iter() .map(|&s| (f64::from(s) * base_step_scale)) .collect() } } impl Iterator for TickSteps { type Item = f64; fn next(&mut self) -> Option { let curr = self.next; // cache the value we're currently on let curr_steps = TickSteps::scaled_steps(self.next); let overflow = curr_steps[0] * 10.0; self.next = *curr_steps.iter().find(|&s| s > &curr).unwrap_or(&overflow); Some(curr) } } fn generate_ticks(min: f64, max: f64, step_size: f64) -> Vec { // "fix" just makes sure there are no floating-point errors fn fix(x: f64) -> f64 { const PRECISION: f64 = 100_000_f64; (x * PRECISION).round() / PRECISION } let mut ticks: Vec = vec![]; if min <= 0.0 { if max >= 0.0 { // standard spanning axis ticks.extend( (1..) .map(|n| -1.0 * fix(f64::from(n) * step_size)) .take_while(|&v| v >= min) .collect::>() .iter() .rev(), ); ticks.push(0.0); ticks.extend( (1..) .map(|n| fix(f64::from(n) * step_size)) .take_while(|&v| v <= max), ); } else { // entirely negative axis ticks.extend( (0..) .map(|n| -1.0 * fix((f64::from(n) * step_size) - max)) .take_while(|&v| v >= min) .collect::>() .iter() .rev(), ); } } else { // entirely positive axis ticks.extend( (0..) .map(|n| fix((f64::from(n) * step_size) + min)) .take_while(|&v| v <= max), ); } ticks } /// Given a range and a step size, work out how many ticks will be displayed fn number_of_ticks(min: f64, max: f64, step_size: f64) -> usize { generate_ticks(min, max, step_size).len() } /// Given a range of values, and a maximum number of ticks, calulate the step between the ticks fn calculate_tick_step_for_range(min: f64, max: f64, max_ticks: usize) -> f64 { let range = max - min; let min_tick_step = range / max_ticks as f64; // Get the first entry which is our smallest possible tick step size let smallest_valid_step = TickSteps::start_at(min_tick_step) .find(|&s| number_of_ticks(min, max, s) <= max_ticks) .expect("ERROR: We've somehow run out of tick step options!"); // Count how many ticks that relates to let actual_num_ticks = number_of_ticks(min, max, smallest_valid_step); // Create a new TickStep iterator, starting at the correct lower bound let tick_steps = TickSteps::start_at(smallest_valid_step); // Get all the possible tick step sizes that give just as many ticks let step_options = tick_steps.take_while(|&s| number_of_ticks(min, max, s) == actual_num_ticks); // Get the largest tick step size from the list step_options.fold(-1. / 0., f64::max) } /// Given an axis range, calculate the sensible places to place the ticks fn calculate_ticks(min: f64, max: f64, max_ticks: usize) -> Vec { let tick_step = calculate_tick_step_for_range(min, max, max_ticks); generate_ticks(min, max, tick_step) } #[cfg(test)] mod tests { use super::*; #[test] fn test_tick_step_generator() { let t = TickSteps::start_at(1.0); let ts: Vec<_> = t.take(7).collect(); assert_eq!(ts, [1.0, 2.0, 4.0, 5.0, 10.0, 20.0, 40.0]); let t = TickSteps::start_at(100.0); let ts: Vec<_> = t.take(5).collect(); assert_eq!(ts, [100.0, 200.0, 400.0, 500.0, 1000.0]); let t = TickSteps::start_at(3.0); let ts: Vec<_> = t.take(5).collect(); assert_eq!(ts, [4.0, 5.0, 10.0, 20.0, 40.0]); let t = TickSteps::start_at(8.0); let ts: Vec<_> = t.take(3).collect(); assert_eq!(ts, [10.0, 20.0, 40.0]); } #[test] fn test_number_of_ticks() { assert_eq!(number_of_ticks(-7.93, 15.58, 4.0), 5); assert_eq!(number_of_ticks(-7.93, 15.58, 5.0), 5); assert_eq!(number_of_ticks(0.0, 15.0, 4.0), 4); assert_eq!(number_of_ticks(0.0, 15.0, 5.0), 4); assert_eq!(number_of_ticks(5.0, 21.0, 4.0), 5); assert_eq!(number_of_ticks(5.0, 21.0, 5.0), 4); assert_eq!(number_of_ticks(-8.0, 15.58, 4.0), 6); assert_eq!(number_of_ticks(-8.0, 15.58, 5.0), 5); } #[test] fn test_calculate_tick_step_for_range() { assert_eq!(calculate_tick_step_for_range(0.0, 3.0, 6), 1.0); assert_eq!(calculate_tick_step_for_range(0.0, 6.0, 6), 2.0); assert_eq!(calculate_tick_step_for_range(0.0, 11.0, 6), 2.0); assert_eq!(calculate_tick_step_for_range(0.0, 14.0, 6), 4.0); assert_eq!(calculate_tick_step_for_range(0.0, 15.0, 6), 5.0); assert_eq!(calculate_tick_step_for_range(-1.0, 5.0, 6), 2.0); assert_eq!(calculate_tick_step_for_range(-7.93, 15.58, 6), 5.0); assert_eq!(calculate_tick_step_for_range(0.0, 0.06, 6), 0.02); } #[test] fn test_calculate_ticks() { macro_rules! assert_approx_eq { ($a:expr, $b:expr) => {{ let (a, b) = (&$a, &$b); assert!( (*a - *b).abs() < 1.0e-6, "{} is not approximately equal to {}", *a, *b ); }}; } for (prod, want) in calculate_ticks(0.0, 1.0, 6) .iter() .zip([0.0, 0.2, 0.4, 0.6, 0.8, 1.0].iter()) { assert_approx_eq!(prod, want); } for (prod, want) in calculate_ticks(0.0, 2.0, 6) .iter() .zip([0.0, 0.4, 0.8, 1.2, 1.6, 2.0].iter()) { assert_approx_eq!(prod, want); } assert_eq!(calculate_ticks(0.0, 3.0, 6), [0.0, 1.0, 2.0, 3.0]); assert_eq!(calculate_ticks(0.0, 4.0, 6), [0.0, 1.0, 2.0, 3.0, 4.0]); assert_eq!(calculate_ticks(0.0, 5.0, 6), [0.0, 1.0, 2.0, 3.0, 4.0, 5.0]); assert_eq!(calculate_ticks(0.0, 6.0, 6), [0.0, 2.0, 4.0, 6.0]); assert_eq!(calculate_ticks(0.0, 7.0, 6), [0.0, 2.0, 4.0, 6.0]); assert_eq!(calculate_ticks(0.0, 8.0, 6), [0.0, 2.0, 4.0, 6.0, 8.0]); assert_eq!(calculate_ticks(0.0, 9.0, 6), [0.0, 2.0, 4.0, 6.0, 8.0]); assert_eq!( calculate_ticks(0.0, 10.0, 6), [0.0, 2.0, 4.0, 6.0, 8.0, 10.0] ); assert_eq!( calculate_ticks(0.0, 11.0, 6), [0.0, 2.0, 4.0, 6.0, 8.0, 10.0] ); assert_eq!(calculate_ticks(0.0, 12.0, 6), [0.0, 4.0, 8.0, 12.0]); assert_eq!(calculate_ticks(0.0, 13.0, 6), [0.0, 4.0, 8.0, 12.0]); assert_eq!(calculate_ticks(0.0, 14.0, 6), [0.0, 4.0, 8.0, 12.0]); assert_eq!(calculate_ticks(0.0, 15.0, 6), [0.0, 5.0, 10.0, 15.0]); assert_eq!(calculate_ticks(0.0, 16.0, 6), [0.0, 4.0, 8.0, 12.0, 16.0]); assert_eq!(calculate_ticks(0.0, 17.0, 6), [0.0, 4.0, 8.0, 12.0, 16.0]); assert_eq!(calculate_ticks(0.0, 18.0, 6), [0.0, 4.0, 8.0, 12.0, 16.0]); assert_eq!(calculate_ticks(0.0, 19.0, 6), [0.0, 4.0, 8.0, 12.0, 16.0]); assert_eq!( calculate_ticks(0.0, 20.0, 6), [0.0, 4.0, 8.0, 12.0, 16.0, 20.0] ); assert_eq!( calculate_ticks(0.0, 21.0, 6), [0.0, 4.0, 8.0, 12.0, 16.0, 20.0] ); assert_eq!( calculate_ticks(0.0, 22.0, 6), [0.0, 4.0, 8.0, 12.0, 16.0, 20.0] ); assert_eq!( calculate_ticks(0.0, 23.0, 6), [0.0, 4.0, 8.0, 12.0, 16.0, 20.0] ); assert_eq!(calculate_ticks(0.0, 24.0, 6), [0.0, 5.0, 10.0, 15.0, 20.0]); assert_eq!( calculate_ticks(0.0, 25.0, 6), [0.0, 5.0, 10.0, 15.0, 20.0, 25.0] ); assert_eq!( calculate_ticks(0.0, 26.0, 6), [0.0, 5.0, 10.0, 15.0, 20.0, 25.0] ); assert_eq!( calculate_ticks(0.0, 27.0, 6), [0.0, 5.0, 10.0, 15.0, 20.0, 25.0] ); assert_eq!( calculate_ticks(0.0, 28.0, 6), [0.0, 5.0, 10.0, 15.0, 20.0, 25.0] ); assert_eq!( calculate_ticks(0.0, 29.0, 6), [0.0, 5.0, 10.0, 15.0, 20.0, 25.0] ); assert_eq!(calculate_ticks(0.0, 30.0, 6), [0.0, 10.0, 20.0, 30.0]); assert_eq!(calculate_ticks(0.0, 31.0, 6), [0.0, 10.0, 20.0, 30.0]); //... assert_eq!(calculate_ticks(0.0, 40.0, 6), [0.0, 10.0, 20.0, 30.0, 40.0]); assert_eq!( calculate_ticks(0.0, 50.0, 6), [0.0, 10.0, 20.0, 30.0, 40.0, 50.0] ); assert_eq!(calculate_ticks(0.0, 60.0, 6), [0.0, 20.0, 40.0, 60.0]); assert_eq!(calculate_ticks(0.0, 70.0, 6), [0.0, 20.0, 40.0, 60.0]); assert_eq!(calculate_ticks(0.0, 80.0, 6), [0.0, 20.0, 40.0, 60.0, 80.0]); assert_eq!(calculate_ticks(0.0, 90.0, 6), [0.0, 20.0, 40.0, 60.0, 80.0]); assert_eq!( calculate_ticks(0.0, 100.0, 6), [0.0, 20.0, 40.0, 60.0, 80.0, 100.0] ); assert_eq!( calculate_ticks(0.0, 110.0, 6), [0.0, 20.0, 40.0, 60.0, 80.0, 100.0] ); assert_eq!(calculate_ticks(0.0, 120.0, 6), [0.0, 40.0, 80.0, 120.0]); assert_eq!(calculate_ticks(0.0, 130.0, 6), [0.0, 40.0, 80.0, 120.0]); assert_eq!(calculate_ticks(0.0, 140.0, 6), [0.0, 40.0, 80.0, 120.0]); assert_eq!(calculate_ticks(0.0, 150.0, 6), [0.0, 50.0, 100.0, 150.0]); //... assert_eq!( calculate_ticks(0.0, 3475.0, 6), [0.0, 1000.0, 2000.0, 3000.0] ); assert_eq!(calculate_ticks(-11.0, -4.0, 6), [-10.0, -8.0, -6.0, -4.0]); // test rounding assert_eq!(calculate_ticks(1.0, 1.5, 6), [1.0, 1.1, 1.2, 1.3, 1.4, 1.5]); assert_eq!(calculate_ticks(0.0, 1.0, 6), [0.0, 0.2, 0.4, 0.6, 0.8, 1.0]); assert_eq!(calculate_ticks(0.0, 0.3, 4), [0.0, 0.1, 0.2, 0.3]); } } plotlib-0.5.1/src/errors.rs010064400017500000144000000001151363346402200140500ustar0000000000000000use failure; pub type Result = ::std::result::Result; plotlib-0.5.1/src/grid.rs010064400017500000144000000035631363377173200135040ustar0000000000000000#![deny(missing_docs)] //! Configure a grid on a plot. //! //! Grids allow for easier estimating of data values. This module allows the configuration of grids //! on plots. //! //! Grids are created by creating a `Grid` definition, and adding it to a plot: //! //! The grid lines for `plotlib` are rendered //! _underneath_ the data so as to not detract from the data. //! //! # Examples //! //! ```rust //! # use plotlib::view::ContinuousView; //! use plotlib::grid::Grid; //! # use plotlib::style::LineStyle; //! # use plotlib::view::View; //! //! # let l1 = plotlib::repr::Plot::new(vec![(0., 1.), (2., 1.5), (3., 1.2), (4., 1.1)]) //! # .line_style(LineStyle::new().colour("burlywood")); //! // let l1 = Plot::new() ... //! let mut v = ContinuousView::new().add(l1); //! //! // 3 vertical lines and 8 horizontal lines //! v.add_grid(Grid::new(3, 8)); //! //! // Render plot //! ``` // Internal type representing the logic of when do we render only horizontal lines, and when do we // render a full grid pub(crate) enum GridType<'a> { HorizontalOnly(&'a Grid), Both(&'a Grid), } /// Configuration for the grid on a plot /// /// Supports changing the number of grid lines for the x and y dimensions. /// **Note:** for categorical plots, only horizontal lines will be shown. pub struct Grid { /// Number of vertical grid lines (defaults to 3) pub nx: u32, /// Number of horizontal grid lines (defaults to 3) pub ny: u32, /// Color of the grid lines (defaults to "darkgrey") pub color: String, } impl Default for Grid { fn default() -> Self { Grid::new(3, 3) } } impl Grid { /// Create a new grid with `nx` vertical and `ny` horizontal grid lines /// /// The default colour is "darkgrey". pub fn new(nx: u32, ny: u32) -> Grid { Grid { nx, ny, color: "darkgrey".to_owned(), } } } plotlib-0.5.1/src/lib.rs010064400017500000144000000061201363377200100133030ustar0000000000000000/*! # plotlib plotlib is a data plotting and rendering library. ## Usage There are five different types of plot currently supported: 1. Box plot (`plotlib::repr::Box`) 1. Histogram (`plotlib::repr::Histogram`) 1. Line and scatter plot (`plotlib::repr::Plot`) ## Technical Five main components of the plotlib pipeline: 1. Data 2. Representation 3. View 4. Page 5. Rendering **Data** is the plain Rust data structure that the user brings along. This might be something like a `Vec`, an `ndarray` or a slice. This will likely be copied or moved from to construct the *representation*. The **representation** is the transformed version of that data which is the base plot object. Each representation has N dimensions of input and one dimension of output. For example a scatter plot has x-values as inputs and for each of those a y-value as an output. A histogram has bins along one axis as its input and counts (or frequencies) as its output. A surface has x and y values as inputs and a z-value as its output. A function has some input value (mapped from the x-dimension) as its input and some value as its output which is projected onto the y-dimension. Each representation also contains a style which knows how it should look in the abstract. A concrete interpretation of this style is deferred to when the rendering happens. So a scatter plot will know what colour and style to use for the markers, and a histogram will know which colours to use for its bars. The **view** is how you want this data to be presented. Each dimension from the representation is mapped onto an axis. A view can contain multiple representations as long as they can be mapped on to the axes. For example a 2D 'matrix' histogram could be displayed as a flat grid or as a 3D LEGO plot. A **page** is the whole page. It can contain multiple views and specifies how they are laid out. Finally the **rendering** is the actual output. This could be an SVG, a PNG, an ASCII plot or an interactive web page. A rendering will not necessarily be able to show all types of views or representations and may choose to ignore some. This structure allows some data set to be represented multiple ways in one view or for a particular representation to be displayed across more than one view. Example - *Data*: A linear sequence of numbers as a Vec - *Representation*: A binned histogram, stored as a list of `Bin`s, each of which is the bounds and the counts. Blue bars with no casing. - *View*: Dimension 0 mapped to x-axis with range 5-19 and counts mapped to y-axis with range 0-60 - *Page*: A single view on the page - *Rendering*: An SVG It starts from the end and works backwards. The Rendering (an SVG in this case) knows how to layout a *page*. It finds a single view inside and so creates the axes for it. It knows how to draw the axes for the view. It also knows how to draw each representation onto that view, in this case, interpreting the bins and colours to create SVG elements. */ pub mod grid; pub mod page; pub mod repr; pub mod style; pub mod view; mod axis; mod errors; mod svg_render; mod text_render; mod utils; plotlib-0.5.1/src/page.rs010064400017500000144000000052651363400011200134460ustar0000000000000000/*! The `page` module provides structures for laying out and rendering multiple views. */ use std::ffi::OsStr; use std::path::Path; use svg; use svg::Document; use svg::Node; use crate::errors::Result; use crate::view::View; use failure::ResultExt; /** A single page page laying out the views in a grid */ pub struct Page<'a> { views: Vec<&'a dyn View>, num_views: u32, dimensions: (u32, u32), } impl<'a> Page<'a> { /** Creates an empty page container for plots to be added to */ pub fn empty() -> Self { Page { views: Vec::new(), num_views: 0, dimensions: (600, 400), } } /** Creates a plot containing a single view */ pub fn single(view: &'a dyn View) -> Self { Page::empty().add_plot(view) } /// Set the dimensions of the plot. pub fn dimensions(mut self, x: u32, y: u32) -> Self { self.dimensions = (x, y); self } /// Add a view to the plot pub fn add_plot(mut self, view: &'a dyn View) -> Self { self.views.push(view); self.num_views += 1; self } /** Render the plot to an svg document */ pub fn to_svg(&self) -> Result { let (width, height) = self.dimensions; let mut document = Document::new().set("viewBox", (0, 0, width, height)); let x_margin = 120; // should actually depend on y-axis label font size let y_margin = 60; let x_offset = 0.6 * f64::from(x_margin); let y_offset = 0.6 * f64::from(y_margin); // TODO put multiple views in correct places for &view in &self.views { let view_group = view .to_svg(f64::from(width - x_margin), f64::from(height - y_margin))? .set( "transform", format!("translate({}, {})", x_offset, f64::from(height) - y_offset), ); document.append(view_group); } Ok(document) } /** Render the plot to an `String` */ pub fn to_text(&self) -> Result { let (width, height) = self.dimensions; // TODO compose multiple views into a page let view = self.views[0]; view.to_text(width, height) } /** Save the plot to a file. The type of file will be based on the file extension. */ pub fn save

(&self, path: P) -> Result<()> where P: AsRef, { match path.as_ref().extension().and_then(OsStr::to_str) { Some("svg") => svg::save(path, &self.to_svg()?) .context("saving svg") .map_err(From::from), _ => Ok(()), } } } plotlib-0.5.1/src/render.rs010064400017500000144000000012501363346402200140140ustar0000000000000000use crate::scatter; use crate::repr; use crate::text_render; use crate::svg_render; pub trait Render { fn to_svg(&self) -> svg_render::SVG; fn to_text(&self) -> text_render::Text; } impl Render for scatter::Scatter { fn to_text(&self) -> text_render::Text { text_render::Text {data: text_render::draw_scatter(self)} } fn to_svg(&self) -> svg_render::SVG { svg_render::draw_scatter(self) } } impl Render for repr::Histogram { fn to_text(&self) -> text_render::Text { text_render::Text {data: text_render::draw_histogram(self)} } fn to_svg(&self) -> svg_render::SVG { svg_render::draw_histogram(self) } } plotlib-0.5.1/src/repr/barchart.rs010064400017500000144000000040071363346402200152760ustar0000000000000000/*! Bar chart # Examples ``` # use plotlib::repr::BarChart; # use plotlib::view::CategoricalView; let b1 = BarChart::new(5.2).label("b1"); let b2 = BarChart::new(1.6).label("b2"); let v = CategoricalView::new().add(b1).add(b2); ``` */ use std::f64; use svg; use crate::axis; use crate::repr::CategoricalRepresentation; use crate::style::BoxStyle; use crate::svg_render; pub struct BarChart { value: f64, label: String, style: BoxStyle, } impl BarChart { pub fn new(v: f64) -> Self { BarChart { value: v, style: BoxStyle::new(), label: String::new(), } } pub fn style(mut self, style: &BoxStyle) -> Self { self.style.overlay(style); self } pub fn get_style(&self) -> &BoxStyle { &self.style } pub fn label(mut self, label: T) -> Self where T: Into, { self.label = label.into(); self } pub fn get_label(&self) -> &String { &self.label } fn get_value(&self) -> f64 { self.value } } impl CategoricalRepresentation for BarChart { /// The maximum range. Used for auto-scaling axis fn range(&self) -> (f64, f64) { (0.0, self.value) } /// The ticks that this representation covers. Used to collect all ticks for display fn ticks(&self) -> Vec { vec![self.label.clone()] } fn to_svg( &self, x_axis: &axis::CategoricalAxis, y_axis: &axis::ContinuousAxis, face_width: f64, face_height: f64, ) -> svg::node::element::Group { svg_render::draw_face_barchart( self.get_value(), &self.label, x_axis, y_axis, face_width, face_height, &self.style, ) } fn to_text( &self, _x_axis: &axis::CategoricalAxis, _y_axis: &axis::ContinuousAxis, _face_width: u32, _face_height: u32, ) -> String { "".into() } } plotlib-0.5.1/src/repr/boxplot.rs010064400017500000144000000051321363352207000151750ustar0000000000000000/*! Box plot # Examples ``` # use plotlib::repr::BoxPlot; # use plotlib::view::CategoricalView; let b1 = BoxPlot::from_slice(&[0., 2., 3., 4.]); let b2 = BoxPlot::from_vec(vec![0., 2., 3., 4.]); let v = CategoricalView::new().add(b1); ``` */ use std::f64; use svg; use crate::axis; use crate::repr::CategoricalRepresentation; use crate::style::BoxStyle; use crate::svg_render; use crate::utils; enum BoxData<'a> { Owned(Vec), Ref(&'a [f64]), } pub struct BoxPlot<'a> { data: BoxData<'a>, label: String, style: BoxStyle, } impl<'a> BoxPlot<'a> { pub fn from_slice(v: &'a [f64]) -> Self { BoxPlot { data: BoxData::Ref(v), style: BoxStyle::new(), label: String::new(), } } pub fn from_vec(v: Vec) -> Self { BoxPlot { data: BoxData::Owned(v), style: BoxStyle::new(), label: String::new(), } } pub fn style(mut self, style: &BoxStyle) -> Self { self.style.overlay(style); self } pub fn get_style(&self) -> &BoxStyle { &self.style } pub fn label(mut self, label: T) -> Self where T: Into, { self.label = label.into(); self } pub fn get_label(&self) -> &String { &self.label } fn get_data(&'a self) -> &'a [f64] { match self.data { BoxData::Owned(ref v) => v, BoxData::Ref(v) => v, } } fn range(&self) -> (f64, f64) { match self.data { BoxData::Owned(ref v) => utils::range(v), BoxData::Ref(v) => utils::range(v), } } } impl<'a> CategoricalRepresentation for BoxPlot<'a> { /// The maximum range. Used for auto-scaling axis fn range(&self) -> (f64, f64) { self.range() } /// The ticks that this representation covers. Used to collect all ticks for display fn ticks(&self) -> Vec { vec![self.label.clone()] } fn to_svg( &self, x_axis: &axis::CategoricalAxis, y_axis: &axis::ContinuousAxis, face_width: f64, face_height: f64, ) -> svg::node::element::Group { svg_render::draw_face_boxplot( self.get_data(), &self.label, x_axis, y_axis, face_width, face_height, &self.style, ) } fn to_text( &self, _x_axis: &axis::CategoricalAxis, _y_axis: &axis::ContinuousAxis, _face_width: u32, _face_height: u32, ) -> String { "".into() } } plotlib-0.5.1/src/repr/histogram.rs010064400017500000144000000127611363352603400155150ustar0000000000000000/*! A module for Histograms # Examples ``` # use plotlib::repr::Histogram; // Create some dummy data let data = vec![0.3, 0.5, 6.4, 5.3, 3.6, 3.6, 3.5, 7.5, 4.0]; // and create a histogram out of it let h = Histogram::from_slice(&data, plotlib::repr::HistogramBins::Count(30)); ``` TODO: - frequency or density option - Variable bins implies frequency - What should be the default? */ use std; use svg; use crate::axis; use crate::repr::ContinuousRepresentation; use crate::style::BoxStyle; use crate::svg_render; use crate::text_render; use crate::utils::PairWise; #[derive(Debug)] enum HistogramType { Count, Density, } #[derive(Debug)] pub enum HistogramBins { Count(usize), Bounds(Vec), } /** A one-dimensional histogram with equal binning. */ #[derive(Debug)] pub struct Histogram { pub bin_bounds: Vec, // will have N_bins + 1 entries pub bin_counts: Vec, // will have N_bins entries pub bin_densities: Vec, // will have N_bins entries style: BoxStyle, h_type: HistogramType, } impl Histogram { pub fn from_slice(v: &[f64], bins: HistogramBins) -> Histogram { let mut max = v.iter().fold(-1. / 0., |a, &b| f64::max(a, b)); let mut min = v.iter().fold(1. / 0., |a, &b| f64::min(a, b)); if (min - max).abs() < std::f64::EPSILON { min -= 0.5; max += 0.5; } let (num_bins, bounds) = match bins { HistogramBins::Count(num_bins) => { let range = max - min; let mut bounds: Vec = (0..num_bins) .map(|n| (n as f64 / num_bins as f64) * range + min) .collect(); bounds.push(max); (num_bins, bounds) } HistogramBins::Bounds(bounds) => (bounds.len(), bounds), }; let mut bins = vec![0; num_bins]; let bin_width = (max - min) / num_bins as f64; // width of bin in real units for &val in v.iter() { let bin = bounds .pairwise() .enumerate() .skip_while(|&(_, (&l, &u))| !(val >= l && val <= u)) .map(|(i, (_, _))| i) .next() .unwrap(); bins[bin] += 1; } let density_per_bin = bins.iter().map(|&x| f64::from(x) / bin_width).collect(); Histogram { bin_bounds: bounds, bin_counts: bins.iter().map(|&x| f64::from(x)).collect(), bin_densities: density_per_bin, style: BoxStyle::new(), h_type: HistogramType::Count, } } pub fn num_bins(&self) -> usize { self.bin_counts.len() } fn x_range(&self) -> (f64, f64) { ( *self.bin_bounds.first().unwrap(), *self.bin_bounds.last().unwrap(), ) } fn y_range(&self) -> (f64, f64) { let max = self .get_values() .iter() .fold(-1. / 0., |a, &b| f64::max(a, b)); (0., max) } pub fn style(mut self, style: &BoxStyle) -> Self { self.style.overlay(style); self } /** Set the histogram to display as normalised densities */ pub fn density(mut self) -> Self { self.h_type = HistogramType::Density; self } pub fn get_style(&self) -> &BoxStyle { &self.style } pub fn get_values(&self) -> &[f64] { match self.h_type { HistogramType::Count => &self.bin_counts, HistogramType::Density => &self.bin_densities, } } } impl ContinuousRepresentation for Histogram { fn range(&self, dim: u32) -> (f64, f64) { match dim { 0 => self.x_range(), 1 => self.y_range(), _ => panic!("Axis out of range"), } } fn to_svg( &self, x_axis: &axis::ContinuousAxis, y_axis: &axis::ContinuousAxis, face_width: f64, face_height: f64, ) -> svg::node::element::Group { svg_render::draw_face_bars(self, x_axis, y_axis, face_width, face_height, &self.style) } fn legend_svg(&self) -> Option { // TODO implement None } fn to_text( &self, x_axis: &axis::ContinuousAxis, y_axis: &axis::ContinuousAxis, face_width: u32, face_height: u32, ) -> String { text_render::render_face_bars(self, x_axis, y_axis, face_width, face_height) } } #[cfg(test)] mod tests { use super::*; #[test] fn test_histogram_from_slice() { assert_eq!( Histogram::from_slice(&[], HistogramBins::Count(3)).get_values(), [0., 0., 0.] ); assert_eq!( Histogram::from_slice(&[0.], HistogramBins::Count(3)).get_values(), [0., 1., 0.] ); assert_eq!( Histogram::from_slice(&[0., 3.], HistogramBins::Count(3)).get_values(), [1., 0., 1.] ); assert_eq!( Histogram::from_slice(&[0., 1., 2., 3.], HistogramBins::Count(3)).get_values(), [2., 1., 1.] ); } #[test] fn test_histogram_define_bin_bounds() { assert_eq!( Histogram::from_slice(&[0., 1.], HistogramBins::Count(3)).bin_bounds, [0., 1. / 3., 2. / 3., 1.] ); assert_eq!( Histogram::from_slice(&[], HistogramBins::Bounds([0., 1., 1.5, 2., 5.6].to_vec())) .bin_bounds, [0., 1., 1.5, 2., 5.6] ); } } plotlib-0.5.1/src/repr/mod.rs010064400017500000144000000043351363346402200142730ustar0000000000000000/*! *Representations* are the interface between the data coming from the user and the rendered output. Each type that implements `Representation` or `CategoricalRepresentation` knows how to read in data and convert that into a concrete element to be incorporated into a larger plot. For example the `scatter::Scatter` representation can be created from a list of coordinates. When `to_svg()` is called on it, it will create the SVG elements showing the points from within the range that was requested by the caller. These points may then be layered with other SVG elements from other representations into a `view::View`. */ use crate::axis; mod barchart; mod boxplot; mod histogram; mod plot; pub use barchart::*; pub use boxplot::*; pub use histogram::*; pub use plot::*; /** A representation of data that is continuous in two dimensions. */ pub trait ContinuousRepresentation { /// The maximum range in each dimension. Used for auto-scaling axes. fn range(&self, dim: u32) -> (f64, f64); fn to_svg( &self, x_axis: &axis::ContinuousAxis, y_axis: &axis::ContinuousAxis, face_width: f64, face_height: f64, ) -> svg::node::element::Group; /// Returns None if no legend has been specified for this representation fn legend_svg(&self) -> Option; fn to_text( &self, x_axis: &axis::ContinuousAxis, y_axis: &axis::ContinuousAxis, face_width: u32, face_height: u32, ) -> String; } /** A representation of data that is categorical in the x-axis but continuous in the y-axis. */ pub trait CategoricalRepresentation { /// The maximum range in the y-axis. Used for auto-scaling the axis. fn range(&self) -> (f64, f64); /// The ticks that this representation covers. Used to collect all ticks for display. fn ticks(&self) -> Vec; fn to_svg( &self, x_axis: &axis::CategoricalAxis, y_axis: &axis::ContinuousAxis, face_width: f64, face_height: f64, ) -> svg::node::element::Group; fn to_text( &self, x_axis: &axis::CategoricalAxis, y_axis: &axis::ContinuousAxis, face_width: u32, face_height: u32, ) -> String; } plotlib-0.5.1/src/repr/plot.rs010064400017500000144000000131551363400562300144710ustar0000000000000000//! Plot line charts //! # Examples //! ``` //! # use plotlib::repr::Plot; //! # use plotlib::view::ContinuousView; //! // y=x^2 between 0 and 10 //! let l = Plot::new(vec![(0., 1.), (2., 1.5), (3., 1.2), (4., 1.1)]); //! let v = ContinuousView::new().add(l); //! ``` use std::f64; use svg; use svg::node; use svg::Node; use crate::axis; use crate::repr::ContinuousRepresentation; use crate::style::*; use crate::svg_render; use crate::text_render; /// Representation of any plot with points in the XY plane, visualized as points and/or with lines /// in-between. #[derive(Debug, Clone)] pub struct Plot { pub data: Vec<(f64, f64)>, /// None if no lines should be displayed pub line_style: Option, /// None if no points should be displayed pub point_style: Option, pub legend: Option, } impl Plot { pub fn new(data: Vec<(f64, f64)>) -> Self { Plot { data, line_style: None, point_style: None, legend: None, } } pub fn from_function f64>(f: F, lower: f64, upper: f64) -> Self { let sampling = (upper - lower) / 200.; let samples = (0..) .map(|x| lower + (f64::from(x) * sampling)) .take_while(|&x| x <= upper); let values = samples.map(|s| (s, f(s))).collect(); Plot { data: values, line_style: None, point_style: None, legend: None, } } pub fn line_style(mut self, other: LineStyle) -> Self { if let Some(ref mut self_style) = self.line_style { self_style.overlay(&other); } else { self.line_style = Some(other); } self } pub fn point_style(mut self, other: PointStyle) -> Self { if let Some(ref mut self_style) = self.point_style { self_style.overlay(&other); } else { self.point_style = Some(other); } self } pub fn legend(mut self, legend: String) -> Self { self.legend = Some(legend); self } fn x_range(&self) -> (f64, f64) { let mut min = f64::INFINITY; let mut max = f64::NEG_INFINITY; for &(x, _) in &self.data { min = min.min(x); max = max.max(x); } (min, max) } fn y_range(&self) -> (f64, f64) { let mut min = f64::INFINITY; let mut max = f64::NEG_INFINITY; for &(_, y) in &self.data { min = min.min(y); max = max.max(y); } (min, max) } } impl ContinuousRepresentation for Plot { fn range(&self, dim: u32) -> (f64, f64) { match dim { 0 => self.x_range(), 1 => self.y_range(), _ => panic!("Axis out of range"), } } fn to_svg( &self, x_axis: &axis::ContinuousAxis, y_axis: &axis::ContinuousAxis, face_width: f64, face_height: f64, ) -> svg::node::element::Group { let mut group = node::element::Group::new(); if let Some(ref line_style) = self.line_style { group.append(svg_render::draw_face_line( &self.data, x_axis, y_axis, face_width, face_height, line_style, )) } if let Some(ref point_style) = self.point_style { group.append(svg_render::draw_face_points( &self.data, x_axis, y_axis, face_width, face_height, point_style, )) } group } fn legend_svg(&self) -> Option { // TODO: add points // TODO: can we use common functionality with svg_render? self.legend.as_ref().map(|legend| { let legend = legend.clone(); let mut group = node::element::Group::new(); const FONT_SIZE: f32 = 12.0; // Draw legend text let legend_text = node::element::Text::new() .set("x", 0) .set("y", 0) .set("text-anchor", "start") .set("font-size", FONT_SIZE) .add(node::Text::new(legend)); group.append(legend_text); if let Some(ref style) = self.line_style { let line = node::element::Line::new() .set("x1", -10) .set("y1", -FONT_SIZE / 2. + 2.) .set("x2", -3) .set("y2", -FONT_SIZE / 2. + 2.) .set("stroke-width", style.get_width()) .set("stroke", style.get_colour()); group.append(line); } group }) } fn to_text( &self, x_axis: &axis::ContinuousAxis, y_axis: &axis::ContinuousAxis, face_width: u32, face_height: u32, ) -> String { let face_lines = if let Some(line_style) = &self.line_style { unimplemented!("Text rendering does not yet support line plots") } else { text_render::empty_face(face_width, face_height) }; let face_points = if let Some(point_style) = &self.point_style { text_render::render_face_points( &self.data, x_axis, y_axis, face_width, face_height, &point_style, ) } else { text_render::empty_face(face_width, face_height) }; text_render::overlay(&face_lines, &face_points, 0, 0) } } plotlib-0.5.1/src/save.rs010064400017500000144000000001361363321433100134710ustar0000000000000000use std::path::Path; pub trait Save { fn save

(&self, path: P) where P: AsRef; } plotlib-0.5.1/src/style.rs010064400017500000144000000104751363352603400137100ustar0000000000000000//! Manage how elements should be drawn //! All style structs follows the 'optional builder' pattern: //! Each field is a `Option` which start as `None`. //! They can all be set with setter methods, and instances //! can be overlaid with another one to set many at once. //! Settings will be cloned in and out of it. /// The style that line corners should use #[derive(Debug, Clone, Copy)] pub enum LineJoin { Miter, Round, } #[derive(Debug, Default, Clone)] pub struct LineStyle { pub colour: Option, pub width: Option, pub linejoin: Option, } impl LineStyle { pub fn new() -> Self { LineStyle { colour: None, width: None, linejoin: None, } } pub fn overlay(&mut self, other: &Self) { if let Some(ref v) = other.colour { self.colour = Some(v.clone()) } if let Some(ref v) = other.width { self.width = Some(*v) } if let Some(ref v) = other.linejoin { self.linejoin = Some(*v) } } pub fn colour(mut self, value: T) -> Self where T: Into, { self.colour = Some(value.into()); self } pub fn get_colour(&self) -> String { self.colour.clone().unwrap_or_else(|| "black".into()) } pub fn width(mut self, value: T) -> Self where T: Into, { self.width = Some(value.into()); self } pub fn get_width(&self) -> f32 { self.width.unwrap_or_else(|| 2.0) } pub fn linejoin(mut self, value: T) -> Self where T: Into, { self.linejoin = Some(value.into()); self } pub fn get_linejoin(&self) -> LineJoin { self.linejoin.unwrap_or_else(|| LineJoin::Round) } } /// The marker that should be used for the points of the scatter plot #[derive(Debug, Clone, Copy)] pub enum PointMarker { Circle, Square, Cross, } #[derive(Debug, Default, Clone)] pub struct PointStyle { marker: Option, colour: Option, size: Option, } impl PointStyle { pub fn new() -> Self { PointStyle { marker: None, colour: None, size: None, } } pub fn overlay(&mut self, other: &Self) { if let Some(ref v) = other.marker { self.marker = Some(*v) } if let Some(ref v) = other.colour { self.colour = Some(v.clone()) } if let Some(v) = other.size { self.size = Some(v) } } pub fn marker(mut self, value: T) -> Self where T: Into, { self.marker = Some(value.into()); self } pub fn get_marker(&self) -> PointMarker { self.marker.unwrap_or(PointMarker::Circle) } pub fn colour(mut self, value: T) -> Self where T: Into, { self.colour = Some(value.into()); self } pub fn get_colour(&self) -> String { self.colour.clone().unwrap_or_else(|| "".into()) } pub fn size(mut self, value: T) -> Self where T: Into, { self.size = Some(value.into()); self } pub fn get_size(&self) -> f32 { self.size.unwrap_or(5.0) } } #[derive(Debug, Default)] pub struct BoxStyle { fill: Option, } impl BoxStyle { pub fn new() -> Self { BoxStyle { fill: None } } pub fn overlay(&mut self, other: &Self) { if let Some(ref v) = other.fill { self.fill = Some(v.clone()) } } pub fn fill(mut self, value: T) -> Self where T: Into, { self.fill = Some(value.into()); self } pub fn get_fill(&self) -> String { self.fill.clone().unwrap_or_else(|| "".into()) } } #[cfg(test)] mod tests { use super::*; #[test] fn test_linestyle_plain_overlay() { let mut p = LineStyle::new(); p.overlay( &LineStyle::new() .colour("red") .linejoin(LineJoin::Miter) .width(1.), ); assert_eq!(p.get_colour(), "red".to_string()); assert_eq!(p.get_width(), 1.); if let LineJoin::Miter = p.get_linejoin() { } else { panic!() } } } plotlib-0.5.1/src/svg_render.rs010064400017500000144000000354071363352603400147100ustar0000000000000000use std; use svg::node; use svg::Node; use crate::axis; use crate::grid::GridType; use crate::repr; use crate::style; use crate::utils; use crate::utils::PairWise; fn value_to_face_offset(value: f64, axis: &axis::ContinuousAxis, face_size: f64) -> f64 { let range = axis.max() - axis.min(); (face_size * (value - axis.min())) / range } fn vertical_line(xpos: f64, ymin: f64, ymax: f64, color: S) -> node::element::Line where S: AsRef, { node::element::Line::new() .set("x1", xpos) .set("x2", xpos) .set("y1", ymin) .set("y2", ymax) .set("stroke", color.as_ref()) .set("stroke-width", 1) } fn horizontal_line(ypos: f64, xmin: f64, xmax: f64, color: S) -> node::element::Line where S: AsRef, { node::element::Line::new() .set("x1", xmin) .set("x2", xmax) .set("y1", ypos) .set("y2", ypos) .set("stroke", color.as_ref()) .set("stroke-width", 1) } pub fn draw_x_axis(a: &axis::ContinuousAxis, face_width: f64) -> node::element::Group { let axis_line = horizontal_line(0.0, 0.0, face_width, "black"); let mut ticks = node::element::Group::new(); let mut labels = node::element::Group::new(); for &tick in a.ticks().iter() { let tick_pos = value_to_face_offset(tick, a, face_width); let tick_mark = node::element::Line::new() .set("x1", tick_pos) .set("y1", 0) .set("x2", tick_pos) .set("y2", 10) .set("stroke", "black") .set("stroke-width", 1); ticks.append(tick_mark); let tick_label = node::element::Text::new() .set("x", tick_pos) .set("y", 20) .set("text-anchor", "middle") .set("font-size", 12) .add(node::Text::new(tick.to_string())); labels.append(tick_label); } let label = node::element::Text::new() .set("x", face_width / 2.) .set("y", 30) .set("text-anchor", "middle") .set("font-size", 12) .add(node::Text::new(a.get_label())); node::element::Group::new() .add(ticks) .add(axis_line) .add(labels) .add(label) } pub fn draw_y_axis(a: &axis::ContinuousAxis, face_height: f64) -> node::element::Group { let axis_line = vertical_line(0.0, 0.0, -face_height, "black"); let mut ticks = node::element::Group::new(); let mut labels = node::element::Group::new(); let y_tick_font_size = 12; for &tick in a.ticks().iter() { let tick_pos = value_to_face_offset(tick, a, face_height); let tick_mark = node::element::Line::new() .set("x1", 0) .set("y1", -tick_pos) .set("x2", -10) .set("y2", -tick_pos) .set("stroke", "black") .set("stroke-width", 1); ticks.append(tick_mark); let tick_label = node::element::Text::new() .set("x", -15) .set("y", -tick_pos) .set("text-anchor", "end") .set("dominant-baseline", "middle") .set("font-size", y_tick_font_size) .add(node::Text::new(tick.to_string())); labels.append(tick_label); } let max_tick_length = a .ticks() .iter() .map(|&t| t.to_string().len()) .max() .expect("Could not calculate max tick length"); let x_offset = -(y_tick_font_size * max_tick_length as i32); let y_label_offset = -(face_height / 2.); let y_label_font_size = 12; let label = node::element::Text::new() .set("x", x_offset) .set("y", y_label_offset - f64::from(y_label_font_size)) .set("text-anchor", "middle") .set("font-size", y_label_font_size) .set( "transform", format!("rotate(-90 {} {})", x_offset, y_label_offset), ) .add(node::Text::new(a.get_label())); node::element::Group::new() .add(ticks) .add(axis_line) .add(labels) .add(label) } pub fn draw_categorical_x_axis(a: &axis::CategoricalAxis, face_width: f64) -> node::element::Group { let axis_line = node::element::Line::new() .set("x1", 0) .set("y1", 0) .set("x2", face_width) .set("y2", 0) .set("stroke", "black") .set("stroke-width", 1); let mut ticks = node::element::Group::new(); let mut labels = node::element::Group::new(); let space_per_tick = face_width / a.ticks().len() as f64; for (i, tick) in a.ticks().iter().enumerate() { let tick_pos = (i as f64 * space_per_tick) + (0.5 * space_per_tick); let tick_mark = node::element::Line::new() .set("x1", tick_pos) .set("y1", 0) .set("x2", tick_pos) .set("y2", 10) .set("stroke", "black") .set("stroke-width", 1); ticks.append(tick_mark); let tick_label = node::element::Text::new() .set("x", tick_pos) .set("y", 20) .set("text-anchor", "middle") .set("font-size", 12) .add(node::Text::new(tick.to_owned())); labels.append(tick_label); } let label = node::element::Text::new() .set("x", face_width / 2.) .set("y", 30) .set("text-anchor", "middle") .set("font-size", 12) .add(node::Text::new(a.get_label())); node::element::Group::new() .add(ticks) .add(axis_line) .add(labels) .add(label) } pub fn draw_face_points( s: &[(f64, f64)], x_axis: &axis::ContinuousAxis, y_axis: &axis::ContinuousAxis, face_width: f64, face_height: f64, style: &style::PointStyle, ) -> node::element::Group { let mut group = node::element::Group::new(); for &(x, y) in s { let x_pos = value_to_face_offset(x, x_axis, face_width); let y_pos = -value_to_face_offset(y, y_axis, face_height); let radius = f64::from(style.get_size()); match style.get_marker() { style::PointMarker::Circle => { group.append( node::element::Circle::new() .set("cx", x_pos) .set("cy", y_pos) .set("r", radius) .set("fill", style.get_colour()), ); } style::PointMarker::Square => { group.append( node::element::Rectangle::new() .set("x", x_pos - radius) .set("y", y_pos - radius) .set("width", 2. * radius) .set("height", 2. * radius) .set("fill", style.get_colour()), ); } style::PointMarker::Cross => { let path = node::element::path::Data::new() .move_to((x_pos - radius, y_pos - radius)) .line_by((radius * 2., radius * 2.)) .move_by((-radius * 2., 0)) .line_by((radius * 2., -radius * 2.)) .close(); group.append( node::element::Path::new() .set("fill", "none") .set("stroke", style.get_colour()) .set("stroke-width", 2) .set("d", path), ); } }; } group } pub fn draw_face_bars( h: &repr::Histogram, x_axis: &axis::ContinuousAxis, y_axis: &axis::ContinuousAxis, face_width: f64, face_height: f64, style: &style::BoxStyle, ) -> node::element::Group { let mut group = node::element::Group::new(); for ((&l, &u), &count) in h.bin_bounds.pairwise().zip(h.get_values()) { let l_pos = value_to_face_offset(l, x_axis, face_width); let u_pos = value_to_face_offset(u, x_axis, face_width); let width = u_pos - l_pos; let count_scaled = value_to_face_offset(count, y_axis, face_height); let rect = node::element::Rectangle::new() .set("x", l_pos) .set("y", -count_scaled) .set("width", width) .set("height", count_scaled) .set("fill", style.get_fill()) .set("stroke", "black"); group.append(rect); } group } pub fn draw_face_line( s: &[(f64, f64)], x_axis: &axis::ContinuousAxis, y_axis: &axis::ContinuousAxis, face_width: f64, face_height: f64, style: &style::LineStyle, ) -> node::element::Group { let mut group = node::element::Group::new(); let mut d: Vec = vec![]; let &(first_x, first_y) = s.first().unwrap(); let first_x_pos = value_to_face_offset(first_x, x_axis, face_width); let first_y_pos = -value_to_face_offset(first_y, y_axis, face_height); d.push(node::element::path::Command::Move( node::element::path::Position::Absolute, (first_x_pos, first_y_pos).into(), )); for &(x, y) in s { let x_pos = value_to_face_offset(x, x_axis, face_width); let y_pos = -value_to_face_offset(y, y_axis, face_height); d.push(node::element::path::Command::Line( node::element::path::Position::Absolute, (x_pos, y_pos).into(), )); } let path = node::element::path::Data::from(d); group.append( node::element::Path::new() .set("fill", "none") .set("stroke", style.get_colour()) .set("stroke-width", style.get_width()) .set( "stroke-linejoin", match style.get_linejoin() { style::LineJoin::Miter => "miter", style::LineJoin::Round => "round", }, ) .set("d", path), ); group } pub fn draw_face_boxplot( d: &[f64], label: &L, x_axis: &axis::CategoricalAxis, y_axis: &axis::ContinuousAxis, face_width: f64, face_height: f64, style: &style::BoxStyle, ) -> node::element::Group where L: Into, String: std::cmp::PartialEq, { let mut group = node::element::Group::new(); let tick_index = x_axis.ticks().iter().position(|t| t == label).unwrap(); // TODO this should raise an error let space_per_tick = face_width / x_axis.ticks().len() as f64; let tick_pos = (tick_index as f64 * space_per_tick) + (0.5 * space_per_tick); let box_width = space_per_tick / 2.; let (q1, median, q3) = utils::quartiles(d); let box_start = -value_to_face_offset(q3, y_axis, face_height); let box_end = -value_to_face_offset(q1, y_axis, face_height); group.append( node::element::Rectangle::new() .set("x", tick_pos - (box_width / 2.)) .set("y", box_start) .set("width", box_width) .set("height", box_end - box_start) .set("fill", style.get_fill()) .set("stroke", "black"), ); let mid_line = -value_to_face_offset(median, y_axis, face_height); group.append( node::element::Line::new() .set("x1", tick_pos - (box_width / 2.)) .set("y1", mid_line) .set("x2", tick_pos + (box_width / 2.)) .set("y2", mid_line) .set("stroke", "black"), ); let (min, max) = utils::range(d); let whisker_bottom = -value_to_face_offset(min, y_axis, face_height); let whisker_top = -value_to_face_offset(max, y_axis, face_height); group.append( node::element::Line::new() .set("x1", tick_pos) .set("y1", whisker_bottom) .set("x2", tick_pos) .set("y2", box_end) .set("stroke", "black"), ); group.append( node::element::Line::new() .set("x1", tick_pos) .set("y1", whisker_top) .set("x2", tick_pos) .set("y2", box_start) .set("stroke", "black"), ); group } pub fn draw_face_barchart( d: f64, label: &L, x_axis: &axis::CategoricalAxis, y_axis: &axis::ContinuousAxis, face_width: f64, face_height: f64, style: &style::BoxStyle, ) -> node::element::Group where L: Into, String: std::cmp::PartialEq, { let mut group = node::element::Group::new(); let tick_index = x_axis.ticks().iter().position(|t| t == label).unwrap(); // TODO this should raise an error let space_per_tick = face_width / x_axis.ticks().len() as f64; let tick_pos = (tick_index as f64 * space_per_tick) + (0.5 * space_per_tick); let box_width = space_per_tick / 2.; let box_start = -value_to_face_offset(d, y_axis, face_height); let box_end = -value_to_face_offset(0.0, y_axis, face_height); group.append( node::element::Rectangle::new() .set("x", tick_pos - (box_width / 2.)) .set("y", box_start) .set("width", box_width) .set("height", box_end - box_start) .set("fill", style.get_fill()) .set("stroke", "black"), ); group } pub(crate) fn draw_grid(grid: GridType, face_width: f64, face_height: f64) -> node::element::Group { match grid { GridType::HorizontalOnly(grid) => { let (ymin, ymax) = (0f64, face_height); let y_step = (ymax - ymin) / f64::from(grid.ny); let mut lines = node::element::Group::new(); for iy in 0..=grid.ny { let y = f64::from(iy) * y_step + ymin; let line = horizontal_line(-y, 0.0, face_width, grid.color.as_str()); lines = lines.add(line); } lines } GridType::Both(grid) => { let (xmin, xmax) = (0f64, face_width); let (ymin, ymax) = (0f64, face_height); let x_step = (xmax - xmin) / f64::from(grid.nx); let y_step = (ymax - ymin) / f64::from(grid.ny); let mut lines = node::element::Group::new(); for iy in 0..=grid.ny { let y = f64::from(iy) * y_step + ymin; let line = horizontal_line(-y, 0.0, face_width, grid.color.as_str()); lines = lines.add(line); } for ix in 0..=grid.nx { let x = f64::from(ix) * x_step + xmin; let line = vertical_line(x, 0.0, -face_height, grid.color.as_str()); lines = lines.add(line); } lines } } } #[cfg(test)] mod tests { use super::*; #[test] fn test_value_to_face_offset() { let axis = axis::ContinuousAxis::new(-2., 5., 6); assert_eq!(value_to_face_offset(-2.0, &axis, 14.0), 0.0); assert_eq!(value_to_face_offset(5.0, &axis, 14.0), 14.0); assert_eq!(value_to_face_offset(0.0, &axis, 14.0), 4.0); assert_eq!(value_to_face_offset(-4.0, &axis, 14.0), -4.0); assert_eq!(value_to_face_offset(7.0, &axis, 14.0), 18.0); } } plotlib-0.5.1/src/text_render.rs010064400017500000144000000542541363400563200150730ustar0000000000000000//! A module for plotting graphs use std; use std::collections::HashMap; use crate::axis; use crate::repr; use crate::style; use crate::utils::PairWise; // Given a value like a tick label or a bin count, // calculate how far from the x-axis it should be plotted fn value_to_axis_cell_offset(value: f64, axis: &axis::ContinuousAxis, face_cells: u32) -> i32 { let data_per_cell = (axis.max() - axis.min()) / f64::from(face_cells); ((value - axis.min()) / data_per_cell).round() as i32 } /// Given a list of ticks to display, /// the total scale of the axis /// and the number of face cells to work with, /// create a mapping of cell offset to tick value fn tick_offset_map(axis: &axis::ContinuousAxis, face_width: u32) -> HashMap { axis.ticks() .iter() .map(|&tick| (value_to_axis_cell_offset(tick, axis, face_width), tick)) .collect() } /// Given a histogram object, /// the total scale of the axis /// and the number of face cells to work with, /// return which cells will contain a bin bound fn bound_cell_offsets( hist: &repr::Histogram, x_axis: &axis::ContinuousAxis, face_width: u32, ) -> Vec { hist.bin_bounds .iter() .map(|&bound| value_to_axis_cell_offset(bound, x_axis, face_width)) .collect() } /// calculate for each cell which bin it is representing /// Cells which straddle bins will return the bin just on the lower side of the centre of the cell /// Will return a vector with (`face_width + 2`) entries to represent underflow and overflow cells /// cells which do not map to a bin will return `None`. fn bins_for_cells(bound_cell_offsets: &[i32], face_width: u32) -> Vec> { let bound_cells = bound_cell_offsets; let bin_width_in_cells = bound_cells.pairwise().map(|(&a, &b)| b - a); let bins_cell_offset = bound_cells.first().unwrap(); let mut cell_bins: Vec> = vec![None]; // start with a prepended negative null for (bin, width) in bin_width_in_cells.enumerate() { // repeat bin, width times for _ in 0..width { cell_bins.push(Some(bin as i32)); } } cell_bins.push(None); // end with an appended positive null if *bins_cell_offset <= 0 { cell_bins = cell_bins .iter() .skip(bins_cell_offset.wrapping_abs() as usize) .cloned() .collect(); } else { let mut new_bins = vec![None; (*bins_cell_offset) as usize]; new_bins.extend(cell_bins.iter()); cell_bins = new_bins; } if cell_bins.len() <= face_width as usize + 2 { let deficit = face_width as usize + 2 - cell_bins.len(); let mut new_bins = cell_bins; new_bins.extend(vec![None; deficit].iter()); cell_bins = new_bins; } else { let new_bins = cell_bins; cell_bins = new_bins .iter() .take(face_width as usize + 2) .cloned() .collect(); } cell_bins } /// An x-axis label for the text output renderer #[derive(Debug)] struct XAxisLabel { text: String, offset: i32, } impl XAxisLabel { fn len(&self) -> usize { self.text.len() } /// The number of cells the label will actually use /// We want this to always be an odd number fn footprint(&self) -> usize { if self.len() % 2 == 0 { self.len() + 1 } else { self.len() } } /// The offset, relative to the zero-point of the axis where the label should start to be drawn fn start_offset(&self) -> i32 { self.offset as i32 - self.footprint() as i32 / 2 } } fn create_x_axis_labels(x_tick_map: &HashMap) -> Vec { let mut ls: Vec<_> = x_tick_map .iter() .map(|(&offset, &tick)| XAxisLabel { text: tick.to_string(), offset, }) .collect(); ls.sort_by_key(|l| l.offset); ls } pub fn render_y_axis_strings(y_axis: &axis::ContinuousAxis, face_height: u32) -> (String, i32) { // Get the strings and offsets we'll use for the y-axis let y_tick_map = tick_offset_map(y_axis, face_height); // Find a minimum size for the left gutter let longest_y_label_width = y_tick_map .values() .map(|n| n.to_string().len()) .max() .expect("ERROR: There are no y-axis ticks"); let y_axis_label = format!( "{: ^width$}", y_axis.get_label(), width = face_height as usize + 1 ); let y_axis_label: Vec<_> = y_axis_label.chars().rev().collect(); // Generate a list of strings to label the y-axis let y_label_strings: Vec<_> = (0..=face_height) .map(|line| match y_tick_map.get(&(line as i32)) { Some(v) => v.to_string(), None => "".to_string(), }) .collect(); // Generate a list of strings to tick the y-axis let y_tick_strings: Vec<_> = (0..=face_height) .map(|line| match y_tick_map.get(&(line as i32)) { Some(_) => "-".to_string(), None => " ".to_string(), }) .collect(); // Generate a list of strings to be the y-axis line itself let y_axis_line_strings: Vec = std::iter::repeat('+') .take(1) .chain(std::iter::repeat('|').take(face_height as usize)) .map(|s| s.to_string()) .collect(); let iter = y_axis_label .iter() .zip(y_label_strings.iter()) .zip(y_tick_strings.iter()) .zip(y_axis_line_strings.iter()) .map(|(((a, x), y), z)| (a, x, y, z)); let axis_string: Vec = iter .rev() .map(|(l, ls, t, a)| { format!( "{} {:>num_width$}{}{}", l, ls, t, a, num_width = longest_y_label_width ) }) .collect(); let axis_string = axis_string.join("\n"); (axis_string, longest_y_label_width as i32) } pub fn render_x_axis_strings(x_axis: &axis::ContinuousAxis, face_width: u32) -> (String, i32) { // Get the strings and offsets we'll use for the x-axis let x_tick_map = tick_offset_map(x_axis, face_width as u32); // Create a string which will be printed to give the x-axis tick marks let x_axis_tick_string: String = (0..=face_width) .map(|cell| match x_tick_map.get(&(cell as i32)) { Some(_) => '|', None => ' ', }) .collect(); // Create a string which will be printed to give the x-axis labels let x_labels = create_x_axis_labels(&x_tick_map); let start_offset = x_labels .iter() .map(|label| label.start_offset()) .min() .expect("ERROR: Could not compute start offset of x-axis"); // This string will be printed, starting at start_offset relative to the x-axis zero cell let mut x_axis_label_string = "".to_string(); for label in (&x_labels).iter() { let spaces_to_append = label.start_offset() - start_offset - x_axis_label_string.len() as i32; if spaces_to_append.is_positive() { for _ in 0..spaces_to_append { x_axis_label_string.push(' '); } } else { for _ in 0..spaces_to_append.wrapping_neg() { x_axis_label_string.pop(); } } let formatted_label = format!("{: ^footprint$}", label.text, footprint = label.footprint()); x_axis_label_string.push_str(&formatted_label); } // Generate a list of strings to be the y-axis line itself let x_axis_line_string: String = std::iter::repeat('+') .take(1) .chain(std::iter::repeat('-').take(face_width as usize)) .collect(); let x_axis_label = format!( "{: ^width$}", x_axis.get_label(), width = face_width as usize ); let x_axis_string = if start_offset.is_positive() { let padding = (0..start_offset).map(|_| " ").collect::(); format!( "{}\n{}\n{}{}\n{}", x_axis_line_string, x_axis_tick_string, padding, x_axis_label_string, x_axis_label ) } else { let padding = (0..start_offset.wrapping_neg()) .map(|_| " ") .collect::(); format!( "{}{}\n{}{}\n{}\n{}{}", padding, x_axis_line_string, padding, x_axis_tick_string, x_axis_label_string, padding, x_axis_label ) }; (x_axis_string, start_offset) } /// Given a histogram, /// the x ands y-axes /// and the face height and width, /// create the strings to be drawn as the face pub fn render_face_bars( h: &repr::Histogram, x_axis: &axis::ContinuousAxis, y_axis: &axis::ContinuousAxis, face_width: u32, face_height: u32, ) -> String { let bound_cells = bound_cell_offsets(h, x_axis, face_width); let cell_bins = bins_for_cells(&bound_cells, face_width); // counts per bin converted to rows per column let cell_heights: Vec<_> = cell_bins .iter() .map(|&bin| match bin { None => 0, Some(b) => value_to_axis_cell_offset(h.get_values()[b as usize], y_axis, face_height), }) .collect(); let mut face_strings: Vec = vec![]; for line in 1..=face_height { let mut line_string = String::new(); for column in 1..=face_width as usize { // maybe use a HashSet for faster `contains()`? line_string.push(if bound_cells.contains(&(column as i32)) { // The value of the column _below_ this one let b = cell_heights[column - 1].cmp(&(line as i32)); // The value of the column _above_ this one let a = cell_heights[column + 1].cmp(&(line as i32)); match b { std::cmp::Ordering::Less => { match a { std::cmp::Ordering::Less => ' ', std::cmp::Ordering::Equal => '-', // or 'r'-shaped corner std::cmp::Ordering::Greater => '|', } } std::cmp::Ordering::Equal => { match a { std::cmp::Ordering::Less => '-', // or backwards 'r' std::cmp::Ordering::Equal => '-', // or 'T'-shaped std::cmp::Ordering::Greater => '|', // or '-|' } } std::cmp::Ordering::Greater => { match a { std::cmp::Ordering::Less => '|', std::cmp::Ordering::Equal => '|', // or '|-' std::cmp::Ordering::Greater => '|', } } } } else { let bin_height_cells = cell_heights[column]; if bin_height_cells == line as i32 { '-' // bar cap } else { ' ' // } }); } face_strings.push(line_string); } let face_strings: Vec = face_strings.iter().rev().cloned().collect(); face_strings.join("\n") } /// Given a scatter plot, /// the x ands y-axes /// and the face height and width, /// create the strings to be drawn as the face pub fn render_face_points( s: &[(f64, f64)], x_axis: &axis::ContinuousAxis, y_axis: &axis::ContinuousAxis, face_width: u32, face_height: u32, style: &style::PointStyle, ) -> String { let points: Vec<_> = s .iter() .map(|&(x, y)| { ( value_to_axis_cell_offset(x, x_axis, face_width), value_to_axis_cell_offset(y, y_axis, face_height), ) }) .collect(); let marker = match style.get_marker() { style::PointMarker::Circle => '●', style::PointMarker::Square => '■', style::PointMarker::Cross => '×', }; let mut face_strings: Vec = vec![]; for line in 1..=face_height { let mut line_string = String::new(); for column in 1..=face_width as usize { line_string.push(if points.contains(&(column as i32, line as i32)) { marker } else { ' ' }); } face_strings.push(line_string); } let face_strings: Vec = face_strings.iter().rev().cloned().collect(); face_strings.join("\n") } /// Given two 'rectangular' strings, overlay the second on the first offset by `x` and `y` pub fn overlay(under: &str, over: &str, x: i32, y: i32) -> String { let split_under: Vec<_> = under.split('\n').collect(); let under_width = split_under.iter().map(|s| s.len()).max().unwrap(); let under_height = split_under.len(); let split_over: Vec = over.split('\n').map(|s| s.to_string()).collect(); let over_width = split_over.iter().map(|s| s.len()).max().unwrap(); // Take `over` and pad it so that it matches `under`'s dimensions // Trim/add lines at beginning let split_over: Vec = if y.is_negative() { split_over.iter().skip(y.abs() as usize).cloned().collect() } else if y.is_positive() { (0..y) .map(|_| (0..over_width).map(|_| ' ').collect()) .chain(split_over.iter().map(|s| s.to_string())) .collect() } else { split_over }; // Trim/add chars at beginning let split_over: Vec = if x.is_negative() { split_over .iter() .map(|l| l.chars().skip(x.abs() as usize).collect()) .collect() } else if x.is_positive() { split_over .iter() .map(|s| (0..x).map(|_| ' ').chain(s.chars()).collect()) .collect() } else { split_over }; // pad out end of vector let over_width = split_over.iter().map(|s| s.len()).max().unwrap(); let over_height = split_over.len(); let lines_deficit = under_height as i32 - over_height as i32; let split_over: Vec = if lines_deficit.is_positive() { let new_lines: Vec = (0..lines_deficit) .map(|_| (0..over_width).map(|_| ' ').collect::()) .collect(); let mut temp = split_over; for new_line in new_lines { temp.push(new_line); } temp } else { split_over }; // pad out end of each line let line_width_deficit = under_width as i32 - over_width as i32; let split_over: Vec = if line_width_deficit.is_positive() { split_over .iter() .map(|l| { l.chars() .chain((0..line_width_deficit).map(|_| ' ')) .collect() }) .collect() } else { split_over }; // Now that the dimensions match, overlay them let mut out: Vec = vec![]; for (l, ol) in split_under.iter().zip(split_over.iter()) { let mut new_line = "".to_string(); for (c, oc) in l.chars().zip(ol.chars()) { new_line.push(if oc == ' ' { c } else { oc }); } out.push(new_line); } out.join("\n") } pub fn empty_face(width: u32, height: u32) -> String { (0..height) .map(|_| " ".repeat(width as usize)) .collect::>() .join("\n") } #[cfg(test)] mod tests { use super::*; #[test] fn test_bins_for_cells() { let face_width = 10; let n = i32::max_value(); let run_bins_for_cells = |bound_cell_offsets: &[i32]| -> Vec<_> { bins_for_cells(&bound_cell_offsets, face_width) .iter() .map(|&a| a.unwrap_or(n)) .collect() }; assert_eq!( run_bins_for_cells(&[-4, -1, 4, 7, 10]), [1, 1, 1, 1, 1, 2, 2, 2, 3, 3, 3, n] ); assert_eq!( run_bins_for_cells(&[0, 2, 4, 8, 10]), [n, 0, 0, 1, 1, 2, 2, 2, 2, 3, 3, n] ); assert_eq!( run_bins_for_cells(&[3, 5, 7, 9, 10]), [n, n, n, n, 0, 0, 1, 1, 2, 2, 3, n] ); assert_eq!( run_bins_for_cells(&[0, 2, 4, 6, 8]), [n, 0, 0, 1, 1, 2, 2, 3, 3, n, n, n] ); assert_eq!( run_bins_for_cells(&[0, 3, 6, 9, 12]), [n, 0, 0, 0, 1, 1, 1, 2, 2, 2, 3, 3] ); assert_eq!( run_bins_for_cells(&[-5, -4, -3, -1, 0]), [3, n, n, n, n, n, n, n, n, n, n, n] ); assert_eq!( run_bins_for_cells(&[10, 12, 14, 16, 18]), [n, n, n, n, n, n, n, n, n, n, n, 0] ); assert_eq!( run_bins_for_cells(&[15, 16, 17, 18, 19]), [n, n, n, n, n, n, n, n, n, n, n, n] ); assert_eq!( run_bins_for_cells(&[-19, -18, -17, -16, -1]), [n, n, n, n, n, n, n, n, n, n, n, n] ); } #[test] fn test_value_to_axis_cell_offset() { assert_eq!( value_to_axis_cell_offset(3.0, &axis::ContinuousAxis::new(5.0, 10.0, 6), 10), -4 ); } #[test] fn test_x_axis_label() { let l = XAxisLabel { text: "3".to_string(), offset: 2, }; assert_eq!(l.len(), 1); assert_ne!(l.footprint() % 2, 0); assert_eq!(l.start_offset(), 2); let l = XAxisLabel { text: "34".to_string(), offset: 2, }; assert_eq!(l.len(), 2); assert_ne!(l.footprint() % 2, 0); assert_eq!(l.start_offset(), 1); let l = XAxisLabel { text: "345".to_string(), offset: 2, }; assert_eq!(l.len(), 3); assert_ne!(l.footprint() % 2, 0); assert_eq!(l.start_offset(), 1); let l = XAxisLabel { text: "3454".to_string(), offset: 1, }; assert_eq!(l.len(), 4); assert_ne!(l.footprint() % 2, 0); assert_eq!(l.start_offset(), -1); } #[test] fn test_render_y_axis_strings() { let y_axis = axis::ContinuousAxis::new(0.0, 10.0, 6); let (y_axis_string, longest_y_label_width) = render_y_axis_strings(&y_axis, 10); assert!(y_axis_string.contains(&"0".to_string())); assert!(y_axis_string.contains(&"6".to_string())); assert!(y_axis_string.contains(&"10".to_string())); assert_eq!(longest_y_label_width, 2); } #[test] fn test_render_x_axis_strings() { let x_axis = axis::ContinuousAxis::new(0.0, 10.0, 6); let (x_axis_string, start_offset) = render_x_axis_strings(&x_axis, 20); assert!(x_axis_string.contains("0 ")); assert!(x_axis_string.contains(" 6 ")); assert!(x_axis_string.contains(" 10")); assert_eq!(x_axis_string.chars().filter(|&c| c == '|').count(), 6); assert_eq!(start_offset, 0); } #[test] fn test_render_face_bars() { let data = vec![0.3, 0.5, 6.4, 5.3, 3.6, 3.6, 3.5, 7.5, 4.0]; let h = repr::Histogram::from_slice(&data, repr::HistogramBins::Count(10)); let x_axis = axis::ContinuousAxis::new(0.3, 7.5, 6); let y_axis = axis::ContinuousAxis::new(0., 3., 6); let strings = render_face_bars(&h, &x_axis, &y_axis, 20, 10); assert_eq!(strings.lines().count(), 10); assert!(strings.lines().all(|s| s.chars().count() == 20)); let comp = vec![ " --- ", " | | ", " | | ", "-- | | ", " | | | ", " | | | ", " | | | ", " | | |---- -----", " | | | | | | | |", " | | | | | | | |", ] .join("\n"); assert_eq!(&strings, &comp); } #[test] fn test_render_face_points() { use crate::style::PointStyle; let data = vec![ (-3.0, 2.3), (-1.6, 5.3), (0.3, 0.7), (4.3, -1.4), (6.4, 4.3), (8.5, 3.7), ]; let x_axis = axis::ContinuousAxis::new(-3.575, 9.075, 6); let y_axis = axis::ContinuousAxis::new(-1.735, 5.635, 6); let style = PointStyle::new(); //TODO NEXT let strings = render_face_points(&data, &x_axis, &y_axis, 20, 10, &style); assert_eq!(strings.lines().count(), 10); assert!(strings.lines().all(|s| s.chars().count() == 20)); let comp = vec![ " ● ", " ", " ● ", " ● ", " ", "● ", " ", " ● ", " ", " ", ] .join("\n"); assert_eq!(&strings, &comp); } #[test] fn test_overlay() { let a = " ooo "; let b = " # "; let r = " o#o "; assert_eq!(overlay(a, b, 0, 0), r); let a = " o o o o o o o o o o "; let b = "# # # # #"; let r = " o#o#o#o#o#o o o o o "; assert_eq!(overlay(a, b, 2, 0), r); let a = " \n o \n o o\nooooo\no o o"; let b = " # \n # \n \n ## \n ##"; let r = " # \n # \n o o\noo##o\no o##"; assert_eq!(overlay(a, b, 0, 0), r); let a = " \n o \n o o\nooooo\no o o"; let b = " #\n## "; let r = " \n o \n o #o\no##oo\no o o"; assert_eq!(overlay(a, b, 1, 2), r); let a = " \n o \n o o\nooooo\no o o"; let b = "###\n###\n###"; let r = "## \n## o \n o o\nooooo\no o o"; assert_eq!(overlay(a, b, -1, -1), r); let a = "oo\noo"; let b = " \n # \n # \n "; let r = "o#\n#o"; assert_eq!(overlay(a, b, -1, -1), r); } #[test] fn test_empty_face() { assert_eq!(empty_face(0, 0), ""); assert_eq!(empty_face(1, 1), " "); assert_eq!(empty_face(2, 2), " \n "); assert_eq!(empty_face(2, 3), " \n \n "); assert_eq!(empty_face(4, 2), " \n "); } } plotlib-0.5.1/src/utils.rs010064400017500000144000000063741363374670300137220ustar0000000000000000use std::f64; use std::iter::{Skip, Zip}; use std::slice::Iter; pub trait PairWise { fn pairwise(&self) -> Zip, Skip>>; } impl PairWise for [T] { fn pairwise(&self) -> Zip, Skip>> { self.iter().zip(self.iter().skip(1)) } } fn _mean(s: &[f64]) -> f64 { s.iter().map(|v| v / s.len() as f64).sum() } pub fn median(s: &[f64]) -> f64 { let mut s = s.to_owned(); s.sort_by(|a, b| a.partial_cmp(b).unwrap()); match s.len() % 2 { 0 => (s[(s.len() / 2) - 1] / 2.) + (s[(s.len() / 2)] / 2.), _ => s[s.len() / 2], } } pub fn quartiles(s: &[f64]) -> (f64, f64, f64) { if s.len() == 1 { return (s[0], s[0], s[0]); } let mut s = s.to_owned(); s.sort_by(|a, b| a.partial_cmp(b).unwrap()); let (a, b) = if s.len() % 2 == 0 { s.split_at(s.len() / 2) } else { (&s[..(s.len() / 2)], &s[((s.len() / 2) + 1)..]) }; (median(a), median(&s), median(b)) } /// Given a slice of numbers, return the minimum and maximum values pub fn range(s: &[f64]) -> (f64, f64) { let mut min = f64::INFINITY; let mut max = f64::NEG_INFINITY; for &v in s { min = min.min(v); max = max.max(v); } (min, max) } /// Floor or ceiling the min or max to zero to avoid them both having the same value pub fn pad_range_to_zero(min: f64, max: f64) -> (f64, f64) { if (min - max).abs() < std::f64::EPSILON { ( if min > 0. { 0. } else { min }, if max < 0. { 0. } else { max }, ) } else { (min, max) } } #[cfg(test)] mod tests { use super::*; #[test] fn test_pairwise() { let a = [1, 2, 3, 4, 5]; assert_eq!(a.pairwise().next().unwrap(), (&1, &2)); assert_eq!(a.pairwise().last().unwrap(), (&4, &5)); assert_eq!(a.pairwise().len(), a.len() - 1); let a = [1, 2]; assert_eq!(a.pairwise().next().unwrap(), (&1, &2)); assert_eq!(a.pairwise().last().unwrap(), (&1, &2)); assert_eq!(a.pairwise().len(), a.len() - 1); let a = [1]; assert!(a.pairwise().next().is_none()); let b: Vec = vec![0.0, 0.1, 0.2]; assert_eq!(b.pairwise().next().unwrap(), (&0.0, &0.1)); } #[test] fn test_mean() { // TODO should error: mean(&[]); assert_eq!(_mean(&[1.]), 1.); assert_eq!(_mean(&[1., 2.]), 1.5); assert_eq!(_mean(&[1., 2., 3.]), 2.); } #[test] fn test_median() { // TODO should error: median(&[]); assert_eq!(median(&[1.]), 1.); assert_eq!(median(&[1., 2.]), 1.5); assert_eq!(median(&[1., 2., 4.]), 2.); assert_eq!(median(&[1., 2., 3., 7.]), 2.5); } #[test] fn test_quartiles() { // TODO should error: quartiles(&[]); assert_eq!(quartiles(&[1.]), (1., 1., 1.)); assert_eq!(quartiles(&[1., 2.]), (1., 1.5, 2.)); assert_eq!(quartiles(&[1., 2., 4.]), (1., 2., 4.)); assert_eq!(quartiles(&[1., 2., 3., 4.]), (1.5, 2.5, 3.5)); } #[test] fn test_pad_range_to_zero() { assert_eq!(pad_range_to_zero(2.0, 2.0), (0.0, 2.0)); assert_eq!(pad_range_to_zero(-2.0, 2.0), (-2.0, 2.0)); assert_eq!(pad_range_to_zero(-2.0, -2.0), (-2.0, 0.0)); } } plotlib-0.5.1/src/view.rs010064400017500000144000000307531363400476500135260ustar0000000000000000/*! *Views* are plotlib's way of combining multiple representations into a single plot. It is analogous to a *subplot* in other plotting libraries. In essence, a view is a collection of representations along with some metadata describing the extent to plot and information about the axes. It knows how to render itself. */ use std; use std::f64; use failure::format_err; use svg::Node; use crate::axis; use crate::errors::Result; use crate::grid::{Grid, GridType}; use crate::repr::{CategoricalRepresentation, ContinuousRepresentation}; use crate::svg_render; use crate::text_render; use crate::utils; pub trait View { fn to_svg(&self, face_width: f64, face_height: f64) -> Result; fn to_text(&self, face_width: u32, face_height: u32) -> Result; fn add_grid(&mut self, grid: Grid); fn grid(&self) -> &Option; } /// Standard 1-dimensional view with a continuous x-axis #[derive(Default)] pub struct ContinuousView { representations: Vec>, x_range: Option, y_range: Option, x_max_ticks: usize, y_max_ticks: usize, x_label: Option, y_label: Option, grid: Option, } impl ContinuousView { /// Create an empty view pub fn new() -> ContinuousView { ContinuousView { representations: vec![], x_range: None, y_range: None, x_max_ticks: 6, y_max_ticks: 6, x_label: None, y_label: None, grid: None, } } /// Set the maximum number of ticks along the x axis. pub fn x_max_ticks(mut self, val: usize) -> Self { self.x_max_ticks = val; self } /// Set the maximum number of ticks along the y axis. pub fn y_max_ticks(mut self, val: usize) -> Self { self.y_max_ticks = val; self } /// Add a representation to the view pub fn add(mut self, repr: R) -> Self { self.representations.push(Box::new(repr)); self } /// Set the x range for the view pub fn x_range(mut self, min: f64, max: f64) -> Self { self.x_range = Some(axis::Range::new(min, max)); self } /// Set the y range for the view pub fn y_range(mut self, min: f64, max: f64) -> Self { self.y_range = Some(axis::Range::new(min, max)); self } /// Set the label for the x-axis pub fn x_label(mut self, value: T) -> Self where T: Into, { self.x_label = Some(value.into()); self } /// Set the label for the y-axis pub fn y_label(mut self, value: T) -> Self where T: Into, { self.y_label = Some(value.into()); self } fn default_x_range(&self) -> axis::Range { let mut x_min = f64::INFINITY; let mut x_max = f64::NEG_INFINITY; for repr in &self.representations { let (this_x_min, this_x_max) = repr.range(0); x_min = x_min.min(this_x_min); x_max = x_max.max(this_x_max); } let (x_min, x_max) = utils::pad_range_to_zero(x_min, x_max); axis::Range::new(x_min, x_max) } fn default_y_range(&self) -> axis::Range { let mut y_min = f64::INFINITY; let mut y_max = f64::NEG_INFINITY; for repr in &self.representations { let (this_y_min, this_y_max) = repr.range(1); y_min = y_min.min(this_y_min); y_max = y_max.max(this_y_max); } let (y_min, y_max) = utils::pad_range_to_zero(y_min, y_max); axis::Range::new(y_min, y_max) } fn create_axes(&self) -> Result<(axis::ContinuousAxis, axis::ContinuousAxis)> { let default_x_range = self.default_x_range(); let x_range = self.x_range.as_ref().unwrap_or(&default_x_range); if !x_range.is_valid() { return Err(format_err!( "Invalid x_range: {} >= {}. Please specify the x_range manually.", x_range.lower, x_range.upper )); } let default_y_range = self.default_y_range(); let y_range = self.y_range.as_ref().unwrap_or(&default_y_range); if !y_range.is_valid() { return Err(format_err!( "Invalid y_range: {} >= {}. Please specify the y_range manually.", y_range.lower, y_range.upper )); } let x_label: String = self.x_label.clone().unwrap_or_else(|| "".to_string()); let y_label: String = self.y_label.clone().unwrap_or_else(|| "".to_string()); let x_axis = axis::ContinuousAxis::new(x_range.lower, x_range.upper, self.x_max_ticks) .label(x_label); let y_axis = axis::ContinuousAxis::new(y_range.lower, y_range.upper, self.y_max_ticks) .label(y_label); Ok((x_axis, y_axis)) } } impl View for ContinuousView { /** Create an SVG rendering of the view */ fn to_svg(&self, face_width: f64, face_height: f64) -> Result { let mut view_group = svg::node::element::Group::new(); let (x_axis, y_axis) = self.create_axes()?; let (legend_x, mut legend_y) = (face_width - 100., -face_height); if let Some(grid) = &self.grid { view_group.append(svg_render::draw_grid( GridType::Both(grid), face_width, face_height, )); } // Then, based on those ranges, draw each repr as an SVG for repr in &self.representations { let repr_group = repr.to_svg(&x_axis, &y_axis, face_width, face_height); view_group.append(repr_group); if let Some(legend_group) = repr.legend_svg() { view_group.append(legend_group.set( "transform", format!("translate({}, {})", legend_x, legend_y), )); legend_y += 18.; } } // Add in the axes view_group.append(svg_render::draw_x_axis(&x_axis, face_width)); view_group.append(svg_render::draw_y_axis(&y_axis, face_height)); Ok(view_group) } /** Create a text rendering of the view */ fn to_text(&self, face_width: u32, face_height: u32) -> Result { let (x_axis, y_axis) = self.create_axes()?; let (y_axis_string, longest_y_label_width) = text_render::render_y_axis_strings(&y_axis, face_height); let (x_axis_string, start_offset) = text_render::render_x_axis_strings(&x_axis, face_width); let left_gutter_width = std::cmp::max( longest_y_label_width as i32 + 3, start_offset.wrapping_neg(), ) as u32; let view_width = face_width + 1 + left_gutter_width + 1; let view_height = face_height + 4; let blank: Vec = (0..view_height) .map(|_| (0..view_width).map(|_| ' ').collect()) .collect(); let mut view_string = blank.join("\n"); for repr in &self.representations { let face_string = repr.to_text(&x_axis, &y_axis, face_width, face_height); view_string = text_render::overlay(&view_string, &face_string, left_gutter_width as i32 + 1, 0); } let view_string = text_render::overlay( &view_string, &y_axis_string, left_gutter_width as i32 - 2 - longest_y_label_width, 0, ); let view_string = text_render::overlay( &view_string, &x_axis_string, left_gutter_width as i32, face_height as i32, ); Ok(view_string) } fn add_grid(&mut self, grid: Grid) { self.grid = Some(grid) } fn grid(&self) -> &Option { &self.grid } } /// A view with categorical entries along the x-axis and continuous values along the y-axis #[derive(Default)] pub struct CategoricalView { representations: Vec>, x_range: Option>, y_range: Option, x_label: Option, y_label: Option, grid: Option, } impl CategoricalView { /** Create an empty view */ pub fn new() -> CategoricalView { CategoricalView { representations: vec![], x_range: None, y_range: None, x_label: None, y_label: None, grid: None, } } /** Add a representation to the view */ pub fn add(mut self, repr: R) -> Self { self.representations.push(Box::new(repr)); self } /** Set the x range for the view */ pub fn x_ticks(mut self, ticks: &[String]) -> Self { self.x_range = Some(ticks.into()); self } /** Set the y range for the view */ pub fn y_range(mut self, min: f64, max: f64) -> Self { self.y_range = Some(axis::Range::new(min, max)); self } /** Set the label for the x-axis */ pub fn x_label(mut self, value: T) -> Self where T: Into, { self.x_label = Some(value.into()); self } /** Set the label for the y-axis */ pub fn y_label(mut self, value: T) -> Self where T: Into, { self.y_label = Some(value.into()); self } fn default_x_ticks(&self) -> Vec { let mut v = vec![]; for repr in &self.representations { for l in repr.ticks() { if !v.contains(&l) { v.push(l.clone()); } } } v } fn default_y_range(&self) -> axis::Range { let mut y_min = f64::INFINITY; let mut y_max = f64::NEG_INFINITY; for repr in &self.representations { let (this_y_min, this_y_max) = repr.range(); y_min = y_min.min(this_y_min); y_max = y_max.max(this_y_max); } let buffer = (y_max - y_min) / 10.; let y_min = if y_min == 0.0 { y_min } else { y_min - buffer }; let y_max = y_max + buffer; axis::Range::new(y_min, y_max) } fn create_axes(&self) -> Result<(axis::CategoricalAxis, axis::ContinuousAxis)> { let default_x_ticks = self.default_x_ticks(); let x_range = self.x_range.as_ref().unwrap_or(&default_x_ticks); let default_y_range = self.default_y_range(); let y_range = self.y_range.as_ref().unwrap_or(&default_y_range); if !y_range.is_valid() { return Err(format_err!("invalid y_range: {:?}", y_range)); } let default_x_label = "".to_string(); let x_label: String = self.x_label.clone().unwrap_or(default_x_label); let default_y_label = "".to_string(); let y_label: String = self.y_label.clone().unwrap_or(default_y_label); let x_axis = axis::CategoricalAxis::new(x_range).label(x_label); let y_axis = axis::ContinuousAxis::new(y_range.lower, y_range.upper, 6).label(y_label); Ok((x_axis, y_axis)) } } impl View for CategoricalView { fn to_svg(&self, face_width: f64, face_height: f64) -> Result { let mut view_group = svg::node::element::Group::new(); let (x_axis, y_axis) = self.create_axes()?; if let Some(grid) = &self.grid { view_group.append(svg_render::draw_grid( GridType::HorizontalOnly(grid), face_width, face_height, )); } // Then, based on those ranges, draw each repr as an SVG for repr in &self.representations { let repr_group = repr.to_svg(&x_axis, &y_axis, face_width, face_height); view_group.append(repr_group); } // Add in the axes view_group.append(svg_render::draw_categorical_x_axis(&x_axis, face_width)); view_group.append(svg_render::draw_y_axis(&y_axis, face_height)); Ok(view_group) } fn to_text(&self, _face_width: u32, _face_height: u32) -> Result { Ok("".into()) } fn add_grid(&mut self, grid: Grid) { self.grid = Some(grid); } fn grid(&self) -> &Option { &self.grid } } /*pub struct AnyView<'a> { representations: Vec<&'a Representation>, axes: Vec<>, x_range: Option, y_range: Option, x_label: Option, y_label: Option, }*/ plotlib-0.5.1/tests/test_no_data.rs010064400017500000144000000045761363350346100155730ustar0000000000000000use plotlib::page::Page; use plotlib::repr::Plot; use plotlib::style::{PointMarker, PointStyle}; use plotlib::view::ContinuousView; #[test] fn test_data_with_one_length() { // Scatter plots expect a list of pairs let data1 = vec![(-3.0, 2.3)]; // We create our scatter plot from the data let s1 = Plot::new(data1).point_style( PointStyle::new() .marker(PointMarker::Square) // setting the marker to be a square .colour("#DD3355"), ); // and a custom colour // The 'view' describes what set of data is drawn let v = ContinuousView::new() .add(s1) .x_range(-5., 10.) .y_range(-2., 6.) .x_label("Some varying variable") .y_label("The response of something"); // A page with a single view is then saved to an SVG file Page::single(&v) .save("/tmp/scatter_one_length.svg") .unwrap(); } #[test] fn test_data_with_no_length() { // Scatter plots expect a list of pairs let data1 = vec![]; // We create our scatter plot from the data let s1 = Plot::new(data1).point_style( PointStyle::new() .marker(PointMarker::Square) // setting the marker to be a square .colour("#DD3355"), ); // and a custom colour // The 'view' describes what set of data is drawn let v = ContinuousView::new() .add(s1) .x_range(-5., 10.) .y_range(-2., 6.) .x_label("Some varying variable") .y_label("The response of something"); // A page with a single view is then saved to an SVG file Page::single(&v) .save("/tmp/scatter_zero_length.svg") .unwrap(); } #[test] fn test_data_with_one_length_and_autoscaling_axes_limits() { // Scatter plots expect a list of pairs let data1 = vec![(-3.0, 2.3)]; // We create our scatter plot from the data let s1 = Plot::new(data1).point_style( PointStyle::new() .marker(PointMarker::Square) // setting the marker to be a square .colour("#DD3355"), ); // and a custom colour // The 'view' describes what set of data is drawn let v = ContinuousView::new() .add(s1) .x_label("Some varying variable") .y_label("The response of something"); // // A page with a single view is then saved to an SVG file Page::single(&v) .save("/tmp/scatter_one_length.svg") .unwrap(); } plotlib-0.5.1/Cargo.lock0000644000000063541363763236700106050ustar00# This file is automatically @generated by Cargo. # It is not intended for manual editing. [[package]] name = "backtrace" version = "0.3.45" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ad235dabf00f36301792cfe82499880ba54c6486be094d1047b02bacb67c14e8" dependencies = [ "backtrace-sys", "cfg-if", "libc", "rustc-demangle", ] [[package]] name = "backtrace-sys" version = "0.1.34" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ca797db0057bae1a7aa2eef3283a874695455cecf08a43bfb8507ee0ebc1ed69" dependencies = [ "cc", "libc", ] [[package]] name = "cc" version = "1.0.50" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "95e28fa049fda1c330bcf9d723be7663a899c4679724b34c81e9f5a326aab8cd" [[package]] name = "cfg-if" version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" [[package]] name = "failure" version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8529c2421efa3066a5cbd8063d2244603824daccb6936b079010bb2aa89464b" dependencies = [ "backtrace", "failure_derive", ] [[package]] name = "failure_derive" version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "030a733c8287d6213886dd487564ff5c8f6aae10278b3588ed177f9d18f8d231" dependencies = [ "proc-macro2", "quote", "syn", "synstructure", ] [[package]] name = "libc" version = "0.2.67" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eb147597cdf94ed43ab7a9038716637d2d1bf2bc571da995d0028dec06bd3018" [[package]] name = "plotlib" version = "0.5.1" dependencies = [ "failure", "svg", ] [[package]] name = "proc-macro2" version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c09721c6781493a2a492a96b5a5bf19b65917fe6728884e7c44dd0c60ca3435" dependencies = [ "unicode-xid", ] [[package]] name = "quote" version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2bdc6c187c65bca4260c9011c9e3132efe4909da44726bad24cf7572ae338d7f" dependencies = [ "proc-macro2", ] [[package]] name = "rustc-demangle" version = "0.1.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4c691c0e608126e00913e33f0ccf3727d5fc84573623b8d65b2df340b5201783" [[package]] name = "svg" version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "99bfc4e5077d9047fe8a5cbeb22f0164251e015f85480599892060c07a4e853f" [[package]] name = "syn" version = "1.0.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "123bd9499cfb380418d509322d7a6d52e5315f064fe4b3ad18a53d6b92c07859" dependencies = [ "proc-macro2", "quote", "unicode-xid", ] [[package]] name = "synstructure" version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67656ea1dc1b41b1451851562ea232ec2e5a80242139f7e679ceccfb5d61f545" dependencies = [ "proc-macro2", "quote", "syn", "unicode-xid", ] [[package]] name = "unicode-xid" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "826e7639553986605ec5979c7dd957c7895e93eabed50ab2ffa7f6128a75097c"