├── .eslintignore
├── test
├── browser
│ ├── excel
│ │ └── tests.js
│ ├── matplotlib
│ │ └── tests.js
│ ├── vega-lite
│ │ └── tests.js
│ ├── observable-plot
│ │ ├── tests.js
│ │ └── scatter-plot-test.js
│ ├── ggplot2
│ │ ├── tests.js
│ │ └── line-chart-test.js
│ ├── d3
│ │ ├── tests.js
│ │ ├── scatter-plot-test.js
│ │ └── bar-chart-test.js
│ ├── multi-view
│ │ ├── tests.js
│ │ └── vega-lite-weather-test.js
│ └── index.html
├── util
│ ├── helper-functions.js
│ └── test-constants.js
└── node
│ └── util-test.js
├── src
├── _d3
│ ├── zoom
│ │ ├── constant.js
│ │ ├── index.js
│ │ ├── noevent.js
│ │ ├── event.js
│ │ └── transform.js
│ ├── identity.js
│ └── axis.js
├── index.js
├── toolbar
│ ├── icons
│ │ ├── brush.svg
│ │ ├── filter.svg
│ │ ├── reset.svg
│ │ ├── download.svg
│ │ ├── link.svg
│ │ ├── navigate.svg
│ │ └── annotate.svg
│ └── menu.js
├── hydrate.js
├── state
│ ├── data-state.js
│ ├── view-state.js
│ └── constants.js
├── parsers
│ ├── helpers
│ │ ├── mark-parser.js
│ │ ├── title-parser.js
│ │ ├── legend-parser.js
│ │ └── axis-parser.js
│ └── engine
│ │ └── parser-engine.js
├── orchestration
│ ├── coordinator.js
│ └── inspect.js
├── util
│ ├── transform.js
│ └── util.js
└── handlers
│ ├── query.js
│ ├── brush.js
│ ├── annotate.js
│ ├── zoom.js
│ ├── select.js
│ └── sort.js
├── .gitignore
├── examples
├── data
│ ├── XYZ.csv
│ ├── cities-lived.csv
│ ├── alphabet.csv
│ ├── stateslived.csv
│ └── data.csv
├── visualizations
│ ├── ggplot2
│ │ ├── multi-view-setup
│ │ │ ├── excel-scatter.png
│ │ │ ├── seattle-weather.png
│ │ │ └── multi-view-setup.R
│ │ └── stock-multiline.R
│ ├── multi-view
│ │ ├── d3-ggplot2.js
│ │ ├── gg-weather-facet.js
│ │ ├── gg-matplot.js
│ │ ├── ggplot2-setup.js
│ │ ├── ggplot2-vegalite.js
│ │ ├── vega-matplotlib.js
│ │ ├── vega-dual-linking.js
│ │ ├── vega-bar-scatter.js
│ │ ├── vega-lite-weather.js
│ │ └── vega-crossfilter.js
│ ├── observable-plot
│ │ ├── bar-chart.js
│ │ ├── scatter-plot.js
│ │ └── facets.js
│ ├── matplotlib
│ │ ├── bar-chart.py
│ │ ├── multi-view.py
│ │ ├── multi-view-setup
│ │ │ └── bar-charts.py
│ │ └── bars2.svg
│ ├── vega-lite
│ │ ├── line-chart.js
│ │ ├── bar-chart.js
│ │ ├── scatter-plot-2.js
│ │ └── scatter-plot-1.js
│ └── d3
│ │ ├── pie-chart.js
│ │ ├── hex-map.js
│ │ ├── bar-chart.js
│ │ ├── line-chart.js
│ │ ├── stacked-area-chart.js
│ │ ├── scatter-plot.js
│ │ ├── log-chart.js
│ │ ├── stacked-bar-chart.js
│ │ ├── glyph-map.js
│ │ └── custom-map.js
└── annotated-visualizations
│ ├── observable-plot
│ └── annotated-scatter-plot.js
│ ├── d3
│ ├── annotated-bar-chart.js
│ └── annotated-scatter-plot.js
│ ├── multi-view
│ └── annotated-vega-lite-weather.js
│ └── ggplot2
│ └── annotated-line-chart.js
├── README.md
├── public
├── pages
│ ├── d3
│ │ ├── hex-map.html
│ │ ├── log-chart.html
│ │ ├── line-chart.html
│ │ ├── stacked-area-chart.html
│ │ ├── stacked-bar-chart.html
│ │ ├── population-map.html
│ │ ├── bar-chart.html
│ │ └── scatter-plot.html
│ ├── vega-lite
│ │ ├── scatter-plot-1.html
│ │ ├── bar-chart.html
│ │ └── scatter-plot-2.html
│ ├── observable-plot
│ │ ├── bar-chart.html
│ │ ├── facets.html
│ │ └── scatter-plot.html
│ ├── ggplot2
│ │ ├── line-chart.html
│ │ └── trendline.html
│ └── multi-view
│ │ ├── d3-ggplot2.html
│ │ ├── gg-weather-facet.html
│ │ ├── vega-matplotlib.html
│ │ ├── vega-ggplot2.html
│ │ ├── vega-lite-weather.html
│ │ ├── vega-crossfilter.html
│ │ ├── gg-matplot.html
│ │ ├── ggplot2-multi-view-setup.html
│ │ ├── vega-dual-linking.html
│ │ └── vega-bar-scatter.html
├── study.html
└── index.html
├── .eslintrc.json
├── .github
└── workflows
│ └── deploy.yml
├── docs
└── index.md
├── rollup.config.js
├── LICENSE
└── package.json
/.eslintignore:
--------------------------------------------------------------------------------
1 | /src/_d3/
2 |
--------------------------------------------------------------------------------
/test/browser/excel/tests.js:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/_d3/zoom/constant.js:
--------------------------------------------------------------------------------
1 | export default x => () => x;
2 |
--------------------------------------------------------------------------------
/src/_d3/identity.js:
--------------------------------------------------------------------------------
1 | export default function(x) {
2 | return x;
3 | }
4 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | /node_modules
3 | /dist
4 | docs/.vitepress/cache
5 | docs/.vitepress/dist
6 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/test/util/helper-functions.js:
--------------------------------------------------------------------------------
1 | export function todo() {
2 | it('TODO', function() {
3 | chai.expect(true).to.equal(true);
4 | });
5 | }
6 |
--------------------------------------------------------------------------------
/examples/visualizations/ggplot2/multi-view-setup/excel-scatter.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/uwdata/divi/main/examples/visualizations/ggplot2/multi-view-setup/excel-scatter.png
--------------------------------------------------------------------------------
/examples/visualizations/ggplot2/multi-view-setup/seattle-weather.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/uwdata/divi/main/examples/visualizations/ggplot2/multi-view-setup/seattle-weather.png
--------------------------------------------------------------------------------
/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/_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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/src/toolbar/icons/brush.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/toolbar/icons/filter.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/src/toolbar/icons/reset.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/src/toolbar/icons/download.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/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/log-chart.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | D3 Log Chart
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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/_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 |
--------------------------------------------------------------------------------
/public/pages/d3/population-map.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | D3 US Population Map
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/src/toolbar/icons/link.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/d3/bar-chart.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | D3 Bar Chart
4 |
5 |
6 |
7 |
8 |
9 |
10 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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/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/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/multi-view/d3-ggplot2.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | D3 ggplot2 Multi-line Chart (Single-attribute linking)
4 |
5 |
6 |
7 |
8 |
9 |
10 |
14 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/src/toolbar/icons/navigate.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/pages/d3/scatter-plot.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | D3 Scatter Plot
4 |
5 |
6 |
7 |
8 |
9 |
10 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/public/pages/multi-view/gg-weather-facet.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | gg weather facet
4 |
5 |
6 |
7 |
8 |
9 |
10 |
15 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/public/pages/multi-view/vega-matplotlib.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | Vega-lite Matplotlib Multi-view Chart
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
15 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/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 |
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 |
16 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/src/toolbar/icons/annotate.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/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
--------------------------------------------------------------------------------
/public/pages/multi-view/vega-crossfilter.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | Vega-lite Multi-view Chart
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
16 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
19 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/public/pages/multi-view/ggplot2-multi-view-setup.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | Matplotlib ggplot2 Setup
4 |
5 |
6 |
7 |
8 |
9 |
10 |
20 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/public/study.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | DIVI
4 |
5 |
6 |
7 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
19 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/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 |
21 |
24 |
27 |
28 |
29 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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/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 |
--------------------------------------------------------------------------------
/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/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/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 |
--------------------------------------------------------------------------------
/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 | import { brush } from '../handlers/brush.js';
9 |
10 | export function coordinate(svg, extState) {
11 | const states = svg.map(d => parseChart(inspect(d)));
12 | link(states, extState);
13 | // createMenu(states);
14 | coordinateInteractions(states);
15 | return states;
16 | }
17 |
18 | function coordinateInteractions(states) {
19 | const select = state => function(event) {
20 | const { target } = event;
21 | const { data } = state;
22 | const roots = getRootNodes(data);
23 |
24 | walkQueryPath(roots, selectPoint(state, target), isMetaKey(event));
25 | applySelections(states);
26 | };
27 |
28 | for (const state of states) {
29 | const { svg } = state;
30 | svg.addEventListener('click', select(state));
31 | // zoom(state);
32 | brush(state);
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | BSD 3-Clause License
2 |
3 | Copyright (c) 2020-2025, UW Interactive Data Lab
4 | All rights reserved.
5 |
6 | Redistribution and use in source and binary forms, with or without
7 | modification, are permitted provided that the following conditions are met:
8 |
9 | 1. Redistributions of source code must retain the above copyright notice, this
10 | list of conditions and the following disclaimer.
11 |
12 | 2. Redistributions in binary form must reproduce the above copyright notice,
13 | this list of conditions and the following disclaimer in the documentation
14 | and/or other materials provided with the distribution.
15 |
16 | 3. Neither the name of the copyright holder nor the names of its
17 | contributors may be used to endorse or promote products derived from
18 | this software without specific prior written permission.
19 |
20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
30 |
--------------------------------------------------------------------------------
/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/_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/brush.js:
--------------------------------------------------------------------------------
1 | import { pointer, select } from 'd3-selection';
2 |
3 | function getBrusher(svg) {
4 | return select(svg)
5 | .append('rect')
6 | .attr('opacity', 0.25)
7 | .attr('x', 0)
8 | .attr('y', 0)
9 | .attr('width', 0)
10 | .attr('height', 0)
11 | .attr('id', svg.id + '-brush-rect')
12 | .style('fill', 'gray')
13 | .style('stroke', '#fff')
14 | .style('stroke-width', '1px');
15 | }
16 |
17 | export function brush(state) {
18 | const { svg } = state;
19 | let brusher, x, y;
20 |
21 | function brushStart(event) {
22 | event.preventDefault();
23 |
24 | brusher = getBrusher(svg);
25 | [x, y] = pointer(event, svg);
26 | brusher.attr('x', x)
27 | .attr('y', y);
28 |
29 | svg.addEventListener('mousemove', brushMove);
30 | svg.addEventListener('mouseup', brushEnd);
31 | }
32 |
33 | function brushMove(event) {
34 | event.preventDefault();
35 |
36 | const [newX, newY] = pointer(event, svg);
37 | const width = newX - x;
38 | const height = newY - y;
39 | const xTranslate = width < 0 ? width : 0;
40 | const yTranslate = height < 0 ? height : 0;
41 |
42 | brusher.attr('width', Math.abs(width))
43 | .attr('height', Math.abs(height))
44 | .attr('transform', 'translate(' + xTranslate + ',' + yTranslate + ')');
45 | }
46 |
47 | function brushEnd(_) {
48 | svg.removeEventListener('mousemove', brushMove);
49 | svg.removeEventListener('mouseup', brushEnd);
50 | brusher.remove();
51 | }
52 |
53 | svg.addEventListener('mousedown', brushStart);
54 | }
55 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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 -w",
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 --open public"
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.6.3"
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-time-format": "^4.1.0",
53 | "d3-transition": "^3.0.1",
54 | "svg-path-parser": "^1.1.0"
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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/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/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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/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/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/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | DIVI
4 |
5 |
6 |
7 |
52 |
53 |
54 |
--------------------------------------------------------------------------------
/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/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/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/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/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/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/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-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 |
--------------------------------------------------------------------------------
/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 | import { MarkRole } from '../state/constants.js';
5 |
6 | function addClipping(svg, marks, xAxis, yAxis) {
7 | const nodes = marks.nodes();
8 | if (nodes.length === 0) return;
9 |
10 | // Find shared parent of all nodes
11 | let sharedParent = nodes[0].parentElement;
12 | if (!sharedParent) return;
13 |
14 | for (let i = 1; i < nodes.length; ++i) {
15 | const current = nodes[i].parentElement;
16 | if (!current) continue;
17 |
18 | if (current.contains(sharedParent)) {
19 | sharedParent = current;
20 | } else {
21 | while (!sharedParent.contains(current)) {
22 | sharedParent = sharedParent.parentElement;
23 | }
24 | }
25 | }
26 |
27 | xAxis.range = xAxis.range.map(d => d - (sharedParent.localTransform?.translate.x || 0));
28 | yAxis.range = yAxis.range.map(d => d - (sharedParent.localTransform?.translate.y || 0));
29 |
30 | svg.append('defs')
31 | .append('clipPath')
32 | .attr('id', 'clip-' + svg.node().id)
33 | .append('rect')
34 | .attr('x', xAxis.range[0])
35 | .attr('y', yAxis.range[1])
36 | .attr('width', xAxis.range[1] - xAxis.range[0])
37 | .attr('height', yAxis.range[0] - yAxis.range[1]);
38 |
39 | const container = select(sharedParent).append('g');
40 | container.attr('clip-path', 'url(#clip-' + svg.node().id + ')');
41 |
42 | for (const child of [...sharedParent.children]) {
43 | if (child.hasAttribute(MarkRole)) {
44 | container.node().appendChild(child);
45 | }
46 | }
47 | }
48 |
49 | const zoomCallback = (state, z, tx, ty, gXAxis, gYAxis, zoomX, zoomY) => function cb({ sourceEvent, transform }) {
50 | let { svg } = state;
51 | svg = select(svg);
52 |
53 | sourceEvent.preventDefault();
54 |
55 | const k = transform.k / z[0].k;
56 | const x = (transform.x - z[0].x) / tx().k;
57 | const y = (transform.y - z[0].y) / ty().k;
58 |
59 | const svgRect = svg.node().getBBoxCustom();
60 | const cliX = sourceEvent.clientX - svgRect.left;
61 | const cliY = sourceEvent.clientY - svgRect.top;
62 |
63 | if (k === 1) {
64 | gXAxis.call(zoomX.translateBy, x, 0);
65 | gYAxis.call(zoomY.translateBy, 0, y);
66 | } else {
67 | gXAxis.call(zoomX.scaleBy, k, [cliX, cliY]);
68 | gYAxis.call(zoomY.scaleBy, k, [cliX, cliY]);
69 | }
70 | for (const _s of [state]) {
71 | if (_s === state) _s.yAxis.axis.scale(ty().rescaleY(_s.yAxis.scale))();
72 | _s.xAxis.axis.scale(tx().rescaleX(_s.xAxis.scale))();
73 |
74 | selectAll(_s.svgMarks).attr('transform', function() {
75 | const translateX = tx().applyX(this.globalPosition.translate.x) - (this.globalPosition.translate.x);
76 | const translateY = _s === state ? ty().applyY(this.globalPosition.translate.y) - (this.globalPosition.translate.y) : 0;
77 |
78 | const scaleX = this.type ? tx().k : 1;
79 | const scaleY = this.type && _s === state ? ty().k : 1;
80 |
81 | return this.localTransform.getTransform(new Transform(translateX, translateY, scaleX, scaleY));
82 | });
83 | }
84 | z[0] = transform;
85 | };
86 |
87 | export function zoom(state) {
88 | const { xAxis, yAxis } = state;
89 | let { svg, svgMarks: marks } = state;
90 | svg = select(svg);
91 | marks = selectAll(marks);
92 |
93 | addClipping(svg, marks, xAxis, yAxis);
94 |
95 | const gXAxis = svg.append('g').attr('id', 'x-axis-zoom-accessor');
96 | const gYAxis = svg.append('g').attr('id', 'y-axis-zoom-accessor');
97 |
98 | const z = [zoomIdentity];
99 | const zoomX = _zoom();
100 | const zoomY = _zoom();
101 | const tx = () => zoomTransform(gXAxis.node());
102 | const ty = () => zoomTransform(gYAxis.node());
103 |
104 | gXAxis.call(zoomX).attr('pointer-events', 'none');
105 | gYAxis.call(zoomY).attr('pointer-events', 'none');
106 |
107 | svg.call(_zoom().on('zoom', zoomCallback(state, z, tx, ty, gXAxis, gYAxis, zoomX, zoomY)));
108 | }
109 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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, svg, 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, svg, isX);
146 | computeScale(state, axisContainer, isX);
147 | });
148 |
149 | return axes;
150 | }
151 |
--------------------------------------------------------------------------------
/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(element.getBBoxCustom()[CenterX], element.getBBoxCustom()[CenterY]);
48 | element.localTransform = parseTransform(element);
49 | }
50 |
51 | function inferTypeFromPath(element, state, transform) {
52 | const commands = parser.parseSVG(element.getAttribute('d'));
53 | if (!commands.length) return;
54 |
55 | element.contour = commands;
56 | const endCmd = commands[commands.length - 1];
57 | const relCommands = ['v', 'h'];
58 | const lineCandidates = containsLines(commands);
59 |
60 | if (commands.length === 3 && commands[1].code === 'A' && endCmd.code === 'A') { // Check for ellipse shape.
61 | element.type = 'ellipse';
62 | } else if (lineCandidates.length) { // Check for extracted gridlines.
63 | const p = select(element.parentElement);
64 |
65 | // Create new elements for each gridline.
66 | lineCandidates.forEach(lc => {
67 | const style = window.getComputedStyle(element);
68 | const el = p.append('path').attr('d', lc)
69 | .style('fill', style.fill)
70 | .style('color', style.color)
71 | .style('stroke', style.stroke)
72 | .style('stroke-width', style.strokeWidth)
73 | .attr('id', 'test');
74 | analyzeDomTree(el.node(), state, transform);
75 | });
76 |
77 | select(element).remove();
78 | } else if (endCmd.code !== 'Z') { // Check for line shape.
79 | element.type = 'line';
80 | } else if (commands.length === 5 && relCommands.includes(commands[1].code) && relCommands.includes(commands[2].code) &&
81 | relCommands.includes(commands[3].code)) { // Check for rectangular shape.
82 | element.type = 'rect';
83 | } else { // Check for polygonal shape.
84 | element.type = 'polygon';
85 | }
86 | }
87 |
88 | function analyzeDomTree(element, state, transform, parent = false) {
89 | if (!element) return;
90 | if (element.nodeName.toLowerCase() === 'style') return; // Ignore style elements.
91 | if (element.className && (element.className.baseVal === Background || // Ignore background elements.
92 | element.className.baseVal === Foreground)) return;
93 | // TODO: Ignore clip elements
94 |
95 | if (parent || element.nodeName === 'svg') { // Store base SVG element.
96 | state.svg = element;
97 | extractElementInformation(state.svg, element, parent);
98 | if (!element.id) element.id = DefaultSvgId + '-' + id++;
99 | } else if (element.nodeName === 'g') { // Maintain element transforms.
100 | parseTransform(element, transform);
101 | element.localTransform = transform;
102 | } else if (element.nodeName === '#text' && element.textContent.trim() !== '') { // Instantiate text elements for non-empty references.
103 | let el = element.parentElement;
104 | if (el.nodeName !== 'text') {
105 | el = select(el).append('text').html(element.textContent).node();
106 | select(element).remove();
107 | }
108 |
109 | extractElementInformation(state.svg, el);
110 | el.removeAttribute('textLength');
111 | state.textMarks.push(el);
112 | el.style['pointer-events'] = 'none';
113 | el.style['user-select'] = 'none';
114 | } else if (markTypes.includes(element.nodeName)) { // Process base SVG marks.
115 | const markType = element.nodeName;
116 |
117 | if (markType === 'path') { // Infer path element type.
118 | inferTypeFromPath(element, state, transform);
119 | element.setAttribute('vector-effect', 'non-scaling-stroke');
120 | } else if (markType === 'polygline' || markType === 'polygon' || markType === 'line') {
121 | element.type = markType;
122 | element.setAttribute('vector-effect', 'non-scaling-stroke');
123 | }
124 |
125 | extractElementInformation(state.svg, element);
126 | element.setAttribute(MarkRole, 'true');
127 | if (!state.svgMarks.includes(element)) state.svgMarks.push(element); // Prevent duplicate references.
128 | }
129 |
130 | for (const child of element.childNodes) {
131 | analyzeDomTree(child, state, new Transform(transform));
132 | }
133 | }
134 |
135 | export function inspect(svg) {
136 | const state = new ViewState();
137 | analyzeDomTree(svg, state, new Transform(), true);
138 | state.svg = svg;
139 | console.log(state);
140 | return state;
141 | }
142 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/examples/visualizations/matplotlib/bars2.svg:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
225 |
--------------------------------------------------------------------------------