├── .eslintignore ├── .eslintrc.json ├── .github └── workflows │ └── deploy.yml ├── .gitignore ├── README.md ├── docs ├── .vitepress │ └── config.js └── index.md ├── examples ├── annotated-visualizations │ ├── d3 │ │ ├── annotated-bar-chart.js │ │ └── annotated-scatter-plot.js │ ├── ggplot2 │ │ └── annotated-line-chart.js │ ├── multi-view │ │ └── annotated-vega-lite-weather.js │ └── observable-plot │ │ └── annotated-scatter-plot.js ├── data │ ├── XYZ.csv │ ├── alphabet.csv │ ├── cities-lived.csv │ ├── data.csv │ ├── flights-2k.json │ ├── penguins.csv │ ├── seattle-weather.csv │ ├── stateslived.csv │ ├── us-states.json │ └── vega-lite-schema-v5.json └── visualizations │ ├── d3 │ ├── bar-chart.js │ ├── custom-map.js │ ├── glyph-map.js │ ├── hex-map.js │ ├── line-chart.js │ ├── log-chart.js │ ├── pie-chart.js │ ├── scatter-plot.js │ ├── stacked-area-chart.js │ └── stacked-bar-chart.js │ ├── ggplot2 │ ├── line-chart.svg │ ├── multi-view-setup │ │ ├── excel-scatter.png │ │ ├── excel-scatter.svg │ │ ├── line.svg │ │ ├── multi-view-setup.R │ │ ├── scatter.svg │ │ ├── seattle-weather.csv │ │ └── seattle-weather.png │ ├── r4-1.svg │ ├── r4-2.svg │ ├── scatter-line-chart.svg │ ├── stock-multiline.R │ ├── stock-multiline.svg │ ├── weather-scatter.svg │ ├── weather-scatter2.svg │ ├── weather-scatter3.svg │ └── weather-scatter4.svg │ ├── matplotlib │ ├── bar-chart.py │ ├── bars.svg │ ├── bars1.svg │ ├── bars2.svg │ ├── multi-view-setup │ │ ├── bar-charts.py │ │ ├── bars1.svg │ │ ├── bars2.svg │ │ └── bars_test.svg │ └── multi-view.py │ ├── multi-view │ ├── d3-ggplot2.js │ ├── gg-matplot.js │ ├── gg-weather-facet.js │ ├── ggplot2-setup.js │ ├── ggplot2-vegalite.js │ ├── vega-bar-scatter.js │ ├── vega-crossfilter.js │ ├── vega-dual-linking.js │ ├── vega-lite-weather.js │ └── vega-matplotlib.js │ ├── observable-plot │ ├── bar-chart.js │ ├── facets.js │ └── scatter-plot.js │ └── vega-lite │ ├── bar-chart.js │ ├── line-chart.js │ ├── scatter-plot-1.js │ └── scatter-plot-2.js ├── package-lock.json ├── package.json ├── public ├── index.html ├── pages │ ├── d3 │ │ ├── bar-chart.html │ │ ├── hex-map.html │ │ ├── line-chart.html │ │ ├── log-chart.html │ │ ├── population-map.html │ │ ├── scatter-plot.html │ │ ├── stacked-area-chart.html │ │ └── stacked-bar-chart.html │ ├── ggplot2 │ │ ├── line-chart.html │ │ └── trendline.html │ ├── multi-view │ │ ├── d3-ggplot2.html │ │ ├── gg-matplot.html │ │ ├── gg-weather-facet.html │ │ ├── ggplot2-multi-view-setup.html │ │ ├── vega-bar-scatter.html │ │ ├── vega-crossfilter.html │ │ ├── vega-dual-linking.html │ │ ├── vega-ggplot2.html │ │ ├── vega-lite-weather.html │ │ └── vega-matplotlib.html │ ├── observable-plot │ │ ├── bar-chart.html │ │ ├── facets.html │ │ └── scatter-plot.html │ └── vega-lite │ │ ├── bar-chart.html │ │ ├── scatter-plot-1.html │ │ └── scatter-plot-2.html └── study.html ├── rollup.config.js ├── src ├── _d3 │ ├── axis.js │ ├── identity.js │ └── zoom │ │ ├── constant.js │ │ ├── event.js │ │ ├── index.js │ │ ├── noevent.js │ │ ├── transform.js │ │ └── zoom.js ├── handlers │ ├── annotate.js │ ├── brush.js │ ├── query.js │ ├── select.js │ ├── sort.js │ └── zoom.js ├── hydrate.js ├── index.js ├── orchestration │ ├── coordinator.js │ └── inspect.js ├── parsers │ ├── engine │ │ ├── parser-engine.js │ │ └── parser-groups.js │ ├── helpers │ │ ├── axis-parser.js │ │ ├── data-parser.js │ │ ├── legend-parser.js │ │ ├── mark-parser.js │ │ └── title-parser.js │ └── multi-view │ │ └── link-parser.js ├── state │ ├── constants.js │ ├── data-state.js │ └── view-state.js ├── toolbar │ ├── icons │ │ ├── annotate.svg │ │ ├── brush.svg │ │ ├── download.svg │ │ ├── filter.svg │ │ ├── link.svg │ │ ├── navigate.svg │ │ └── reset.svg │ └── menu.js └── util │ ├── transform.js │ └── util.js └── test ├── browser ├── d3 │ ├── bar-chart-test.js │ ├── scatter-plot-test.js │ └── tests.js ├── excel │ └── tests.js ├── ggplot2 │ ├── line-chart-test.js │ └── tests.js ├── index.html ├── matplotlib │ └── tests.js ├── multi-view │ ├── tests.js │ └── vega-lite-weather-test.js ├── observable-plot │ ├── scatter-plot-test.js │ └── tests.js └── vega-lite │ └── tests.js ├── node └── util-test.js └── util ├── core-structure-test-functions.js ├── helper-functions.js └── test-constants.js /.eslintignore: -------------------------------------------------------------------------------- 1 | /src/_d3/ 2 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": false, 4 | "es6": true, 5 | "node": true, 6 | "mocha": true 7 | }, 8 | "extends": "standard", 9 | "overrides": [ 10 | ], 11 | "parserOptions": { 12 | "ecmaVersion": "latest", 13 | "sourceType": "module" 14 | }, 15 | "rules": { 16 | "indent": ["error", 4], 17 | "semi": ["error", "always"], 18 | "space-before-function-paren": ["error", "never"] 19 | }, 20 | "globals": { 21 | "chai": "readonly" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | name: Deploy 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | branches: 7 | - main 8 | 9 | permissions: 10 | contents: read 11 | pages: write 12 | id-token: write 13 | 14 | concurrency: 15 | group: "pages" 16 | cancel-in-progress: false 17 | 18 | jobs: 19 | deploy: 20 | runs-on: ubuntu-latest 21 | 22 | environment: 23 | name: github-pages 24 | url: ${{ steps.deployment.outputs.page_url }} 25 | 26 | steps: 27 | - uses: actions/checkout@v4 28 | with: 29 | fetch-depth: 0 30 | 31 | - uses: actions/setup-node@v4 32 | with: 33 | node-version: "20.x" 34 | cache: "npm" 35 | 36 | - name: Install Node dependencies 37 | run: npm ci 38 | 39 | - name: Build 40 | run: npm run docs:build 41 | 42 | - uses: actions/configure-pages@v5 43 | - uses: actions/upload-pages-artifact@v3 44 | with: 45 | path: docs/.vitepress/dist 46 | - name: Deploy 47 | id: deployment 48 | uses: actions/deploy-pages@v4 49 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /node_modules 3 | /dist 4 | docs/.vitepress/cache 5 | docs/.vitepress/dist 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # DIVI 2 | 3 | DIVI: Dynamically Interactive Visualization. 4 | 5 | This repository is in the process of being updated. 6 | 7 | Please email snyderl AT cs DOT washington DOT edu to be notified when the update is complete. 8 | 9 | ## Instructions 10 | 11 | - Run `npm install` to install dependencies. 12 | - Run `npm run serve` to launch development server and view test examples. 13 | - Run `npm run build` to produce an output bundle. 14 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | # https://vitepress.dev/reference/default-theme-home-page 3 | layout: home 4 | 5 | hero: 6 | text: | 7 | Dynamically Interactive Visualization 8 | tagline: | 9 | DIVI automatically orchestrates interactions within and across SVG visualizations. 10 | NOTE: This page is being actively updated. 11 | actions: 12 | - theme: brand 13 | text: What is DIVI? 14 | link: /what-is-mosaic/ 15 | - theme: alt 16 | text: Get started 17 | link: /get-started/ 18 | - theme: alt 19 | text: Examples 20 | link: /examples/ 21 | - theme: alt 22 | text: GitHub 23 | link: 'https://github.com/uwdata/divi' 24 | 25 | features: 26 | - icon: 📊 27 | title: Interact automatically 28 | details: Explore charts on-the-fly without writing complex interaction handling code. 29 | - icon: 🔗 30 | title: Perform linked interactions 31 | details: Analyze data via linked selection & brushing, navigation, filtering, annotation, and sorting. 32 | - icon: 🛠️ 33 | title: Interoperable across tools 34 | details: Interact with SVG charts from popular tools, including Matplotlib, ggplot2, and Excel. 35 | - icon: 🪡 36 | title: Customize chart 37 | details: Access deconstruction metadata such as axes and legends to tailor plotting behavior. 38 | --- 39 | -------------------------------------------------------------------------------- /examples/annotated-visualizations/d3/annotated-bar-chart.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-undef */ 2 | import { TEST_MARK, TEST_X_AXIS_LABEL, TEST_X_AXIS_TICK, TEST_Y_AXIS_LABEL, TEST_Y_AXIS_TICK } from '../../../test/util/test-constants.js'; 3 | 4 | export async function createBarChart() { 5 | // set the dimensions and margins of the graph 6 | const margin = { top: 30, right: 30, bottom: 70, left: 60 }; 7 | const width = 720 - margin.left - margin.right; 8 | const height = 720 - margin.top - margin.bottom; 9 | 10 | // append the svg object to the body of the page 11 | let svg = d3.create('svg') 12 | .attr('width', width + margin.left + margin.right) 13 | .attr('height', height + margin.top + margin.bottom) 14 | .attr('id', 'chart'); 15 | 16 | const r = svg; 17 | svg = svg.append('g').attr('transform', 'translate(' + margin.left + ',' + margin.top + ')'); 18 | 19 | // Parse the Data 20 | const data = await d3.csv('https://raw.githubusercontent.com/holtzy/data_to_viz/master/Example_dataset/7_OneCatOneNum_header.csv'); 21 | 22 | // sort data 23 | data.sort(function(b, a) { 24 | return a.Value - b.Value; 25 | }); 26 | 27 | // X axis 28 | const x = d3.scaleBand() 29 | .range([0, width]) 30 | .domain(data.map(function(d) { return d.Country; })) 31 | .padding(0.2); 32 | 33 | const xAxis = svg.append('g') 34 | .attr('transform', 'translate(0,' + height + ')') 35 | .call(d3.axisBottom(x)); 36 | xAxis.selectAll('text') 37 | .attr('transform', 'translate(-10,0)rotate(-45)') 38 | .style('text-anchor', 'end'); 39 | 40 | // x-axis annotations 41 | xAxis.selectAll('text').classed(TEST_X_AXIS_LABEL, true); 42 | xAxis.selectAll('line').classed(TEST_X_AXIS_TICK, true); 43 | 44 | // Add Y axis 45 | const y = d3.scaleLinear() 46 | .domain([0, 13000]) 47 | .range([height, 0]); 48 | const yAxis = svg.append('g') 49 | .call(d3.axisLeft(y)); 50 | 51 | // y-axis annotations 52 | yAxis.selectAll('text').classed(TEST_Y_AXIS_LABEL, true); 53 | yAxis.selectAll('line').classed(TEST_Y_AXIS_TICK, true); 54 | 55 | // Bars 56 | svg.selectAll('mybar') 57 | .data(data) 58 | .enter() 59 | .append('rect') 60 | .attr('x', function(d) { return x(d.Country); }) 61 | .attr('y', function(d) { return y(d.Value); }) 62 | .attr('width', x.bandwidth()) 63 | .attr('height', function(d) { return height - y(d.Value); }) 64 | .attr('fill', '#69b3a2') 65 | .classed(TEST_MARK, true); // Mark annotations 66 | 67 | return r.node(); 68 | } 69 | -------------------------------------------------------------------------------- /examples/annotated-visualizations/d3/annotated-scatter-plot.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-undef */ 2 | import { TEST_MARK, TEST_X_AXIS_LABEL, TEST_X_AXIS_TICK, TEST_X_AXIS_TITLE, TEST_Y_AXIS_LABEL, TEST_Y_AXIS_TICK, TEST_Y_AXIS_TITLE } from '../../../test/util/test-constants.js'; 3 | 4 | export async function createScatterPlot() { 5 | // set the dimensions and margins of the graph 6 | const margin = { top: 10, right: 30, bottom: 40, left: 50 }; 7 | const width = 720 - margin.left - margin.right; 8 | const height = 720 - margin.top - margin.bottom; 9 | 10 | // append the svg object to the body of the page 11 | let svg = d3.create('svg') 12 | .attr('width', width + margin.left + margin.right) 13 | .attr('height', height + margin.top + margin.bottom) 14 | .attr('id', 'chart'); 15 | 16 | const r = svg; 17 | svg = svg.append('g').attr('transform', 'translate(' + margin.left + ',' + margin.top + ')'); 18 | 19 | // Read the data 20 | const data = await d3.csv('https://raw.githubusercontent.com/holtzy/D3-graph-gallery/master/DATA/iris.csv'); 21 | 22 | // Add X axis 23 | const x = d3.scaleLinear() 24 | .domain([4 * 0.95, 8 * 1.001]) 25 | .range([0, width]); 26 | 27 | const xAxis = svg.append('g') 28 | .attr('transform', 'translate(0,' + height + ')') 29 | .call(d3.axisBottom(x).tickSize(-height * 1.3).ticks(10)); 30 | 31 | // x-axis annotations 32 | xAxis.selectAll('text').classed(TEST_X_AXIS_LABEL, true); 33 | xAxis.selectAll('line').classed(TEST_X_AXIS_TICK, true); 34 | xAxis.select('.domain').remove(); 35 | 36 | // Add Y axis 37 | const y = d3.scaleLinear() 38 | .domain([-0.001, 9 * 1.01]) 39 | .range([height, 0]) 40 | .nice(); 41 | 42 | const yAxis = svg.append('g') 43 | .call(d3.axisLeft(y).tickSize(-width * 1.3).ticks(7)); 44 | 45 | // y-axis annotations 46 | yAxis.selectAll('text').classed(TEST_Y_AXIS_LABEL, true); 47 | yAxis.selectAll('line').classed(TEST_Y_AXIS_TICK, true); 48 | yAxis.select('.domain').remove(); 49 | 50 | // Customization 51 | svg.selectAll('.tick line').attr('stroke', 'black').attr('opacity', 0.3); 52 | 53 | // Add X axis label: 54 | svg.append('text') 55 | .attr('text-anchor', 'end') 56 | .attr('x', width / 2 + margin.left) 57 | .attr('y', height + margin.top + 20) 58 | .text('Sepal Length') 59 | .classed(TEST_X_AXIS_TITLE, true); // x-axis title annotation 60 | 61 | // Y axis label: 62 | svg.append('text') 63 | .attr('text-anchor', 'end') 64 | .attr('transform', 'rotate(-90)') 65 | .attr('y', -margin.left + 20) 66 | .attr('x', -margin.top - height / 2 + 20) 67 | .text('Petal Length') 68 | .classed(TEST_Y_AXIS_TITLE, true); // y-axis title annotation 69 | 70 | // Color scale: give me a specie name, I return a color 71 | const color = d3.scaleOrdinal() 72 | .domain(['setosa', 'versicolor', 'virginica']) 73 | .range(['#F8766D', '#00BA38', '#619CFF']); 74 | 75 | // Add dots 76 | svg.append('g') 77 | .selectAll('dot') 78 | .data(data) 79 | .enter() 80 | .append('circle') 81 | .attr('cx', function(d) { return x(d.Sepal_Length); }) 82 | .attr('cy', function(d) { return y(d.Petal_Length); }) 83 | .attr('r', 5) 84 | .style('fill', function(d) { return color(d.Species); }) 85 | .classed(TEST_MARK, true); // Mark annotations 86 | 87 | return r.node(); 88 | } 89 | -------------------------------------------------------------------------------- /examples/annotated-visualizations/ggplot2/annotated-line-chart.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-undef */ 2 | 3 | import { 4 | TEST_LEGEND_LABEL, TEST_LEGEND_MARK, TEST_LEGEND_TITLE, TEST_MARK, 5 | TEST_X_AXIS_LABEL, TEST_X_AXIS_TICK, TEST_X_AXIS_TITLE, TEST_Y_AXIS_LABEL, 6 | TEST_Y_AXIS_TICK, TEST_Y_AXIS_TITLE 7 | } from '../../../test/util/test-constants.js'; 8 | 9 | const baseID = 'svg_c9f6953f-273d-40cc-a962-d74ae6872c72_'; 10 | 11 | const xAxisTickIDs = [ 12 | 'el_7', 'el_8', 'el_9', 'el_16', 'el_17' 13 | ]; 14 | 15 | const xAxisLabelIDs = [ 16 | 'el_34', 'el_35' 17 | ]; 18 | 19 | const yAxisTickIDs = [ 20 | 'el_2', 'el_3', 'el_4', 'el_5', 'el_6', 'el_10', 21 | 'el_11', 'el_12', 'el_13', 'el_14', 'el_15' 22 | ]; 23 | 24 | const yAxisLabelIDs = [ 25 | 'el_28', 'el_29', 'el_30', 'el_31', 'el_32', 'el_33' 26 | ]; 27 | 28 | const markIDs = [ 29 | 'el_18', 'el_19', 'el_20', 'el_21', 'el_22', 'el_23', 30 | 'el_24', 'el_25', 'el_26', 'el_27' 31 | ]; 32 | 33 | const legendMarkIDs = [ 34 | 'el_39', 'el_40', 'el_41', 'el_42', 'el_43', 'el_44', 35 | 'el_45', 'el_46', 'el_47', 'el_48' 36 | ]; 37 | 38 | const legendLabelIDs = [ 39 | 'el_49', 'el_50', 'el_51', 'el_52', 'el_53', 'el_54', 40 | 'el_55', 'el_56', 'el_57', 'el_58' 41 | ]; 42 | 43 | const legendTitleID = 'el_38'; 44 | const xAxisTitleID = 'el_36'; 45 | const yAxisTitleID = 'el_37'; 46 | 47 | export async function createLineChart() { 48 | const response = await fetch('/examples/visualizations/ggplot2/line-chart.svg'); 49 | const text = await response.text(); 50 | 51 | // Add annotations 52 | const svg = d3.create('div').html(text).select('svg'); 53 | 54 | // Marks 55 | for (const id of markIDs) { 56 | svg.select('#' + baseID + id).classed(TEST_MARK, true); 57 | } 58 | 59 | // x-axis ticks 60 | for (const id of xAxisTickIDs) { 61 | svg.select('#' + baseID + id).classed(TEST_X_AXIS_TICK, true); 62 | } 63 | 64 | // x-axis labels 65 | for (const id of xAxisLabelIDs) { 66 | svg.select('#' + baseID + id).classed(TEST_X_AXIS_LABEL, true); 67 | } 68 | 69 | // x-axis title 70 | svg.select('#' + baseID + xAxisTitleID).classed(TEST_X_AXIS_TITLE, true); 71 | 72 | // y-axis ticks 73 | for (const id of yAxisTickIDs) { 74 | svg.select('#' + baseID + id).classed(TEST_Y_AXIS_TICK, true); 75 | } 76 | 77 | // y-axis labels 78 | for (const id of yAxisLabelIDs) { 79 | svg.select('#' + baseID + id).classed(TEST_Y_AXIS_LABEL, true); 80 | } 81 | 82 | // y-axis title 83 | svg.select('#' + baseID + yAxisTitleID).classed(TEST_Y_AXIS_TITLE, true); 84 | 85 | // Legend marks 86 | for (const id of legendMarkIDs) { 87 | svg.select('#' + baseID + id).classed(TEST_LEGEND_MARK, true); 88 | } 89 | 90 | // Legend labels 91 | for (const id of legendLabelIDs) { 92 | svg.select('#' + baseID + id).classed(TEST_LEGEND_LABEL, true); 93 | } 94 | 95 | // Legend title 96 | svg.select('#' + baseID + legendTitleID).classed(TEST_LEGEND_TITLE, true); 97 | 98 | return svg.node(); 99 | } 100 | -------------------------------------------------------------------------------- /examples/annotated-visualizations/multi-view/annotated-vega-lite-weather.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-undef */ 2 | export async function createVegaMultiView() { 3 | const spec1 = { 4 | $schema: '/examples/data/vega-lite-schema-v5.json', 5 | title: 'Seattle Weather, 2012-2015', 6 | data: { 7 | url: '/examples/data/seattle-weather.csv' 8 | }, 9 | encoding: { 10 | color: { 11 | title: 'Weather', 12 | field: 'weather', 13 | type: 'nominal', 14 | scale: { 15 | domain: ['sun', 'fog', 'drizzle', 'rain', 'snow'], 16 | range: ['#e7ba52', '#a7a7a7', '#aec7e8', '#1f77b4', '#9467bd'] 17 | } 18 | }, 19 | size: { 20 | title: 'Precipitation', 21 | field: 'precipitation', 22 | scale: { type: 'linear', domain: [-1, 50] }, 23 | type: 'quantitative' 24 | }, 25 | x: { 26 | field: 'date', 27 | timeUnit: 'utcyearmonthdate', 28 | title: 'Date', 29 | axis: { format: '%b %Y' } 30 | }, 31 | y: { 32 | title: 'Maximum Daily Temperature (C)', 33 | field: 'temp_max', 34 | scale: { domain: [-5, 40] }, 35 | type: 'quantitative' 36 | } 37 | }, 38 | width: 700, 39 | height: 400, 40 | mark: 'point' 41 | }; 42 | 43 | const spec2 = { 44 | $schema: '/examples/data/vega-lite-schema-v5.json', 45 | title: 'Seattle Weather, 2012-2015', 46 | data: { 47 | url: '/examples/data/seattle-weather.csv' 48 | }, 49 | encoding: { 50 | color: { 51 | field: 'weather', 52 | scale: { 53 | domain: ['sun', 'fog', 'drizzle', 'rain', 'snow'], 54 | range: ['#e7ba52', '#a7a7a7', '#aec7e8', '#1f77b4', '#9467bd'] 55 | } 56 | }, 57 | x: { aggregate: 'max', field: 'wind' }, 58 | y: { title: 'Weather', field: 'weather' } 59 | }, 60 | width: 700, 61 | mark: 'bar' 62 | }; 63 | 64 | const view1 = new vega.View(vega.parse(vegaLite.compile(spec1).spec), { renderer: 'svg' }); 65 | const view2 = new vega.View(vega.parse(vegaLite.compile(spec2).spec), { renderer: 'svg' }); 66 | 67 | const svgText1 = await view1.toSVG(); 68 | const svgText2 = await view2.toSVG(); 69 | 70 | const svg1 = d3.create('div').html(svgText1).select('svg').node(); 71 | const svg2 = d3.create('div').html(svgText2).select('svg').node(); 72 | 73 | return [svg1, svg2]; 74 | } 75 | -------------------------------------------------------------------------------- /examples/annotated-visualizations/observable-plot/annotated-scatter-plot.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-undef */ 2 | 3 | import { TEST_MARK, TEST_X_AXIS_LABEL, TEST_X_AXIS_TICK, TEST_X_AXIS_TITLE, TEST_Y_AXIS_LABEL, TEST_Y_AXIS_TICK, TEST_Y_AXIS_TITLE } from '../../../test/util/test-constants.js'; 4 | 5 | export async function createScatterPlot() { 6 | let data = await d3.csv('/examples/data/penguins.csv', d3.autoType); 7 | data = data.filter(d => d.sex != null); 8 | 9 | const p = Plot.plot({ 10 | inset: 8, 11 | grid: true, 12 | color: { 13 | legend: false, 14 | style: { marginLeft: 500 } 15 | }, 16 | marks: [ 17 | Plot.dot(data, { x: 'flipper_length_mm', y: 'body_mass_g', stroke: 'sex' }), 18 | Plot.axisY({ label: '↑ body_mass_g', marginLeft: 50 }) 19 | ] 20 | }); 21 | 22 | d3.select(p).attr('id', 'chart1'); 23 | 24 | // Add annotations 25 | const svg = d3.select(p); 26 | svg.selectAll('circle').classed(TEST_MARK, true); // Marks 27 | 28 | // x-axis ticks 29 | svg.selectAll('g').filter(function() { 30 | const attr = d3.select(this).attr('aria-label'); 31 | return attr === 'x-grid' || attr === 'x-axis tick'; 32 | }).selectAll('line, path').classed(TEST_X_AXIS_TICK, true); 33 | 34 | // x-axis labels 35 | svg.selectAll('g').filter(function() { 36 | return d3.select(this).attr('aria-label') === 'x-axis tick label'; 37 | }).selectAll('text').classed(TEST_X_AXIS_LABEL, true); 38 | 39 | // x-axis title 40 | svg.selectAll('g').filter(function() { 41 | return d3.select(this).attr('aria-label') === 'x-axis label'; 42 | }).select('text').classed(TEST_X_AXIS_TITLE, true); 43 | 44 | // y-axis ticks 45 | svg.selectAll('g').filter(function() { 46 | const attr = d3.select(this).attr('aria-label'); 47 | return attr === 'y-grid' || attr === 'y-axis tick'; 48 | }).selectAll('line, path').classed(TEST_Y_AXIS_TICK, true); 49 | 50 | // y-axis labels 51 | svg.selectAll('g').filter(function() { 52 | return d3.select(this).attr('aria-label') === 'y-axis tick label'; 53 | }).selectAll('text').classed(TEST_Y_AXIS_LABEL, true); 54 | 55 | // y-axis title 56 | svg.selectAll('g').filter(function() { 57 | return d3.select(this).attr('aria-label') === 'y-axis label'; 58 | }).select('text').classed(TEST_Y_AXIS_TITLE, true); 59 | 60 | return p; 61 | } 62 | -------------------------------------------------------------------------------- /examples/data/XYZ.csv: -------------------------------------------------------------------------------- 1 | year,value 2 | 2011,45 3 | 2012,47 4 | 2013,52 5 | 2014,70 6 | 2015,75 7 | 2016,78 8 | -------------------------------------------------------------------------------- /examples/data/alphabet.csv: -------------------------------------------------------------------------------- 1 | letter,frequency 2 | E,0.12702 3 | T,0.09056 4 | A,0.08167 5 | O,0.07507 6 | I,0.06966 7 | N,0.06749 8 | S,0.06327 9 | H,0.06094 10 | R,0.05987 11 | D,0.04253 12 | L,0.04025 13 | C,0.02782 14 | U,0.02758 15 | M,0.02406 16 | W,0.0236 17 | F,0.02288 18 | G,0.02015 19 | Y,0.01974 20 | P,0.01929 21 | B,0.01492 22 | V,0.00978 23 | K,0.00772 24 | J,0.00153 25 | X,0.0015 26 | Q,0.00095 27 | Z,0.00074 28 | -------------------------------------------------------------------------------- /examples/data/cities-lived.csv: -------------------------------------------------------------------------------- 1 | years,place,lat,lon 2 | 2,New York City,40.71455,-74.007124 3 | 6,San Francisco,37.7771187,-122.4196396 4 | 8,Santa Cruz,36.9740181,-122.0309525 5 | 3,Santa Barbara,34.4193802,-119.6990509 6 | 10,Tucson,32.22155,-110.9697571 7 | 1,Washington DC,38.8903694,-77.0319595 8 | -------------------------------------------------------------------------------- /examples/data/data.csv: -------------------------------------------------------------------------------- 1 | State,Under 5 Years,5 to 13 Years,14 to 17 Years,18 to 24 Years,25 to 44 Years,45 to 64 Years,65 Years and Over 2 | AL,310504,552339,259034,450818,1231572,1215966,641667 3 | AK,52083,85640,42153,74257,198724,183159,50277 4 | AZ,515910,828669,362642,601943,1804762,1523681,862573 5 | AR,202070,343207,157204,264160,754420,727124,407205 6 | CA,2704659,4499890,2159981,3853788,10604510,8819342,4114496 7 | CO,358280,587154,261701,466194,1464939,1290094,511094 8 | CT,211637,403658,196918,325110,916955,968967,478007 9 | DE,59319,99496,47414,84464,230183,230528,121688 10 | DC,36352,50439,25225,75569,193557,140043,70648 11 | FL,1140516,1938695,925060,1607297,4782119,4746856,3187797 12 | GA,740521,1250460,557860,919876,2846985,2389018,981024 13 | HI,87207,134025,64011,124834,356237,331817,190067 14 | ID,121746,201192,89702,147606,406247,375173,182150 15 | IL,894368,1558919,725973,1311479,3596343,3239173,1575308 16 | IN,443089,780199,361393,605863,1724528,1647881,813839 17 | IA,201321,345409,165883,306398,750505,788485,444554 18 | KS,202529,342134,155822,293114,728166,713663,366706 19 | KY,284601,493536,229927,381394,1179637,1134283,565867 20 | LA,310716,542341,254916,471275,1162463,1128771,540314 21 | ME,71459,133656,69752,112682,331809,397911,199187 22 | MD,371787,651923,316873,543470,1556225,1513754,679565 23 | MA,383568,701752,341713,665879,1782449,1751508,871098 24 | MI,625526,1179503,585169,974480,2628322,2706100,1304322 25 | MN,358471,606802,289371,507289,1416063,1391878,650519 26 | MS,220813,371502,174405,305964,764203,730133,371598 27 | MO,399450,690476,331543,560463,1569626,1554812,805235 28 | MT,61114,106088,53156,95232,236297,278241,137312 29 | NE,132092,215265,99638,186657,457177,451756,240847 30 | NV,199175,325650,142976,212379,769913,653357,296717 31 | NH,75297,144235,73826,119114,345109,388250,169978 32 | NJ,557421,1011656,478505,769321,2379649,2335168,1150941 33 | NM,148323,241326,112801,203097,517154,501604,260051 34 | NY,1208495,2141490,1058031,1999120,5355235,5120254,2607672 35 | NC,652823,1097890,492964,883397,2575603,2380685,1139052 36 | ND,41896,67358,33794,82629,154913,166615,94276 37 | OH,743750,1340492,646135,1081734,3019147,3083815,1570837 38 | OK,266547,438926,200562,369916,957085,918688,490637 39 | OR,243483,424167,199925,338162,1044056,1036269,503998 40 | PA,737462,1345341,679201,1203944,3157759,3414001,1910571 41 | RI,60934,111408,56198,114502,277779,282321,147646 42 | SC,303024,517803,245400,438147,1193112,1186019,596295 43 | SD,58566,94438,45305,82869,196738,210178,116100 44 | TN,416334,725948,336312,550612,1719433,1646623,819626 45 | TX,2027307,3277946,1420518,2454721,7017731,5656528,2472223 46 | UT,268916,413034,167685,329585,772024,538978,246202 47 | VT,32635,62538,33757,61679,155419,188593,86649 48 | VA,522672,887525,413004,768475,2203286,2033550,940577 49 | WA,433119,750274,357782,610378,1850983,1762811,783877 50 | WV,105435,189649,91074,157989,470749,514505,285067 51 | WI,362277,640286,311849,553914,1487457,1522038,750146 52 | WY,38253,60890,29314,53980,137338,147279,65614 53 | -------------------------------------------------------------------------------- /examples/data/stateslived.csv: -------------------------------------------------------------------------------- 1 | state,visited 2 | Alabama,0 3 | Alaska,0 4 | Arkansas,0 5 | Arizona,2 6 | California,2 7 | Colorado,1 8 | Connecticut,1 9 | Delaware,0 10 | Florida,0 11 | Georgia,0 12 | Hawaii,0 13 | Iowa,0 14 | Idaho,1 15 | Illinois,1 16 | Indiana,0 17 | Kansas,0 18 | Kentucky,0 19 | Louisiana,0 20 | Maine,1 21 | Maryland,1 22 | Massachusetts,1 23 | Michigan,0 24 | Minnesota,1 25 | Missouri,0 26 | Mississippi,0 27 | Montana,1 28 | North Carolina,0 29 | North Dakota,0 30 | Nebraska,0 31 | New Hampshire,1 32 | New Jersey,0 33 | New Mexico,1 34 | Nevada,1 35 | New York,2 36 | Ohio,1 37 | Oklahoma,0 38 | Oregon,1 39 | Pennsylvania,1 40 | Rhode Island,1 41 | South Carolina,0 42 | South Dakota,0 43 | Tennessee,0 44 | Texas,0 45 | Utah,1 46 | Virginia,0 47 | Vermont,0 48 | Washington,1 49 | Wisconsin,0 50 | West Virginia,0 51 | Wyoming,1 -------------------------------------------------------------------------------- /examples/visualizations/d3/bar-chart.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-undef */ 2 | // eslint-disable-next-line no-unused-vars 3 | async function createBarChart() { 4 | // set the dimensions and margins of the graph 5 | const margin = { top: 30, right: 30, bottom: 70, left: 60 }; 6 | const width = 720 - margin.left - margin.right; 7 | const height = 720 - margin.top - margin.bottom; 8 | 9 | // append the svg object to the body of the page 10 | let svg = d3.create('svg') 11 | .attr('width', width + margin.left + margin.right) 12 | .attr('height', height + margin.top + margin.bottom) 13 | .attr('id', 'chart'); 14 | 15 | const r = svg; 16 | svg = svg.append('g').attr('transform', 'translate(' + margin.left + ',' + margin.top + ')'); 17 | 18 | // Parse the Data 19 | const data = await d3.csv('https://raw.githubusercontent.com/holtzy/data_to_viz/master/Example_dataset/7_OneCatOneNum_header.csv'); 20 | 21 | // sort data 22 | data.sort(function(b, a) { 23 | return a.Value - b.Value; 24 | }); 25 | 26 | // X axis 27 | const x = d3.scaleBand() 28 | .range([0, width]) 29 | .domain(data.map(function(d) { return d.Country; })) 30 | .padding(0.2); 31 | svg.append('g') 32 | .attr('transform', 'translate(0,' + height + ')') 33 | .call(d3.axisBottom(x)) 34 | .selectAll('text') 35 | .attr('transform', 'translate(-10,0)rotate(-45)') 36 | .style('text-anchor', 'end'); 37 | 38 | // Add Y axis 39 | const y = d3.scaleLinear() 40 | .domain([0, 13000]) 41 | .range([height, 0]); 42 | svg.append('g') 43 | .call(d3.axisLeft(y)); 44 | 45 | // Bars 46 | svg.selectAll('mybar') 47 | .data(data) 48 | .enter() 49 | .append('rect') 50 | .attr('x', function(d) { return x(d.Country); }) 51 | .attr('y', function(d) { return y(d.Value); }) 52 | .attr('width', x.bandwidth()) 53 | .attr('height', function(d) { return height - y(d.Value); }) 54 | .attr('fill', '#69b3a2'); 55 | 56 | return r.node(); 57 | } 58 | -------------------------------------------------------------------------------- /examples/visualizations/d3/custom-map.js: -------------------------------------------------------------------------------- 1 | function createMap() { 2 | //Width and height of map 3 | var width = 960; 4 | var height = 500; 5 | 6 | // D3 Projection 7 | var projection = d3.geoAlbersUsa() 8 | .translate([width/2, height/2]) // translate to center of screen 9 | .scale([1000]); // scale things down so see entire US 10 | 11 | // Define path generator 12 | var path = d3.geoPath() // path generator that will convert GeoJSON to SVG paths 13 | .projection(projection); // tell path generator to use albersUsa projection 14 | 15 | 16 | // Define linear scale for output 17 | var color = d3.scaleLinear() 18 | .range(["rgb(213,222,217)","rgb(69,173,168)","rgb(84,36,55)","rgb(217,91,67)"]); 19 | 20 | var legendText = ["Cities Lived", "States Lived", "States Visited", "Nada"]; 21 | 22 | //Create SVG element and append map to the SVG 23 | var svg = d3.select("#map") 24 | .append("svg") 25 | .attr("id", "mapsvg") 26 | .attr("width", width) 27 | .attr("height", height); 28 | 29 | // Append Div for tooltip to SVG 30 | // var div = d3.select("body") 31 | // .append("div") 32 | // .attr("class", "tooltip") 33 | // .style("opacity", 0); 34 | 35 | // Load in my states data! 36 | d3.csv("https://raw.githubusercontent.com/Luke-S-Snyder/divi/main/examples/data/stateslived.csv").then(function(data) { 37 | color.domain([0,1,2,3]); // setting the range of the input data 38 | // Load GeoJSON data and merge with states data 39 | d3.json("https://gist.githubusercontent.com/michellechandra/0b2ce4923dc9b5809922/raw/a476b9098ba0244718b496697c5b350460d32f99/us-states.json").then(function(json) { 40 | 41 | // Loop through each state data value in the .csv file 42 | for (var i = 0; i < data.length; i++) { 43 | 44 | // Grab State Name 45 | var dataState = data[i].state; 46 | 47 | // Grab data value 48 | var dataValue = data[i].visited; 49 | 50 | // Find the corresponding state inside the GeoJSON 51 | for (var j = 0; j < json.features.length; j++) { 52 | var jsonState = json.features[j].properties.name; 53 | 54 | if (dataState == jsonState) { 55 | // Copy the data value into the JSON 56 | json.features[j].properties.visited = dataValue; 57 | 58 | // Stop looking through the JSON 59 | break; 60 | } 61 | } 62 | } 63 | 64 | // Bind the data to the SVG and create one path per GeoJSON feature 65 | svg.selectAll("path") 66 | .data(json.features) 67 | .enter() 68 | .append("path") 69 | .attr("d", path) 70 | .style("stroke", "#fff") 71 | .style("stroke-width", "1") 72 | .style("fill", function(d) { 73 | // Get data value 74 | var value = d.properties.visited; 75 | 76 | if (value) { 77 | //If value exists… 78 | return color(value); 79 | } else { 80 | //If value is undefined… 81 | return "rgb(213,222,217)"; 82 | } 83 | }); 84 | 85 | // Map the cities I have lived in! 86 | d3.csv("https://raw.githubusercontent.com/Luke-S-Snyder/divi/main/examples/data/cities-lived.csv").then(function(data) { 87 | 88 | svg.selectAll("circle") 89 | .data(data) 90 | .enter() 91 | .append("circle") 92 | .attr("cx", function(d) { 93 | return projection([d.lon, d.lat])[0]; 94 | }) 95 | .attr("cy", function(d) { 96 | return projection([d.lon, d.lat])[1]; 97 | }) 98 | .attr("r", function(d) { 99 | return Math.sqrt(d.years) * 4; 100 | }) 101 | .style("fill", "rgb(217,91,67)") 102 | .style("opacity", 0.85) 103 | 104 | // // Modified Legend Code from Mike Bostock: http://bl.ocks.org/mbostock/3888852 105 | // var legend = d3.select("body").append("svg") 106 | // .attr("class", "legend") 107 | // .attr("width", 140) 108 | // .attr("height", 200) 109 | // .selectAll("g") 110 | // .data(color.domain().slice().reverse()) 111 | // .enter() 112 | // .append("g") 113 | // .attr("transform", function(d, i) { return "translate(0," + i * 20 + ")"; }); 114 | 115 | // legend.append("rect") 116 | // .attr("width", 18) 117 | // .attr("height", 18) 118 | // .style("fill", color); 119 | 120 | // legend.append("text") 121 | // .data(legendText) 122 | // .attr("x", 24) 123 | // .attr("y", 9) 124 | // .attr("dy", ".35em") 125 | // .text(function(d) { return d; }); 126 | divi.hydrate("#mapsvg"); 127 | }); 128 | }); 129 | }); 130 | } 131 | -------------------------------------------------------------------------------- /examples/visualizations/d3/glyph-map.js: -------------------------------------------------------------------------------- 1 | function createGlyphMap() { 2 | // Load in my states data! 3 | d3.json("https://gist.githubusercontent.com/michellechandra/0b2ce4923dc9b5809922/raw/a476b9098ba0244718b496697c5b350460d32f99/us-states.json").then(function(map_data) { 4 | d3.json("https://cdn.jsdelivr.net/npm/us-atlas@3/counties-albers-10m.json").then(function(us){ 5 | // Load GeoJSON data and merge with states data 6 | d3.json("https://api.census.gov/data/2016/acs/acs5/cprofile?get=CP05_2012_2016_001E,NAME&for=county:*").then(function(json) { 7 | 8 | 9 | // D3 Projection 10 | var projection = d3.geoAlbersUsa() 11 | // let path1 = d3.geoPath(d3.geoAlbersUsa().translate([500, 500]).scale(750)); 12 | // .translate([500/2, 500/2]) // translate to center of screen 13 | // .scale([1000]); // scale things down so see entire US 14 | 15 | // Define path generator 16 | var path = d3.geoPath() // path generator that will convert GeoJSON to SVG paths 17 | // .projection(projection); // tell path generator to use albersUsa projection 18 | 19 | let features = new Map(topojson.feature(us, us.objects.counties).features.map(d => [d.id, d])); 20 | let data = json.slice(1).map(([population, name, state, county]) => { 21 | const id = state + county; 22 | const feature = features.get(id); 23 | // console.log(feature) 24 | // console.log([path1.centroid(feature), path.centroid(feature)]) 25 | return { 26 | id, 27 | position: feature && path.centroid(feature), 28 | title: feature && feature.properties.name, 29 | value: +population 30 | }; 31 | }); 32 | // console.log(data) 33 | let length = d3.scaleLinear([0, d3.max(data, d => d.value)], [0, 200]); 34 | let spike = (length, width = 7) => `M${-width / 2},0L0,${-length}L${width / 2},0`; 35 | 36 | const svg = d3.select("#container") 37 | .append("svg") 38 | .attr("id", "chart") 39 | .attr("width", 2000) 40 | .attr('height', 2000); 41 | 42 | // svg.append("path") 43 | // .datum(topojson.feature(us, us.objects.nation)) 44 | // .attr("fill", "#e0e0e0") 45 | // .attr("d", path); 46 | 47 | // svg.append("path") 48 | // .datum(topojson.mesh(us, us.objects.states, (a, b) => a !== b)) 49 | // .attr("fill", "none") 50 | // .attr("stroke", "white") 51 | // .attr("stroke-linejoin", "round") 52 | // .attr("d", path); 53 | 54 | 55 | // Bind the data to the SVG and create one path per GeoJSON feature 56 | // Bind the data to the SVG and create one path per GeoJSON feature 57 | 58 | var t = topojson.feature(us, us.objects.states); 59 | svg.selectAll("path") 60 | .data(t.features) 61 | .enter() 62 | .append("path") 63 | .attr("d", path) 64 | .style("stroke", "#fff") 65 | .style("stroke-width", "1") 66 | .style("fill", function(d) { 67 | // Get data value 68 | var value = d.properties.visited; 69 | value = false; 70 | 71 | if (value) { 72 | //If value exists… 73 | return color(value); 74 | } else { 75 | //If value is undefined… 76 | return "rgb(213,222,217)"; 77 | } 78 | }); 79 | 80 | const legend = svg.append("g") 81 | .attr("fill", "#777") 82 | .attr("text-anchor", "middle") 83 | .attr("font-family", "sans-serif") 84 | .attr("font-size", 10) 85 | .selectAll("g") 86 | .data(length.ticks(4).slice(1).reverse()) 87 | .join("g") 88 | .attr("transform", (d, i) => `translate(${1000 - (i + 1) * 18},525)`); 89 | 90 | legend.append("path") 91 | .attr("fill", "red") 92 | .attr("fill-opacity", 0.3) 93 | .attr("stroke", "red") 94 | .attr("d", d => spike(length(d))); 95 | 96 | legend.append("text") 97 | .attr("dy", "1.3em") 98 | .text(length.tickFormat(4, "s")); 99 | 100 | svg.append("g") 101 | .attr("fill", "red") 102 | .attr("fill-opacity", 0.3) 103 | .attr("stroke", "red") 104 | .selectAll("path") 105 | .data(data 106 | .filter(d => d.position) 107 | .sort((a, b) => d3.ascending(a.position[1], b.position[1]) 108 | || d3.ascending(a.position[0], b.position[0]))) 109 | .join("path") 110 | .attr("transform", d => `translate(${d.position})`) 111 | .attr("d", d => spike(length(d.value))) 112 | .append("title") 113 | .text(d => `${d.title} 114 | ${d3.format(d.value)}`); 115 | 116 | divi.hydrate("#chart") 117 | }); 118 | }); 119 | }); 120 | } 121 | -------------------------------------------------------------------------------- /examples/visualizations/d3/hex-map.js: -------------------------------------------------------------------------------- 1 | function createHexChart() { 2 | var width = 500; 3 | var height = 300; 4 | 5 | // The svg 6 | var svg = d3.select("#container") 7 | .append("svg") 8 | .attr("width", width) 9 | .attr("height", height) 10 | .attr("id", "chart"); 11 | 12 | // Map and projection 13 | var projection = d3.geoMercator() 14 | .scale(350) // This is the zoom 15 | .translate([850, 440]); // You have to play with these values to center your map 16 | 17 | // Path generator 18 | var path = d3.geoPath() 19 | .projection(projection) 20 | 21 | // Load external data and boot 22 | d3.json("https://raw.githubusercontent.com/holtzy/D3-graph-gallery/master/DATA/us_states_hexgrid.geojson.json").then(function(data){ 23 | // Draw the map 24 | svg.append("g") 25 | .selectAll("path") 26 | .data(data.features) 27 | .enter() 28 | .append("path") 29 | .attr("fill", "#69a2a2") 30 | .attr("d", path) 31 | .attr("stroke", "white") 32 | 33 | // Add the labels 34 | svg.append("g") 35 | .selectAll("labels") 36 | .data(data.features) 37 | .enter() 38 | .append("text") 39 | .attr("x", function(d){return path.centroid(d)[0]}) 40 | .attr("y", function(d){return path.centroid(d)[1]}) 41 | .text(function(d){ return d.properties.iso3166_2}) 42 | .attr("text-anchor", "middle") 43 | .attr("alignment-baseline", "central") 44 | .style("font-size", 11) 45 | .style("fill", "white") 46 | 47 | divi.hydrate("#chart"); 48 | }) 49 | 50 | } 51 | -------------------------------------------------------------------------------- /examples/visualizations/d3/line-chart.js: -------------------------------------------------------------------------------- 1 | function createLineChart() { 2 | // set the dimensions and margins of the graph 3 | // var margin = {top: 10, right: 30, bottom: 30, left: 60}, 4 | // width = 460 - margin.left - margin.right, 5 | // height = 400 - margin.top - margin.bottom; 6 | var margin = {top: 10, right: 30, bottom: 40, left: 50}, 7 | width = 720 - margin.left - margin.right, 8 | height = 720 - margin.top - margin.bottom 9 | 10 | // append the svg object to the body of the page 11 | var svg_line = d3.select("#container") 12 | .append("svg") 13 | .attr("width", width + margin.left + margin.right) 14 | .attr("height", height + margin.top + margin.bottom) 15 | .attr("id", "chart") 16 | .append("g") 17 | .attr("transform", 18 | "translate(" + margin.left + "," + margin.top + ")"); 19 | 20 | //Read the data 21 | d3.csv("https://raw.githubusercontent.com/holtzy/data_to_viz/master/Example_dataset/3_TwoNumOrdered_comma.csv").then(function(data) { 22 | 23 | // // When reading the csv, I must format variables: 24 | // function(d){ 25 | // return { date : d3.timeParse("%Y-%m-%d")(d.date), value : d.value } 26 | // } 27 | data = data.map(function(d) { 28 | return { date : d3.timeParse("%Y-%m-%d")(d.date), value : d.value } 29 | }); 30 | 31 | // Add X axis --> it is a date format 32 | var x = d3.scaleTime() 33 | .domain(d3.extent(data, function(d) { return d.date; })) 34 | .range([ 0, width ]); 35 | svg_line.append("g") 36 | .attr("transform", "translate(0," + height + ")") 37 | .call(d3.axisBottom(x)); 38 | 39 | // Add Y axis 40 | var y = d3.scaleLinear() 41 | .domain([0, d3.max(data, function(d) { return +d.value; })]) 42 | .range([ height, 0 ]); 43 | svg_line.append("g") 44 | .call(d3.axisLeft(y)); 45 | 46 | // Add the line 47 | svg_line.append("g").append("path") 48 | .datum(data) 49 | .attr("fill", "none") 50 | .attr("stroke", "steelblue") 51 | .attr("stroke-width", 1.5) 52 | .attr("d", d3.line() 53 | .x(function(d) { return x(d.date) }) 54 | .y(function(d) { return y(d.value) }) 55 | ) 56 | 57 | divi.hydrate("#chart"); 58 | }) 59 | } 60 | -------------------------------------------------------------------------------- /examples/visualizations/d3/log-chart.js: -------------------------------------------------------------------------------- 1 | function createLogChart() { 2 | var superscript = "⁰¹²³⁴⁵⁶⁷⁸⁹", 3 | formatPower = function(d) { return (d + "").split("").map(function(c) { return superscript[c]; }).join(""); }; 4 | 5 | var margin = {top: 40.5, right: 40.5, bottom: 50.5, left: 60.5}, 6 | width = 720 - margin.left - margin.right, 7 | height = 720 - margin.top - margin.bottom; 8 | 9 | // var x = d3.scaleLinear() 10 | // .domain([0, 100]) 11 | // .range([0, width]); 12 | 13 | // var y = d3.scaleLog() 14 | // .base(Math.E) 15 | // .domain([Math.exp(0), Math.exp(9)]) 16 | // .range([height, 0]); 17 | 18 | let data = d3.range(10).map(function(x) { return [Math.exp(x), x * Math.exp(2.5)]; }); 19 | 20 | var x = d3.scaleLog() 21 | // .base(Math.E) 22 | .domain([Math.exp(0), Math.exp(9)]) 23 | .range([0, width]); 24 | 25 | var y = d3.scaleLinear() 26 | .domain(d3.extent(data.map(d => d[1]))) 27 | .range([height, 0]); 28 | 29 | 30 | var xAxis = d3.axisBottom() 31 | .scale(x) 32 | // .tickFormat(function(d) { return "e" + formatPower(Math.round(Math.log(d))); }); 33 | 34 | var yAxis = d3.axisLeft() 35 | .scale(y); 36 | 37 | var line = d3.line() 38 | .x(function(d) { return x(d[0]); }) 39 | .y(function(d) { return y(d[1]); }); 40 | 41 | var svg = d3.select("#container").append("svg") 42 | .attr("id", "chart") 43 | .attr("width", width + margin.left + margin.right) 44 | .attr("height", height + margin.top + margin.bottom) 45 | .append("g") 46 | .attr("transform", "translate(" + margin.left + "," + margin.top + ")"); 47 | 48 | svg.append("g") 49 | .attr("class", "axis axis--y") 50 | .attr("transform", "translate(-10,0)") 51 | .call(yAxis); 52 | 53 | var gx= svg.append("g") 54 | .attr("class", "axis axis--x") 55 | .attr("transform", "translate(0," + (height + 10) + ")") 56 | .call(xAxis); 57 | 58 | 59 | svg.append("path") 60 | .datum(data) 61 | .attr("class", "line") 62 | .attr("d", line) 63 | .attr("fill", "none") 64 | .attr("stroke", "steelblue") 65 | .attr("strokewidth", "1.5px"); 66 | 67 | // let svg_ = d3.select("svg"); 68 | 69 | // svg_.call(d3.zoom().on("zoom", function({transform}) { 70 | // console.log('here') 71 | // gx.call(xAxis.scale(transform.rescaleX(x))); 72 | // x.range([margin.left, width - margin.right] 73 | // .map(d => transform.applyX(d))); 74 | // svg_.select(".line") 75 | // .attr("d", d3.line() 76 | // .x(function(d) { return x(d[0]); }) 77 | // .y(function(d) { return y(d[1]); }) 78 | // ) 79 | // })); 80 | 81 | divi.hydrate("#chart"); 82 | } 83 | -------------------------------------------------------------------------------- /examples/visualizations/d3/pie-chart.js: -------------------------------------------------------------------------------- 1 | 2 | // set the dimensions and margins of the graph 3 | var width = 450 4 | height = 450 5 | margin = 40 6 | 7 | // The radius of the pieplot is half the width or half the height (smallest one). I subtract a bit of margin. 8 | var radius = Math.min(width, height) / 2 - margin 9 | 10 | // append the svg object to the div called 'my_dataviz' 11 | var svg = d3.select("#vis") 12 | .append("svg") 13 | .attr("width", width) 14 | .attr("height", height) 15 | .attr("id", "svg_plot") 16 | .append("g") 17 | .attr("transform", "translate(" + width / 2 + "," + height / 2 + ")"); 18 | 19 | // Create dummy data 20 | var data = {a: 9, b: 20, c:30, d:8, e:12} 21 | 22 | // set the color scale 23 | var color = d3.scaleOrdinal() 24 | .domain(data) 25 | .range(["#98abc5", "#8a89a6", "#7b6888", "#6b486b", "#a05d56"]) 26 | 27 | // Compute the position of each group on the pie: 28 | var pie = d3.pie() 29 | .value(function(d) {return d.value; }) 30 | var data_ready = pie(d3.entries(data)) 31 | 32 | // Build the pie chart: Basically, each part of the pie is a path that we build using the arc function. 33 | svg 34 | .selectAll('whatever') 35 | .data(data_ready) 36 | .enter() 37 | .append('path') 38 | .attr('d', d3.arc() 39 | .innerRadius(0) 40 | .outerRadius(radius) 41 | ) 42 | .attr('fill', function(d){ return(color(d.data.key)) }) 43 | .attr("stroke", "black") 44 | .style("stroke-width", "2px") 45 | .style("opacity", 0.7) 46 | -------------------------------------------------------------------------------- /examples/visualizations/d3/scatter-plot.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-undef */ 2 | // eslint-disable-next-line no-unused-vars 3 | async function createScatterPlot() { 4 | // set the dimensions and margins of the graph 5 | const margin = { top: 10, right: 30, bottom: 40, left: 50 }; 6 | const width = 720 - margin.left - margin.right; 7 | const height = 720 - margin.top - margin.bottom; 8 | 9 | // append the svg object to the body of the page 10 | let svg = d3.create('svg') 11 | .attr('width', width + margin.left + margin.right) 12 | .attr('height', height + margin.top + margin.bottom) 13 | .attr('id', 'chart'); 14 | 15 | const r = svg; 16 | svg = svg.append('g').attr('transform', 'translate(' + margin.left + ',' + margin.top + ')'); 17 | 18 | // Read the data 19 | const data = await d3.csv('https://raw.githubusercontent.com/holtzy/D3-graph-gallery/master/DATA/iris.csv'); 20 | 21 | // Add X axis 22 | const x = d3.scaleLinear() 23 | .domain([4 * 0.95, 8 * 1.001]) 24 | .range([0, width]); 25 | 26 | const xAxis = svg.append('g') 27 | .attr('transform', 'translate(0,' + height + ')') 28 | .call(d3.axisBottom(x).tickSize(-height * 1.3).ticks(10)); 29 | 30 | xAxis.select('.domain').remove(); 31 | 32 | // Add Y axis 33 | const y = d3.scaleLinear() 34 | .domain([-0.001, 9 * 1.01]) 35 | .range([height, 0]) 36 | .nice(); 37 | 38 | svg.append('g') 39 | .call(d3.axisLeft(y).tickSize(-width * 1.3).ticks(7)) 40 | .select('.domain').remove(); 41 | 42 | // Customization 43 | svg.selectAll('.tick line').attr('stroke', 'black').attr('opacity', 0.3); 44 | 45 | // Add X axis label: 46 | svg.append('text') 47 | .attr('text-anchor', 'end') 48 | .attr('x', width / 2 + margin.left) 49 | .attr('y', height + margin.top + 20) 50 | .text('Sepal Length'); 51 | 52 | // Y axis label: 53 | svg.append('text') 54 | .attr('text-anchor', 'end') 55 | .attr('transform', 'rotate(-90)') 56 | .attr('y', -margin.left + 20) 57 | .attr('x', -margin.top - height / 2 + 20) 58 | .text('Petal Length'); 59 | 60 | // Color scale: give me a specie name, I return a color 61 | const color = d3.scaleOrdinal() 62 | .domain(['setosa', 'versicolor', 'virginica']) 63 | .range(['#F8766D', '#00BA38', '#619CFF']); 64 | 65 | // Add dots 66 | svg.append('g') 67 | .selectAll('dot') 68 | .data(data) 69 | .enter() 70 | .append('circle') 71 | .attr('cx', function(d) { return x(d.Sepal_Length); }) 72 | .attr('cy', function(d) { return y(d.Petal_Length); }) 73 | .attr('r', 5) 74 | .style('fill', function(d) { return color(d.Species); }); 75 | 76 | return r.node(); 77 | } 78 | -------------------------------------------------------------------------------- /examples/visualizations/d3/stacked-area-chart.js: -------------------------------------------------------------------------------- 1 | function createStackedChart() { 2 | // // set the dimensions and margins of the graph 3 | // var margin = {top: 20, right: 30, bottom: 30, left: 55}, 4 | // width = 460 - margin.left - margin.right, 5 | // height = 400 - margin.top - margin.bottom; 6 | let margin = {top: 10, right: 30, bottom: 40, left: 50}, 7 | width = 720 - margin.left - margin.right, 8 | height = 720 - margin.top - margin.bottom 9 | 10 | // append the svg object to the body of the page 11 | let svg = d3.select("#container") 12 | .append("svg") 13 | .attr("width", width + margin.left + margin.right) 14 | .attr("height", height + margin.top + margin.bottom) 15 | .attr("id", "chart") 16 | .append("g") 17 | .attr("transform", 18 | "translate(" + margin.left + "," + margin.top + ")"); 19 | 20 | // Parse the Data 21 | d3.csv("https://raw.githubusercontent.com/holtzy/data_to_viz/master/Example_dataset/5_OneCatSevNumOrdered_wide.csv").then(function(data) { 22 | 23 | // List of groups = header of the csv files 24 | let keys = data.columns.slice(1) 25 | 26 | // Add X axis 27 | let x = d3.scaleLinear() 28 | .domain(d3.extent(data, function(d) { return d.year; })) 29 | .range([ 0, width ]); 30 | svg.append("g") 31 | .attr("transform", "translate(0," + height + ")") 32 | .call(d3.axisBottom(x).ticks(5)); 33 | 34 | // Add Y axis 35 | let y = d3.scaleLinear() 36 | .domain([0, 200000]) 37 | .range([ height, 0 ]); 38 | svg.append("g") 39 | .call(d3.axisLeft(y)); 40 | 41 | // color palette 42 | let color = d3.scaleOrdinal() 43 | .domain(keys) 44 | .range(['#e41a1c','#377eb8','#4daf4a','#984ea3','#ff7f00','#ffff33','#a65628','#f781bf']) 45 | 46 | //stack the data? 47 | let stackedData = d3.stack() 48 | .keys(keys) 49 | (data) 50 | //console.log("This is the stack result: ", stackedData) 51 | 52 | // Show the areas 53 | svg 54 | .append("g") 55 | .selectAll("mylayers") 56 | .data(stackedData) 57 | .enter() 58 | .append("path") 59 | .style("fill", function(d) { return color(d.key); }) 60 | .attr("d", d3.area() 61 | .x(function(d, i) { return x(d.data.year); }) 62 | .y0(function(d) { return y(d[0]); }) 63 | .y1(function(d) { return y(d[1]); }) 64 | ) 65 | 66 | divi.hydrate("#chart"); 67 | }) 68 | } 69 | -------------------------------------------------------------------------------- /examples/visualizations/d3/stacked-bar-chart.js: -------------------------------------------------------------------------------- 1 | function createStackedBarChart() { 2 | // var svg = d3.select("stackedbarsvg"), 3 | var margin = {top: 20, right: 20, bottom: 30, left: 40}, 4 | width = 1000 - margin.left - margin.right, 5 | height = 720 - margin.top - margin.bottom; 6 | // g = svg.append("g").attr("transform", "translate(" + margin.left + "," + margin.top + ")"); 7 | 8 | let svg = d3.select("#container") 9 | .append("svg") 10 | .attr("width", width + margin.left + margin.right) 11 | .attr("height", height + margin.top + margin.bottom) 12 | .attr("id", "chart") 13 | .append("g") 14 | .attr("transform", "translate(" + margin.left + "," + margin.top + ")") 15 | 16 | var x = d3.scaleBand() 17 | .rangeRound([0, width]) 18 | .paddingInner(0.05) 19 | .align(0.1); 20 | 21 | var y = d3.scaleLinear() 22 | .rangeRound([height, 0]); 23 | 24 | var z = d3.scaleOrdinal() 25 | .range(["#98abc5", "#8a89a6", "#7b6888", "#6b486b", "#a05d56", "#d0743c", "#ff8c00"]); 26 | 27 | d3.csv("../../../examples/data/data.csv").then(function(data) { 28 | for (const d of data) { 29 | for (i = 1, t = 0; i < data.columns.length; ++i) t += d[data.columns[i]] = +d[data.columns[i]]; 30 | d.total = t; 31 | } 32 | 33 | var keys = data.columns.slice(1); 34 | 35 | data.sort(function(a, b) { return b.total - a.total; }); 36 | x.domain(data.map(function(d) { return d.State; })); 37 | y.domain([0, d3.max(data, function(d) { return d.total; })]).nice(); 38 | z.domain(keys); 39 | 40 | svg.append("g") 41 | .selectAll("g") 42 | .data(d3.stack().keys(keys)(data)) 43 | .enter().append("g") 44 | .attr("fill", function(d) { return z(d.key); }) 45 | .selectAll("rect") 46 | .data(function(d) { return d; }) 47 | .enter().append("rect") 48 | .attr("x", function(d) { return x(d.data.State); }) 49 | .attr("y", function(d) { return y(d[1]); }) 50 | .attr("height", function(d) { return y(d[0]) - y(d[1]); }) 51 | .attr("width", x.bandwidth()); 52 | 53 | svg.append("g") 54 | .attr("class", "axis") 55 | .attr("transform", "translate(0," + height + ")") 56 | .call(d3.axisBottom(x)) 57 | .select(".domain").remove();; 58 | 59 | svg.append("g") 60 | .attr("class", "axis") 61 | .call(d3.axisLeft(y).ticks(null, "s")) 62 | .select(".domain").remove() 63 | .append("text") 64 | .attr("x", 2) 65 | .attr("y", y(y.ticks().pop()) + 0.5) 66 | .attr("dy", "0.32em") 67 | .attr("fill", "#000") 68 | .attr("font-weight", "bold") 69 | .attr("text-anchor", "start") 70 | .text("Population"); 71 | 72 | var legend = svg.append("g") 73 | .attr("font-family", "sans-serif") 74 | .attr("font-size", 10) 75 | .attr("text-anchor", "end") 76 | .selectAll("g") 77 | .data(keys.slice().reverse()) 78 | .enter().append("g") 79 | .attr("transform", function(d, i) { return "translate(0," + i * 20 + ")"; }); 80 | 81 | legend.append("rect") 82 | .attr("x", width - 19) 83 | .attr("width", 19) 84 | .attr("height", 19) 85 | .attr("fill", z); 86 | 87 | legend.append("text") 88 | .attr("x", width - 24) 89 | .attr("y", 9.5) 90 | .attr("dy", "0.32em") 91 | .text(function(d) { return d; }); 92 | 93 | divi.hydrate("#chart"); 94 | }); 95 | } -------------------------------------------------------------------------------- /examples/visualizations/ggplot2/multi-view-setup/excel-scatter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uwdata/divi/41890655aef95e5f6f1f51c930374739da6a1954/examples/visualizations/ggplot2/multi-view-setup/excel-scatter.png -------------------------------------------------------------------------------- /examples/visualizations/ggplot2/multi-view-setup/multi-view-setup.R: -------------------------------------------------------------------------------- 1 | library(ggplot2) 2 | library(svglite) 3 | library(tidyverse) 4 | # theme_set(theme_minimal()) 5 | theme_set(theme_gray() + theme(legend.key=element_blank())) 6 | 7 | X <- read.csv(url("https://vega.github.io/vega-datasets/data/seattle-weather.csv")) 8 | X$date <- as.Date(X$date, format="%Y-%m-%d") 9 | X$cycle <- as.Date(format(X$date, format="%b %d"), format='%b %d') 10 | X <- na.omit(X) 11 | library(lubridate) 12 | year(X$cycle) <- 1990 13 | X 14 | scatter1 <- ggplot(X, aes(x = temp_max, y = precipitation, color = weather)) + 15 | geom_point(size=10) + 16 | # geom_point(aes(size=precipitation)) + 17 | # scale_x_date(date_labels = "%Y-%m-%d") + 18 | # ggtitle('Seattle Weather Data 2012-2015') + 19 | ylab('Precip. (in.)') + 20 | xlab('Max. temp') + 21 | theme(plot.title = element_text(size=50)) + 22 | theme(axis.text = element_text(size=40)) + 23 | theme(axis.title = element_text(size=35)) + 24 | theme(legend.title = element_text(size=35)) + 25 | theme(legend.text = element_text(size=40)) + 26 | theme(legend.key.size = unit(5, 'cm')) + 27 | theme(legend.spacing.x = unit(0.5, 'cm')) + 28 | theme(legend.position = 'right') + 29 | guides(colour = guide_legend(title = 'Weather', override.aes = list(size=10))) 30 | # theme(legend.justification = 'top') 31 | scatter1 32 | 33 | write.csv(X, '~/GitHub/divi/examples/data/seattle-weather.csv', row.names=FALSE) 34 | ggsave(file="~/GitHub/divi/examples/visualizations/ggplot2/multi-view-setup/scatter.svg", plot=scatter1, width=40, height=15) 35 | 36 | X2 <- X 37 | X2$cycle <- format(X2$cycle, format='%b %d') 38 | X2 <- X2 %>% 39 | group_by(cycle) %>% 40 | summarise(precipitation = mean(precipitation)) 41 | 42 | X2$cycle <- as.Date(X2$cycle, format='%b %d') 43 | 44 | scatter2 <- ggplot(X2, aes(x = cycle, y = precipitation)) + 45 | geom_line(aes()) + 46 | # geom_point(aes(size=precipitation)) + 47 | scale_x_date(date_labels = "%b %d") + 48 | ggtitle('Seattle Weather Data 2012-2015') + 49 | xlab('Date') + 50 | ylab('Daily Avg. Precipitation (in.)') + 51 | theme(plot.title = element_text(size=20)) + 52 | theme(axis.text = element_text(size=12)) + 53 | theme(axis.title = element_text(size=15)) + 54 | theme(legend.title = element_text(size=15)) + 55 | theme(legend.text = element_text(size=15)) + 56 | theme(legend.key.size = unit(1, 'cm')) + 57 | guides(colour = guide_legend(title = 'Weather', override.aes = list(size=3))) 58 | # theme(legend.justification = 'top') 59 | scatter2 60 | 61 | ggsave(file="~/GitHub/divi/examples/visualizations/ggplot2/multi-view-setup/line.svg", plot=scatter2, width=10.75, height=6) 62 | -------------------------------------------------------------------------------- /examples/visualizations/ggplot2/multi-view-setup/seattle-weather.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uwdata/divi/41890655aef95e5f6f1f51c930374739da6a1954/examples/visualizations/ggplot2/multi-view-setup/seattle-weather.png -------------------------------------------------------------------------------- /examples/visualizations/ggplot2/stock-multiline.R: -------------------------------------------------------------------------------- 1 | library(ggplot2) 2 | library(svglite) 3 | library(tidyverse) 4 | theme_set(theme_minimal()) 5 | 6 | X <- read.csv(url("https://vega.github.io/vega-datasets/data/seattle-weather.csv")) 7 | X$date <- as.Date(X$date,format="%Y-%m-%d") 8 | 9 | X1 <- X %>% 10 | filter(weather == "rain") 11 | # X 12 | 13 | image <- ggplot(X1, aes(x = date, y = temp_max)) + 14 | geom_point() + 15 | scale_x_date(date_labels = "%b %Y") 16 | #ggtitle('Rain') 17 | image 18 | # image <- ggplot(X, aes(x = date, y = price, group = symbol, color = symbol)) + 19 | # geom_line() + 20 | # scale_x_date(date_labels = "%b %d %Y") 21 | # ggsave(file="weather-scatter.svg", plot=image, width=12, height=7) 22 | # ggsave(file="weather-scatter2.svg", plot=image, width=14, height=8) 23 | 24 | ggsave(file="weather-scatter3.svg", plot=image, width=12, height=6) 25 | 26 | X2 <- X %>% 27 | filter(weather == "sun") 28 | 29 | image <- ggplot(X1, aes(x = date, y = temp_min)) + 30 | geom_point() + 31 | scale_x_date(date_labels = "%b %Y") 32 | # ggtitle('Sun') 33 | image 34 | ggsave(file="weather-scatter4.svg", plot=image, width=12, height=6) 35 | -------------------------------------------------------------------------------- /examples/visualizations/matplotlib/bar-chart.py: -------------------------------------------------------------------------------- 1 | import matplotlib.pyplot as plt; plt.rcdefaults() 2 | import numpy as np 3 | import pandas as pd 4 | import matplotlib.pyplot as plt 5 | 6 | df = pd.read_csv('../../data/seattle-weather.csv') 7 | df = df[['weather', 'temp_max']] 8 | df = df.groupby(['weather'], as_index=False).sum() 9 | 10 | bar_widths = df['temp_max'].to_numpy() 11 | 12 | 13 | plt.barh(np.arange(len(df['weather'].to_numpy())), df['temp_max'].to_numpy(), align='center') 14 | plt.yticks(np.arange(len(df['weather'].to_numpy())), df['weather'].to_numpy()) 15 | plt.xlabel('Sum of maximum temp.') 16 | plt.ylabel('Weather') 17 | plt.rcParams['svg.fonttype'] = 'none' 18 | plt.grid() 19 | plt.savefig('bars.svg') 20 | -------------------------------------------------------------------------------- /examples/visualizations/matplotlib/bars2.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 2023-03-29T10:18:28.511892 11 | image/svg+xml 12 | 13 | 14 | Matplotlib v3.3.4, https://matplotlib.org/ 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 31 | 32 | 33 | 34 | 40 | 41 | 42 | 43 | 44 | 47 | 48 | 49 | 50 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 0 60 | 61 | 62 | 63 | 64 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 1 75 | 76 | 77 | 78 | 79 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 2 90 | 91 | 92 | 93 | 94 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 3 105 | 106 | 107 | 108 | 109 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 4 120 | 121 | 122 | 123 | 124 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 5 135 | 136 | 137 | 138 | Std temp_max 139 | 140 | 141 | 142 | 143 | 144 | 147 | 148 | 149 | 150 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | rain 160 | 161 | 162 | 163 | 164 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | snow 175 | 176 | 177 | 178 | Weather 179 | 180 | 181 | 182 | 185 | 186 | 187 | 190 | 191 | 192 | 195 | 196 | 197 | 200 | 201 | 202 | 208 | 209 | 210 | 216 | 217 | 218 | 219 | 224 | 225 | -------------------------------------------------------------------------------- /examples/visualizations/matplotlib/multi-view-setup/bar-charts.py: -------------------------------------------------------------------------------- 1 | import matplotlib.pyplot as plt; plt.rcdefaults() 2 | import numpy as np 3 | import pandas as pd 4 | import matplotlib as m 5 | import matplotlib.pyplot as plt 6 | 7 | font = {'size': 50} 8 | 9 | m.rc('font', **font) 10 | 11 | df = pd.read_csv('../../../data/seattle-weather.csv') 12 | 13 | # _df1 = df[['weather', 'wind']] 14 | # _df1 = _df1.groupby(['weather'], as_index=False).mean() 15 | 16 | # bar_widths = _df1['wind'].to_numpy() 17 | 18 | # plt.barh(np.arange(len(_df1['weather'].to_numpy())), _df1['wind'].to_numpy(), align='center', zorder=3) 19 | # plt.yticks(np.arange(len(_df1['weather'].to_numpy())), _df1['weather'].to_numpy()) 20 | # plt.xlabel('Avg., wind') 21 | # plt.ylabel('Weather') 22 | # plt.xlim([0, 8]) 23 | # plt.rcParams['svg.fonttype'] = 'none' 24 | # plt.tight_layout() 25 | # plt.grid(zorder=0, linewidth=0.3) 26 | 27 | # plt.savefig('bars1.svg') 28 | 29 | plt.clf() 30 | _df = df[['weather', 'precipitation']] 31 | # _df = df[(df['weather'] == 'rain') | (df['weather'] == 'snow')] 32 | _df = _df.groupby(['weather'], as_index=False).sum() 33 | 34 | bar_widths = _df['precipitation'].to_numpy() 35 | 36 | plt.figure(figsize=(30, 10)) 37 | plt.barh(np.arange(len(_df['weather'].to_numpy())), _df['precipitation'].to_numpy(), align='center', zorder=3) 38 | plt.yticks(np.arange(len(_df['weather'].to_numpy())), _df['weather'].to_numpy()) 39 | plt.xlabel('Sum, precipitation') 40 | plt.ylabel('Weather') 41 | plt.rcParams['svg.fonttype'] = 'none' 42 | plt.tight_layout() 43 | plt.grid(zorder=0, linewidth=0.4) 44 | plt.savefig('bars_test.svg') 45 | -------------------------------------------------------------------------------- /examples/visualizations/matplotlib/multi-view.py: -------------------------------------------------------------------------------- 1 | import matplotlib.pyplot as plt; plt.rcdefaults() 2 | import numpy as np 3 | import pandas as pd 4 | import matplotlib.pyplot as plt 5 | 6 | df = pd.read_csv('../../data/seattle-weather.csv') 7 | 8 | _df = df[['weather', 'temp_max']] 9 | _df = _df.groupby(['weather'], as_index=False).count() 10 | 11 | bar_widths = _df['temp_max'].to_numpy() 12 | 13 | plt.barh(np.arange(len(_df['weather'].to_numpy())), _df['temp_max'].to_numpy(), align='center', zorder=3) 14 | plt.yticks(np.arange(len(_df['weather'].to_numpy())), _df['weather'].to_numpy()) 15 | plt.xlabel('# of Records') 16 | plt.ylabel('Weather') 17 | plt.rcParams['svg.fonttype'] = 'none' 18 | plt.tight_layout() 19 | plt.grid(zorder=0, linewidth=0.4) 20 | # plt.savefig('bars1.svg') 21 | 22 | plt.clf() 23 | _df = df[['weather', 'temp_max']] 24 | _df = df[(df['weather'] == 'rain') | (df['weather'] == 'snow')] 25 | _df = _df.groupby(['weather'], as_index=False).std() 26 | 27 | bar_widths = _df['temp_max'].to_numpy() 28 | 29 | plt.barh(np.arange(len(_df['weather'].to_numpy())), _df['temp_max'].to_numpy(), align='center', zorder=3) 30 | plt.yticks(np.arange(len(_df['weather'].to_numpy())), _df['weather'].to_numpy()) 31 | plt.xlabel('Std temp_max') 32 | plt.ylabel('Weather') 33 | plt.rcParams['svg.fonttype'] = 'none' 34 | plt.tight_layout() 35 | plt.grid(zorder=0, linewidth=0.4) 36 | plt.savefig('bars2.svg') 37 | -------------------------------------------------------------------------------- /examples/visualizations/multi-view/d3-ggplot2.js: -------------------------------------------------------------------------------- 1 | async function createGgplot2D3() { 2 | let svg1 = await fetch('/examples/visualizations/ggplot2/stock-multiline.svg'); 3 | svg1 = await svg1.text(); 4 | 5 | document.querySelector("#chart1").innerHTML = svg1; 6 | document.querySelector("#chart1 svg").id = "chart1"; 7 | 8 | divi.hydrate(['#chart1']); 9 | } 10 | -------------------------------------------------------------------------------- /examples/visualizations/multi-view/gg-matplot.js: -------------------------------------------------------------------------------- 1 | async function createGgMatplot() { 2 | let svg1 = await fetch('/examples/visualizations/ggplot2/weather-scatter2.svg'); 3 | svg1 = await svg1.text(); 4 | 5 | let svg2 = await fetch('/examples/visualizations/matplotlib/bars1.svg'); 6 | svg2 = await svg2.text(); 7 | 8 | let svg3 = await fetch('/examples/visualizations/matplotlib/bars2.svg'); 9 | svg3 = await svg3.text(); 10 | 11 | document.querySelector("#chart1").innerHTML = svg1; 12 | document.querySelector("#chart1 svg").id = "chart1"; 13 | 14 | document.querySelector("#chart2").innerHTML = svg2; 15 | document.querySelector("#chart2 svg").id = "chart2"; 16 | 17 | document.querySelector("#chart3").innerHTML = svg3; 18 | document.querySelector("#chart3 svg").id = "chart3"; 19 | 20 | divi.hydrate(['#chart1', '#chart2', '#chart3']); 21 | } 22 | -------------------------------------------------------------------------------- /examples/visualizations/multi-view/gg-weather-facet.js: -------------------------------------------------------------------------------- 1 | async function createGgFacet() { 2 | let svg1 = await fetch('/examples/visualizations/ggplot2/weather-scatter3.svg'); 3 | svg1 = await svg1.text(); 4 | 5 | let svg2 = await fetch('/examples/visualizations/ggplot2/weather-scatter4.svg'); 6 | svg2 = await svg2.text(); 7 | 8 | document.querySelector("#chart1").innerHTML = svg1; 9 | document.querySelector("#chart1 svg").id = "chart1"; 10 | 11 | document.querySelector("#chart2").innerHTML = svg2; 12 | document.querySelector("#chart2 svg").id = "chart2"; 13 | 14 | divi.hydrate(['#chart1', '#chart2']); 15 | } 16 | -------------------------------------------------------------------------------- /examples/visualizations/multi-view/ggplot2-setup.js: -------------------------------------------------------------------------------- 1 | async function createGgplot2Setup() { 2 | let svg1 = await fetch('/examples/visualizations/ggplot2/multi-view-setup/excel-scatter.svg'); 3 | svg1 = await svg1.text(); 4 | 5 | let svg2 = await fetch('/examples/visualizations/ggplot2/multi-view-setup/scatter.svg'); 6 | svg2 = await svg2.text(); 7 | 8 | let svg3 = await fetch('/examples/visualizations/matplotlib/multi-view-setup/bars_test.svg'); 9 | svg3 = await svg3.text(); 10 | 11 | let svg4 = await fetch('/examples/visualizations/matplotlib/multi-view-setup/bars1.svg'); 12 | svg4 = await svg4.text(); 13 | 14 | document.querySelector("#chart1").innerHTML = svg1; 15 | document.querySelector("#chart1 svg").id = "svg1"; 16 | 17 | document.querySelector("#chart2").innerHTML = svg2; 18 | document.querySelector("#chart2 svg").id = "svg2"; 19 | 20 | document.querySelector("#chart3").innerHTML = svg3; 21 | document.querySelector("#chart3 svg").id = "svg3"; 22 | 23 | // document.querySelector("#chart4").innerHTML = svg4; 24 | // document.querySelector("#chart4 svg").id = "svg4"; 25 | 26 | divi.hydrate(['#svg1', '#svg2', '#svg3'], {url:'/examples/data/seattle-weather.csv'}); 27 | } 28 | -------------------------------------------------------------------------------- /examples/visualizations/multi-view/ggplot2-vegalite.js: -------------------------------------------------------------------------------- 1 | async function createGgplot2VegaLite() { 2 | const spec1 = { 3 | "$schema": "https://vega.github.io/schema/vega-lite/v5.json", 4 | "title": "Seattle Weather, 2012-2015", 5 | "data": { 6 | "url": "https://vega.github.io/vega-datasets/data/seattle-weather.csv" 7 | }, 8 | "encoding": { 9 | "color": { 10 | "title": "Weather", 11 | "field": "weather", 12 | "type": "nominal", 13 | "scale": { 14 | "domain": ["sun", "fog", "drizzle", "rain", "snow"], 15 | "range": ["#e7ba52", "#a7a7a7", "#aec7e8", "#1f77b4", "#9467bd"] 16 | } 17 | }, 18 | "size": { 19 | "title": "Precipitation", 20 | "field": "precipitation", 21 | "scale": {"type": "linear", "domain": [-1, 50]}, 22 | "type": "quantitative" 23 | }, 24 | "x": { 25 | "field": "date", 26 | "timeUnit": "utcyearmonthdate", 27 | "title": "Date", 28 | "axis": {"format": "%b %Y"} 29 | }, 30 | "y": { 31 | "title": "Maximum Daily Temperature (C)", 32 | "field": "temp_max", 33 | "scale": {"domain": [-5, 40]}, 34 | "type": "quantitative" 35 | } 36 | }, 37 | "width": 700, 38 | "height": 400, 39 | "mark": "point" 40 | } 41 | 42 | let view1 = new vega.View(vega.parse(vegaLite.compile(spec1).spec), { renderer: 'svg' }); 43 | let svg1 = await view1.toSVG(); 44 | 45 | let svg2 = await fetch('/examples/visualizations/ggplot2/weather-scatter.svg'); 46 | svg2 = await svg2.text(); 47 | 48 | document.querySelector("#chart1").innerHTML = svg1; 49 | document.querySelector("#chart1 svg").id = "chart1"; 50 | 51 | document.querySelector("#chart2").innerHTML = svg2; 52 | document.querySelector("#chart2 svg").id = "chart2"; 53 | 54 | divi.hydrate(['#chart1 svg', '#chart2 svg']); 55 | } 56 | -------------------------------------------------------------------------------- /examples/visualizations/multi-view/vega-bar-scatter.js: -------------------------------------------------------------------------------- 1 | async function createVegaBarScatter() { 2 | const spec1 = { 3 | "data": {"url": "https://vega.github.io/vega-datasets/data/cars.json"}, 4 | "mark": "bar", 5 | "encoding": { 6 | "y": {"field": "Horsepower", "bin": true}, 7 | "x": {"aggregate": "sum", "field": "Acceleration"}, 8 | // "color": {"field": "Origin", "legend": null}, 9 | }, 10 | width: 200, 11 | height: 200 12 | }; 13 | 14 | const spec2 = { 15 | "data": {"url": "https://vega.github.io/vega-datasets/data/cars.json"}, 16 | "mark": "bar", 17 | "encoding": { 18 | "y": {"field": "Miles_per_Gallon", "bin": true}, 19 | "x": {"aggregate": "max", "field": "Horsepower"}, 20 | // "color": {"field": "Origin", "legend": null} 21 | }, 22 | width: 200, 23 | height: 200 24 | }; 25 | 26 | const spec3 = { 27 | "data": {"url": "https://vega.github.io/vega-datasets/data/cars.json"}, 28 | "mark": "bar", 29 | "encoding": { 30 | "y": {"field": "Acceleration", "bin": true}, 31 | "x": {"aggregate": "count"}, 32 | // "color": {"field": "Origin"}, 33 | }, 34 | width: 200, 35 | height: 200 36 | }; 37 | 38 | const spec4 = { 39 | "data": {"url": "https://vega.github.io/vega-datasets/data/cars.json"}, 40 | "mark": "bar", 41 | "encoding": { 42 | "y": {"field": "Displacement", "bin": true}, 43 | "x": {"aggregate": "count"}, 44 | // "color": {"field": "Origin", "legend": null}, 45 | }, 46 | width: 200, 47 | height: 200 48 | }; 49 | 50 | const spec5 = { 51 | "data": {"url": "https://vega.github.io/vega-datasets/data/cars.json"}, 52 | "mark": "point", 53 | "encoding": { 54 | "x": {"field": "Acceleration", "type": "quantitative"}, 55 | "y": {"field": "Displacement", "type": "quantitative"}, 56 | // "size": {"field": "Horsepower", "type": "quantitative"}, 57 | }, 58 | width: 400, 59 | height: 400 60 | }; 61 | 62 | var view1 = new vega.View(vega.parse(vegaLite.compile(spec1).spec), { renderer: 'svg' }); 63 | var view2 = new vega.View(vega.parse(vegaLite.compile(spec2).spec), { renderer: 'svg' }); 64 | var view3 = new vega.View(vega.parse(vegaLite.compile(spec3).spec), { renderer: 'svg' }); 65 | var view4 = new vega.View(vega.parse(vegaLite.compile(spec4).spec), { renderer: 'svg' }); 66 | var view5 = new vega.View(vega.parse(vegaLite.compile(spec5).spec), { renderer: 'svg' }); 67 | 68 | var svg1 = await view1.toSVG(); 69 | var svg2 = await view2.toSVG(); 70 | var svg3 = await view3.toSVG(); 71 | var svg4 = await view4.toSVG(); 72 | var svg5 = await view5.toSVG(); 73 | 74 | document.querySelector("#chart1").innerHTML = svg1; 75 | document.querySelector("#chart1 svg").id = "chart1"; 76 | 77 | // document.querySelector("#chart2").innerHTML = svg2; 78 | // document.querySelector("#chart2 svg").id = "chart2"; 79 | 80 | // document.querySelector("#chart3").innerHTML = svg3; 81 | // document.querySelector("#chart3 svg").id = "chart3"; 82 | 83 | document.querySelector("#chart4").innerHTML = svg4; 84 | document.querySelector("#chart4 svg").id = "chart4"; 85 | 86 | // document.querySelector("#chart5").innerHTML = svg5; 87 | // document.querySelector("#chart5 svg").id = "chart5"; 88 | 89 | divi.hydrate(["#chart1 svg","#chart4 svg"], 90 | { url: "https://vega.github.io/vega-datasets/data/cars.json" }); 91 | } 92 | -------------------------------------------------------------------------------- /examples/visualizations/multi-view/vega-crossfilter.js: -------------------------------------------------------------------------------- 1 | function createVegaCrossfilter() { 2 | const spec1 = { 3 | "$schema": "https://vega.github.io/schema/vega-lite/v5.json", 4 | "data": { 5 | "url": "https://vega.github.io/vega-datasets/data/flights-2k.json", 6 | "format": {"parse": {"date": "date"}} 7 | }, 8 | "transform": [{"calculate": "hours(datum.date)", "as": "time"}], 9 | "encoding": { 10 | "y": { 11 | "field": "distance", 12 | "bin": {"maxbins": 20} 13 | }, 14 | "x": {"aggregate": "count"} 15 | }, 16 | "mark": "bar", 17 | width: 200, 18 | height: 200 19 | }; 20 | 21 | const spec2 = { 22 | "$schema": "https://vega.github.io/schema/vega-lite/v5.json", 23 | "data": { 24 | "url": "https://vega.github.io/vega-datasets/data/flights-2k.json", 25 | "format": {"parse": {"date": "date"}} 26 | }, 27 | "transform": [{"calculate": "hours(datum.date)", "as": "time", "filter": {"field": "origin", 28 | "oneOf": ["TUL", "TUS", "LAX", "ABQ", "OKC", "SAN", "LAS"]}}], 29 | "encoding": { 30 | "y": { 31 | "field": "origin", 32 | }, 33 | "x": {"aggregate": "stdev", "field": "delay", "scale": {"domain": [0, 70]}} 34 | }, 35 | "mark": "bar", 36 | width: 200, 37 | height: 200 38 | }; 39 | 40 | // const spec3 = { 41 | // "$schema": "https://vega.github.io/schema/vega-lite/v5.json", 42 | // "data": { 43 | // "url": "https://vega.github.io/vega-datasets/data/flights-2k.json", 44 | // "format": {"parse": {"date": "date"}} 45 | // }, 46 | // "transform": [{"calculate": "hours(datum.date)", "as": "time"}], 47 | // "encoding": { 48 | // "y": { 49 | // "field": "time", 50 | // "bin": {"maxbins": 20} 51 | // }, 52 | // "x": {"aggregate": "count"} 53 | // }, 54 | // "mark": "bar", 55 | // width: 400, 56 | // height: 400 57 | // }; 58 | 59 | const spec4 = { 60 | "$schema": "https://vega.github.io/schema/vega-lite/v5.json", 61 | "data": { 62 | "url": "https://vega.github.io/vega-datasets/data/flights-2k.json", 63 | "format": {"parse": {"date": "date"}} 64 | }, 65 | "encoding": { 66 | "y": { 67 | "field": "delay", 68 | "bin": {"maxbins": 20} 69 | }, 70 | "x": {"aggregate": "count"} 71 | }, 72 | "mark": "bar", 73 | width: 200, 74 | height: 200 75 | }; 76 | 77 | const spec5 = { 78 | "$schema": "https://vega.github.io/schema/vega-lite/v5.json", 79 | "data": { 80 | "url": "https://vega.github.io/vega-datasets/data/flights-2k.json", 81 | "format": {"parse": {"date": "date"}} 82 | }, 83 | "transform": [{"calculate": "hours(datum.date)", "as": "time", "filter": {"field": "origin", 84 | "oneOf": ["TUL", "TUS", "LAX", "ABQ", "OKC", "SAN", "LAS"]}}], 85 | "encoding": { 86 | "y": { "title": "Origin", "field": "origin" }, 87 | "x": {"aggregate": "mean", "field": "distance"} 88 | }, 89 | "mark": "bar", 90 | width: 200, 91 | height: 200 92 | }; 93 | 94 | var view1 = new vega.View(vega.parse(vegaLite.compile(spec1).spec), { renderer: 'svg' }); 95 | var view2 = new vega.View(vega.parse(vegaLite.compile(spec4).spec), { renderer: 'svg' }); 96 | var view3 = new vega.View(vega.parse(vegaLite.compile(spec2).spec), { renderer: 'svg' }); 97 | view1.toSVG().then(function(svg1) { 98 | view2.toSVG().then(function(svg2) { 99 | view3.toSVG().then(function(svg3) { 100 | document.querySelector("#chart1").innerHTML = svg1; 101 | document.querySelector("#chart1 svg").id = "chart1"; 102 | 103 | document.querySelector("#chart2").innerHTML = svg2; 104 | document.querySelector("#chart2 svg").id = "chart2"; 105 | 106 | document.querySelector("#chart3").innerHTML = svg3; 107 | document.querySelector("#chart3 svg").id = "chart3"; 108 | 109 | divi.hydrate(["#chart1 svg", "#chart2 svg", "#chart3 svg"], { url: "https://vega.github.io/vega-datasets/data/flights-2k.json" }); 110 | }) 111 | }) 112 | }); 113 | } 114 | -------------------------------------------------------------------------------- /examples/visualizations/multi-view/vega-dual-linking.js: -------------------------------------------------------------------------------- 1 | async function getPlot(id) { 2 | let data = await d3.json('https://vega.github.io/vega-datasets/data/cars.json', d3.autoType); 3 | // const origins = [...new Set(data.map(d => d.Origin))]; 4 | // const incomes = [31,133, 42,000, 17,390]; 5 | 6 | // data = [ 7 | // {Origin: origins[0], Income: incomes[0]}, 8 | // {Origin: origins[1], Income: incomes[1]}, 9 | // {Origin: origins[2], Income: incomes[2]} 10 | // ]; 11 | const p = Plot.plot({ 12 | width: 350, 13 | height: 350, 14 | color: { 15 | legend: false, 16 | style: { marginLeft: 300 }, 17 | scheme: "category10" 18 | }, 19 | x: { 20 | grid: true 21 | }, 22 | y: { 23 | grid: true 24 | }, 25 | marks: [ 26 | Plot.dot(data, {x: "Weight_in_lbs", y: "Acceleration", stroke: "Origin"}) 27 | ] 28 | }); 29 | 30 | return p; 31 | } 32 | 33 | async function createVegaDualLinking() { 34 | const spec1 = { 35 | "data": {"url": "https://vega.github.io/vega-datasets/data/cars.json"}, 36 | "mark": "bar", 37 | "encoding": { 38 | "y": {"field": "Horsepower", "bin": true}, 39 | "x": {"aggregate": "mean", "field": "Acceleration"}, 40 | }, 41 | width: 300, 42 | height: 300 43 | }; 44 | 45 | const spec2 = { 46 | "data": {"url": "https://vega.github.io/vega-datasets/data/cars.json"}, 47 | "mark": "bar", 48 | "encoding": { 49 | "y": {"field": "Acceleration", "bin": true}, 50 | "x": {"aggregate": "max", "field": "Cylinders"}, 51 | }, 52 | width: 300, 53 | height: 300 54 | }; 55 | 56 | const spec3 = { 57 | "data": {"url": "https://vega.github.io/vega-datasets/data/cars.json"}, 58 | "mark": "point", 59 | "encoding": { 60 | "x": {"field": "Horsepower", "type": "quantitative"}, 61 | "y": {"field": "Displacement", "type": "quantitative"}, 62 | "color": {"field": "Origin"}, 63 | }, 64 | width: 300, 65 | height: 300 66 | }; 67 | 68 | var view1 = new vega.View(vega.parse(vegaLite.compile(spec1).spec), { renderer: 'svg' }); 69 | var view2 = new vega.View(vega.parse(vegaLite.compile(spec2).spec), { renderer: 'svg' }); 70 | var view3 = new vega.View(vega.parse(vegaLite.compile(spec3).spec), { renderer: 'svg' }); 71 | 72 | var svg1 = await view1.toSVG(); 73 | var svg2 = await view2.toSVG(); 74 | var svg3 = await view3.toSVG(); 75 | var svg4 = await getPlot('chart4'); 76 | 77 | document.querySelector("#chart1").innerHTML = svg1; 78 | document.querySelector("#chart1 svg").id = "chart1"; 79 | 80 | document.querySelector("#chart2").innerHTML = svg2; 81 | document.querySelector("#chart2 svg").id = "chart2"; 82 | 83 | document.querySelector("#chart3").innerHTML = svg3; 84 | document.querySelector("#chart3 svg").id = "chart3"; 85 | 86 | d3.select("#chart4").node().append(svg4); 87 | d3.select("#chart4 svg").node().id = "chart4"; 88 | d3.select('#chart4 svg').style('max-width', 350) 89 | 90 | divi.hydrate([ "#chart1 svg", "#chart2 svg", "#chart3 svg", "#chart4 svg"], 91 | { url: "https://vega.github.io/vega-datasets/data/cars.json" }); 92 | } 93 | -------------------------------------------------------------------------------- /examples/visualizations/multi-view/vega-lite-weather.js: -------------------------------------------------------------------------------- 1 | async function createVegaMultiView() { 2 | const spec1 = { 3 | "$schema": "https://vega.github.io/schema/vega-lite/v5.json", 4 | "title": "Seattle Weather, 2012-2015", 5 | "data": { 6 | "url": "https://vega.github.io/vega-datasets/data/seattle-weather.csv" 7 | }, 8 | "encoding": { 9 | "color": { 10 | "title": "Weather", 11 | "field": "weather", 12 | "type": "nominal", 13 | "scale": { 14 | "domain": ["sun", "fog", "drizzle", "rain", "snow"], 15 | "range": ["#e7ba52", "#a7a7a7", "#aec7e8", "#1f77b4", "#9467bd"] 16 | } 17 | }, 18 | "size": { 19 | "title": "Precipitation", 20 | "field": "precipitation", 21 | "scale": {"type": "linear", "domain": [-1, 50]}, 22 | "type": "quantitative" 23 | }, 24 | "x": { 25 | "field": "date", 26 | "timeUnit": "utcyearmonthdate", 27 | "title": "Date", 28 | "axis": {"format": "%b %Y"} 29 | }, 30 | "y": { 31 | "title": "Maximum Daily Temperature (C)", 32 | "field": "temp_max", 33 | "scale": {"domain": [-5, 40]}, 34 | "type": "quantitative" 35 | } 36 | }, 37 | "width": 700, 38 | "height": 400, 39 | "mark": "point" 40 | } 41 | 42 | const spec2 = { 43 | "$schema": "https://vega.github.io/schema/vega-lite/v5.json", 44 | "title": "Seattle Weather, 2012-2015", 45 | "data": { 46 | "url": "https://vega.github.io/vega-datasets/data/seattle-weather.csv" 47 | }, 48 | "encoding": { 49 | "color": { 50 | "field": "weather", 51 | "scale": { 52 | "domain": ["sun", "fog", "drizzle", "rain", "snow"], 53 | "range": ["#e7ba52", "#a7a7a7", "#aec7e8", "#1f77b4", "#9467bd"] 54 | } 55 | }, 56 | "x": {"aggregate": "max", "field": "wind"}, 57 | "y": {"title": "Weather", "field": "weather"} 58 | }, 59 | "width": 700, 60 | "mark": "bar" 61 | } 62 | 63 | const spec3 = { 64 | "$schema": "https://vega.github.io/schema/vega-lite/v5.json", 65 | "title": "Seattle Weather, 2012-2015", 66 | "data": { 67 | "url": "https://vega.github.io/vega-datasets/data/seattle-weather.csv" 68 | }, 69 | "encoding": { 70 | "x": {"aggregate": "max", "field": "wind"}, 71 | "y": {"field": "precipitation", "bin": true} 72 | }, 73 | "width": 700, 74 | "mark": "bar" 75 | } 76 | 77 | var view1 = new vega.View(vega.parse(vegaLite.compile(spec1).spec), { renderer: 'svg' }); 78 | var view2 = new vega.View(vega.parse(vegaLite.compile(spec2).spec), { renderer: 'svg' }); 79 | var view3 = new vega.View(vega.parse(vegaLite.compile(spec3).spec), { renderer: 'svg' }); 80 | 81 | var svg3 = await view3.toSVG(); 82 | 83 | view1.toSVG().then(function(svg1) { 84 | view2.toSVG().then(function(svg2) { 85 | document.querySelector("#chart1").innerHTML = svg1; 86 | document.querySelector("#chart1 svg").id = "chart1"; 87 | document.querySelector("#chart2").innerHTML = svg2; 88 | document.querySelector("#chart2 svg").id = "chart2"; 89 | // document.querySelector("#chart3").innerHTML = svg3; 90 | // document.querySelector("#chart3 svg").id = "chart3"; 91 | divi.hydrate(["#chart1 svg", "#chart2 svg"], { url: "https://vega.github.io/vega-datasets/data/seattle-weather.csv" }); 92 | }) 93 | }); 94 | } 95 | -------------------------------------------------------------------------------- /examples/visualizations/multi-view/vega-matplotlib.js: -------------------------------------------------------------------------------- 1 | async function createVegaMatplotlib() { 2 | const spec1 = { 3 | "$schema": "https://vega.github.io/schema/vega-lite/v5.json", 4 | "title": "Seattle Weather, 2012-2015", 5 | "data": { 6 | "url": "https://vega.github.io/vega-datasets/data/seattle-weather.csv" 7 | }, 8 | "encoding": { 9 | "color": { 10 | "title": "Weather", 11 | "field": "weather", 12 | "type": "nominal", 13 | "scale": { 14 | "domain": ["sun", "fog", "drizzle", "rain", "snow"], 15 | "range": ["#e7ba52", "#a7a7a7", "#aec7e8", "#1f77b4", "#9467bd"] 16 | } 17 | }, 18 | "size": { 19 | "title": "Precipitation", 20 | "field": "precipitation", 21 | "scale": {"type": "linear", "domain": [-1, 50]}, 22 | "type": "quantitative" 23 | }, 24 | "x": { 25 | "field": "date", 26 | "timeUnit": "utcyearmonthdate", 27 | "title": "Date", 28 | "axis": {"format": "%b %Y"} 29 | }, 30 | "y": { 31 | "title": "Maximum Daily Temperature (C)", 32 | "field": "temp_max", 33 | "scale": {"domain": [-5, 40]}, 34 | "type": "quantitative" 35 | } 36 | }, 37 | "width": 700, 38 | "height": 400, 39 | "mark": "point" 40 | } 41 | 42 | var view1 = new vega.View(vega.parse(vegaLite.compile(spec1).spec), { renderer: 'svg' }); 43 | var svg2 = await fetch('/examples/visualizations/matplotlib/bars.svg'); 44 | svg2 = await svg2.text(); 45 | 46 | view1.toSVG().then(function(svg1) { 47 | document.querySelector("#chart1").innerHTML = svg1; 48 | document.querySelector("#chart1 svg").id = "chart1"; 49 | document.querySelector("#chart2").innerHTML = svg2; 50 | document.querySelector("#chart2 svg").id = "chart2"; 51 | divi.hydrate(["#chart1 svg", "#chart2 svg"], { url: "https://vega.github.io/vega-datasets/data/seattle-weather.csv" }); 52 | }); 53 | } 54 | -------------------------------------------------------------------------------- /examples/visualizations/observable-plot/bar-chart.js: -------------------------------------------------------------------------------- 1 | async function createBarChart() { 2 | let data = await d3.csv('https://raw.githubusercontent.com/observablehq/plot/main/test/data/alphabet.csv', d3.autoType); 3 | 4 | const p = Plot.plot({ 5 | width: 640, 6 | height: 400, 7 | y: { 8 | grid: true 9 | }, 10 | marks: [ 11 | Plot.barY(data, {x: "letter", y: "frequency"}) 12 | ] 13 | }); 14 | 15 | d3.select(p).attr('id', 'chart1'); 16 | d3.select('#container').node().append(p); 17 | divi.hydrate("#chart1"); 18 | } 19 | -------------------------------------------------------------------------------- /examples/visualizations/observable-plot/facets.js: -------------------------------------------------------------------------------- 1 | async function createFacets() { 2 | let data = await d3.json('https://vega.github.io/vega-datasets/data/cars.json'); 3 | 4 | const p1 = Plot.plot({ 5 | width: 300, 6 | height: 300, 7 | y: { 8 | grid: true 9 | }, 10 | color: { 11 | legend: false 12 | }, 13 | marks: [ 14 | Plot.dot(data, {x: "Acceleration", y: "Horsepower", stroke: "Origin"}) 15 | ] 16 | }); 17 | 18 | const p2 = Plot.plot({ 19 | width: 300, 20 | height: 300, 21 | y: { 22 | grid: true 23 | }, 24 | color: { 25 | legend: false 26 | }, 27 | marks: [ 28 | Plot.dot(data, {x: "Acceleration", y: "Displacement", stroke: "Origin"}) 29 | ] 30 | }); 31 | const p3 = Plot.plot({ 32 | width: 300, 33 | height: 300, 34 | y: { 35 | grid: true 36 | }, 37 | color: { 38 | legend: false 39 | }, 40 | marks: [ 41 | Plot.dot(data, {x: "Weight_in_lbs", y: "Horsepower", stroke: "Origin"}) 42 | ] 43 | }); 44 | const p4 = Plot.plot({ 45 | width: 300, 46 | height: 300, 47 | color: { 48 | legend: false 49 | }, 50 | y: { 51 | grid: true 52 | }, 53 | marks: [ 54 | Plot.dot(data, {x: "Cylinders", y: "Acceleration", stroke: "Origin"}) 55 | ] 56 | }); 57 | 58 | d3.select(p1).attr('id', 'chart1'); 59 | d3.select('#container').node().append(p1); 60 | 61 | d3.select(p2).attr('id', 'chart2'); 62 | d3.select('#container').node().append(p2); 63 | 64 | d3.select(p3).attr('id', 'chart3'); 65 | d3.select('#container').node().append(p3); 66 | 67 | d3.select(p4).attr('id', 'chart4'); 68 | d3.select('#container').node().append(p4); 69 | 70 | divi.hydrate(['#chart1', '#chart2', '#chart3', "#chart4"], { url: 'https://vega.github.io/vega-datasets/data/cars.json' }); 71 | } 72 | -------------------------------------------------------------------------------- /examples/visualizations/observable-plot/scatter-plot.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-undef */ 2 | // eslint-disable-next-line no-unused-vars 3 | async function createScatterPlot() { 4 | let data = await d3.csv('/examples/data/penguins.csv', d3.autoType); 5 | data = data.filter(d => d.sex != null); 6 | 7 | const p = Plot.plot({ 8 | inset: 8, 9 | grid: true, 10 | color: { 11 | legend: false, 12 | style: { marginLeft: 500 } 13 | }, 14 | marks: [ 15 | Plot.dot(data, { x: 'flipper_length_mm', y: 'body_mass_g', stroke: 'sex' }), 16 | Plot.axisY({ label: '↑ body_mass_g', marginLeft: 50 }) 17 | ] 18 | }); 19 | 20 | d3.select(p).attr('id', 'chart1'); 21 | return p; 22 | } 23 | -------------------------------------------------------------------------------- /examples/visualizations/vega-lite/bar-chart.js: -------------------------------------------------------------------------------- 1 | function createBarChart() { 2 | const spec = { 3 | "$schema": "https://vega.github.io/schema/vega-lite/v5.json", 4 | "title": "Seattle Weather, 2012-2015", 5 | "data": { 6 | "url": "https://vega.github.io/vega-datasets/data/seattle-weather.csv" 7 | }, 8 | "encoding": { 9 | "color": { 10 | "field": "weather", 11 | "scale": { 12 | "domain": ["sun", "fog", "drizzle", "rain", "snow"], 13 | "range": ["#e7ba52", "#a7a7a7", "#aec7e8", "#1f77b4", "#9467bd"] 14 | } 15 | }, 16 | "x": {"aggregate": "count"}, 17 | "y": {"title": "Weather", "field": "weather"} 18 | }, 19 | "width": 800, 20 | "mark": "bar" 21 | } 22 | var view = new vega.View(vega.parse(vegaLite.compile(spec).spec), { renderer: 'svg' }); 23 | view.toSVG().then(function(svg) { 24 | document.querySelector("#container").innerHTML = svg; 25 | document.querySelector("#container svg").id = "chart"; 26 | divi.hydrate("#chart"); 27 | }); 28 | } 29 | -------------------------------------------------------------------------------- /examples/visualizations/vega-lite/line-chart.js: -------------------------------------------------------------------------------- 1 | function createLineChart() { 2 | const spec = { 3 | "$schema": "https://vega.github.io/schema/vega-lite/v5.json", 4 | "description": "Stock prices of 5 Tech Companies over Time.", 5 | "data": {"url": "https://vega.github.io/vega-datasets/data/stocks.csv"}, 6 | "mark": "line", 7 | "height": 450, 8 | "width": 450, 9 | "encoding": { 10 | "x": {"field": "date", "type": "temporal"}, 11 | "y": {"field": "price", "type": "quantitative"}, 12 | "color": {"field": "symbol", "type": "nominal"} 13 | } 14 | } 15 | 16 | var view = new vega.View(vega.parse(vegaLite.compile(spec).spec), { renderer: 'svg' }); 17 | view.toSVG().then(function(svg) { 18 | document.querySelector("#container1").innerHTML = svg; 19 | document.querySelector("#container1 svg").id = "chart"; 20 | // divi.hydrate("#chart"); 21 | }); 22 | } 23 | -------------------------------------------------------------------------------- /examples/visualizations/vega-lite/scatter-plot-1.js: -------------------------------------------------------------------------------- 1 | function createScatterPlot() { 2 | const spec = { 3 | "$schema": "https://vega.github.io/schema/vega/v5.json", 4 | "description": "A basic scatter plot example depicting automobile statistics.", 5 | "width": 450, 6 | "height": 450, 7 | "padding": 5, 8 | 9 | "data": [ 10 | { 11 | "name": "source", 12 | "url": "https://vega.github.io/vega-datasets/data/cars.json", 13 | "transform": [ 14 | { 15 | "type": "filter", 16 | "expr": "datum['Horsepower'] != null && datum['Miles_per_Gallon'] != null && datum['Acceleration'] != null" 17 | } 18 | ] 19 | } 20 | ], 21 | 22 | "scales": [ 23 | { 24 | "name": "x", 25 | "type": "linear", 26 | "round": true, 27 | "nice": true, 28 | "zero": true, 29 | "domain": {"data": "source", "field": "Horsepower"}, 30 | "range": "width" 31 | }, 32 | { 33 | "name": "y", 34 | "type": "linear", 35 | "round": true, 36 | "nice": true, 37 | "zero": true, 38 | "domain": {"data": "source", "field": "Miles_per_Gallon"}, 39 | "range": "height" 40 | }, 41 | { 42 | "name": "size", 43 | "type": "linear", 44 | "round": true, 45 | "nice": false, 46 | "zero": true, 47 | "domain": {"data": "source", "field": "Acceleration"}, 48 | "range": [4,361] 49 | } 50 | ], 51 | 52 | "axes": [ 53 | { 54 | "scale": "x", 55 | "grid": true, 56 | "domain": false, 57 | "orient": "bottom", 58 | "tickCount": 5, 59 | "title": "Horsepower" 60 | }, 61 | { 62 | "scale": "y", 63 | "grid": true, 64 | "domain": false, 65 | "orient": "left", 66 | "titlePadding": 5, 67 | "title": "Miles_per_Gallon" 68 | } 69 | ], 70 | 71 | "legends": [ 72 | { 73 | "size": "size", 74 | "title": "Acceleration", 75 | "format": "s", 76 | "symbolStrokeColor": "#4682b4", 77 | "symbolStrokeWidth": 2, 78 | "symbolOpacity": 1, 79 | "symbolType": "circle" 80 | } 81 | ], 82 | 83 | "marks": [ 84 | { 85 | "name": "marks", 86 | "type": "symbol", 87 | "from": {"data": "source"}, 88 | "encode": { 89 | "update": { 90 | "x": {"scale": "x", "field": "Horsepower"}, 91 | "y": {"scale": "y", "field": "Miles_per_Gallon"}, 92 | "size": {"scale": "size", "field": "Acceleration"}, 93 | "shape": {"value": "circle"}, 94 | "strokeWidth": {"value": 2}, 95 | "opacity": {"value": 1}, 96 | "stroke": {"value": "#4682b4"}, 97 | "fill": {"value": "transparent"} 98 | } 99 | } 100 | } 101 | ] 102 | } 103 | 104 | var view = new vega.View(vega.parse(spec), { renderer: 'svg' }); 105 | view.toSVG().then(function(svg) { 106 | document.querySelector("#container").innerHTML = svg; 107 | document.querySelector("#container svg").id = "chart"; 108 | divi.hydrate("#chart"); 109 | }); 110 | } 111 | -------------------------------------------------------------------------------- /examples/visualizations/vega-lite/scatter-plot-2.js: -------------------------------------------------------------------------------- 1 | function createScatterPlot() { 2 | const spec = { 3 | "$schema": "https://vega.github.io/schema/vega-lite/v5.json", 4 | "title": "Seattle Weather, 2012-2015", 5 | "data": { 6 | "url": "https://vega.github.io/vega-datasets/data/seattle-weather.csv" 7 | }, 8 | "encoding": { 9 | "color": { 10 | "title": "Weather", 11 | "field": "weather", 12 | "type": "nominal", 13 | "scale": { 14 | "domain": ["sun", "fog", "drizzle", "rain", "snow"], 15 | "range": ["#e7ba52", "#a7a7a7", "#aec7e8", "#1f77b4", "#9467bd"] 16 | } 17 | }, 18 | "size": { 19 | "title": "Precipitation", 20 | "field": "precipitation", 21 | "scale": {"domain": [-1, 50]}, 22 | "type": "quantitative" 23 | }, 24 | "x": { 25 | "field": "date", 26 | "timeUnit": "monthdate", 27 | "title": "Date", 28 | "axis": {"format": "%m"} 29 | }, 30 | "y": { 31 | "title": "Maximum Daily Temperature (C)", 32 | "field": "temp_max", 33 | "scale": {"domain": [-5, 40]}, 34 | "type": "quantitative" 35 | } 36 | }, 37 | "width": 800, 38 | "height": 500, 39 | "mark": "point" 40 | } 41 | var view = new vega.View(vega.parse(vegaLite.compile(spec).spec), { renderer: 'svg' }); 42 | view.toSVG().then(function(svg) { 43 | document.querySelector("#container").innerHTML = svg; 44 | document.querySelector("#container svg").id = "chart"; 45 | divi.hydrate("#chart"); 46 | }); 47 | } 48 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@uwdata/divi", 3 | "version": "0.0.0", 4 | "description": "Dynamic interaction for SVG visualizations", 5 | "main": "src/index.js", 6 | "repository": { 7 | "type": "git", 8 | "url": "git+https://github.com/uwdata/DIVI.git" 9 | }, 10 | "type": "module", 11 | "author": "Luke Snyder (https://luke-s-snyder.github.io)", 12 | "license": "BSD-3-Clause", 13 | "scripts": { 14 | "docs:dev": "vitepress dev docs", 15 | "docs:build": "vitepress build docs", 16 | "docs:preview": "vitepress preview docs", 17 | "prebuild": "rimraf dist && mkdir dist", 18 | "build": "rollup -c", 19 | "lint": "eslint src test --ext .js", 20 | "test": "mocha 'test/node/**/*-test.js'", 21 | "prepublishOnly": "npm run test && npm run lint && npm run build", 22 | "serve": "wds --watch" 23 | }, 24 | "devDependencies": { 25 | "@rollup/plugin-commonjs": "^25.0.7", 26 | "@rollup/plugin-json": "^6.1.0", 27 | "@rollup/plugin-terser": "^0.4.4", 28 | "@web/dev-server": "^0.4.6", 29 | "eslint": "^8.41.0", 30 | "eslint-config-standard": "^17.1.0", 31 | "eslint-plugin-import": "^2.27.5", 32 | "eslint-plugin-n": "^16.0.0", 33 | "eslint-plugin-promise": "^6.1.1", 34 | "mocha": "^10.4.0", 35 | "rimraf": "^5.0.5", 36 | "rollup": "^4.16.4", 37 | "rollup-plugin-bundle-size": "^1.0.3", 38 | "rollup-plugin-postcss": "^4.0.2", 39 | "rollup-plugin-svg": "^2.0.0", 40 | "vitepress": "^1.1.4" 41 | }, 42 | "dependencies": { 43 | "arquero": "^5.4.0", 44 | "bootstrap": "^5.2.3", 45 | "d3-array": "^3.2.3", 46 | "d3-dispatch": "^3.0.1", 47 | "d3-drag": "^3.0.0", 48 | "d3-format": "^3.1.0", 49 | "d3-path": "^3.1.0", 50 | "d3-scale": "^4.0.2", 51 | "d3-selection": "^3.0.0", 52 | "d3-svg-annotation": "^2.5.1", 53 | "d3-time-format": "^4.1.0", 54 | "d3-transition": "^3.0.1", 55 | "svg-path-parser": "^1.1.0" 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | DIVI 4 | 5 | 6 | 7 |
8 | D3 Examples
9 | Scatter Plot
10 | Line Chart
11 | Stacked Area Chart
12 | Stacked Bar Chart
13 | Bar Chart
14 | Log Chart
15 | Hex Map
16 | US Population Map
17 |
18 | 19 | Vega-lite Examples
20 | Scatter Plot 1
21 | Scatter Plot 2
22 | Bar Chart
23 |
24 | 25 | Matplotlib Examples
26 |
27 | 28 | Observable Plot Examples
29 | Bar Chart
30 | Scatter Plot
31 | Facets
32 |
33 | 34 | ggplot2 Examples
35 | Line Chart
36 | Trendline
37 |
38 | 39 | Multi-view Examples
40 | Vega Multi-view
41 | Vega Multi-view Dual Linking
42 | Vega Multi-view Bar & Scatter Charts
43 | Vega Crossfilter
44 | Vega Matplotlib
45 | ggplot2 Line / D3 Line Single-attribute Linking
46 | ggplot2 / Vega-Lite Scatter Facet / Single-attribute Linking
47 | ggplot2 / Matplot
48 | ggplot2 weather facet
49 | ggplot2 / Matplot Setup
50 |
51 |
52 | 53 | 54 | -------------------------------------------------------------------------------- /public/pages/d3/bar-chart.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | D3 Bar Chart 4 | 5 | 6 | 7 | 8 | 9 |
10 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /public/pages/d3/hex-map.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | D3 Hex Map 4 | 5 | 6 | 7 | 8 | 9 |
10 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /public/pages/d3/line-chart.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | D3 Line Chart 4 | 5 | 6 | 7 | 8 | 9 |
10 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /public/pages/d3/log-chart.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | D3 Log Chart 4 | 5 | 6 | 7 | 8 | 9 |
10 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /public/pages/d3/population-map.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | D3 US Population Map 4 | 5 | 6 | 7 | 8 | 9 | 10 |
11 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /public/pages/d3/scatter-plot.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | D3 Scatter Plot 4 | 5 | 6 | 7 | 8 | 9 |
10 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /public/pages/d3/stacked-area-chart.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | D3 Stacked Area Chart 4 | 5 | 6 | 7 | 8 | 9 |
10 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /public/pages/d3/stacked-bar-chart.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | D3 Stacked Bar Chart 4 | 5 | 6 | 7 | 8 | 9 |
10 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /public/pages/ggplot2/line-chart.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | ggplot2 Line Chart 4 | 5 | 6 | 7 | 8 | 9 |
10 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /public/pages/ggplot2/trendline.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | ggplot2 Trendline 4 | 5 | 6 | 7 | 8 | 9 |
10 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /public/pages/multi-view/d3-ggplot2.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | D3 ggplot2 Multi-line Chart (Single-attribute linking) 4 | 5 | 6 | 7 | 8 | 9 | 10 |
11 |
12 |
13 |
14 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /public/pages/multi-view/gg-matplot.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | D3 ggplot2 Multi-line Chart (Single-attribute linking) 4 | 5 | 6 | 7 | 8 | 9 | 10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /public/pages/multi-view/gg-weather-facet.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | gg weather facet 4 | 5 | 6 | 7 | 8 | 9 | 10 |
11 |
12 |
13 |
14 |
15 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /public/pages/multi-view/ggplot2-multi-view-setup.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Matplotlib ggplot2 Setup 4 | 5 | 6 | 7 | 8 | 9 | 10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 | 19 |
20 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /public/pages/multi-view/vega-bar-scatter.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Vega-lite Multi-view Bar & Scatter Plots 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /public/pages/multi-view/vega-crossfilter.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Vega-lite Multi-view Chart 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 |
13 |
14 |
15 |
16 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /public/pages/multi-view/vega-dual-linking.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Vega-lite Multi-view Dual Linking 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |
14 |
15 |
16 |
17 |
18 |
19 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /public/pages/multi-view/vega-ggplot2.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | ggplot2 Vega-Lite Scatter Facet (Single-attribute linking) 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 |
13 |
14 |
15 | 18 | 19 | -------------------------------------------------------------------------------- /public/pages/multi-view/vega-lite-weather.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Vega-lite Multi-view Chart 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 |
13 |
14 |
15 |
16 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /public/pages/multi-view/vega-matplotlib.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Vega-lite Matplotlib Multi-view Chart 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 |
13 |
14 |
15 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /public/pages/observable-plot/bar-chart.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Observable Plot Bar Chart 4 | 5 | 6 | 7 | 8 | 9 | 10 |
11 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /public/pages/observable-plot/facets.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Observable Plot Facets 4 | 5 | 6 | 7 | 8 | 9 | 10 |
11 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /public/pages/observable-plot/scatter-plot.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Observable Plot Scatter Plot 4 | 5 | 6 | 7 | 8 | 9 | 10 |
11 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /public/pages/vega-lite/bar-chart.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Vega-lite Bar Chart 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /public/pages/vega-lite/scatter-plot-1.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Vega-lite Scatter Plot 1 4 | 5 | 6 | 7 | 8 | 9 | 10 |
11 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /public/pages/vega-lite/scatter-plot-2.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Vega-lite Scatter Plot 2 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /public/study.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | DIVI 4 | 5 | 6 | 7 |
8 | Single-view
9 | Vega-Lite Scatter Plot
10 | D3 Stacked Bar Chart
11 |
12 | 13 | Multi-view
14 | Vega-Lite Multi-view
15 | Vega-Lite Crossfilter
16 |
17 | 18 | Cross-tool
19 | Vega-Lite + Matplotlib
20 | D3 + ggplot2
21 |
22 |
23 | 24 | 25 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import bundleSize from 'rollup-plugin-bundle-size'; 2 | import terser from '@rollup/plugin-terser'; 3 | import nodeResolve from '@rollup/plugin-node-resolve'; 4 | import commonJS from '@rollup/plugin-commonjs'; 5 | import svg from 'rollup-plugin-svg'; 6 | import postcss from 'rollup-plugin-postcss'; 7 | import json from '@rollup/plugin-json'; 8 | 9 | function onwarn(warning, defaultHandler) { 10 | if (warning.code !== 'CIRCULAR_DEPENDENCY') { 11 | defaultHandler(warning); 12 | } 13 | } 14 | 15 | export default [ 16 | { 17 | input: 'src/index.js', 18 | plugins: [ 19 | bundleSize(), 20 | nodeResolve({ browser: true }), 21 | commonJS({ 22 | include: 'node_modules/**' 23 | }), 24 | svg(), 25 | postcss({ extensions: ['.css'] }), 26 | json() 27 | ], 28 | onwarn, 29 | output: [ 30 | { 31 | file: 'dist/divi.mjs', 32 | format: 'es' 33 | }, 34 | { 35 | file: 'dist/divi.min.js', 36 | format: 'umd', 37 | sourcemap: true, 38 | plugins: [ 39 | terser({ ecma: 2018 }) 40 | ], 41 | name: 'divi' 42 | } 43 | ] 44 | } 45 | ]; 46 | -------------------------------------------------------------------------------- /src/_d3/axis.js: -------------------------------------------------------------------------------- 1 | import identity from './identity.js'; 2 | import { copyElement } from '../util/util.js'; 3 | import { Transform } from '../util/transform.js'; 4 | 5 | const top = 1; 6 | const right = 2; 7 | const bottom = 3; 8 | const left = 4; 9 | 10 | function number(scale) { 11 | return d => +scale(d); 12 | } 13 | 14 | function center(scale, offset) { 15 | offset = Math.max(0, scale.bandwidth() - offset * 2) / 2; 16 | if (scale.round()) offset = Math.round(offset); 17 | return d => +scale(d) + offset; 18 | } 19 | 20 | function axis(orient, scale, state) { 21 | let tickArguments = []; 22 | let tickValues = null; 23 | let tickFormat = null; 24 | let tickSizeInner = 6; 25 | let tickSizeOuter = 6; 26 | let tickPadding = 3; 27 | let offset = typeof window !== 'undefined' && window.devicePixelRatio > 1 ? 0 : 0.5; 28 | const svgAxis = orient === top || orient === bottom ? state.xAxis : state.yAxis; 29 | const ticks = svgAxis.ticks; 30 | 31 | function axis() { 32 | let values = tickValues == null ? (scale.ticks ? scale.ticks.apply(scale, tickArguments) : scale.domain()) : tickValues; 33 | const format = tickFormat == null ? (scale.tickFormat ? scale.tickFormat.apply(scale, tickArguments) : identity) : tickFormat; 34 | const position = (scale.bandwidth ? center : number)(scale.copy(), offset); 35 | values = orient === left || orient === right ? values.reverse() : values; 36 | 37 | function updateTick(tick, value) { 38 | tick.value = value; 39 | const label = tick.label; const tickMarks = tick.marks; 40 | label.innerHTML = svgAxis.ordinal.length || label.value === format(value) ? label.value : format(value); 41 | 42 | const lx = label.globalPosition.translate.x; const ly = label.globalPosition.translate.y; 43 | const translateX = orient === bottom ? position(value) - lx : 0; 44 | const translateY = orient === left ? position(value) - ly : 0; 45 | label.setAttribute('transform', label.localTransform.getTransform(new Transform(translateX, translateY))); 46 | 47 | for (const mark of tickMarks) { 48 | const tx = mark.globalPosition.translate.x; const ty = mark.globalPosition.translate.y; 49 | const translateX = orient === bottom ? position(value) - tx : 0; 50 | const translateY = orient === left ? position(value) - ty : 0; 51 | 52 | mark.setAttribute('transform', mark.localTransform.getTransform(new Transform(translateX, translateY))); 53 | } 54 | } 55 | 56 | let counter; 57 | for (counter = 0; counter < values.length && counter < ticks.length; ++counter) { 58 | updateTick(ticks[counter], svgAxis.ordinal.length ? svgAxis.ordinal[counter] : values[counter]); 59 | } 60 | 61 | for (; counter < values.length; ++counter) { 62 | const newTick = { 63 | label: copyElement(ticks[0].label), 64 | marks: ticks[0].marks.map(tick => copyElement(tick)), 65 | value: 0 66 | }; 67 | 68 | updateTick(newTick, values[counter]); 69 | ticks[0].label.parentElement.appendChild(newTick.label); 70 | ticks[0].marks.forEach((d, i) => d.parentElement.insertBefore(newTick.marks[i], d)); 71 | ticks.push(newTick); 72 | } 73 | 74 | const length = ticks.length; 75 | for (; counter < length; ++counter) { 76 | const pos = ticks.length - 1; 77 | if (ticks[pos].label) ticks[pos].label.remove(); 78 | ticks[pos].marks.forEach(d => d.remove()); 79 | ticks.pop(); 80 | } 81 | } 82 | 83 | axis.applyTransform = function(_) { 84 | return arguments.length ? (scale_transform = _, axis) : axis; 85 | }; 86 | 87 | axis.scale = function(_) { 88 | return arguments.length ? (scale = _, axis) : scale; 89 | }; 90 | 91 | axis.ticks = function() { 92 | return tickArguments = Array.from(arguments), axis; 93 | }; 94 | 95 | axis.tickArguments = function(_) { 96 | return arguments.length ? (tickArguments = _ == null ? [] : Array.from(_), axis) : tickArguments.slice(); 97 | }; 98 | 99 | axis.tickValues = function(_) { 100 | return arguments.length ? (tickValues = _ == null ? null : Array.from(_), axis) : tickValues && tickValues.slice(); 101 | }; 102 | 103 | axis.tickFormat = function(_) { 104 | return arguments.length ? (tickFormat = _, axis) : tickFormat; 105 | }; 106 | 107 | axis.tickSize = function(_) { 108 | return arguments.length ? (tickSizeInner = tickSizeOuter = +_, axis) : tickSizeInner; 109 | }; 110 | 111 | axis.tickSizeInner = function(_) { 112 | return arguments.length ? (tickSizeInner = +_, axis) : tickSizeInner; 113 | }; 114 | 115 | axis.tickSizeOuter = function(_) { 116 | return arguments.length ? (tickSizeOuter = +_, axis) : tickSizeOuter; 117 | }; 118 | 119 | axis.tickPadding = function(_) { 120 | return arguments.length ? (tickPadding = +_, axis) : tickPadding; 121 | }; 122 | 123 | axis.offset = function(_) { 124 | return arguments.length ? (offset = +_, axis) : offset; 125 | }; 126 | 127 | return axis; 128 | } 129 | 130 | export function axisTop(scale, SVG) { 131 | return axis(top, scale, SVG); 132 | } 133 | 134 | export function axisRight(scale, SVG) { 135 | return axis(right, scale, SVG); 136 | } 137 | 138 | export function axisBottom(scale, SVG) { 139 | return axis(bottom, scale, SVG); 140 | } 141 | 142 | export function axisLeft(scale, SVG) { 143 | return axis(left, scale, SVG); 144 | } 145 | -------------------------------------------------------------------------------- /src/_d3/identity.js: -------------------------------------------------------------------------------- 1 | export default function(x) { 2 | return x; 3 | } 4 | -------------------------------------------------------------------------------- /src/_d3/zoom/constant.js: -------------------------------------------------------------------------------- 1 | export default x => () => x; 2 | -------------------------------------------------------------------------------- /src/_d3/zoom/event.js: -------------------------------------------------------------------------------- 1 | export default function ZoomEvent(type, { 2 | sourceEvent, 3 | target, 4 | transform, 5 | dispatch 6 | }) { 7 | Object.defineProperties(this, { 8 | type: { value: type, enumerable: true, configurable: true }, 9 | sourceEvent: { value: sourceEvent, enumerable: true, configurable: true }, 10 | target: { value: target, enumerable: true, configurable: true }, 11 | transform: { value: transform, enumerable: true, configurable: true }, 12 | _: { value: dispatch } 13 | }); 14 | } 15 | -------------------------------------------------------------------------------- /src/_d3/zoom/index.js: -------------------------------------------------------------------------------- 1 | export { default as zoom } from './zoom.js'; 2 | export { default as zoomTransform, identity as zoomIdentity, Transform as ZoomTransform } from './transform.js'; 3 | -------------------------------------------------------------------------------- /src/_d3/zoom/noevent.js: -------------------------------------------------------------------------------- 1 | export function nopropagation(event) { 2 | event.stopImmediatePropagation(); 3 | } 4 | 5 | export default function(event) { 6 | event.preventDefault(); 7 | event.stopImmediatePropagation(); 8 | } 9 | -------------------------------------------------------------------------------- /src/_d3/zoom/transform.js: -------------------------------------------------------------------------------- 1 | export function Transform(k, x, y) { 2 | this.k = k; 3 | this.x = x; 4 | this.y = y; 5 | } 6 | 7 | Transform.prototype = { 8 | constructor: Transform, 9 | scale: function(k) { 10 | return k === 1 ? this : new Transform(this.k * k, this.x, this.y); 11 | }, 12 | translate: function(x, y) { 13 | return x === 0 & y === 0 ? this : new Transform(this.k, this.x + this.k * x, this.y + this.k * y); 14 | }, 15 | apply: function(point) { 16 | return [point[0] * this.k + this.x, point[1] * this.k + this.y]; 17 | }, 18 | applyX: function(x) { 19 | return x * this.k + this.x; 20 | }, 21 | applyY: function(y) { 22 | return y * this.k + this.y; 23 | }, 24 | invert: function(location) { 25 | return [(location[0] - this.x) / this.k, (location[1] - this.y) / this.k]; 26 | }, 27 | invertX: function(x) { 28 | return (x - this.x) / this.k; 29 | }, 30 | invertY: function(y) { 31 | return (y - this.y) / this.k; 32 | }, 33 | rescaleX: function(x) { 34 | return x.copy().domain(x.range().map(this.invertX, this).map(x.invert, x)); 35 | }, 36 | rescaleY: function(y) { 37 | return y.copy().domain(y.range().map(this.invertY, this).map(y.invert, y)); 38 | }, 39 | toString: function() { 40 | return 'translate(' + this.x + ',' + this.y + ') scale(' + this.k + ')'; 41 | } 42 | }; 43 | 44 | export var identity = new Transform(1, 0, 0); 45 | 46 | transform.prototype = Transform.prototype; 47 | 48 | export default function transform(node) { 49 | while (!node.__zoom) if (!(node = node.parentNode)) return identity; 50 | return node.__zoom; 51 | } 52 | -------------------------------------------------------------------------------- /src/handlers/annotate.js: -------------------------------------------------------------------------------- 1 | import { select } from 'd3-selection'; 2 | import { annotation, annotationLabel } from 'd3-svg-annotation'; 3 | 4 | export function annotate(state, x, y, text) { 5 | bind(state, x, y, text); 6 | } 7 | 8 | // annotate.unbind = function() { 9 | // document.removeEventListener('click', listener); 10 | // } 11 | 12 | const bind = function(state, x, y, text) { 13 | // function listener(event) { 14 | // if (!state.interactions.annotate.flag || state.svg.parentElement.style.visibility === 'hidden') return; 15 | 16 | // const x_click = event.clientX - state.svg.getBoundingClientRect().left; 17 | // const y_click = event.clientY - state.svg.getBoundingClientRect().top; 18 | 19 | // if (x_click < state.xAxis.range[0] || y_click < state.yAxis.range[1]) return; 20 | 21 | // const text = prompt('Annotation text:'); 22 | // if (!text) return; 23 | 24 | // const annotations = [ 25 | // { 26 | // note: { 27 | // label: text 28 | // // title: "d3.annotationLabel" 29 | // }, 30 | // x: event.clientX - state.svg.getBoundingClientRect().left, 31 | // y: event.clientY - state.svg.getBoundingClientRect().top, 32 | // dy: 20, 33 | // dx: 20, 34 | // connector: { 35 | // end: 'dot' // 'dot' also available 36 | // } 37 | // }]; 38 | 39 | // const makeAnnotations = d3.annotation() 40 | // .type(d3.annotationLabel) 41 | // .annotations(annotations); 42 | 43 | // d3.select('#' + state.svg.id) 44 | // .append('g') 45 | // .attr('class', 'annotation-group') 46 | // .call(makeAnnotations); 47 | 48 | // // var keys = (event.ctrlKey ? " ctrl " : "") + (event.shiftKey ? " shift " : "") + (event.altKey ? " alt " : ""); 49 | // // document.getElementById("logfile").innerHTML += event.type + " [" + keys + "] " + SVG.state().svg.id + " to annotate
"; 50 | // } 51 | const annotations = [ 52 | { 53 | note: { 54 | label: text 55 | // title: "d3.annotationLabel" 56 | }, 57 | // x: event.clientX - state.svg.getBoundingClientRect().left, 58 | // y: event.clientY - state.svg.getBoundingClientRect().top, 59 | x: state.xAxis.scale(x), 60 | y: state.yAxis.scale(y), 61 | dy: 20, 62 | dx: 20, 63 | connector: { 64 | end: 'dot' // 'dot' also available 65 | } 66 | }]; 67 | 68 | const makeAnnotations = annotation() 69 | .type(annotationLabel) 70 | .annotations(annotations); 71 | 72 | select('#' + state.svg.id) 73 | .append('g') 74 | .attr('class', 'annotation-group') 75 | .call(makeAnnotations); 76 | 77 | // document.addEventListener('mouseup', listener); 78 | }; 79 | -------------------------------------------------------------------------------- /src/handlers/brush.js: -------------------------------------------------------------------------------- 1 | import { pointer, select } from 'd3-selection'; 2 | 3 | let brush; 4 | 5 | function getBrush(svg) { 6 | return select(svg) 7 | .append('rect') 8 | .attr('opacity', 0.35) 9 | .attr('x', 0) 10 | .attr('y', 0) 11 | .attr('width', 0) 12 | .attr('height', 0) 13 | .attr('id', svg.id + '-brush-rect') 14 | .style('fill', 'gray') 15 | .style('stroke', '#fff'); 16 | } 17 | 18 | export function brushStart(state, event) { 19 | brush = getBrush(state.svg); 20 | const [x, y] = pointer(event, state.svg); 21 | brush.attr('x', x) 22 | .attr('y', y); 23 | } 24 | 25 | export function brushMove(state, event) { 26 | event.preventDefault(); 27 | 28 | const [x, y] = pointer(event, state.svg); 29 | const rectX = +brush.node().getAttribute('x'); const rectY = +brush.node().getAttribute('y'); 30 | const width = x - rectX; const height = y - rectY; 31 | const xTranslate = width < 0 ? width : 0; 32 | const yTranslate = height < 0 ? height : 0; 33 | 34 | brush.attr('width', Math.abs(width)) 35 | .attr('height', Math.abs(height)) 36 | .attr('transform', 'translate(' + xTranslate + ',' + yTranslate + ')'); 37 | return [rectX, rectY, width, height]; 38 | } 39 | 40 | export function brushEnd(event) { 41 | brush.remove(); 42 | } 43 | 44 | function brusher(event) { 45 | let x1, x2, y1, y2; 46 | function bMove(event) { 47 | event.stopPropagation(); 48 | event.preventDefault(); 49 | // filter( 50 | // state, 51 | // +rect.getAttribute("x") + +svg.getBoundingClientRect().left + x_translate, 52 | // +rect.getAttribute("y") + +svg.getBoundingClientRect().top + y_translate, 53 | // Math.abs(+rect.getAttribute("width")), 54 | // Math.abs(+rect.getAttribute("height")), 55 | // e.ctrlKey || e.metaKey || e.altKey || e.shiftKey 56 | // ); 57 | const mousePos = pointer(event); 58 | if (Math.hypot(mouseStart[0] - mousePos[0], mouseStart[1] - mousePos[1]) < 5) return; 59 | mouseMoved = true; 60 | 61 | const [x, y, width, height] = brushMove(state, event); 62 | x1 = state.xAxis.scale.invert(x); 63 | x2 = state.xAxis.scale.invert(x + width); 64 | y1 = state.yAxis.scale.invert(y); 65 | y2 = state.yAxis.scale.invert(y + height); 66 | // brushedMarks = state.svgMarks.filter(function(d) { 67 | // const xField = d.__inferred__data__[state.xAxis.title.innerHTML]; 68 | // const yField = d.__inferred__data__[state.yAxis.title.innerHTML]; 69 | // return xField >= min([x1, x2]) && xField <= max([x1, x2]) && yField >= min([y1, y2]) && yField <= max([y1, y2]); 70 | // }); 71 | } 72 | 73 | function bEnd(event) { 74 | brushEnd(state, event); 75 | if (mouseMoved) { 76 | const p = generateBrushPredicates(state.xAxis.title.innerHTML.toLowerCase(), 77 | state.yAxis.title.innerHTML.toLowerCase(), [x1, x2], [y1, y2]); 78 | walkQueryPath(roots, p, isMetaKey(event)); 79 | applySelections(states); 80 | } 81 | // selectMarks(state.svgMarks, brushedMarks); 82 | // propogateSelection(state.xAxis, brushedMarks); 83 | state.svg.removeEventListener('mousemove', bMove); 84 | state.svg.removeEventListener('mouseup', bEnd); 85 | } 86 | 87 | mouseMoved = false; 88 | const mouseStart = pointer(event); 89 | 90 | if (!state.interactions.brush) return; 91 | brushStart(state, event); 92 | state.svg.addEventListener('mousemove', bMove); 93 | state.svg.addEventListener('mouseup', bEnd); 94 | } 95 | -------------------------------------------------------------------------------- /src/handlers/query.js: -------------------------------------------------------------------------------- 1 | import { escape, query } from 'arquero'; 2 | 3 | export const COMMAND_TYPE = { 4 | SELECT: 0, 5 | APPEND: 1, 6 | FILTER: 2 7 | }; 8 | 9 | export const SELECT_TYPE = { 10 | POINT: 0, 11 | RANGE: 1 12 | }; 13 | 14 | export function generateQuery(predicates) { 15 | const queries = []; 16 | for (const predicate of predicates) { 17 | const field = Object.keys(predicate)[0]; 18 | const { value, cond, type } = predicate[field]; 19 | let q; 20 | 21 | if (type === SELECT_TYPE.POINT) { 22 | q = query().filter(escape(d => d[field] === value)).reify(); 23 | } else if (type === SELECT_TYPE.RANGE) { 24 | q = query().filter(escape(d => { 25 | return (cond === '>=' ? d[field] >= value : d[field] <= value); 26 | })).reify(); 27 | } 28 | queries.push(q); 29 | } 30 | return queries; 31 | } 32 | 33 | export function generatePredicates(field, object, type) { 34 | if (type === SELECT_TYPE.POINT) { 35 | return [{ [field]: { value: object[field], type } }]; 36 | } else { 37 | console.log('TODO'); 38 | } 39 | } 40 | 41 | export function generateBrushPredicates(field1, field2, xR, yR) { 42 | return [ 43 | { [field1]: { value: xR[0], cond: '>=', type: SELECT_TYPE.RANGE } }, 44 | { [field1]: { value: xR[1], cond: '<=', type: SELECT_TYPE.RANGE } }, 45 | { [field2]: { value: yR[0], cond: '<=', type: SELECT_TYPE.RANGE } }, 46 | { [field2]: { value: yR[1], cond: '>=', type: SELECT_TYPE.RANGE } } 47 | ]; 48 | } 49 | -------------------------------------------------------------------------------- /src/handlers/select.js: -------------------------------------------------------------------------------- 1 | import { path } from 'd3-path'; 2 | import { select, selectAll } from 'd3-selection'; 3 | import { LINK_TYPES } from '../parsers/multi-view/link-parser.js'; 4 | import { 5 | DataAttr, LegendRole, MarkRole, OpacityField, RoleProperty, 6 | SelectOpacity, tableIndexField, tableMarkField, UnselectOpacity 7 | } from '../state/constants.js'; 8 | import { generatePredicates, SELECT_TYPE } from './query.js'; 9 | 10 | function setSelection(marks, opacity) { 11 | selectAll(marks).attr('opacity', opacity); 12 | } 13 | 14 | export function selectAllMarks(marks) { 15 | setSelection(marks, marks[0][OpacityField] || SelectOpacity); 16 | } 17 | 18 | function unselectAllMarks(marks) { 19 | setSelection(marks, UnselectOpacity); 20 | } 21 | 22 | export function selectMarks(allMarks, marks) { 23 | unselectAllMarks(allMarks); 24 | setSelection(marks, SelectOpacity); 25 | } 26 | 27 | function selectLegends(legends, data) { 28 | for (const legend of legends) { 29 | if (!legend.title) continue; 30 | const attr = legend.title.innerHTML.toLowerCase(); 31 | const attrData = data.array(attr); 32 | if (attr === 'precipitation') continue; 33 | const _marks = legend.marks 34 | .filter(d => attrData.includes(d.mark[DataAttr][attr])) 35 | .map(d => d.mark); 36 | 37 | selectMarks(legend.marks.map(d => d.mark), _marks); 38 | } 39 | } 40 | 41 | function drawAggregates(id, selected, xAxis) { 42 | const marks = selected.array(tableMarkField); 43 | selectAll('.' + id + '.AGGREGATE_LAYER').remove(); 44 | const newMarks = []; 45 | 46 | for (let i = 0; i < marks.length; ++i) { 47 | const markRect = marks[i]._getBBox(); 48 | const newMark = select(marks[i].parentElement).append('path').classed(id, true) 49 | .classed('AGGREGATE_LAYER', true) 50 | .attr('fill', window.getComputedStyle(marks[i]).fill); 51 | 52 | if (marks[i].tagName === 'path') { 53 | const x = marks[i].contour[0].x; //, y = marks[i].contour[0].y; 54 | // if (marks[i].globalPosition.translate.y) { 55 | // var y = marks[i].globalPosition.translate.y - marks[i].globalPosition.translate.y / 2; 56 | // console.log(x, y) 57 | // } else { 58 | const y = marks[i].contour[0].y - marks[i]._getBBox().height; 59 | // const lx = marks[i].globalPosition.translate.x - marks[i].globalPosition.translate.x / 2, 60 | // ly = marks[i].globalPosition.translate.y - marks[i].globalPosition.translate.y / 2; 61 | // const t = marks[i].localTransform; 62 | // const x = 63 | // } 64 | const h = markRect.height; 65 | const w = xAxis.scale(selected.array(xAxis.title.innerHTML.toLowerCase())[i]) - xAxis.range[0]; 66 | 67 | const p = path(); 68 | p.rect(x, y, w, h); 69 | newMark.attr('d', p.toString()); 70 | newMarks.push(newMark); 71 | } 72 | } 73 | 74 | selectAll([...marks, ...newMarks]).raise(); 75 | } 76 | 77 | export function applySelections(states) { 78 | for (const state of states) { 79 | const { data, legends, xAxis } = state; 80 | const { table, active } = data; 81 | const { selected, type } = active; 82 | 83 | let selectedMarks = selected.array(tableMarkField); 84 | 85 | if (type === LINK_TYPES.AGGREGATE) { 86 | selectedMarks = drawAggregates(state.svg.id, selected, xAxis); 87 | } else { 88 | selectAll('.' + state.svg.id + 'AGGREGATE_LAYER').remove(); 89 | } 90 | selectMarks(table.array(tableMarkField), selectedMarks); 91 | selectLegends(legends, selected); 92 | } 93 | } 94 | 95 | export function selectPoint(state, target) { 96 | if (target[RoleProperty] === MarkRole) { 97 | return generatePredicates(tableIndexField, target, SELECT_TYPE.POINT); 98 | } else if (target[RoleProperty] === LegendRole) { 99 | return generatePredicates(Object.keys(target[DataAttr])[0], target[DataAttr], SELECT_TYPE.POINT); 100 | } else { 101 | selectAllMarks(state.svgMarks); 102 | state.legends.forEach(d => selectAllMarks(d.marks.map(e => e.mark))); 103 | return null; 104 | } 105 | } 106 | 107 | // function getLegendFields(state, mark) { 108 | // const legend = mark.legend; 109 | 110 | // if (legend.type === CategoricalColorLegend) { 111 | // const val = legend.scale.domain()[legend.scale.range().indexOf(window.getComputedStyle(mark)[legend.matchingAttr])]; 112 | // var condition = [val]; 113 | // } else { 114 | // const val = legend.scale.invert(mark._getBBox().width); 115 | // var condition = [val, val]; 116 | // } 117 | 118 | // const candidateMarks = state.svgMarks.filter(function(d) { 119 | // const data = d.__inferred__data__[legend.title.innerHTML]; 120 | // return typeof condition[0] === 'string' ? condition.includes(data) : data >= condition[0] && data <= condition[1]; 121 | // }); 122 | 123 | // selectMarks(legend.marks.map(d => d.mark), [mark]); 124 | // selectMarks(state.svgMarks, candidateMarks); 125 | // } 126 | -------------------------------------------------------------------------------- /src/handlers/sort.js: -------------------------------------------------------------------------------- 1 | // import { INTERACTION_CONSTANTS } from './constants'; 2 | 3 | // function constrain_elements(SVG) { 4 | // const d = [...SVG.state().x_axis.ticks]; 5 | // d.sort((a, b) => { 6 | // return +a.label.getBoundingClientRect().left < +b.label.getBoundingClientRect().left ? -1 : 1; 7 | // }); 8 | 9 | // const labels = d.map(d => d.label.innerHTML); 10 | // SVG.state().x_axis.ordinal = labels; 11 | // SVG.state().x_axis.scale.domain(labels); 12 | 13 | // return d; 14 | // } 15 | 16 | // function dragElement(SVG) { 17 | // let pos1 = 0; let pos2 = 0; let pos3 = 0; let pos4 = 0; 18 | // document.addEventListener('mousedown', dragMouseDown); 19 | // let elmnt; let tick; let original_positions; let sorted = false; 20 | 21 | // function dragMouseDown(e) { 22 | // if (SVG.state().svg.parentElement.style.visibility === 'hidden') return; 23 | 24 | // e = e || window.event; 25 | // // e.preventDefault(); 26 | // // get the mouse cursor position at startup: 27 | // pos3 = e.clientX; 28 | // pos4 = e.clientY; 29 | // // elmnt.onmouseup = closeDragElement; 30 | // // call a function whenever the cursor moves: 31 | // // elmnt.onmousemove = elementDrag; 32 | // elmnt = null; 33 | // tick = SVG.state().x_axis.ticks[0]; 34 | // original_positions = []; 35 | // for (const mark of SVG.state().svg_marks) { 36 | // if (mark.hasAttribute('__legend__')) continue; 37 | // const bb = mark.getBoundingClientRect(); 38 | // if (e.clientX >= +bb.left && e.clientX <= +bb.right && e.clientY >= +bb.top && e.clientY <= +bb.bottom) { 39 | // elmnt = mark; 40 | // break; 41 | // } 42 | // } 43 | // if (elmnt) { 44 | // const keys = (e.ctrlKey ? ' ctrl ' : '') + (e.shiftKey ? ' shift ' : '') + (e.altKey ? ' alt ' : ''); 45 | // // document.getElementById("logfile").innerHTML += e.type + " [" + keys + "] " + SVG.state().svg.id + " to sort
"; 46 | // d3.select('#tooltip').attr('display', 'none'); 47 | 48 | // document.addEventListener('mousemove', elementDrag); 49 | // document.addEventListener('mouseup', closeDragElement); 50 | // elmnt.__x__ = e.clientX; 51 | 52 | // SVG.state().svg_marks.sort((a, b) => { 53 | // +a.getBoundingClientRect().left < +b.getBoundingClientRect().left ? -1 : 1; 54 | // }); 55 | 56 | // const pos = (+elmnt.getBoundingClientRect().left + +elmnt.getBoundingClientRect().right) / 2; 57 | // let min_diff = 1000; 58 | // for (let i = 0; i < SVG.state().x_axis.ticks.length; ++i) { 59 | // if (Math.abs(+SVG.state().x_axis.ticks[i].ticks[0].getBoundingClientRect().left - pos) < min_diff) { 60 | // min_diff = Math.abs(+SVG.state().x_axis.ticks[i].ticks[0].getBoundingClientRect().left - pos); 61 | // tick = SVG.state().x_axis.ticks[i]; 62 | // } 63 | 64 | // const p = SVG.state().x_axis.ticks[i].ticks[0]; 65 | // original_positions.push((+p.getBoundingClientRect().left + +p.getBoundingClientRect().right) / 2); 66 | 67 | // SVG.state().x_axis.ticks[i].mark = SVG.state().svg_marks[i]; 68 | // } 69 | 70 | // tick.__x__ = e.clientX; 71 | // } 72 | // } 73 | 74 | // function elementDrag(e) { 75 | // if (sorted) return; 76 | 77 | // e = e || window.event; 78 | // e.preventDefault(); 79 | // // calculate the new cursor position: 80 | // pos1 = pos3 - e.clientX; 81 | // pos2 = pos4 - e.clientY; 82 | // pos3 = e.clientX; 83 | // pos4 = e.clientY; 84 | // // set the element's new position: 85 | // elmnt.setAttribute('transform', 'translate(' + (e.clientX - elmnt.__x__) + ', 0)'); 86 | 87 | // const label = tick.label; 88 | // const rotate = label.hasAttribute('transform') && label.getAttribute('transform').includes('rotate') ? +label.getAttribute('transform').match(/(-?\d+\.?\d*e?-?\d*)/g).pop() : null; 89 | // label.setAttribute('transform', 'translate(' + (e.clientX - elmnt.__x__) + ', 0)' + (rotate ? ' rotate(' + rotate + ')' : '')); 90 | 91 | // for (const t of tick.ticks) { 92 | // // t.setAttribute("transform", "translate(" + (e.clientX - elmnt.__x__) + ", 0)"); 93 | // } 94 | 95 | // const d = constrain_elements(SVG); 96 | // console.log(d.map(d => d.mark)); 97 | // for (let i = 0; i < d.length; ++i) { 98 | // if (d[i].label.innerHTML === tick.label.innerHTML) continue; 99 | 100 | // const t = d[i]; 101 | // const curr_pos = (+t.ticks[0].getBoundingClientRect().left + 102 | // +t.ticks[0].getBoundingClientRect().right) / 2; 103 | 104 | // const l = t.label; 105 | // const rotate = label.hasAttribute('transform') && l.getAttribute('transform').includes('rotate') ? +l.getAttribute('transform').match(/(-?\d+\.?\d*e?-?\d*)/g).pop() : null; 106 | // l.setAttribute('transform', 'translate(' + (original_positions[i] - curr_pos) + ', 0)' + (rotate ? ' rotate(' + rotate + ')' : '')); 107 | 108 | // for (const _t of d[i].ticks) { 109 | // // _t.setAttribute("transform", "translate(" + (original_positions[i] - curr_pos) + ", 0)"); 110 | // } 111 | 112 | // t.mark.setAttribute('transform', 'translate(' + (original_positions[i] - curr_pos) + ', 0)'); 113 | // } 114 | 115 | // const sorted_position_map = new Map(); 116 | // const curr = SVG.state().x_axis.ticks.map(d => d.mark); 117 | // curr.sort(function(a, b) { 118 | // const width1 = +a.getBoundingClientRect().left; 119 | // const width2 = +b.getBoundingClientRect().left; 120 | // return width1 < width2 ? -1 : 1; 121 | // }); 122 | 123 | // const sorted_marks = SVG.state().x_axis.ticks.map(d => d.mark); 124 | // sorted_marks.sort(function(a, b) { 125 | // const height1 = +a.getBoundingClientRect().top; 126 | // const height2 = +b.getBoundingClientRect().top; 127 | // return height1 > height2 ? -1 : 1; 128 | // }); 129 | // for (let i = 0; i < sorted_marks.length; ++i) { 130 | // sorted_position_map[sorted_marks[i].getAttribute('x')] = i; 131 | // } 132 | 133 | // if (sorted_marks[sorted_marks.length - 1] === curr[curr.length - 1]) { 134 | // for (let i = 0; i < SVG.state().x_axis.ticks.length; ++i) { 135 | // const m = SVG.state().x_axis.ticks[i].mark; 136 | // const sorted_pos = sorted_position_map[m.getAttribute('x')]; 137 | // // console.log(m) 138 | // // console.log(sorted_pos) 139 | // // console.log(original_positions[sorted_pos]) 140 | // // console.log(original_positions[i]) 141 | // // console.log(original_positions[sorted_pos] - original_positions[i]) 142 | // // console.log('') 143 | 144 | // const l = SVG.state().x_axis.ticks[i].label; 145 | // const rotate = label.hasAttribute('transform') && l.getAttribute('transform').includes('rotate') ? +l.getAttribute('transform').match(/(-?\d+\.?\d*e?-?\d*)/g).pop() : null; 146 | // l.setAttribute('transform', 'translate(' + (original_positions[sorted_pos] - original_positions[i]) + ', 0)' + (rotate ? ' rotate(' + rotate + ')' : '')); 147 | 148 | // m.setAttribute('transform', 'translate(' + (original_positions[sorted_pos] - original_positions[i]) + ', 0)'); 149 | // } 150 | // sorted = true; 151 | // } 152 | // } 153 | 154 | // function closeDragElement() { 155 | // // stop moving when mouse button is released: 156 | // // elmnt.onmouseup = null; 157 | // // elmnt.onmousemove = null; 158 | // document.removeEventListener('mousemove', elementDrag); 159 | // document.removeEventListener('mouseup', closeDragElement); 160 | // // constrain_elements(SVG); 161 | // } 162 | // } 163 | 164 | // export function sort(SVG) { 165 | // if (!SVG.state().x_axis.ordinal.length) return; 166 | // dragElement(SVG); 167 | // } 168 | -------------------------------------------------------------------------------- /src/handlers/zoom.js: -------------------------------------------------------------------------------- 1 | import { zoomTransform, zoom as _zoom, zoomIdentity } from '../_d3/zoom'; 2 | import { select, selectAll } from 'd3-selection'; 3 | import { Transform } from '../util/transform.js'; 4 | 5 | function addClipping(svg, marks, xAxis, yAxis) { 6 | svg.append('defs') 7 | .append('clipPath') 8 | .attr('id', 'clip-' + svg.node().id) 9 | .append('rect') 10 | .attr('x', 0) 11 | .attr('y', 0) 12 | .attr('width', Math.abs(xAxis.range[1] - xAxis.range[0])) 13 | .attr('height', Math.abs(yAxis.range[0] - yAxis.range[1])); 14 | 15 | for (const node of marks.nodes()) { 16 | if (node.parentElement.hasAttribute('clip-path')) continue; 17 | let container = node.parentElement; 18 | if (container.id !== '_g_clip') { 19 | container = document.createElementNS('http://www.w3.org/2000/svg', 'g'); 20 | container.id = '_g_clip'; 21 | container.setAttribute('clip-path', 'url(#clip-' + svg.node().id + ')'); 22 | 23 | node.parentElement.appendChild(container); 24 | } 25 | container.appendChild(node); 26 | } 27 | } 28 | 29 | const zoomCallback = (state, z, tx, ty, gXAxis, gYAxis, zoomX, zoomY) => function cb({ sourceEvent, transform }) { 30 | let { svg } = state; 31 | svg = select(svg); 32 | 33 | sourceEvent.preventDefault(); 34 | 35 | const k = transform.k / z[0].k; 36 | const x = (transform.x - z[0].x) / tx().k; 37 | const y = (transform.y - z[0].y) / ty().k; 38 | 39 | const svgRect = svg.node().getBBoxCustom(); 40 | const cliX = sourceEvent.clientX - svgRect.left; 41 | const cliY = sourceEvent.clientY - svgRect.top; 42 | 43 | if (k === 1) { 44 | gXAxis.call(zoomX.translateBy, x, 0); 45 | gYAxis.call(zoomY.translateBy, 0, y); 46 | } else { 47 | gXAxis.call(zoomX.scaleBy, k, [cliX, cliY]); 48 | gYAxis.call(zoomY.scaleBy, k, [cliX, cliY]); 49 | } 50 | for (const _s of [state]) { 51 | if (_s === state) _s.yAxis.axis.scale(ty().rescaleY(_s.yAxis.scale))(); 52 | _s.xAxis.axis.scale(tx().rescaleX(_s.xAxis.scale))(); 53 | 54 | selectAll(_s.svgMarks).attr('transform', function() { 55 | const translateX = tx().applyX(this.globalPosition.translate.x) - (this.globalPosition.translate.x); 56 | const translateY = _s === state ? ty().applyY(this.globalPosition.translate.y) - (this.globalPosition.translate.y) : 0; 57 | 58 | const scaleX = this.type ? tx().k : 1; 59 | const scaleY = this.type && _s === state ? ty().k : 1; 60 | 61 | return this.localTransform.getTransform(new Transform(translateX, translateY, scaleX, scaleY)); 62 | }); 63 | } 64 | z[0] = transform; 65 | }; 66 | 67 | export function zoom(state) { 68 | const { xAxis, yAxis } = state; 69 | let { svg, svgMarks: marks } = state; 70 | svg = select(svg); 71 | marks = selectAll(marks); 72 | 73 | xAxis.axis.scale(xAxis.scale).ticks(xAxis.ticks.length)(); 74 | yAxis.axis.scale(yAxis.scale).ticks(yAxis.ticks.length)(); 75 | addClipping(svg, marks, xAxis, yAxis); 76 | 77 | const gXAxis = svg.append('g').attr('id', 'x-axis-zoom-accessor'); 78 | const gYAxis = svg.append('g').attr('id', 'y-axis-zoom-accessor'); 79 | 80 | const z = [zoomIdentity]; 81 | const zoomX = _zoom(); 82 | const zoomY = _zoom(); 83 | const tx = () => zoomTransform(gXAxis.node()); 84 | const ty = () => zoomTransform(gYAxis.node()); 85 | 86 | gXAxis.call(zoomX).attr('pointer-events', 'none'); 87 | gYAxis.call(zoomY).attr('pointer-events', 'none'); 88 | 89 | svg.call(_zoom().on('zoom', zoomCallback(state, z, tx, ty, gXAxis, gYAxis, zoomX, zoomY))); 90 | } 91 | -------------------------------------------------------------------------------- /src/hydrate.js: -------------------------------------------------------------------------------- 1 | import { coordinate } from './orchestration/coordinator.js'; 2 | import { parseDataset } from './parsers/helpers/data-parser.js'; 3 | 4 | export async function hydrate(svg, options = {}) { 5 | if (!svg) return; 6 | if (!Array.isArray(svg)) svg = [svg]; 7 | 8 | svg = svg.map(d => typeof d === 'string' ? document.querySelector(d) : d); 9 | return coordinate(svg, await parseDataset(options)); 10 | } 11 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | export { hydrate } from './hydrate.js'; 2 | export { selectMarks, selectAllMarks } from './handlers/select.js'; 3 | export { annotate } from './handlers/annotate.js'; 4 | -------------------------------------------------------------------------------- /src/orchestration/coordinator.js: -------------------------------------------------------------------------------- 1 | import { inspect } from './inspect.js'; 2 | import { getRootNodes, link, walkQueryPath } from '../parsers/multi-view/link-parser.js'; 3 | import { parseChart } from '../parsers/engine/parser-engine.js'; 4 | import { createMenu } from '../toolbar/menu.js'; 5 | import { applySelections, selectPoint } from '../handlers/select.js'; 6 | import { isMetaKey } from '../util/util.js'; 7 | import { zoom } from '../handlers/zoom.js'; 8 | 9 | export function coordinate(svg, extState) { 10 | const states = svg.map(d => parseChart(inspect(d))); 11 | link(states, extState); 12 | // createMenu(states); 13 | coordinateInteractions(states); 14 | return states; 15 | } 16 | 17 | function coordinateInteractions(states) { 18 | const select = state => function(event) { 19 | const { target } = event; 20 | const { data } = state; 21 | const roots = getRootNodes(data); 22 | 23 | walkQueryPath(roots, selectPoint(state, target), isMetaKey(event)); 24 | applySelections(states); 25 | }; 26 | 27 | for (const state of states) { 28 | const { svg } = state; 29 | svg.addEventListener('click', select(state)); 30 | // zoom(state); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/orchestration/inspect.js: -------------------------------------------------------------------------------- 1 | import { 2 | Background, Foreground, DefaultSvgId, MarkRole, CenterX, CenterY, 3 | markTypes 4 | } from '../state/constants.js'; 5 | 6 | import { Transform } from '../util/transform.js'; 7 | import { ViewState } from '../state/view-state.js'; 8 | import { parseTransform } from '../parsers/helpers/data-parser.js'; 9 | import * as parser from 'svg-path-parser'; 10 | import { SVGToScreen, containsLines } from '../util/util.js'; 11 | import { select } from 'd3-selection'; 12 | 13 | let id = 1; 14 | 15 | function extractElementInformation(svg, element, parent = false) { 16 | element.getBBoxCustom = parent 17 | ? function() { return this.getBoundingClientRect(); } 18 | : function() { 19 | const containerRect = svg.getBBoxCustom(); 20 | const clientRect = this.getBoundingClientRect(); 21 | const svgRect = this.getBBox ? this.getBBox() : clientRect; 22 | 23 | const p1 = this.getBBox 24 | ? SVGToScreen(svg, this, svgRect.x, svgRect.y) 25 | : { x: svgRect.x, y: svgRect.y }; 26 | const p2 = this.getBBox 27 | ? SVGToScreen(svg, this, svgRect.x + svgRect.width, svgRect.y + svgRect.height) 28 | : { x: svgRect.x + svgRect.width, y: svgRect.y + svgRect.height }; 29 | 30 | const width = Math.abs(p1.x - p2.x); 31 | const height = Math.abs(p1.y - p2.y); 32 | const left = clientRect.left + clientRect.width / 2 - width / 2; 33 | const top = clientRect.top + clientRect.height / 2 - height / 2; 34 | 35 | return { 36 | width, 37 | height, 38 | left: left - containerRect.left, 39 | top: top - containerRect.top, 40 | right: (left + width) - containerRect.left, 41 | bottom: (top + height) - containerRect.top, 42 | [CenterX]: (left + width / 2) - containerRect.left, 43 | [CenterY]: (top + height / 2) - containerRect.top 44 | }; 45 | }; 46 | 47 | element.globalPosition = new Transform( 48 | element.getBBoxCustom()[CenterX] - svg.getBBoxCustom().left, 49 | element.getBBoxCustom()[CenterY] - svg.getBBoxCustom().top 50 | ); 51 | element.localTransform = parseTransform(element); 52 | } 53 | 54 | function inferTypeFromPath(element, state, transform) { 55 | const commands = parser.parseSVG(element.getAttribute('d')); 56 | if (!commands.length) return; 57 | 58 | element.contour = commands; 59 | const endCmd = commands[commands.length - 1]; 60 | const relCommands = ['v', 'h']; 61 | const lineCandidates = containsLines(commands); 62 | 63 | if (commands.length === 3 && commands[1].code === 'A' && endCmd.code === 'A') { // Check for ellipse shape. 64 | element.type = 'ellipse'; 65 | } else if (lineCandidates.length) { // Check for extracted gridlines. 66 | const p = select(element.parentElement); 67 | 68 | // Create new elements for each gridline. 69 | lineCandidates.forEach(lc => { 70 | const style = window.getComputedStyle(element); 71 | const el = p.append('path').attr('d', lc) 72 | .style('fill', style.fill) 73 | .style('color', style.color) 74 | .style('stroke', style.stroke) 75 | .style('stroke-width', style.strokeWidth) 76 | .attr('id', 'test'); 77 | analyzeDomTree(el.node(), state, transform); 78 | }); 79 | 80 | select(element).remove(); 81 | } else if (endCmd.code !== 'Z') { // Check for line shape. 82 | element.type = 'line'; 83 | } else if (commands.length === 5 && relCommands.includes(commands[1].code) && relCommands.includes(commands[2].code) && 84 | relCommands.includes(commands[3].code)) { // Check for rectangular shape. 85 | element.type = 'rect'; 86 | } else { // Check for polygonal shape. 87 | element.type = 'polygon'; 88 | } 89 | } 90 | 91 | function analyzeDomTree(element, state, transform, parent = false) { 92 | if (!element) return; 93 | if (element.nodeName.toLowerCase() === 'style') return; // Ignore style elements. 94 | if (element.className && (element.className.baseVal === Background || // Ignore background elements. 95 | element.className.baseVal === Foreground)) return; 96 | // TODO: Ignore clip elements 97 | 98 | if (parent || element.nodeName === 'svg') { // Store base SVG element. 99 | state.svg = element; 100 | extractElementInformation(state.svg, element, parent); 101 | if (!element.id) element.id = DefaultSvgId + '-' + id++; 102 | } else if (element.nodeName === 'g') { // Maintain element transforms. 103 | parseTransform(element, transform); 104 | } else if (element.nodeName === '#text' && element.textContent.trim() !== '') { // Instantiate text elements for non-empty references. 105 | let el = element.parentElement; 106 | if (el.nodeName !== 'text') { 107 | el = select(el).append('text').html(element.textContent).node(); 108 | select(element).remove(); 109 | } 110 | 111 | extractElementInformation(state.svg, el); 112 | el.removeAttribute('textLength'); 113 | state.textMarks.push(el); 114 | el.style['pointer-events'] = 'none'; 115 | el.style['user-select'] = 'none'; 116 | } else if (markTypes.includes(element.nodeName)) { // Process base SVG marks. 117 | const markType = element.nodeName; 118 | 119 | if (markType === 'path') { // Infer path element type. 120 | inferTypeFromPath(element, state, transform); 121 | element.setAttribute('vector-effect', 'non-scaling-stroke'); 122 | } else if (markType === 'polygline' || markType === 'polygon' || markType === 'line') { 123 | element.type = markType; 124 | element.setAttribute('vector-effect', 'non-scaling-stroke'); 125 | } 126 | 127 | extractElementInformation(state.svg, element); 128 | element.setAttribute(MarkRole, 'true'); 129 | if (!state.svgMarks.includes(element)) state.svgMarks.push(element); // Prevent duplicate references. 130 | } 131 | 132 | for (const child of element.childNodes) { 133 | analyzeDomTree(child, state, new Transform(transform)); 134 | } 135 | } 136 | 137 | export function inspect(svg) { 138 | const state = new ViewState(); 139 | analyzeDomTree(svg, state, new Transform(), true); 140 | state.svg = svg; 141 | console.log(state); 142 | return state; 143 | } 144 | -------------------------------------------------------------------------------- /src/parsers/engine/parser-engine.js: -------------------------------------------------------------------------------- 1 | import { inferAxes } from '../helpers/axis-parser.js'; 2 | import { collectCandidateMarkGroups } from './parser-groups.js'; 3 | import { inferLegends } from '../helpers/legend-parser.js'; 4 | import { inferMarks } from '../helpers/mark-parser.js'; 5 | import { inferTitles } from '../helpers/title-parser.js'; 6 | import { inferMarkAttributes, parseDataFromMarks } from '../helpers/data-parser.js'; 7 | 8 | export function parseChart(state) { 9 | const { textMarks, svgMarks } = state; 10 | 11 | let [candidateTextMarkGroups, candidateTickMarkGroups, candidateLegendMarkGroups] = collectCandidateMarkGroups(textMarks, svgMarks); 12 | const axes = inferAxes(state, candidateTextMarkGroups, candidateTickMarkGroups); 13 | 14 | // Remove axis text marks prior to legend inference. 15 | candidateTextMarkGroups = candidateTextMarkGroups.filter(d => !axes.map(a => a.text).includes(d.marks)); 16 | const legends = inferLegends(state, candidateTextMarkGroups, candidateLegendMarkGroups); 17 | 18 | // Remove legend text marks prior to mark and title inference. 19 | candidateTextMarkGroups = candidateTextMarkGroups.filter(d => !legends.map(l => l.text).includes(d.marks)); 20 | inferMarks(state); 21 | inferTitles(state, candidateTextMarkGroups.map(d => d.marks).flat()); 22 | 23 | // Infer data. 24 | inferMarkAttributes(state); 25 | state.data = parseDataFromMarks(state.svgMarks); 26 | 27 | return state; 28 | } 29 | -------------------------------------------------------------------------------- /src/parsers/helpers/axis-parser.js: -------------------------------------------------------------------------------- 1 | import { max, min } from 'd3-array'; 2 | import { CenterX, CenterY, OrphanTickRole, RoleProperty, Tick, horizAlign } from '../../state/constants.js'; 3 | import { computeCenterPos, sortByViewPos } from '../../util/util.js'; 4 | import { getFormatVal } from './data-parser.js'; 5 | import { pairGroups } from '../engine/parser-groups.js'; 6 | import { scaleBand, scaleLinear, scaleTime } from 'd3-scale'; 7 | import { axisBottom, axisLeft } from '../../_d3/axis.js'; 8 | 9 | // Construct requisite metadata for an inferred axis group. 10 | function describeAxis(axis, container) { 11 | const { alignment, group } = axis; 12 | const axisMap = new Map(); 13 | const orphanTicks = []; 14 | 15 | function addTicks(text, ticks) { 16 | container.ticks.push({ label: text, marks: ticks }); 17 | ticks.forEach(tick => { text ? tick[RoleProperty] = Tick : tick[RoleProperty] = OrphanTickRole; }); 18 | } 19 | 20 | for (const g of group) { 21 | const [axis, allTicks] = g; 22 | const seen = new Map(); 23 | 24 | // Merge multiple tick marks mapping to a single text mark. 25 | for (const [text, tick] of axis) { 26 | seen.set(tick, true); 27 | axisMap.has(text) 28 | ? axisMap.get(text).ticks.push(tick) 29 | : axisMap.set(text, { alignment, ticks: [tick] }); 30 | } 31 | 32 | // Unmatched tick marks are orphan ticks (usually gridlines, e.g., in ggplot2). 33 | allTicks.filter(d => !seen.has(d)).forEach(d => { 34 | orphanTicks.push({ alignment, tick: d }); 35 | }); 36 | } 37 | 38 | for (const [text, { ticks }] of axisMap) { 39 | addTicks(text, ticks); 40 | } 41 | 42 | for (const { tick } of orphanTicks) { 43 | addTicks(null, [tick]); 44 | } 45 | 46 | sortByViewPos('label', container.ticks); 47 | } 48 | 49 | // Compute axis domain (data space) extents. 50 | function computeAxisDomain(axis) { 51 | let isDate = true; 52 | 53 | // Check for date formatting. 54 | for (const [, value] of Object.entries(axis.ticks)) { 55 | if (value.label == null) continue; 56 | if (Object.prototype.toString.call(getFormatVal(value.label, true).value) !== '[object Date]') { 57 | isDate = false; 58 | break; 59 | } 60 | } 61 | 62 | // Iterate over axis tick values to compute min / max extents. 63 | for (const [, value] of Object.entries(axis.ticks)) { 64 | if (!value.label) continue; 65 | 66 | let formatVal = getFormatVal(value.label, isDate); 67 | if (formatVal.value) { 68 | axis.formatter = { format: formatVal.format }; 69 | formatVal = formatVal.value; 70 | } 71 | value.value = formatVal; 72 | 73 | if (typeof formatVal === 'string') { 74 | axis.ordinal.push(formatVal); 75 | } else { 76 | axis.domain[0] = axis.domain[0] === null ? formatVal : min([axis.domain[0], formatVal]); 77 | axis.domain[1] = axis.domain[1] === null ? formatVal : max([axis.domain[1], formatVal]); 78 | } 79 | } 80 | } 81 | 82 | function computeAxisRange(axis, isX) { 83 | const axisTicks = axis.ticks; 84 | const firstTickBBox = axisTicks[0].marks[0].getBBoxCustom(); 85 | const lastTickBBox = axisTicks[axisTicks.length - 1].marks[0].getBBoxCustom(); 86 | axis.range = isX ? [firstTickBBox[CenterX], lastTickBBox[CenterX]] : [lastTickBBox[CenterY], firstTickBBox[CenterY]]; 87 | } 88 | 89 | function computeScale(state, axis, isX) { 90 | const { domain } = axis; 91 | axis.scale = (domain[0] instanceof Date 92 | ? scaleTime() 93 | : axis.ordinal.length 94 | ? scaleBand() 95 | : scaleLinear()) 96 | .domain(axis.ordinal.length ? axis.ordinal.reverse() : axis.domain) 97 | .range(axis.range); 98 | 99 | const axisFn = isX ? axisBottom : axisLeft; 100 | axis.axis = axisFn(axis.scale, state).ticks(axis.ticks.length); 101 | if (axis.domain[0] instanceof Date) axis.axis = axis.axis.tickFormat(state.xAxis.formatter.format); 102 | 103 | // Reconfigure axis to prevent tick / gridline change. 104 | if (isX && !axis.ordinal.length) { 105 | const tickLeft = computeCenterPos( 106 | axis.ticks.filter(d => d.value === axis.domain[0])[0].marks[0], 'left' 107 | ); 108 | const tickRight = computeCenterPos( 109 | axis.ticks.filter(d => d.value === axis.domain[1])[0].marks[0], 'left' 110 | ); 111 | 112 | const ticks = [tickLeft, tickRight].map(d => d); 113 | const newDomainX = axis.range.map( 114 | axis.scale.copy().range(ticks).invert, axis.scale 115 | ); 116 | 117 | axis.scale.domain(newDomainX); 118 | } else if (!isX && !axis.ordinal.length) { 119 | const tickTop = computeCenterPos( 120 | axis.ticks.filter(d => d.value === axis.domain[0])[0].marks[0], 'top' 121 | ); 122 | const tickBottom = computeCenterPos( 123 | axis.ticks.filter(d => d.value === axis.domain[1])[0].marks[0], 'top' 124 | ); 125 | 126 | const ticks = [tickTop, tickBottom].map(d => d); 127 | const newDomainY = axis.range.map( 128 | axis.scale.copy().range(ticks).invert, axis.scale 129 | ); 130 | 131 | axis.scale.domain(newDomainY); 132 | } 133 | } 134 | 135 | export function inferAxes(state, textGroups, markGroups) { 136 | const { svg, xAxis, yAxis } = state; 137 | const axes = pairGroups(svg, textGroups, markGroups); 138 | 139 | axes.forEach(a => { 140 | const isX = horizAlign.includes(a.alignment); 141 | const axisContainer = isX ? xAxis : yAxis; 142 | 143 | describeAxis(a, axisContainer); 144 | computeAxisDomain(axisContainer); 145 | computeAxisRange(axisContainer, isX); 146 | computeScale(state, axisContainer, isX); 147 | }); 148 | 149 | return axes; 150 | } 151 | -------------------------------------------------------------------------------- /src/parsers/helpers/legend-parser.js: -------------------------------------------------------------------------------- 1 | import { max, mean, min } from 'd3-array'; 2 | import { scaleLinear, scaleOrdinal } from 'd3-scale'; 3 | import { 4 | CategoricalColorLegend, DataAttr, LegendRole, OpacityField, RoleProperty, 5 | SelectOpacity, SizeLegend 6 | } from '../../state/constants.js'; 7 | import { sortByViewPos } from '../../util/util.js'; 8 | import { pairGroups } from '../engine/parser-groups.js'; 9 | 10 | // Invert band scale (categorical as position encoding). 11 | export function invertBand(scale, value) { 12 | const step = scale.step(); 13 | const start = scale(scale.domain()[0]) + scale.paddingOuter() * step; 14 | const bandwidth = scale.bandwidth(); 15 | 16 | let index = Math.round(Math.abs((value - start - bandwidth / 2)) / step); 17 | index = max([0, min([scale.domain().length - 1, index])]); 18 | return scale.domain()[scale.domain().length - 1 - index]; 19 | } 20 | 21 | // Invert categorical / ordinal scale. 22 | export function invertOrdinal(scale, value) { 23 | return scale.domain()[scale.range().indexOf(value)]; 24 | } 25 | 26 | function bindLegendData(legend) { 27 | const { scale, title, type, matchingAttr } = legend; 28 | for (const { mark } of legend.marks) { 29 | const style = type === CategoricalColorLegend 30 | ? window.getComputedStyle(mark)[matchingAttr] 31 | : mark.getBBoxCustom().width ** 2; 32 | 33 | const data = invertOrdinal(scale, style); 34 | mark[DataAttr] = { [title ? title.innerHTML.toLowerCase() : 'legend-0']: data }; 35 | } 36 | } 37 | 38 | function inferScale(legend) { 39 | let scale; 40 | if (legend.type === CategoricalColorLegend) { // Ordinal legends 41 | const domain = legend.marks.map(d => d.label.innerHTML); 42 | const range = legend.marks.map(d => window.getComputedStyle(d.mark)[legend.matchingAttr]); 43 | scale = scaleOrdinal().domain(domain).range(range); 44 | } else { // Size legend 45 | const domain = legend.marks.map(d => +d.label.innerHTML); 46 | const range = legend.marks.map(d => d.mark.getBBoxCustom().width ** 2); 47 | scale = scaleLinear().domain(domain).range(range); 48 | } 49 | 50 | legend.scale = scale; 51 | } 52 | 53 | function formatLegend(legend) { 54 | const group = Array.from(legend.group[0][0]); 55 | return { 56 | title: null, 57 | marks: group.map(([text, mark]) => { 58 | return { label: text, mark }; 59 | }) 60 | }; 61 | } 62 | 63 | function parseLegends(state, legends) { 64 | for (let legend of legends) { 65 | legend = formatLegend(legend); 66 | const mark1Style = window.getComputedStyle(legend.marks[0].mark); 67 | const mark2Style = window.getComputedStyle(legend.marks[1].mark); 68 | let matchingAttr = null; 69 | 70 | // Infer legend type (color, size, shape). 71 | if (mark1Style.stroke !== mark2Style.stroke) { 72 | matchingAttr = 'stroke'; 73 | } else if (mark1Style.color !== mark2Style.color) { 74 | matchingAttr = 'color'; 75 | } else if (mark1Style.fill !== mark2Style.fill) { 76 | matchingAttr = 'fill'; 77 | } 78 | 79 | if (matchingAttr) { 80 | legend.type = CategoricalColorLegend; 81 | legend.matchingAttr = matchingAttr; 82 | } else { 83 | const widths = []; const heights = []; 84 | for (let i = 1; i < legend.marks.length; ++i) { 85 | const bbox1 = legend.marks[i - 1].mark.getBBoxCustom(); 86 | const bbox2 = legend.marks[i].mark.getBBoxCustom(); 87 | widths.push(Math.abs(bbox1.width - bbox2.width)); 88 | heights.push(Math.abs(bbox1.height - bbox2.height)); 89 | } 90 | 91 | if (mean(widths) > 2 || mean(heights) > 2) { 92 | legend.type = SizeLegend; 93 | } else { // Remove legend. 94 | legends.splice(legends.indexOf(legend), 1); 95 | return; 96 | } 97 | } 98 | 99 | // Update legend mark properties. 100 | for (const { label, mark } of legend.marks) { 101 | label[RoleProperty] = mark[RoleProperty] = LegendRole; 102 | mark.style['pointer-events'] = 'fill'; 103 | mark.legend = legend; 104 | mark[OpacityField] = mark.hasAttribute('opacity') 105 | ? +mark.getAttribute('opacity') 106 | : window.getComputedStyle(mark).opacity || SelectOpacity; 107 | } 108 | 109 | inferScale(legend); 110 | sortByViewPos('label', legend.marks, legend.type === SizeLegend); 111 | state.legends.push(legend); 112 | } 113 | } 114 | 115 | export function inferLegends(state, textGroups, markGroups) { 116 | const { svg } = state; 117 | const legends = pairGroups(svg, textGroups, markGroups, false); 118 | if (legends.length) { 119 | parseLegends(state, legends); 120 | state.legends.forEach(l => bindLegendData(l)); 121 | } 122 | return legends; 123 | } 124 | -------------------------------------------------------------------------------- /src/parsers/helpers/mark-parser.js: -------------------------------------------------------------------------------- 1 | import { MarkRole, RoleProperty } from '../../state/constants.js'; 2 | import { flattenRGB, sortByViewPos } from '../../util/util.js'; 3 | 4 | export function inferMarks(state) { 5 | state.svgMarks = state.svgMarks.filter(d => !d[RoleProperty]); 6 | state.svgMarks = state.svgMarks.filter(d => flattenRGB(window.getComputedStyle(d).fill) !== 255 * 3); // Ignore white fill marks. 7 | state.svgMarks.forEach(d => { d[RoleProperty] = MarkRole; }); 8 | sortByViewPos(null, state.svgMarks, false); 9 | } 10 | -------------------------------------------------------------------------------- /src/parsers/helpers/title-parser.js: -------------------------------------------------------------------------------- 1 | import { mean } from 'd3-array'; 2 | import { CenterX, CenterY, RoleProperty, TitleRole } from '../../state/constants.js'; 3 | 4 | export function inferTitles(state, titles) { 5 | const titleAssignment = new Map(); 6 | 7 | function calculatePos(el) { 8 | const elBBox = el.getBBoxCustom(); 9 | return [elBBox[CenterX], elBBox[CenterY]]; 10 | } 11 | 12 | function getClosestTitle(x, y) { 13 | let closestTitle = { title: null, dist: Number.MAX_SAFE_INTEGER }; 14 | for (const title of titles) { 15 | const [titleX, titleY] = calculatePos(title); 16 | const posDiff = Math.abs(titleX - x) + Math.abs(titleY - y); 17 | 18 | if (posDiff < closestTitle.dist) { 19 | closestTitle = { title, dist: posDiff }; 20 | } 21 | } 22 | 23 | return closestTitle; 24 | } 25 | 26 | const groups = [state.xAxis, state.yAxis, ...state.legends]; 27 | for (const group of groups) { 28 | const _g = 'ticks' in group ? group.ticks : group.marks; 29 | const pos = _g.filter(d => d.label).map(mark => calculatePos(mark.label)); 30 | const x = mean(pos.map(d => d[0])); const y = mean(pos.map(d => d[1])); 31 | 32 | const titleGroup = getClosestTitle(x, y); 33 | const { title, dist } = titleGroup; 34 | if (!title) continue; 35 | 36 | if (!titleAssignment.has(title) || dist < titleAssignment.get(title).dist) { 37 | group.title = title; 38 | title[RoleProperty] = TitleRole; 39 | titleAssignment.set(title, titleGroup); 40 | } 41 | } 42 | 43 | state.title = titles.filter(d => !d[RoleProperty])[0]; 44 | } 45 | -------------------------------------------------------------------------------- /src/state/constants.js: -------------------------------------------------------------------------------- 1 | // Interactions 2 | export const Select = 'select'; 3 | export const Zoom = 'zoom'; 4 | export const Pan = 'pan'; 5 | export const Filter = 'filter'; 6 | export const Sort = 'sort'; 7 | export const Brush = 'brush'; 8 | export const Annotate = 'annotate'; 9 | export const Arrange = 'arrange'; 10 | 11 | // Interaction defaults 12 | export const SelectOpacity = 1; 13 | export const UnselectOpacity = 0.1; 14 | export const OpacityField = '_opacity_'; 15 | export const SelectField = '_selected_'; 16 | 17 | // Chart components 18 | export const DefaultSvgId = 'svgPlot'; 19 | export const Axis = 'axis'; 20 | export const Tick = 'tick'; 21 | export const TickDomain = 'domain'; 22 | export const Background = 'background'; 23 | export const Foreground = 'foreground'; 24 | export const markTypes = [ 25 | 'circle', 'ellipse', 'line', 'polygon', 'polyline', 'rect', 'path', 'use' 26 | ]; 27 | 28 | // Custom view fields 29 | export const CenterX = 'centerX'; 30 | export const CenterY = 'centerY'; 31 | export const vertAlign = ['left', 'right', CenterX]; 32 | export const horizAlign = ['top', 'bottom', CenterY]; 33 | 34 | // Legend and title constants 35 | export const SizeLegend = 'size'; 36 | export const CategoricalColorLegend = 'colorCat'; 37 | export const DataAttr = '_inferred_data_'; 38 | 39 | export const RoleProperty = '_role_'; 40 | export const LegendRole = 'legend'; 41 | export const AxisDomainRole = 'axis-domain'; 42 | export const TitleRole = 'title'; 43 | export const OrphanTickRole = 'orphan-tick'; 44 | export const MarkRole = 'mark'; 45 | 46 | export const tableMarkField = '_mark_'; 47 | export const tableIndexField = '_I_'; 48 | export const tableGroupIndexField = '_gI_'; 49 | -------------------------------------------------------------------------------- /src/state/data-state.js: -------------------------------------------------------------------------------- 1 | import { LINK_TYPES } from '../parsers/multi-view/link-parser.js'; 2 | 3 | export class DataState { 4 | constructor(table) { 5 | this.table = table; 6 | this.active = { 7 | table, 8 | selected: table, 9 | filtered: null, 10 | type: LINK_TYPES.NONE 11 | }; 12 | this.children = []; 13 | this.parents = []; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/state/view-state.js: -------------------------------------------------------------------------------- 1 | // import { DataState } from './data-state'; 2 | 3 | export class ViewState { 4 | constructor() { 5 | this.hasDomain = false; 6 | this.svg = null; 7 | this.svgMarks = []; 8 | this.textMarks = []; 9 | this.data = null; 10 | 11 | this.xAxis = { 12 | domain: [null, null], 13 | ordinal: [], 14 | range: [null, null], 15 | ticks: [], 16 | scale: null, 17 | axis: null, 18 | title: null 19 | }; 20 | this.yAxis = { 21 | domain: [null, null], 22 | ordinal: [], 23 | range: [null, null], 24 | ticks: [], 25 | scale: null, 26 | axis: null, 27 | title: null 28 | }; 29 | 30 | this.legends = []; 31 | this.title = null; 32 | 33 | this.interactions = { 34 | selection: true, 35 | brush: true, 36 | navigate: false, 37 | filter: false, 38 | sort: false, 39 | annotate: false 40 | }; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/toolbar/icons/annotate.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/toolbar/icons/brush.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/toolbar/icons/download.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/toolbar/icons/filter.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/toolbar/icons/link.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/toolbar/icons/navigate.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/toolbar/icons/reset.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/toolbar/menu.js: -------------------------------------------------------------------------------- 1 | import { select } from 'd3-selection'; 2 | import navigateIcon from './icons/navigate.svg'; 3 | import annotateIcon from './icons/annotate.svg'; 4 | import filterIcon from './icons/filter.svg'; 5 | import linkIcon from './icons/link.svg'; 6 | import brushIcon from './icons/brush.svg'; 7 | import 'bootstrap/dist/css/bootstrap.min.css'; 8 | import { SelectOpacity, UnselectOpacity } from '../state/constants.js'; 9 | 10 | export function createMenu(states) { 11 | const { svg } = states[0]; 12 | const container = select(svg.parentElement); 13 | const menu = container.append('div') 14 | .attr('id', 'menu') 15 | .classed('card', 'true') 16 | .classed('bg-light', true) 17 | .style('position', 'relative') 18 | .style('display', 'inline-block') 19 | .style('margin-top', '10px') 20 | .style('margin-bottom', '10px') 21 | .style('margin-left', '10px') 22 | .style('margin-right', '10px'); 23 | container.node().appendChild(svg); 24 | 25 | const nav = menu.append('div') 26 | .html(navigateIcon) 27 | .style('opacity', UnselectOpacity); 28 | 29 | const brush = menu.append('div') 30 | .html(brushIcon); 31 | 32 | menu.append('div') 33 | .html(filterIcon); 34 | 35 | menu.append('div') 36 | .html(annotateIcon); 37 | 38 | menu.append('div') 39 | .classed('btn-secondary', true) 40 | .html(linkIcon); 41 | 42 | menu.selectAll('div') 43 | .classed('btn', true) 44 | .classed('m-1', true) 45 | .selectAll('svg') 46 | .attr('width', 20) 47 | .attr('height', 20); 48 | 49 | nav.on('click', function() { 50 | states.forEach(s => { 51 | s.interactions.navigate = !s.interactions.navigate; 52 | if (s.interactions.navigate) { 53 | s.interactions.brush = false; 54 | brush.style('opacity', s.interactions.brush ? SelectOpacity : UnselectOpacity); 55 | } 56 | nav.style('opacity', s.interactions.navigate ? SelectOpacity : UnselectOpacity); 57 | }); 58 | }); 59 | 60 | brush.on('click', function() { 61 | states.forEach(s => { 62 | s.interactions.brush = !s.interactions.brush; 63 | if (s.interactions.brush) { 64 | s.interactions.navigate = false; 65 | nav.style('opacity', s.interactions.navigate ? SelectOpacity : UnselectOpacity); 66 | } 67 | brush.style('opacity', s.interactions.brush ? SelectOpacity : UnselectOpacity); 68 | }); 69 | }); 70 | } 71 | -------------------------------------------------------------------------------- /src/util/transform.js: -------------------------------------------------------------------------------- 1 | export class Transform { 2 | constructor(...args) { 3 | if (args[0] instanceof Transform) { 4 | const [other] = args; 5 | this.translate = { ...other.translate }; 6 | this.scale = { ...other.scale }; 7 | this.rotate = other.rotate; 8 | } else { 9 | const [tx, ty, sx, sy, r] = args; 10 | this.translate = { x: tx || 0, y: ty || 0 }; 11 | this.scale = { x: sx || 1, y: sy || 1 }; 12 | this.rotate = r || 0; 13 | } 14 | } 15 | 16 | addTransform(appendTransform) { 17 | this.translate.x += appendTransform.translate.x; 18 | this.translate.y += appendTransform.translate.y; 19 | this.scale.x *= appendTransform.scale.x; 20 | this.scale.y *= appendTransform.scale.y; 21 | this.rotate += appendTransform.rotate; 22 | 23 | return this; 24 | } 25 | 26 | getTransform(appendTransform = new Transform()) { 27 | return 'translate(' + (this.translate.x + appendTransform.translate.x) + ',' + 28 | (this.translate.y + appendTransform.translate.y) + ') scale(' + 29 | (this.scale.x * appendTransform.scale.x) + ',' + 30 | (this.scale.y * appendTransform.scale.y) + ') rotate(' + 31 | (this.rotate + appendTransform.rotate) + ')'; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/util/util.js: -------------------------------------------------------------------------------- 1 | import { CenterX, CenterY } from '../state/constants.js'; 2 | import { sum } from 'd3-array'; 3 | import { path } from 'd3-path'; 4 | 5 | export function isMetaKey(event) { 6 | return event.metaKey || event.ctrlKey || event.altKey || event.shiftKey; 7 | } 8 | 9 | export function copyElement(element) { 10 | const newElement = element.cloneNode(true); 11 | for (const [key, value] of Object.entries(element)) { 12 | newElement[key] = value; 13 | } 14 | 15 | return newElement; 16 | } 17 | 18 | export function computeCenterPos(element, orient) { 19 | const clientRect = element.getBBoxCustom(); 20 | const offset = orient === 'right' || orient === 'left' ? clientRect.width / 2 : clientRect.height / 2; 21 | return clientRect[orient] + (orient === 'left' || orient === 'top' ? offset : -offset); 22 | } 23 | 24 | export function flattenRGB(rgb) { 25 | return sum(rgb.replace(/[^\d,]/g, '').split(',')); 26 | } 27 | 28 | export function convertPtToPx(pt) { 29 | if (!pt || !pt.includes('pt')) return pt; 30 | return +pt.split('pt')[0] * 4 / 3; 31 | } 32 | 33 | export function SVGToScreen(svg, element, svgX, svgY) { 34 | const p = svg.createSVGPoint(); 35 | p.x = svgX; 36 | p.y = svgY; 37 | return p.matrixTransform(element.getScreenCTM()); 38 | } 39 | 40 | export function sortByViewPos(field, objects, useField = false) { 41 | const comparator = (dim) => (a, b) => field == null 42 | ? (a.getBBoxCustom()[dim] - b.getBBoxCustom()[dim]) 43 | : useField 44 | ? a[field] - b[field] 45 | : ((a[field] ? a[field] : a.marks[0]).getBBoxCustom()[dim] - 46 | (b[field] ? b[field] : b.marks[0]).getBBoxCustom()[dim]); 47 | objects.sort(comparator(CenterX)); 48 | objects.sort(comparator(CenterY)); 49 | } 50 | 51 | // Parse single-element lines into separate SVG elements. 52 | export function containsLines(commands) { 53 | if (commands.length <= 2 || commands.length % 2 !== 0) return []; 54 | 55 | const lines = []; 56 | for (let i = 0; i < commands.length; i += 2) { 57 | if (commands[i].code !== 'M' || commands[i + 1].code !== 'L') { 58 | return []; 59 | } 60 | 61 | const p = path(); 62 | p.moveTo(commands[i].x, commands[i].y); 63 | p.lineTo(commands[i + 1].x, commands[i + 1].y); 64 | lines.push(p.toString()); 65 | } 66 | 67 | return lines; 68 | } 69 | -------------------------------------------------------------------------------- /test/browser/d3/bar-chart-test.js: -------------------------------------------------------------------------------- 1 | import { hydrate } from '../../../dist/divi.mjs'; 2 | import { select } from '../../../node_modules/d3-selection/src/index.js'; 3 | import { createBarChart } from '../../../examples/annotated-visualizations/d3/annotated-bar-chart.js'; 4 | import { testChartMetadata } from '../../util/core-structure-test-functions.js'; 5 | 6 | const groundMetadatas = { 7 | xAxis: { 8 | domain: [null, null], 9 | ordinal: ['United States', 'Russia', 'France', 'Germany (FRG)', 'Israel', 10 | 'United Kingdom', 'Netherlands', 'China', 'Spain', 'Italy'], 11 | tickValues: ['United States', 'Russia', 'France', 'Germany (FRG)', 'Israel', 12 | 'United Kingdom', 'Netherlands', 'China', 'Spain', 'Italy'] 13 | }, 14 | yAxis: { 15 | domain: [0, 13000], 16 | ordinal: [], 17 | tickValues: [0, 1000, 2000, 3000, 4000, 5000, 6000, 18 | 7000, 8000, 9000, 10000, 11000, 12000, 13000] 19 | } 20 | }; 21 | 22 | export function testBarChart() { 23 | const divi = { }; 24 | let root; 25 | 26 | before(async function() { 27 | const chart = await createBarChart(); 28 | root = select('#root').append('div'); 29 | root.node().appendChild(chart); 30 | 31 | divi.metadatas = await hydrate(chart); 32 | divi.groundMetadatas = [{ ...groundMetadatas, chart }]; 33 | }); 34 | 35 | describe('Chart Metadata', function() { testChartMetadata(divi); }); 36 | 37 | after(function() { 38 | root.remove(); 39 | }); 40 | } 41 | -------------------------------------------------------------------------------- /test/browser/d3/scatter-plot-test.js: -------------------------------------------------------------------------------- 1 | import { hydrate } from '../../../dist/divi.mjs'; 2 | import { select } from '../../../node_modules/d3-selection/src/index.js'; 3 | import { createScatterPlot } from '../../../examples/annotated-visualizations/d3/annotated-scatter-plot.js'; 4 | import { testChartMetadata } from '../../util/core-structure-test-functions.js'; 5 | 6 | const groundMetadatas = { 7 | xAxis: { 8 | domain: [4, 8], 9 | ordinal: [], 10 | tickValues: [4, 4.5, 5, 5.5, 6, 6.5, 7, 7.5, 8] 11 | }, 12 | yAxis: { 13 | domain: [0, 10], 14 | ordinal: [], 15 | tickValues: [0, 2, 4, 6, 8, 10] 16 | } 17 | }; 18 | 19 | export function testScatterPlot() { 20 | const divi = { }; 21 | let root; 22 | 23 | before(async function() { 24 | const chart = await createScatterPlot(); 25 | root = select('#root').append('div'); 26 | root.node().appendChild(chart); 27 | 28 | divi.metadatas = await hydrate(chart); 29 | divi.groundMetadatas = [{ ...groundMetadatas, chart }]; 30 | }); 31 | 32 | describe('Chart Metadata', function() { testChartMetadata(divi); }); 33 | 34 | after(function() { 35 | root.remove(); 36 | }); 37 | } 38 | -------------------------------------------------------------------------------- /test/browser/d3/tests.js: -------------------------------------------------------------------------------- 1 | import { todo } from '../../util/helper-functions.js'; 2 | import { testBarChart } from './bar-chart-test.js'; 3 | import { testScatterPlot } from './scatter-plot-test.js'; 4 | 5 | describe('D3', function() { 6 | describe('Scatter plot', testScatterPlot); 7 | describe.skip('Line chart', todo); 8 | describe.skip('Stacked area chart', todo); 9 | describe.skip('Stacked bar chart', todo); 10 | describe('Bar chart', testBarChart); 11 | describe.skip('Log chart', todo); 12 | describe.skip('Hex map', todo); 13 | describe.skip('US population map', todo); 14 | }); 15 | -------------------------------------------------------------------------------- /test/browser/excel/tests.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uwdata/divi/41890655aef95e5f6f1f51c930374739da6a1954/test/browser/excel/tests.js -------------------------------------------------------------------------------- /test/browser/ggplot2/line-chart-test.js: -------------------------------------------------------------------------------- 1 | import { hydrate } from '../../../dist/divi.mjs'; 2 | import { select } from '../../../node_modules/d3-selection/src/index.js'; 3 | import { createLineChart } from '../../../examples/annotated-visualizations/ggplot2/annotated-line-chart.js'; 4 | import { testChartMetadata } from '../../util/core-structure-test-functions.js'; 5 | 6 | const groundMetadatas = { 7 | xAxis: { 8 | domain: [new Date(0, 0, 1), new Date(0, 3, 1)], 9 | ordinal: [], 10 | tickValues: [new Date(0, 0, 1), new Date(0, 3, 1)] 11 | }, 12 | yAxis: { 13 | domain: [-30, 20], 14 | ordinal: [], 15 | tickValues: [-30, -20, -10, 0, 10, 20] 16 | } 17 | }; 18 | 19 | export function testLineChart() { 20 | const divi = { }; 21 | let root; 22 | 23 | before(async function() { 24 | const chart = await createLineChart(); 25 | root = select('#root').append('div'); 26 | root.node().appendChild(chart); 27 | 28 | divi.metadatas = await hydrate(chart); 29 | divi.groundMetadatas = [{ ...groundMetadatas, chart }]; 30 | }); 31 | 32 | describe('Chart Metadata', function() { testChartMetadata(divi); }); 33 | 34 | after(function() { 35 | root.remove(); 36 | }); 37 | } 38 | -------------------------------------------------------------------------------- /test/browser/ggplot2/tests.js: -------------------------------------------------------------------------------- 1 | import { todo } from '../../util/helper-functions.js'; 2 | import { testLineChart } from './line-chart-test.js'; 3 | 4 | describe('ggplot2', function() { 5 | describe('Line chart - plant species', testLineChart); 6 | describe.skip('Scatter plot w/ trendline', todo); 7 | describe.skip('Line chart - stocks', todo); 8 | }); 9 | -------------------------------------------------------------------------------- /test/browser/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | DIVI Tests 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 |
17 | 18 | 19 | 20 | 21 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /test/browser/matplotlib/tests.js: -------------------------------------------------------------------------------- 1 | import { todo } from '../../util/helper-functions.js'; 2 | 3 | describe('Matplotlib', function() { 4 | describe.skip('Bar chart', todo); 5 | }); 6 | -------------------------------------------------------------------------------- /test/browser/multi-view/tests.js: -------------------------------------------------------------------------------- 1 | import { todo } from '../../util/helper-functions.js'; 2 | import { testVegaLiteMultiView } from './vega-lite-weather-test.js'; 3 | 4 | describe('Multi-view', function() { 5 | describe('Vega-Lite - weather', testVegaLiteMultiView); 6 | describe.skip('Vega-Lite + Observable Plot - cars', todo); 7 | describe.skip('Vega-Lite crossfiltering - cars', todo); 8 | describe.skip('Vega-Lite crossfiltering - flights', todo); 9 | describe.skip('Vega-Lite + Matplotlib - weather', todo); 10 | describe.skip('Vega-Lite + ggplot2 - weather', todo); 11 | describe.skip('ggplot2 + Matplotlib - weather', todo); 12 | describe.skip('ggplot2 facets - weather', todo); 13 | describe.skip('Observable plot facets - cars', todo); 14 | describe.skip('Excel + Matplotlib + ggplot2 - weather', todo); 15 | }); 16 | -------------------------------------------------------------------------------- /test/browser/multi-view/vega-lite-weather-test.js: -------------------------------------------------------------------------------- 1 | import { hydrate } from '../../../dist/divi.mjs'; 2 | import { select } from '../../../node_modules/d3-selection/src/index.js'; 3 | import { createVegaMultiView } from '../../../examples/annotated-visualizations/multi-view/annotated-vega-lite-weather.js'; 4 | import { testChartMetadata } from '../../util/core-structure-test-functions.js'; 5 | 6 | const groundMetadatas = [ 7 | { 8 | xAxis: { 9 | domain: [null, null], 10 | ordinal: ['United States', 'Russia', 'France', 'Germany (FRG)', 'Israel', 11 | 'United Kingdom', 'Netherlands', 'China', 'Spain', 'Italy'], 12 | tickValues: ['United States', 'Russia', 'France', 'Germany (FRG)', 'Israel', 13 | 'United Kingdom', 'Netherlands', 'China', 'Spain', 'Italy'] 14 | }, 15 | yAxis: { 16 | domain: [0, 13000], 17 | ordinal: [], 18 | tickValues: [0, 1000, 2000, 3000, 4000, 5000, 6000, 19 | 7000, 8000, 9000, 10000, 11000, 12000, 13000] 20 | } 21 | }, 22 | { 23 | xAxis: 'test' 24 | } 25 | ]; 26 | 27 | export function testVegaLiteMultiView() { 28 | const divi = { }; 29 | let root; 30 | 31 | before(async function() { 32 | const charts = await createVegaMultiView(); 33 | root = select('#root').append('div'); 34 | charts.forEach(c => root.node().appendChild(c)); 35 | 36 | divi.metadatas = await hydrate(charts); 37 | divi.groundMetadatas = groundMetadatas.map((m, i) => { return { ...m, chart: charts[i] }; }); 38 | }); 39 | 40 | describe('Chart Metadata', function() { testChartMetadata(divi); }); 41 | 42 | after(function() { 43 | // root.remove(); 44 | }); 45 | } 46 | -------------------------------------------------------------------------------- /test/browser/observable-plot/scatter-plot-test.js: -------------------------------------------------------------------------------- 1 | import { hydrate } from '../../../dist/divi.mjs'; 2 | import { select } from '../../../node_modules/d3-selection/src/index.js'; 3 | import { createScatterPlot } from '../../../examples/annotated-visualizations/observable-plot/annotated-scatter-plot.js'; 4 | import { testChartMetadata } from '../../util/core-structure-test-functions.js'; 5 | 6 | const groundMetadatas = { 7 | xAxis: { 8 | domain: [180, 230], 9 | ordinal: [], 10 | tickValues: [180, 190, 200, 210, 220, 230] 11 | }, 12 | yAxis: { 13 | domain: [3000, 6000], 14 | ordinal: [], 15 | tickValues: [3000, 3500, 4000, 4500, 5000, 5500, 6000] 16 | } 17 | }; 18 | 19 | export function testScatterPlot() { 20 | const divi = { }; 21 | let root; 22 | 23 | before(async function() { 24 | const chart = await createScatterPlot(); 25 | root = select('#root').append('div'); 26 | root.node().appendChild(chart); 27 | 28 | divi.metadatas = await hydrate(chart); 29 | divi.groundMetadatas = [{ ...groundMetadatas, chart }]; 30 | }); 31 | 32 | describe('Chart Metadata', function() { testChartMetadata(divi); }); 33 | 34 | after(function() { 35 | root.remove(); 36 | }); 37 | } 38 | -------------------------------------------------------------------------------- /test/browser/observable-plot/tests.js: -------------------------------------------------------------------------------- 1 | import { todo } from '../../util/helper-functions.js'; 2 | import { testScatterPlot } from './scatter-plot-test.js'; 3 | 4 | describe('Observable Plot', function() { 5 | describe.skip('Scatter plot', todo); 6 | describe('Scatter plot', testScatterPlot); 7 | }); 8 | -------------------------------------------------------------------------------- /test/browser/vega-lite/tests.js: -------------------------------------------------------------------------------- 1 | import { todo } from '../../util/helper-functions.js'; 2 | 3 | describe('Vega-Lite', function() { 4 | describe.skip('Scatter plot - cars', todo); 5 | describe.skip('Scatter plot - weather', todo); 6 | describe.skip('Bar chart', todo); 7 | }); 8 | -------------------------------------------------------------------------------- /test/node/util-test.js: -------------------------------------------------------------------------------- 1 | import assert from 'node:assert'; 2 | import { flattenRGB } from '../../src/util/util.js'; 3 | 4 | process.on('unhandledRejection', function(err) { 5 | console.log(err); 6 | process.exit(1); 7 | }); 8 | 9 | describe('Util', async function() { 10 | describe('#flattenRGB()', function() { 11 | it('should flatten without error', function() { 12 | assert.strictEqual(flattenRGB('rgb(255, 210, 130)'), (255 + 210 + 130)); 13 | }); 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /test/util/helper-functions.js: -------------------------------------------------------------------------------- 1 | export function todo() { 2 | it('TODO', function() { 3 | chai.expect(true).to.equal(true); 4 | }); 5 | } 6 | -------------------------------------------------------------------------------- /test/util/test-constants.js: -------------------------------------------------------------------------------- 1 | // x-axis annotations 2 | export const TEST_X_AXIS_LABEL = 'test-x-axis-label'; 3 | export const TEST_X_AXIS_TICK = 'test-x-axis-tick'; 4 | export const TEST_X_AXIS_TITLE = 'test-x-axis-title'; 5 | 6 | // y-axis annotations 7 | export const TEST_Y_AXIS_LABEL = 'test-y-axis-label'; 8 | export const TEST_Y_AXIS_TICK = 'test-y-axis-tick'; 9 | export const TEST_Y_AXIS_TITLE = 'test-y-axis-title'; 10 | 11 | // Chart annotations (marks, title) 12 | export const TEST_MARK = 'test-mark'; 13 | export const TEST_TITLE = 'test-title'; 14 | 15 | // Legend annotations 16 | export const TEST_LEGEND_TITLE = 'test-legend-title'; 17 | export const TEST_LEGEND_MARK = 'test-legend-mark'; 18 | export const TEST_LEGEND_LABEL = 'test-legend-label'; 19 | --------------------------------------------------------------------------------