├── .codeclimate.yml ├── .eslintrc.js ├── .github ├── CODEOWNERS ├── logo.svg └── workflows │ ├── gh-pages.yml │ ├── pr-gate.yml │ └── publish.yml ├── .gitignore ├── .img └── screenshot.png ├── README.md ├── app ├── .gitignore ├── README.md ├── components │ ├── Layout.tsx │ ├── Logo.tsx │ ├── NavLink.tsx │ ├── ParameterTable.tsx │ └── charts │ │ ├── Renderer.tsx │ │ ├── histogram │ │ └── Simple.tsx │ │ ├── line │ │ ├── Active.tsx │ │ ├── Aggregated.tsx │ │ ├── Baseline.tsx │ │ ├── Broken.tsx │ │ ├── Confidence.tsx │ │ ├── Multi.tsx │ │ └── Simple.tsx │ │ └── scatter │ │ ├── Categories.tsx │ │ ├── Complex.tsx │ │ └── Simple.tsx ├── data │ ├── confidenceBand.json │ ├── fakeUsers1.json │ ├── fakeUsers2.json │ ├── missing.json │ ├── points1.json │ ├── ufoDates.json │ └── ufoSightings.json ├── helpers │ └── format.ts ├── next-env.d.ts ├── next.config.js ├── package.json ├── pages │ ├── _app.tsx │ ├── _document.tsx │ ├── histogram.mdx │ ├── index.tsx │ ├── line.mdx │ ├── mg-api.mdx │ └── scatter.mdx ├── postcss.config.js ├── public │ └── favicon.svg ├── styles │ └── globals.css ├── tailwind.config.js └── tsconfig.json ├── lib ├── .gitignore ├── esbuild.mjs ├── package.json ├── src │ ├── charts │ │ ├── abstractChart.ts │ │ ├── histogram.ts │ │ ├── line.ts │ │ └── scatter.ts │ ├── components │ │ ├── abstractShape.ts │ │ ├── area.ts │ │ ├── axis.ts │ │ ├── delaunay.ts │ │ ├── legend.ts │ │ ├── line.ts │ │ ├── point.ts │ │ ├── rect.ts │ │ ├── rug.ts │ │ ├── scale.ts │ │ └── tooltip.ts │ ├── index.ts │ ├── mg.css │ └── misc │ │ ├── constants.ts │ │ ├── typings.ts │ │ └── utility.ts └── tsconfig.json ├── package.json └── yarn.lock /.codeclimate.yml: -------------------------------------------------------------------------------- 1 | exclude_patterns: 2 | - "packages/docs/src/data/" -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | ignorePatterns: ['lib/dist/**/*'], 3 | extends: [ 4 | 'standard', 5 | 'plugin:react/recommended', 6 | 'plugin:@typescript-eslint/recommended', 7 | 'plugin:react-hooks/recommended', 8 | 'plugin:prettier/recommended' 9 | ], 10 | parser: '@typescript-eslint/parser', 11 | parserOptions: { 12 | ecmaVersion: '2020' 13 | }, 14 | plugins: ['react', '@typescript-eslint', 'prettier'], 15 | rules: { 16 | camelcase: 'off', 17 | 'no-use-before-define': 'off', 18 | '@typescript-eslint/no-use-before-define': ['error'], 19 | 'prettier/prettier': [ 20 | 'error', 21 | { 22 | tabWidth: 2, 23 | printWidth: 120, 24 | singleQuote: true, 25 | trailingComma: 'none', 26 | semi: false, 27 | overrides: [ 28 | { 29 | files: '*.json', 30 | options: { 31 | parser: 'json' 32 | } 33 | }, 34 | { 35 | files: '*.html', 36 | options: { 37 | parser: 'html' 38 | } 39 | }, 40 | { 41 | files: '*.css', 42 | options: { 43 | parser: 'css' 44 | } 45 | }, 46 | { 47 | files: '*.md', 48 | options: { 49 | parser: 'markdown' 50 | } 51 | } 52 | ] 53 | } 54 | ], 55 | 'import/order': 'error', 56 | 'react/react-in-jsx-scope': 'off', 57 | 'react/prop-types': 'off' 58 | }, 59 | settings: { 60 | react: { 61 | version: 'detect' 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @jens-ox 2 | -------------------------------------------------------------------------------- /.github/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 25 | 34 | 41 | 51 | 61 | 70 | 81 | 94 | 104 | 116 | 130 | 144 | 155 | 164 | 176 | 179 | 191 | 203 | 204 | 205 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | -------------------------------------------------------------------------------- /.github/workflows/gh-pages.yml: -------------------------------------------------------------------------------- 1 | name: Deploy GitHub Pages 2 | on: 3 | push: 4 | branches: [main] 5 | 6 | jobs: 7 | build-and-deploy: 8 | name: Deploy to GitHub Pages 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v3 12 | - uses: actions/setup-node@v3 13 | with: 14 | node-version: "16" 15 | cache: "yarn" 16 | - run: yarn 17 | - name: Build library 18 | run: yarn build 19 | working-directory: ./lib 20 | - name: Build frontend 21 | run: yarn build 22 | working-directory: ./app 23 | - name: Deploy 24 | uses: JamesIves/github-pages-deploy-action@v4.3.0 25 | with: 26 | branch: gh-pages 27 | folder: ./app/out -------------------------------------------------------------------------------- /.github/workflows/pr-gate.yml: -------------------------------------------------------------------------------- 1 | name: PR Gate 2 | on: 3 | push: 4 | branches-ignore: 5 | - gh-pages 6 | 7 | jobs: 8 | lint-lib: 9 | name: Lint Library 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v3 13 | - uses: actions/setup-node@v3 14 | with: 15 | node-version: "16" 16 | cache: "yarn" 17 | - run: yarn 18 | - name: Run ESLint 19 | run: yarn lint 20 | working-directory: ./lib -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish Package to NPM 2 | on: 3 | release: 4 | types: [created] 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v3 10 | - uses: actions/setup-node@v3 11 | with: 12 | node-version: '16' 13 | registry-url: 'https://registry.npmjs.org' 14 | cache: 'yarn' 15 | - run: yarn 16 | - name: Build library 17 | run: yarn build 18 | working-directory: ./lib 19 | - run: npm publish 20 | working-directory: ./lib 21 | env: 22 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .vscode 3 | .DS_Store -------------------------------------------------------------------------------- /.img/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/metricsgraphics/metrics-graphics/60429d802e3444c209a1e3d6f7fb9742b76fa838/.img/screenshot.png -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![MetricsGraphics Logo](.github/logo.svg)](https://metricsgraphicsjs.org) 2 | 3 | [![BundlePhobia](https://badgen.net/bundlephobia/minzip/mg2)](https://bundlephobia.com/result?p=mg2) [![CodeClimate](https://api.codeclimate.com/v1/badges/dc22d28ce4d8bece4504/maintainability)](https://codeclimate.com/github/jens-ox/metrics-graphics/maintainability) [![Netlify Status](https://api.netlify.com/api/v1/badges/797ef16b-da9e-461f-851b-e50ddfd905ab/deploy-status)](https://app.netlify.com/sites/affectionate-benz-6e3cf9/deploys) 4 | 5 | *MetricsGraphics* is a library built for visualizing and laying out time-series data. At around 15kB (gzipped), it provides a simple way to produce common types of graphics in a principled and consistent way. The library currently supports line charts, scatterplots and histograms, as well as features like rug plots. 6 | 7 | ## Example 8 | 9 | All you need to do is add an entry node to your document: 10 | 11 | ```html 12 |
13 | ``` 14 | 15 | Then, use the id to mount the chart: 16 | 17 | ```js 18 | import LineChart from 'metrics-graphics' 19 | 20 | new LineChart({ 21 | data, // some array of data objects 22 | width: 600, 23 | height: 200, 24 | target: '#chart', 25 | area: true, 26 | xAccessor: 'date', 27 | yAccessor: 'value' 28 | }) 29 | ``` 30 | 31 | That's it! 32 | 33 | ![Sample Screenshot](.img/screenshot.png) 34 | 35 | The raw data for this example can be found [here](packages/examples/src/assets/data/ufoSightings.js) 36 | 37 | ## Documentation 38 | 39 | If you want to use *MetricsGraphics*, you can find the public API [here](packages/lib/docs/API.md). 40 | 41 | If you want to extend *MetricsGraphics*, you can read up on the [components](packages/lib/docs/Components.md) and [utilities](packages/lib/docs/Utility.md). 42 | 43 | ## Development Setup 44 | 45 | This project uses [Yarn Workspaces](https://classic.yarnpkg.com/lang/en/docs/workspaces/). Please make sure that Yarn is installed. 46 | 47 | ```bash 48 | # clone and setup 49 | git clone https://github.com/metricsgraphics/metrics-graphics 50 | cd metrics-graphics 51 | yarn install 52 | ``` 53 | 54 | Run both the development setup of the library and the development setup of the examples 55 | 56 | ```bash 57 | # inside packages/lib 58 | yarn dev 59 | 60 | # inside packages/examples 61 | yarn dev 62 | ``` -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | .pnpm-debug.log* 27 | 28 | # local env files 29 | .env*.local 30 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | -------------------------------------------------------------------------------- /app/README.md: -------------------------------------------------------------------------------- 1 | This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). 2 | 3 | ## Getting Started 4 | 5 | First, run the development server: 6 | 7 | ```bash 8 | npm run dev 9 | # or 10 | yarn dev 11 | ``` 12 | 13 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 14 | 15 | You can start editing the page by modifying `pages/index.tsx`. The page auto-updates as you edit the file. 16 | 17 | [API routes](https://nextjs.org/docs/api-routes/introduction) can be accessed on [http://localhost:3000/api/hello](http://localhost:3000/api/hello). This endpoint can be edited in `pages/api/hello.ts`. 18 | 19 | The `pages/api` directory is mapped to `/api/*`. Files in this directory are treated as [API routes](https://nextjs.org/docs/api-routes/introduction) instead of React pages. 20 | 21 | ## Learn More 22 | 23 | To learn more about Next.js, take a look at the following resources: 24 | 25 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. 26 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. 27 | 28 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! 29 | 30 | ## Deploy on Vercel 31 | 32 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. 33 | 34 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. 35 | -------------------------------------------------------------------------------- /app/components/Layout.tsx: -------------------------------------------------------------------------------- 1 | import Head from 'next/head' 2 | import { PropsWithChildren } from 'react' 3 | 4 | interface LayoutProps { 5 | title: string 6 | } 7 | 8 | const Layout: React.FC> = ({ title, children }) => ( 9 | <> 10 | 11 | {title} 12 | 13 | {children} 14 | 15 | ) 16 | 17 | export default Layout 18 | -------------------------------------------------------------------------------- /app/components/Logo.tsx: -------------------------------------------------------------------------------- 1 | const Logo = () => ( 2 | 3 | 4 | 8 | 9 | 10 | ) 11 | 12 | export default Logo 13 | -------------------------------------------------------------------------------- /app/components/NavLink.tsx: -------------------------------------------------------------------------------- 1 | import Link, { LinkProps } from 'next/link' 2 | import { useRouter } from 'next/router' 3 | import { PropsWithChildren } from 'react' 4 | import cx from 'classnames' 5 | 6 | const NavLink: React.FC> = ({ href, children, ...linkProps }) => { 7 | const router = useRouter() 8 | 9 | return ( 10 | 11 | 12 | {children} 13 | 14 | 15 | ) 16 | } 17 | 18 | export default NavLink 19 | -------------------------------------------------------------------------------- /app/components/ParameterTable.tsx: -------------------------------------------------------------------------------- 1 | interface ParameterTableProps { 2 | props: Array<{ 3 | name: string 4 | type: string 5 | default?: string 6 | description: string 7 | }> 8 | } 9 | 10 | const ParameterTable: React.FC = ({ props }) => ( 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | {props.map((p) => ( 22 | 23 | 24 | 27 | 28 | 29 | 30 | ))} 31 | 32 |
NameTypeDefaultDescription
{p.name} 25 | {p.type} 26 | {p.default ? {p.default} : '-'}{p.description}
33 | ) 34 | 35 | export default ParameterTable 36 | -------------------------------------------------------------------------------- /app/components/charts/Renderer.tsx: -------------------------------------------------------------------------------- 1 | import { MutableRefObject, PropsWithChildren, useEffect, useRef } from 'react' 2 | 3 | interface RendererProps { 4 | chartRenderer: (chartRef: MutableRefObject) => unknown 5 | } 6 | 7 | const Renderer: React.FC> = ({ chartRenderer, children }) => { 8 | const chartRef = useRef(null) 9 | 10 | // render chart 11 | useEffect(() => { 12 | // if react is still rendering, wait 13 | if (!chartRef.current) return 14 | 15 | // call render function with ref 16 | chartRenderer(chartRef.current) 17 | }) 18 | 19 | return ( 20 |
21 |
22 |
{children}
23 |
24 | ) 25 | } 26 | 27 | export default Renderer 28 | -------------------------------------------------------------------------------- /app/components/charts/histogram/Simple.tsx: -------------------------------------------------------------------------------- 1 | import { HistogramChart } from 'metrics-graphics' 2 | import { PropsWithChildren } from 'react' 3 | import Renderer from '../Renderer' 4 | import ufoData from '../../../data/ufoDates.json' 5 | 6 | const Simple: React.FC> = ({ children }) => ( 7 | 9 | new HistogramChart({ 10 | data: ufoData.map((date) => date / 30).sort(), 11 | width: 600, 12 | height: 200, 13 | binCount: 150, 14 | target: ref as any, 15 | brush: 'x', 16 | yAxis: { 17 | extendedTicks: true 18 | }, 19 | tooltipFunction: (bar) => `${bar.time} months, volume ${bar.count}` 20 | }) 21 | } 22 | > 23 | {children} 24 | 25 | ) 26 | 27 | export default Simple 28 | -------------------------------------------------------------------------------- /app/components/charts/line/Active.tsx: -------------------------------------------------------------------------------- 1 | import { LineChart } from 'metrics-graphics' 2 | import { PropsWithChildren } from 'react' 3 | import Renderer from '../Renderer' 4 | import { formatCompact, formatDate } from '../../../helpers/format' 5 | 6 | import fakeUsers from '../../../data/fakeUsers1.json' 7 | 8 | const Active: React.FC> = ({ children }) => ( 9 | 11 | new LineChart({ 12 | data: [ 13 | fakeUsers.map((entry, i) => ({ 14 | ...entry, 15 | date: new Date(entry.date), 16 | active: i % 5 === 0 17 | })) 18 | ], 19 | width: 600, 20 | height: 200, 21 | target: ref as any, 22 | activeAccessor: 'active', 23 | activePoint: { 24 | radius: 2 25 | }, 26 | tooltipFunction: (point) => `${formatDate(point.date)}: ${formatCompact(point.value)}` 27 | }) 28 | } 29 | > 30 | {children} 31 | 32 | ) 33 | 34 | export default Active 35 | -------------------------------------------------------------------------------- /app/components/charts/line/Aggregated.tsx: -------------------------------------------------------------------------------- 1 | import { LineChart } from 'metrics-graphics' 2 | import { PropsWithChildren } from 'react' 3 | import Renderer from '../Renderer' 4 | import { formatCompact, formatDate } from '../../../helpers/format' 5 | 6 | import fakeUsers from '../../../data/fakeUsers2.json' 7 | 8 | const Aggregated: React.FC> = ({ children }) => ( 9 | 11 | new LineChart({ 12 | data: fakeUsers.map((fakeArray) => 13 | fakeArray.map((fakeEntry) => ({ 14 | ...fakeEntry, 15 | date: new Date(fakeEntry.date) 16 | })) 17 | ), 18 | width: 600, 19 | height: 200, 20 | target: ref as any, 21 | xAccessor: 'date', 22 | yAccessor: 'value', 23 | legend: ['Line 1', 'Line 2', 'Line 3'], 24 | voronoi: { 25 | aggregate: true 26 | }, 27 | tooltipFunction: (point) => `${formatDate(point.date)}: ${formatCompact(point.value)}` 28 | }) 29 | } 30 | > 31 | {children} 32 | 33 | ) 34 | 35 | export default Aggregated 36 | -------------------------------------------------------------------------------- /app/components/charts/line/Baseline.tsx: -------------------------------------------------------------------------------- 1 | import { LineChart } from 'metrics-graphics' 2 | import { PropsWithChildren } from 'react' 3 | import Renderer from '../Renderer' 4 | import { formatCompact, formatDate } from '../../../helpers/format' 5 | 6 | import fakeUsers from '../../../data/fakeUsers1.json' 7 | 8 | const Baseline: React.FC> = ({ children }) => ( 9 | 11 | new LineChart({ 12 | data: [ 13 | fakeUsers.map((entry) => ({ 14 | ...entry, 15 | date: new Date(entry.date) 16 | })) 17 | ], 18 | baselines: [{ value: 160000000, label: 'a baseline' }], 19 | width: 600, 20 | height: 200, 21 | target: ref as any, 22 | tooltipFunction: (point) => `${formatDate(point.date)}: ${formatCompact(point.value)}` 23 | }) 24 | } 25 | > 26 | {children} 27 | 28 | ) 29 | 30 | export default Baseline 31 | -------------------------------------------------------------------------------- /app/components/charts/line/Broken.tsx: -------------------------------------------------------------------------------- 1 | import { LineChart } from 'metrics-graphics' 2 | import { PropsWithChildren } from 'react' 3 | import Renderer from '../Renderer' 4 | import { formatDate } from '../../../helpers/format' 5 | 6 | import missing from '../../../data/missing.json' 7 | 8 | const Broken: React.FC> = ({ children }) => ( 9 | 11 | new LineChart({ 12 | data: [missing.map((e) => ({ ...e, date: new Date(e.date) }))], 13 | width: 600, 14 | height: 200, 15 | target: ref as any, 16 | defined: (d) => !d.dead, 17 | area: true, 18 | tooltipFunction: (point) => `${formatDate(point.date)}: ${point.value}` 19 | }) 20 | } 21 | > 22 | {children} 23 | 24 | ) 25 | 26 | export default Broken 27 | -------------------------------------------------------------------------------- /app/components/charts/line/Confidence.tsx: -------------------------------------------------------------------------------- 1 | import { LineChart } from 'metrics-graphics' 2 | import { PropsWithChildren } from 'react' 3 | import Renderer from '../Renderer' 4 | import confidence from '../../../data/confidenceBand.json' 5 | import { formatDate, formatPercent } from '../../../helpers/format' 6 | 7 | const Confidence: React.FC> = ({ children }) => ( 8 | 10 | new LineChart({ 11 | data: [ 12 | confidence.map((entry) => ({ 13 | ...entry, 14 | date: new Date(entry.date) 15 | })) 16 | ], 17 | xAxis: { 18 | extendedTicks: true 19 | }, 20 | yAxis: { 21 | tickFormat: 'percentage' 22 | }, 23 | width: 600, 24 | height: 200, 25 | target: ref as any, 26 | confidenceBand: ['l', 'u'], 27 | tooltipFunction: (point) => `${formatDate(point.date)}: ${formatPercent(point.value)}` 28 | }) 29 | } 30 | > 31 | {children} 32 | 33 | ) 34 | 35 | export default Confidence 36 | -------------------------------------------------------------------------------- /app/components/charts/line/Multi.tsx: -------------------------------------------------------------------------------- 1 | import { LineChart } from 'metrics-graphics' 2 | import { PropsWithChildren } from 'react' 3 | import Renderer from '../Renderer' 4 | import fakeUsers from '../../../data/fakeUsers2.json' 5 | import { formatCompact, formatDate } from '../../../helpers/format' 6 | 7 | const Multi: React.FC> = ({ children }) => ( 8 | 10 | new LineChart({ 11 | data: fakeUsers.map((fakeArray) => 12 | fakeArray.map((fakeEntry) => ({ 13 | ...fakeEntry, 14 | date: new Date(fakeEntry.date) 15 | })) 16 | ), 17 | width: 600, 18 | height: 200, 19 | target: ref as any, 20 | xAccessor: 'date', 21 | yAccessor: 'value', 22 | legend: ['Line 1', 'Line 2', 'Line 3'], 23 | tooltipFunction: (point) => `${formatDate(point.date)}: ${formatCompact(point.value)}` 24 | }) 25 | } 26 | > 27 | {children} 28 | 29 | ) 30 | 31 | export default Multi 32 | -------------------------------------------------------------------------------- /app/components/charts/line/Simple.tsx: -------------------------------------------------------------------------------- 1 | import { LineChart } from 'metrics-graphics' 2 | import { PropsWithChildren } from 'react' 3 | import Renderer from '../Renderer' 4 | import fakeUsers from '../../../data/fakeUsers1.json' 5 | import { formatCompact, formatDate } from '../../../helpers/format' 6 | 7 | const Simple: React.FC> = ({ children }) => ( 8 | 10 | new LineChart({ 11 | data: [fakeUsers.map(({ date, value }) => ({ date: new Date(date), value }))], 12 | width: 600, 13 | height: 200, 14 | yScale: { 15 | minValue: 0 16 | }, 17 | target: ref as any, 18 | brush: 'xy', 19 | area: true, 20 | xAccessor: 'date', 21 | yAccessor: 'value', 22 | tooltipFunction: (point) => `${formatDate(point.date)}: ${formatCompact(point.value)}` 23 | }) 24 | } 25 | > 26 | {children} 27 | 28 | ) 29 | 30 | export default Simple 31 | -------------------------------------------------------------------------------- /app/components/charts/scatter/Categories.tsx: -------------------------------------------------------------------------------- 1 | import { ScatterChart } from 'metrics-graphics' 2 | import { PropsWithChildren } from 'react' 3 | import Renderer from '../Renderer' 4 | import points1 from '../../../data/points1.json' 5 | import { formatDecimal } from '../../../helpers/format' 6 | 7 | const groupByArray = (xs: Array, key: string) => 8 | xs.reduce((rv, x) => { 9 | const v = x[key] 10 | const el = rv.find((r: any) => r && r.key === v) 11 | if (el) el.values.push(x) 12 | else rv.push({ key: v, values: [x] }) 13 | return rv 14 | }, []) 15 | const points2 = groupByArray(points1, 'v') 16 | 17 | const Categories: React.FC> = ({ children }) => ( 18 | 20 | new ScatterChart({ 21 | data: points2.map((x: any) => x.values), 22 | legend: points2.map((x: any) => x.key), 23 | width: 500, 24 | height: 200, 25 | xAccessor: 'x', 26 | yAccessor: 'y', 27 | yRug: true, 28 | target: ref as any, 29 | tooltipFunction: (point) => `${formatDecimal(point.x)} - ${formatDecimal(point.y)}` 30 | }) 31 | } 32 | > 33 | {children} 34 | 35 | ) 36 | 37 | export default Categories 38 | -------------------------------------------------------------------------------- /app/components/charts/scatter/Complex.tsx: -------------------------------------------------------------------------------- 1 | import { ScatterChart } from 'metrics-graphics' 2 | import { PropsWithChildren } from 'react' 3 | import Renderer from '../Renderer' 4 | import points1 from '../../../data/points1.json' 5 | import { formatDecimal } from '../../../helpers/format' 6 | 7 | const groupByArray = (xs: Array, key: string) => 8 | xs.reduce((rv, x) => { 9 | const v = x[key] 10 | const el = rv.find((r: any) => r && r.key === v) 11 | if (el) el.values.push(x) 12 | else rv.push({ key: v, values: [x] }) 13 | return rv 14 | }, []) 15 | const points2 = groupByArray(points1, 'v') 16 | 17 | const Complex: React.FC> = ({ children }) => ( 18 | 20 | new ScatterChart({ 21 | data: points2.map((x: any) => x.values), 22 | legend: points2.map((x: any) => x.key), 23 | width: 500, 24 | height: 200, 25 | target: ref as any, 26 | xAccessor: 'x', 27 | yAccessor: 'y', 28 | sizeAccessor: (x: any) => Math.abs(x.w) * 3, 29 | tooltipFunction: (point) => `${formatDecimal(point.x)} - ${formatDecimal(point.y)}: ${formatDecimal(point.w)}` 30 | }) 31 | } 32 | > 33 | {children} 34 | 35 | ) 36 | 37 | export default Complex 38 | -------------------------------------------------------------------------------- /app/components/charts/scatter/Simple.tsx: -------------------------------------------------------------------------------- 1 | import { ScatterChart } from 'metrics-graphics' 2 | import { PropsWithChildren } from 'react' 3 | import Renderer from '../Renderer' 4 | import points1 from '../../../data/points1.json' 5 | import { formatDecimal } from '../../../helpers/format' 6 | 7 | const Simple: React.FC> = ({ children }) => ( 8 | 10 | new ScatterChart({ 11 | data: [points1], 12 | width: 500, 13 | height: 200, 14 | target: ref as any, 15 | xAccessor: 'x', 16 | yAccessor: 'y', 17 | brush: 'xy', 18 | xRug: true, 19 | tooltipFunction: (point) => `${formatDecimal(point.x)} - ${formatDecimal(point.y)}` 20 | }) 21 | } 22 | > 23 | {children} 24 | 25 | ) 26 | 27 | export default Simple 28 | -------------------------------------------------------------------------------- /app/data/confidenceBand.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "value": -1.1618426259, 4 | "date": "2012-08-28", 5 | "l": -2.6017329022, 6 | "u": 0.2949717757 7 | }, 8 | { 9 | "value": -0.5828247293, 10 | "date": "2012-08-29", 11 | "l": -1.3166963635, 12 | "u": 0.1324086347 13 | }, 14 | { 15 | "value": -0.3790770636, 16 | "date": "2012-08-30", 17 | "l": -0.8712221305, 18 | "u": 0.0956413566 19 | }, 20 | { 21 | "value": -0.2792926002, 22 | "date": "2012-08-31", 23 | "l": -0.6541832008, 24 | "u": 0.0717120241 25 | }, 26 | { 27 | "value": -0.2461165469, 28 | "date": "2012-09-01", 29 | "l": -0.5222677907, 30 | "u": 0.0594188803 31 | }, 32 | { 33 | "value": -0.2017354137, 34 | "date": "2012-09-02", 35 | "l": -0.4434280535, 36 | "u": 0.0419213465 37 | }, 38 | { 39 | "value": -0.1457476871, 40 | "date": "2012-09-03", 41 | "l": -0.3543957712, 42 | "u": 0.0623761171 43 | }, 44 | { 45 | "value": -0.002610973, 46 | "date": "2012-09-04", 47 | "l": -0.3339911495, 48 | "u": 0.031286929 49 | }, 50 | { 51 | "value": -0.0080692734, 52 | "date": "2012-09-05", 53 | "l": -0.2951839941, 54 | "u": 0.0301762553 55 | }, 56 | { 57 | "value": -0.0296490933, 58 | "date": "2012-09-06", 59 | "l": -0.2964395801, 60 | "u": -0.0029821004 61 | }, 62 | { 63 | "value": 0.001317397, 64 | "date": "2012-09-07", 65 | "l": -0.2295443759, 66 | "u": 0.037903312 67 | }, 68 | { 69 | "value": -0.0117649838, 70 | "date": "2012-09-08", 71 | "l": -0.2226376418, 72 | "u": 0.0239720183 73 | }, 74 | { 75 | "value": 0.0059394263, 76 | "date": "2012-09-09", 77 | "l": -0.2020479849, 78 | "u": 0.0259489347 79 | }, 80 | { 81 | "value": -0.0115565898, 82 | "date": "2012-09-10", 83 | "l": -0.2042048037, 84 | "u": 0.0077863806 85 | }, 86 | { 87 | "value": 0.0041183019, 88 | "date": "2012-09-11", 89 | "l": -0.1837263172, 90 | "u": 0.0137898406 91 | }, 92 | { 93 | "value": 0.0353559544, 94 | "date": "2012-09-12", 95 | "l": -0.136610008, 96 | "u": 0.051403828 97 | }, 98 | { 99 | "value": 0.0070046011, 100 | "date": "2012-09-13", 101 | "l": -0.1569988647, 102 | "u": 0.0202266411 103 | }, 104 | { 105 | "value": -0.0004251807, 106 | "date": "2012-09-14", 107 | "l": -0.1410340292, 108 | "u": 0.0273410185 109 | }, 110 | { 111 | "value": -0.0035461023, 112 | "date": "2012-09-15", 113 | "l": -0.1438653689, 114 | "u": 0.0165445684 115 | }, 116 | { 117 | "value": 0.007797889, 118 | "date": "2012-09-16", 119 | "l": -0.1291975355, 120 | "u": 0.0232461153 121 | }, 122 | { 123 | "value": 0.0025402723, 124 | "date": "2012-09-17", 125 | "l": -0.133972479, 126 | "u": 0.0116753921 127 | }, 128 | { 129 | "value": -0.005317381, 130 | "date": "2012-09-18", 131 | "l": -0.1269266586, 132 | "u": 0.0129723291 133 | }, 134 | { 135 | "value": -0.0075841521, 136 | "date": "2012-09-19", 137 | "l": -0.1283478383, 138 | "u": 0.0056371616 139 | }, 140 | { 141 | "value": -0.0391388721, 142 | "date": "2012-09-20", 143 | "l": -0.1571172198, 144 | "u": -0.0311678828 145 | }, 146 | { 147 | "value": 0.0075430252, 148 | "date": "2012-09-21", 149 | "l": -0.1097354417, 150 | "u": 0.0141132062 151 | }, 152 | { 153 | "value": 0.1850284663, 154 | "date": "2012-09-22", 155 | "l": 0.0333682152, 156 | "u": 0.2140709422 157 | }, 158 | { 159 | "value": 0.076629596, 160 | "date": "2012-09-23", 161 | "l": -0.0068472967, 162 | "u": 0.1101280569 163 | }, 164 | { 165 | "value": -0.0314292271, 166 | "date": "2012-09-24", 167 | "l": -0.1074281762, 168 | "u": 0.0032669363 169 | }, 170 | { 171 | "value": -0.0232608674, 172 | "date": "2012-09-25", 173 | "l": -0.0905197842, 174 | "u": 0.0164250295 175 | }, 176 | { 177 | "value": -0.01968615, 178 | "date": "2012-09-26", 179 | "l": -0.084319856, 180 | "u": 0.0193319465 181 | }, 182 | { 183 | "value": -0.0310196816, 184 | "date": "2012-09-27", 185 | "l": -0.0914356781, 186 | "u": 0.0094436256 187 | }, 188 | { 189 | "value": -0.0758746967, 190 | "date": "2012-09-28", 191 | "l": -0.1169814745, 192 | "u": -0.019659551 193 | }, 194 | { 195 | "value": 0.0233974572, 196 | "date": "2012-09-29", 197 | "l": -0.0356839258, 198 | "u": 0.0610712506 199 | }, 200 | { 201 | "value": 0.011073579, 202 | "date": "2012-09-30", 203 | "l": -0.0558712863, 204 | "u": 0.0346160081 205 | }, 206 | { 207 | "value": -0.002094822, 208 | "date": "2012-10-01", 209 | "l": -0.0707143388, 210 | "u": 0.0152899266 211 | }, 212 | { 213 | "value": -0.1083707096, 214 | "date": "2012-10-02", 215 | "l": -0.1718101335, 216 | "u": -0.0886271057 217 | }, 218 | { 219 | "value": -0.1098258972, 220 | "date": "2012-10-03", 221 | "l": -0.1881274065, 222 | "u": -0.1072157972 223 | }, 224 | { 225 | "value": -0.0872970297, 226 | "date": "2012-10-04", 227 | "l": -0.1731903321, 228 | "u": -0.064381434 229 | }, 230 | { 231 | "value": -0.0761992047, 232 | "date": "2012-10-05", 233 | "l": -0.1770373817, 234 | "u": 0.100085727 235 | }, 236 | { 237 | "value": -0.0416654249, 238 | "date": "2012-10-06", 239 | "l": -0.1502479611, 240 | "u": 0.0751148102 241 | }, 242 | { 243 | "value": -0.0410128962, 244 | "date": "2012-10-07", 245 | "l": -0.1618694445, 246 | "u": 0.0881453482 247 | }, 248 | { 249 | "value": -0.0214289042, 250 | "date": "2012-10-08", 251 | "l": -0.1590852977, 252 | "u": 0.0871880288 253 | }, 254 | { 255 | "value": 0.2430880604, 256 | "date": "2012-10-09", 257 | "l": 0.063624221, 258 | "u": 0.2455101587 259 | }, 260 | { 261 | "value": 0.3472823479, 262 | "date": "2012-10-10", 263 | "l": 0.1553854927, 264 | "u": 0.3583991097 265 | }, 266 | { 267 | "value": 0.3360734074, 268 | "date": "2012-10-11", 269 | "l": 0.2055952772, 270 | "u": 0.3812162823 271 | }, 272 | { 273 | "value": -0.0463648355, 274 | "date": "2012-10-12", 275 | "l": -0.0626466998, 276 | "u": 0.0037342957 277 | }, 278 | { 279 | "value": -0.0867009379, 280 | "date": "2012-10-13", 281 | "l": -0.0867594055, 282 | "u": -0.0223791074 283 | }, 284 | { 285 | "value": -0.1288672826, 286 | "date": "2012-10-14", 287 | "l": -0.1161709129, 288 | "u": -0.0534789124 289 | }, 290 | { 291 | "value": -0.1474426821, 292 | "date": "2012-10-15", 293 | "l": -0.1559759048, 294 | "u": -0.0646995092 295 | }, 296 | { 297 | "value": -0.1502405066, 298 | "date": "2012-10-16", 299 | "l": -0.1604364638, 300 | "u": -0.0602562376 301 | }, 302 | { 303 | "value": -0.1203765529, 304 | "date": "2012-10-17", 305 | "l": -0.1569023195, 306 | "u": -0.0578129637 307 | }, 308 | { 309 | "value": -0.0649122919, 310 | "date": "2012-10-18", 311 | "l": -0.0782987564, 312 | "u": -0.0501999174 313 | }, 314 | { 315 | "value": -0.015525562, 316 | "date": "2012-10-19", 317 | "l": -0.1103873808, 318 | "u": -0.0132131311 319 | }, 320 | { 321 | "value": -0.006051357, 322 | "date": "2012-10-20", 323 | "l": -0.1089644497, 324 | "u": 0.0230384197 325 | }, 326 | { 327 | "value": 0.0003154213, 328 | "date": "2012-10-21", 329 | "l": -0.1073849227, 330 | "u": 0.0017290437 331 | }, 332 | { 333 | "value": -0.0063018298, 334 | "date": "2012-10-22", 335 | "l": -0.1120298155, 336 | "u": 0.0173284555 337 | }, 338 | { 339 | "value": -0.004294834, 340 | "date": "2012-10-23", 341 | "l": -0.1076841119, 342 | "u": 0.0547933965 343 | }, 344 | { 345 | "value": -0.0053400832, 346 | "date": "2012-10-24", 347 | "l": -0.1096991408, 348 | "u": 0.0560555803 349 | }, 350 | { 351 | "value": 0.0070057212, 352 | "date": "2012-10-25", 353 | "l": -0.0940613813, 354 | "u": 0.0425517607 355 | }, 356 | { 357 | "value": 0.0082121656, 358 | "date": "2012-10-26", 359 | "l": -0.0906810455, 360 | "u": 0.0396884383 361 | }, 362 | { 363 | "value": 0.0141422884, 364 | "date": "2012-10-27", 365 | "l": -0.0841305678, 366 | "u": 0.0340050012 367 | }, 368 | { 369 | "value": 0.0041613553, 370 | "date": "2012-10-28", 371 | "l": -0.0886723749, 372 | "u": 0.039426727 373 | }, 374 | { 375 | "value": -0.0013614287, 376 | "date": "2012-10-29", 377 | "l": -0.0923481608, 378 | "u": 0.0438725574 379 | }, 380 | { 381 | "value": -0.0052144933, 382 | "date": "2012-10-30", 383 | "l": -0.0937763043, 384 | "u": 0.0459998555 385 | }, 386 | { 387 | "value": 0.0078904741, 388 | "date": "2012-10-31", 389 | "l": -0.0807028001, 390 | "u": 0.0334824169 391 | }, 392 | { 393 | "value": 0.0099598702, 394 | "date": "2012-11-01", 395 | "l": -0.0740001323, 396 | "u": 0.0280264274 397 | }, 398 | { 399 | "value": 0.0001146029, 400 | "date": "2012-11-02", 401 | "l": -0.0820430294, 402 | "u": 0.0326771125 403 | }, 404 | { 405 | "value": 0.0047572651, 406 | "date": "2012-11-03", 407 | "l": -0.0754113825, 408 | "u": 0.0294912577 409 | }, 410 | { 411 | "value": 0.006204557, 412 | "date": "2012-11-04", 413 | "l": -0.0750627059, 414 | "u": 0.029693607 415 | }, 416 | { 417 | "value": 0.0115231406, 418 | "date": "2012-11-05", 419 | "l": -0.0663484142, 420 | "u": 0.0214084056 421 | }, 422 | { 423 | "value": -0.0032634994, 424 | "date": "2012-11-06", 425 | "l": -0.0793170451, 426 | "u": 0.0355159827 427 | }, 428 | { 429 | "value": -0.0108985452, 430 | "date": "2012-11-07", 431 | "l": -0.0846123893, 432 | "u": 0.0409797057 433 | }, 434 | { 435 | "value": -0.0092766813, 436 | "date": "2012-11-08", 437 | "l": -0.0802668328, 438 | "u": 0.0373886301 439 | }, 440 | { 441 | "value": 0.0095972086, 442 | "date": "2012-11-09", 443 | "l": -0.0623739694, 444 | "u": 0.0194918693 445 | }, 446 | { 447 | "value": -0.0111809358, 448 | "date": "2012-11-10", 449 | "l": -0.0819555908, 450 | "u": 0.038335749 451 | }, 452 | { 453 | "value": -0.0023572296, 454 | "date": "2012-11-11", 455 | "l": -0.0745443377, 456 | "u": 0.0306093592 457 | }, 458 | { 459 | "value": 0.0084213775, 460 | "date": "2012-11-12", 461 | "l": -0.0657707155, 462 | "u": 0.0227270619 463 | }, 464 | { 465 | "value": 0.0107446453, 466 | "date": "2012-11-13", 467 | "l": -0.0617995017, 468 | "u": 0.0196547867 469 | }, 470 | { 471 | "value": 0.009457792, 472 | "date": "2012-11-14", 473 | "l": -0.0597697849, 474 | "u": 0.0191832343 475 | }, 476 | { 477 | "value": 0.0031194779, 478 | "date": "2012-11-15", 479 | "l": -0.0589126783, 480 | "u": 0.0186409442 481 | }, 482 | { 483 | "value": -0.0115128213, 484 | "date": "2012-11-16", 485 | "l": -0.0767105447, 486 | "u": 0.0370292452 487 | }, 488 | { 489 | "value": 0.0058347339, 490 | "date": "2012-11-17", 491 | "l": -0.0592236472, 492 | "u": 0.0198181452 493 | }, 494 | { 495 | "value": -0.0235630436, 496 | "date": "2012-11-18", 497 | "l": -0.083529944, 498 | "u": 0.046280909 499 | }, 500 | { 501 | "value": -0.0479795964, 502 | "date": "2012-11-19", 503 | "l": -0.1086422529, 504 | "u": 0.0113044645 505 | }, 506 | { 507 | "value": -0.0218184359, 508 | "date": "2012-11-21", 509 | "l": -0.0881634878, 510 | "u": 0.0448568265 511 | }, 512 | { 513 | "value": -0.0071361172, 514 | "date": "2012-11-28", 515 | "l": -0.0807350229, 516 | "u": 0.0453599734 517 | }, 518 | { 519 | "value": -0.0151966912, 520 | "date": "2012-12-05", 521 | "l": -0.089995793, 522 | "u": 0.0558329569 523 | }, 524 | { 525 | "value": -0.0097784855, 526 | "date": "2012-12-12", 527 | "l": -0.089466481, 528 | "u": 0.0550191387 529 | }, 530 | { 531 | "value": -0.0095681495, 532 | "date": "2012-12-19", 533 | "l": -0.090513354, 534 | "u": 0.057073314 535 | }, 536 | { 537 | "value": -0.0034165915, 538 | "date": "2012-12-27", 539 | "l": -0.0907151292, 540 | "u": 0.0561479112 541 | }, 542 | { 543 | "value": 0.3297981389, 544 | "date": "2012-12-31", 545 | "l": 0.1537781522, 546 | "u": 0.3499473316 547 | } 548 | ] -------------------------------------------------------------------------------- /app/data/fakeUsers1.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "date": "2014-01-01", 4 | "value": 190000000 5 | }, 6 | { 7 | "date": "2014-01-02", 8 | "value": 190379978 9 | }, 10 | { 11 | "date": "2014-01-03", 12 | "value": 90493749 13 | }, 14 | { 15 | "date": "2014-01-04", 16 | "value": 190785250 17 | }, 18 | { 19 | "date": "2014-01-05", 20 | "value": 197391904 21 | }, 22 | { 23 | "date": "2014-01-06", 24 | "value": 191576838 25 | }, 26 | { 27 | "date": "2014-01-07", 28 | "value": 191413854 29 | }, 30 | { 31 | "date": "2014-01-08", 32 | "value": 142177211 33 | }, 34 | { 35 | "date": "2014-01-09", 36 | "value": 103762210 37 | }, 38 | { 39 | "date": "2014-01-10", 40 | "value": 144381072 41 | }, 42 | { 43 | "date": "2014-01-11", 44 | "value": 154352310 45 | }, 46 | { 47 | "date": "2014-01-12", 48 | "value": 165531790 49 | }, 50 | { 51 | "date": "2014-01-13", 52 | "value": 175748881 53 | }, 54 | { 55 | "date": "2014-01-14", 56 | "value": 187064037 57 | }, 58 | { 59 | "date": "2014-01-15", 60 | "value": 197520685 61 | }, 62 | { 63 | "date": "2014-01-16", 64 | "value": 210176418 65 | }, 66 | { 67 | "date": "2014-01-17", 68 | "value": 196122924 69 | }, 70 | { 71 | "date": "2014-01-18", 72 | "value": 157337480 73 | }, 74 | { 75 | "date": "2014-01-19", 76 | "value": 200258882 77 | }, 78 | { 79 | "date": "2014-01-20", 80 | "value": 186829538 81 | }, 82 | { 83 | "date": "2014-01-21", 84 | "value": 112456897 85 | }, 86 | { 87 | "date": "2014-01-22", 88 | "value": 114299711 89 | }, 90 | { 91 | "date": "2014-01-23", 92 | "value": 122759017 93 | }, 94 | { 95 | "date": "2014-01-24", 96 | "value": 203596183 97 | }, 98 | { 99 | "date": "2014-01-25", 100 | "value": 208107346 101 | }, 102 | { 103 | "date": "2014-01-26", 104 | "value": 196359852 105 | }, 106 | { 107 | "date": "2014-01-27", 108 | "value": 192570783 109 | }, 110 | { 111 | "date": "2014-01-28", 112 | "value": 177967768 113 | }, 114 | { 115 | "date": "2014-01-29", 116 | "value": 190632803 117 | }, 118 | { 119 | "date": "2014-01-30", 120 | "value": 203725316 121 | }, 122 | { 123 | "date": "2014-01-31", 124 | "value": 118226177 125 | }, 126 | { 127 | "date": "2014-02-01", 128 | "value": 210698669 129 | }, 130 | { 131 | "date": "2014-02-02", 132 | "value": 217640656 133 | }, 134 | { 135 | "date": "2014-02-03", 136 | "value": 216142362 137 | }, 138 | { 139 | "date": "2014-02-04", 140 | "value": 201410971 141 | }, 142 | { 143 | "date": "2014-02-05", 144 | "value": 196704289 145 | }, 146 | { 147 | "date": "2014-02-06", 148 | "value": 190436945 149 | }, 150 | { 151 | "date": "2014-02-07", 152 | "value": 178891686 153 | }, 154 | { 155 | "date": "2014-02-08", 156 | "value": 171613962 157 | }, 158 | { 159 | "date": "2014-02-09", 160 | "value": 107579773 161 | }, 162 | { 163 | "date": "2014-02-10", 164 | "value": 158677098 165 | }, 166 | { 167 | "date": "2014-02-11", 168 | "value": 147129977 169 | }, 170 | { 171 | "date": "2014-02-12", 172 | "value": 151561876 173 | }, 174 | { 175 | "date": "2014-02-13", 176 | "value": 151627421 177 | }, 178 | { 179 | "date": "2014-02-14", 180 | "value": 143543872 181 | }, 182 | { 183 | "date": "2014-02-15", 184 | "value": 136581057 185 | }, 186 | { 187 | "date": "2014-02-16", 188 | "value": 135560715 189 | }, 190 | { 191 | "date": "2014-02-17", 192 | "value": 122625263 193 | }, 194 | { 195 | "date": "2014-02-18", 196 | "value": 112091484 197 | }, 198 | { 199 | "date": "2014-02-19", 200 | "value": 98810329 201 | }, 202 | { 203 | "date": "2014-02-20", 204 | "value": 99882912 205 | }, 206 | { 207 | "date": "2014-02-21", 208 | "value": 94943095 209 | }, 210 | { 211 | "date": "2014-02-22", 212 | "value": 104875743 213 | }, 214 | { 215 | "date": "2014-02-23", 216 | "value": 116383678 217 | }, 218 | { 219 | "date": "2014-02-24", 220 | "value": 105028841 221 | }, 222 | { 223 | "date": "2014-02-25", 224 | "value": 123967310 225 | }, 226 | { 227 | "date": "2014-02-26", 228 | "value": 133167029 229 | }, 230 | { 231 | "date": "2014-02-27", 232 | "value": 128577263 233 | }, 234 | { 235 | "date": "2014-02-28", 236 | "value": 115836969 237 | }, 238 | { 239 | "date": "2014-03-01", 240 | "value": 119264529 241 | }, 242 | { 243 | "date": "2014-03-02", 244 | "value": 109363374 245 | }, 246 | { 247 | "date": "2014-03-03", 248 | "value": 113985628 249 | }, 250 | { 251 | "date": "2014-03-04", 252 | "value": 114650999 253 | }, 254 | { 255 | "date": "2014-03-05", 256 | "value": 110866108 257 | }, 258 | { 259 | "date": "2014-03-06", 260 | "value": 96473454 261 | }, 262 | { 263 | "date": "2014-03-07", 264 | "value": 84075886 265 | }, 266 | { 267 | "date": "2014-03-08", 268 | "value": 103568384 269 | }, 270 | { 271 | "date": "2014-03-09", 272 | "value": 101534883 273 | }, 274 | { 275 | "date": "2014-03-10", 276 | "value": 115825447 277 | }, 278 | { 279 | "date": "2014-03-11", 280 | "value": 126133916 281 | }, 282 | { 283 | "date": "2014-03-12", 284 | "value": 116502109 285 | }, 286 | { 287 | "date": "2014-03-13", 288 | "value": 80169411 289 | }, 290 | { 291 | "date": "2014-03-14", 292 | "value": 84296886 293 | }, 294 | { 295 | "date": "2014-03-15", 296 | "value": 86347399 297 | }, 298 | { 299 | "date": "2014-03-16", 300 | "value": 31483669 301 | }, 302 | { 303 | "date": "2014-03-17", 304 | "value": 142811333 305 | }, 306 | { 307 | "date": "2014-03-18", 308 | "value": 89675396 309 | }, 310 | { 311 | "date": "2014-03-19", 312 | "value": 115514483 313 | }, 314 | { 315 | "date": "2014-03-20", 316 | "value": 117630630 317 | }, 318 | { 319 | "date": "2014-03-21", 320 | "value": 122340239 321 | }, 322 | { 323 | "date": "2014-03-22", 324 | "value": 132349091 325 | }, 326 | { 327 | "date": "2014-03-23", 328 | "value": 125613305 329 | }, 330 | { 331 | "date": "2014-03-24", 332 | "value": 135592466 333 | }, 334 | { 335 | "date": "2014-03-25", 336 | "value": 123408762 337 | }, 338 | { 339 | "date": "2014-03-26", 340 | "value": 111991454 341 | }, 342 | { 343 | "date": "2014-03-27", 344 | "value": 116123955 345 | }, 346 | { 347 | "date": "2014-03-28", 348 | "value": 112817214 349 | }, 350 | { 351 | "date": "2014-03-29", 352 | "value": 113029590 353 | }, 354 | { 355 | "date": "2014-03-30", 356 | "value": 108753398 357 | }, 358 | { 359 | "date": "2014-03-31", 360 | "value": 99383763 361 | }, 362 | { 363 | "date": "2014-04-01", 364 | "value": 100151737 365 | }, 366 | { 367 | "date": "2014-04-02", 368 | "value": 94985209 369 | }, 370 | { 371 | "date": "2014-04-03", 372 | "value": 82913669 373 | }, 374 | { 375 | "date": "2014-04-04", 376 | "value": 78748268 377 | }, 378 | { 379 | "date": "2014-04-05", 380 | "value": 63829135 381 | }, 382 | { 383 | "date": "2014-04-06", 384 | "value": 78694727 385 | }, 386 | { 387 | "date": "2014-04-07", 388 | "value": 80868994 389 | }, 390 | { 391 | "date": "2014-04-08", 392 | "value": 93799013 393 | }, 394 | { 395 | "date": "2014-04-09", 396 | "value": 9042416 397 | }, 398 | { 399 | "date": "2014-04-10", 400 | "value": 97298692 401 | }, 402 | { 403 | "date": "2014-04-11", 404 | "value": 53353499 405 | }, 406 | { 407 | "date": "2014-04-12", 408 | "value": 71248129 409 | }, 410 | { 411 | "date": "2014-04-13", 412 | "value": 75253744 413 | }, 414 | { 415 | "date": "2014-04-14", 416 | "value": 68976648 417 | }, 418 | { 419 | "date": "2014-04-15", 420 | "value": 71002284 421 | }, 422 | { 423 | "date": "2014-04-16", 424 | "value": 75052401 425 | }, 426 | { 427 | "date": "2014-04-17", 428 | "value": 83894030 429 | }, 430 | { 431 | "date": "2014-04-18", 432 | "value": 50236528 433 | }, 434 | { 435 | "date": "2014-04-19", 436 | "value": 59739114 437 | }, 438 | { 439 | "date": "2014-04-20", 440 | "value": 56407136 441 | }, 442 | { 443 | "date": "2014-04-21", 444 | "value": 108323177 445 | }, 446 | { 447 | "date": "2014-04-22", 448 | "value": 101578914 449 | }, 450 | { 451 | "date": "2014-04-23", 452 | "value": 115877608 453 | }, 454 | { 455 | "date": "2014-04-24", 456 | "value": 132088857 457 | }, 458 | { 459 | "date": "2014-04-25", 460 | "value": 112071353 461 | }, 462 | { 463 | "date": "2014-04-26", 464 | "value": 81790062 465 | }, 466 | { 467 | "date": "2014-04-27", 468 | "value": 105003761 469 | }, 470 | { 471 | "date": "2014-04-28", 472 | "value": 100457727 473 | }, 474 | { 475 | "date": "2014-04-29", 476 | "value": 118253926 477 | }, 478 | { 479 | "date": "2014-04-30", 480 | "value": 67956992 481 | } 482 | ] -------------------------------------------------------------------------------- /app/data/missing.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "date": "2014-01-08", 4 | "value": 500 5 | }, 6 | { 7 | "date": "2014-01-09", 8 | "value": 500 9 | }, 10 | { 11 | "date": "2014-01-10", 12 | "value": 400 13 | }, 14 | { 15 | "date": "2014-01-11", 16 | "value": 500, 17 | "dead": true 18 | }, 19 | { 20 | "date": "2014-01-12", 21 | "value": 400 22 | }, 23 | { 24 | "date": "2014-01-13", 25 | "value": 430 26 | }, 27 | { 28 | "date": "2014-01-14", 29 | "value": 410 30 | }, 31 | { 32 | "date": "2014-01-15", 33 | "value": 200, 34 | "dead": true 35 | }, 36 | { 37 | "date": "2014-01-16", 38 | "value": 500 39 | }, 40 | { 41 | "date": "2014-01-17", 42 | "value": 100 43 | }, 44 | { 45 | "date": "2014-01-18", 46 | "value": 30 47 | }, 48 | { 49 | "date": "2014-01-19", 50 | "value": 300 51 | }, 52 | { 53 | "date": "2014-01-20", 54 | "value": 200 55 | } 56 | ] -------------------------------------------------------------------------------- /app/data/points1.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "u": "cat_10", 4 | "w": 1.243871075541485, 5 | "v": "other", 6 | "y": 211.80029085913867, 7 | "x": 156.56698521169255, 8 | "z": 1.2592730408041488 9 | }, 10 | { 11 | "u": "cat_9", 12 | "w": 0.18317089873596637, 13 | "v": "other", 14 | "y": 196.93116746887526, 15 | "x": 182.9226627644747, 16 | "z": 1.5420852412869692 17 | }, 18 | { 19 | "u": "cat_11", 20 | "w": 1.6440310398846552, 21 | "v": "other", 22 | "y": 198.15275935129918, 23 | "x": 180.46587284398524, 24 | "z": 1.0607952297441374 25 | }, 26 | { 27 | "u": "cat_7", 28 | "w": -0.7500939816017782, 29 | "v": "other", 30 | "y": 146.31202991730444, 31 | "x": 102.35636312891461, 32 | "z": 0.30940991631448456 33 | }, 34 | { 35 | "u": "cat_10", 36 | "w": 0.46988959503208527, 37 | "v": "other", 38 | "y": 118.66304545624911, 39 | "x": 175.8035980586206, 40 | "z": 1.589820005614669 41 | }, 42 | { 43 | "u": "cat_3", 44 | "w": 2.4359184943127667, 45 | "v": "other", 46 | "y": 214.0123663650676, 47 | "x": 146.24883586964125, 48 | "z": -0.8716888194991463 49 | }, 50 | { 51 | "u": "cat_8", 52 | "w": -1.2794477449179462, 53 | "v": "other", 54 | "y": 165.45000531931404, 55 | "x": 138.83167553877533, 56 | "z": 0.365531221729956 57 | }, 58 | { 59 | "u": "other", 60 | "w": 0.6533051004485967, 61 | "v": "cat_0", 62 | "y": 133.99465910929834, 63 | "x": 119.39730303633817, 64 | "z": 1.270978774871001 65 | }, 66 | { 67 | "u": "cat_13", 68 | "w": -0.4527608603464446, 69 | "v": "other", 70 | "y": 262.89541671133776, 71 | "x": 209.42459012646566, 72 | "z": 0.15317285949553272 73 | }, 74 | { 75 | "u": "cat_12", 76 | "w": 0.06915365942882012, 77 | "v": "cat_0", 78 | "y": 139.0123425273913, 79 | "x": 140.27809963809628, 80 | "z": 1.0850789531923752 81 | }, 82 | { 83 | "u": "cat_5", 84 | "w": 0.18422289588498686, 85 | "v": "other", 86 | "y": 123.5696526089444, 87 | "x": 178.47540577185424, 88 | "z": 0.40291991519951875 89 | }, 90 | { 91 | "u": "cat_4", 92 | "w": -0.4338210953783319, 93 | "v": "cat_1", 94 | "y": 161.93728020049016, 95 | "x": 166.16502625602917, 96 | "z": 0.5093424182003234 97 | }, 98 | { 99 | "u": "cat_7", 100 | "w": 1.40583885575462, 101 | "v": "other", 102 | "y": 155.63962440916566, 103 | "x": 99.85204107456539, 104 | "z": 1.2530442449107233 105 | }, 106 | { 107 | "u": "cat_10", 108 | "w": -0.4275099099676807, 109 | "v": "other", 110 | "y": 176.85285727542032, 111 | "x": 161.42701652535786, 112 | "z": 0.8230669881454445 113 | }, 114 | { 115 | "u": "cat_4", 116 | "w": 1.463656501437303, 117 | "v": "cat_1", 118 | "y": 246.6642430026098, 119 | "x": 73.48368170456627, 120 | "z": 1.0052929735375302 121 | }, 122 | { 123 | "u": "cat_11", 124 | "w": 0.013089161287933138, 125 | "v": "other", 126 | "y": 156.36354536529558, 127 | "x": 243.2026715373837, 128 | "z": 0.39667694750274274 129 | }, 130 | { 131 | "u": "cat_12", 132 | "w": 0.36730326802039404, 133 | "v": "other", 134 | "y": 156.71697413402487, 135 | "x": 148.27450037397765, 136 | "z": -1.4105618351561287 137 | }, 138 | { 139 | "u": "cat_14", 140 | "w": -0.06953747774947772, 141 | "v": "other", 142 | "y": 141.22433267951084, 143 | "x": 69.06616491304716, 144 | "z": 0.475924622911404 145 | }, 146 | { 147 | "u": "other", 148 | "w": 0.517415189557197, 149 | "v": "other", 150 | "y": 115.95680666029197, 151 | "x": 153.0898637311175, 152 | "z": 0.9443947812526814 153 | }, 154 | { 155 | "u": "cat_3", 156 | "w": 0.5670223685982718, 157 | "v": "other", 158 | "y": 227.27960266143467, 159 | "x": 153.22490080491232, 160 | "z": 1.32067405076989 161 | }, 162 | { 163 | "u": "cat_7", 164 | "w": 0.5376290623874869, 165 | "v": "other", 166 | "y": 260.9119300068415, 167 | "x": 158.76997596111525, 168 | "z": 0.26196386810779426 169 | }, 170 | { 171 | "u": "cat_8", 172 | "w": 2.168516664994767, 173 | "v": "other", 174 | "y": 170.77570545745968, 175 | "x": 135.65940169018805, 176 | "z": 0.9162849263421061 177 | }, 178 | { 179 | "u": "cat_5", 180 | "w": 2.311955371948698, 181 | "v": "other", 182 | "y": 243.37468134084048, 183 | "x": 20.66820288532847, 184 | "z": -0.322645627898664 185 | }, 186 | { 187 | "u": "cat_4", 188 | "w": 1.8314759939943595, 189 | "v": "other", 190 | "y": 214.1727802647768, 191 | "x": 216.61746891013505, 192 | "z": 2.4147498286441165 193 | }, 194 | { 195 | "u": "other", 196 | "w": 0.8365279958031904, 197 | "v": "other", 198 | "y": 100.90575192733515, 199 | "x": 151.6306779403549, 200 | "z": 0.8606212265228668 201 | }, 202 | { 203 | "u": "cat_13", 204 | "w": -0.26452281419648993, 205 | "v": "cat_0", 206 | "y": 194.59598730955793, 207 | "x": 129.361227705571, 208 | "z": 1.0322894496099724 209 | }, 210 | { 211 | "u": "cat_13", 212 | "w": 2.6043512596594365, 213 | "v": "other", 214 | "y": 213.8128177957785, 215 | "x": 194.6414121394208, 216 | "z": 3.280204884184917 217 | }, 218 | { 219 | "u": "cat_6", 220 | "w": 1.6310983407120137, 221 | "v": "other", 222 | "y": 163.2658097189387, 223 | "x": 187.70506769054072, 224 | "z": 1.3488889788600984 225 | }, 226 | { 227 | "u": "cat_8", 228 | "w": 0.6487974580274094, 229 | "v": "other", 230 | "y": 194.3209257079902, 231 | "x": 192.10028088047798, 232 | "z": -0.07036372606337338 233 | }, 234 | { 235 | "u": "cat_10", 236 | "w": 1.3927144738318111, 237 | "v": "other", 238 | "y": 161.3486689563104, 239 | "x": 86.73894049392862, 240 | "z": 0.1537876062955914 241 | }, 242 | { 243 | "u": "cat_9", 244 | "w": -0.3771368838057665, 245 | "v": "other", 246 | "y": 182.55687300867896, 247 | "x": 201.1112292465731, 248 | "z": -0.058098755927163515 249 | }, 250 | { 251 | "u": "cat_0", 252 | "w": -0.6345913552330389, 253 | "v": "other", 254 | "y": 165.80314397714827, 255 | "x": 206.7182591446124, 256 | "z": 1.4845564495064427 257 | }, 258 | { 259 | "u": "cat_0", 260 | "w": 0.6855019579009463, 261 | "v": "cat_1", 262 | "y": 159.66118937446728, 263 | "x": 192.39649008863876, 264 | "z": 0.055873071009385766 265 | }, 266 | { 267 | "u": "cat_6", 268 | "w": 2.964882131756106, 269 | "v": "other", 270 | "y": 241.84769149238159, 271 | "x": 111.35023940160411, 272 | "z": 0.167414971004892 273 | }, 274 | { 275 | "u": "other", 276 | "w": -0.4506378010737888, 277 | "v": "other", 278 | "y": 90.4639669582562, 279 | "x": 158.90001697899683, 280 | "z": 1.1696787253371819 281 | }, 282 | { 283 | "u": "cat_0", 284 | "w": 1.2656151949587624, 285 | "v": "other", 286 | "y": 183.02945581640645, 287 | "x": 180.4213167593254, 288 | "z": 2.0539930046863035 289 | }, 290 | { 291 | "u": "cat_13", 292 | "w": 1.228801369090003, 293 | "v": "other", 294 | "y": 277.13369884311936, 295 | "x": 80.15664163346004, 296 | "z": 2.2937904251948638 297 | }, 298 | { 299 | "u": "cat_5", 300 | "w": 2.0896213510871937, 301 | "v": "other", 302 | "y": 163.72382240169802, 303 | "x": 215.41216187620037, 304 | "z": 2.553028570447125 305 | }, 306 | { 307 | "u": "cat_15", 308 | "w": 1.0981586322470924, 309 | "v": "other", 310 | "y": 109.2838491053391, 311 | "x": 184.09570421897956, 312 | "z": 0.44759620112619647 313 | }, 314 | { 315 | "u": "other", 316 | "w": 1.3457067544720736, 317 | "v": "other", 318 | "y": 171.16629782677862, 319 | "x": 213.12963803437316, 320 | "z": 0.7270493828061404 321 | }, 322 | { 323 | "u": "cat_13", 324 | "w": -0.7076617244518462, 325 | "v": "other", 326 | "y": 144.05587078713071, 327 | "x": 184.28906651578978, 328 | "z": 1.382134562867984 329 | }, 330 | { 331 | "u": "cat_0", 332 | "w": -0.2800595737017382, 333 | "v": "other", 334 | "y": 193.27473606725474, 335 | "x": 154.94633537134789, 336 | "z": -0.33791235399367947 337 | }, 338 | { 339 | "u": "cat_14", 340 | "w": 0.4456793621586681, 341 | "v": "other", 342 | "y": 125.1308061934427, 343 | "x": 123.90267987241343, 344 | "z": 2.3230978938654645 345 | }, 346 | { 347 | "u": "cat_5", 348 | "w": 3.075103103171694, 349 | "v": "cat_0", 350 | "y": 154.8755877624397, 351 | "x": 61.020238047163375, 352 | "z": 1.6674654330131888 353 | }, 354 | { 355 | "u": "other", 356 | "w": 0.16319305741807733, 357 | "v": "cat_0", 358 | "y": 114.08645682777497, 359 | "x": 225.28654361195518, 360 | "z": 0.4242182000961613 361 | }, 362 | { 363 | "u": "cat_6", 364 | "w": 1.9417162589422323, 365 | "v": "other", 366 | "y": 185.28537858495014, 367 | "x": 155.5736376536581, 368 | "z": 1.1747492195973144 369 | }, 370 | { 371 | "u": "cat_4", 372 | "w": 1.155787735870216, 373 | "v": "cat_1", 374 | "y": 183.24930292147863, 375 | "x": 115.40643586463635, 376 | "z": 0.19242660628339903 377 | }, 378 | { 379 | "u": "other", 380 | "w": 0.6070329867687532, 381 | "v": "other", 382 | "y": 57.398818613660296, 383 | "x": 219.87611544574744, 384 | "z": 1.1095208041769122 385 | }, 386 | { 387 | "u": "cat_6", 388 | "w": 0.4083905726447342, 389 | "v": "other", 390 | "y": 175.40387812166918, 391 | "x": 224.68335734038368, 392 | "z": -0.42207104629857617 393 | }, 394 | { 395 | "u": "cat_5", 396 | "w": 0.6585907631358738, 397 | "v": "cat_1", 398 | "y": 124.05051069396585, 399 | "x": 127.07766432478591, 400 | "z": 0.1354114111502046 401 | }, 402 | { 403 | "u": "cat_10", 404 | "w": 1.734882212854999, 405 | "v": "cat_1", 406 | "y": 143.78518104207086, 407 | "x": 198.75776145611215, 408 | "z": -0.12283925137929064 409 | }, 410 | { 411 | "u": "other", 412 | "w": 0.20503963437937356, 413 | "v": "other", 414 | "y": 151.75327292745928, 415 | "x": 148.9695770940796, 416 | "z": 0.564428194218838 417 | }, 418 | { 419 | "u": "cat_15", 420 | "w": 1.8574692523422356, 421 | "v": "other", 422 | "y": 306.0898140884456, 423 | "x": 147.43144660079602, 424 | "z": -0.22413411708624853 425 | }, 426 | { 427 | "u": "cat_4", 428 | "w": 1.1062688896220425, 429 | "v": "other", 430 | "y": 212.44397654702615, 431 | "x": 195.86829581464062, 432 | "z": 0.7453160939357952 433 | }, 434 | { 435 | "u": "cat_15", 436 | "w": -0.27774340110996487, 437 | "v": "cat_0", 438 | "y": 122.63707978879633, 439 | "x": 95.64182307323996, 440 | "z": 0.6430476399500114 441 | }, 442 | { 443 | "u": "cat_11", 444 | "w": 1.693891428609414, 445 | "v": "other", 446 | "y": 161.59041244043587, 447 | "x": 64.83892325218056, 448 | "z": 0.6409468598992659 449 | }, 450 | { 451 | "u": "cat_0", 452 | "w": 1.8192638452039414, 453 | "v": "cat_0", 454 | "y": 203.44169192711857, 455 | "x": 175.9035258508178, 456 | "z": 1.0101903553514529 457 | }, 458 | { 459 | "u": "cat_10", 460 | "w": 1.6693901234237167, 461 | "v": "other", 462 | "y": 266.02281722067806, 463 | "x": 193.7229721388815, 464 | "z": 1.3287927803604402 465 | }, 466 | { 467 | "u": "cat_2", 468 | "w": 1.6276382654036277, 469 | "v": "cat_0", 470 | "y": 174.54476940927518, 471 | "x": 85.70090208863968, 472 | "z": 1.3166929052154481 473 | }, 474 | { 475 | "u": "cat_4", 476 | "w": 0.893609545984623, 477 | "v": "other", 478 | "y": 135.20127141724655, 479 | "x": 161.0852992807473, 480 | "z": 0.7423369081120028 481 | }, 482 | { 483 | "u": "cat_8", 484 | "w": -0.25726080091728387, 485 | "v": "other", 486 | "y": 87.98074634203813, 487 | "x": 194.6466767641028, 488 | "z": 1.171709779521613 489 | }, 490 | { 491 | "u": "cat_6", 492 | "w": 1.4386749809763226, 493 | "v": "other", 494 | "y": 154.56889252680085, 495 | "x": 137.16494490842896, 496 | "z": 1.8319953052292166 497 | }, 498 | { 499 | "u": "cat_1", 500 | "w": 1.9751827147081238, 501 | "v": "other", 502 | "y": 150.52324196509258, 503 | "x": 51.69078624012406, 504 | "z": -0.23529144151246717 505 | }, 506 | { 507 | "u": "cat_14", 508 | "w": 1.3843184328271165, 509 | "v": "other", 510 | "y": 160.51323171649904, 511 | "x": 169.7361988952806, 512 | "z": 1.3970600816965812 513 | }, 514 | { 515 | "u": "cat_15", 516 | "w": -0.475868599212677, 517 | "v": "other", 518 | "y": 132.27225542990598, 519 | "x": 103.56729490389614, 520 | "z": 0.4512210085364755 521 | }, 522 | { 523 | "u": "cat_4", 524 | "w": 1.2081442447975173, 525 | "v": "cat_0", 526 | "y": 56.102440394233255, 527 | "x": 124.56829572524175, 528 | "z": 1.2994110022657026 529 | }, 530 | { 531 | "u": "cat_13", 532 | "w": 1.736143244910195, 533 | "v": "cat_0", 534 | "y": 212.5650565277553, 535 | "x": 140.4278102663895, 536 | "z": 2.3771380059458744 537 | }, 538 | { 539 | "u": "cat_14", 540 | "w": -0.461785512082463, 541 | "v": "cat_1", 542 | "y": 101.5707842719089, 543 | "x": 196.16957370342342, 544 | "z": 2.0950787429883846 545 | }, 546 | { 547 | "u": "cat_5", 548 | "w": 1.3260330340794257, 549 | "v": "cat_1", 550 | "y": 195.74556071781365, 551 | "x": 36.939138857293585, 552 | "z": -0.05847270264086335 553 | }, 554 | { 555 | "u": "cat_9", 556 | "w": 0.3220908087517964, 557 | "v": "other", 558 | "y": 221.78294482102933, 559 | "x": 74.76586558666337, 560 | "z": -0.05817419869022866 561 | }, 562 | { 563 | "u": "cat_5", 564 | "w": 0.8170251870816604, 565 | "v": "other", 566 | "y": 179.17790605043118, 567 | "x": 232.48747575985655, 568 | "z": 1.7029885149441673 569 | }, 570 | { 571 | "u": "cat_3", 572 | "w": 0.2659811986182221, 573 | "v": "cat_1", 574 | "y": 192.81824199631728, 575 | "x": 140.710358385573, 576 | "z": 1.5793646916427102 577 | }, 578 | { 579 | "u": "cat_8", 580 | "w": 1.2271687489820546, 581 | "v": "other", 582 | "y": 111.59013545108343, 583 | "x": 101.3489243773156, 584 | "z": 3.21128318091031 585 | }, 586 | { 587 | "u": "cat_13", 588 | "w": 1.3499209090064648, 589 | "v": "other", 590 | "y": 91.89209852687142, 591 | "x": 154.6035518322702, 592 | "z": 0.6817267324899431 593 | }, 594 | { 595 | "u": "cat_0", 596 | "w": 2.159235435849202, 597 | "v": "other", 598 | "y": 158.63631854616588, 599 | "x": 120.35802170671863, 600 | "z": 0.6067162231097979 601 | }, 602 | { 603 | "u": "cat_14", 604 | "w": 0.8523794752014957, 605 | "v": "other", 606 | "y": 76.05793322732642, 607 | "x": 138.4115475783541, 608 | "z": 0.2967991752860232 609 | }, 610 | { 611 | "u": "cat_12", 612 | "w": 1.0558373735944235, 613 | "v": "other", 614 | "y": 124.74002651703651, 615 | "x": 157.02907657551447, 616 | "z": -0.025374565392791038 617 | }, 618 | { 619 | "u": "cat_0", 620 | "w": 1.3648422432834806, 621 | "v": "cat_1", 622 | "y": 203.82847317695288, 623 | "x": 152.02003088030492, 624 | "z": -0.4421819240850271 625 | }, 626 | { 627 | "u": "cat_4", 628 | "w": 0.8156055831409262, 629 | "v": "other", 630 | "y": 95.97296459484079, 631 | "x": 78.51455593195435, 632 | "z": 1.1994133990162583 633 | }, 634 | { 635 | "u": "cat_3", 636 | "w": 0.727605537039985, 637 | "v": "other", 638 | "y": 168.70196209212494, 639 | "x": 212.41730028904536, 640 | "z": 0.6997491454171789 641 | }, 642 | { 643 | "u": "cat_6", 644 | "w": 1.262490286926595, 645 | "v": "other", 646 | "y": 134.65200754480446, 647 | "x": 42.54634367887225, 648 | "z": 0.5177600909542267 649 | }, 650 | { 651 | "u": "cat_8", 652 | "w": 1.4837053114286998, 653 | "v": "cat_0", 654 | "y": 112.28208956177541, 655 | "x": 193.95863032046006, 656 | "z": 1.0019859410064678 657 | }, 658 | { 659 | "u": "cat_8", 660 | "w": -0.16895357221403362, 661 | "v": "cat_0", 662 | "y": 192.98985884826888, 663 | "x": 144.96182365337268, 664 | "z": 1.4190462447457899 665 | }, 666 | { 667 | "u": "cat_16", 668 | "w": 1.0468806482899717, 669 | "v": "cat_0", 670 | "y": 152.86277692393685, 671 | "x": 137.2022037296142, 672 | "z": 0.4593974450208086 673 | }, 674 | { 675 | "u": "cat_6", 676 | "w": 2.462089222004976, 677 | "v": "cat_1", 678 | "y": 178.5621123552027, 679 | "x": 142.01082685227874, 680 | "z": 0.580699096900331 681 | }, 682 | { 683 | "u": "other", 684 | "w": -0.9097490420343397, 685 | "v": "other", 686 | "y": 137.21823856264854, 687 | "x": 94.98376759227347, 688 | "z": 2.118286289288478 689 | }, 690 | { 691 | "u": "cat_5", 692 | "w": 0.6155123359814616, 693 | "v": "other", 694 | "y": 52.94291552982141, 695 | "x": 85.57431217669814, 696 | "z": -0.09704681018895633 697 | }, 698 | { 699 | "u": "cat_11", 700 | "w": 1.9468348457441005, 701 | "v": "other", 702 | "y": 243.07706417763518, 703 | "x": 163.35061475046334, 704 | "z": 1.178015608927323 705 | }, 706 | { 707 | "u": "cat_13", 708 | "w": -0.11396040656336393, 709 | "v": "other", 710 | "y": 102.69550510842956, 711 | "x": 105.80547059051209, 712 | "z": 1.7309952065474825 713 | }, 714 | { 715 | "u": "cat_0", 716 | "w": 1.0201819734167836, 717 | "v": "other", 718 | "y": 226.24093931458543, 719 | "x": 128.26413056781655, 720 | "z": 0.602354545792653 721 | }, 722 | { 723 | "u": "cat_9", 724 | "w": 0.41269060095932775, 725 | "v": "other", 726 | "y": 22.06560060765568, 727 | "x": 169.41836118747312, 728 | "z": 0.6422396319927962 729 | }, 730 | { 731 | "u": "cat_13", 732 | "w": 1.1423761297271733, 733 | "v": "other", 734 | "y": 122.16006750791158, 735 | "x": 64.32267243426844, 736 | "z": 0.5677725122287971 737 | }, 738 | { 739 | "u": "cat_0", 740 | "w": -0.4940883354815302, 741 | "v": "other", 742 | "y": 146.45339448820994, 743 | "x": 172.009924725858, 744 | "z": 0.5733149663059203 745 | }, 746 | { 747 | "u": "cat_12", 748 | "w": 2.0427267231236836, 749 | "v": "other", 750 | "y": 105.09233739863171, 751 | "x": 151.8604441328228, 752 | "z": -1.0417341971519445 753 | }, 754 | { 755 | "u": "cat_0", 756 | "w": -0.06844772614894712, 757 | "v": "cat_0", 758 | "y": 208.74646965359665, 759 | "x": 211.0589178490544, 760 | "z": 3.033892234816319 761 | }, 762 | { 763 | "u": "cat_10", 764 | "w": 0.1407531030532979, 765 | "v": "other", 766 | "y": 212.1295737404453, 767 | "x": 157.62156278215423, 768 | "z": -0.17802781876760476 769 | }, 770 | { 771 | "u": "cat_7", 772 | "w": 0.654514275645782, 773 | "v": "cat_1", 774 | "y": 127.45107550739947, 775 | "x": 94.87901525572023, 776 | "z": 1.169027403658316 777 | }, 778 | { 779 | "u": "cat_15", 780 | "w": 2.7361843748627708, 781 | "v": "other", 782 | "y": 184.8888620082486, 783 | "x": 116.81017887336674, 784 | "z": -0.5196787907040106 785 | }, 786 | { 787 | "u": "other", 788 | "w": 0.7134526835944346, 789 | "v": "other", 790 | "y": 169.9779808978828, 791 | "x": 108.48468655071922, 792 | "z": 2.4985146493801524 793 | }, 794 | { 795 | "u": "cat_1", 796 | "w": 2.0017599837707474, 797 | "v": "other", 798 | "y": 193.69331303548242, 799 | "x": 161.953677145996, 800 | "z": 0.9928254618909004 801 | } 802 | ] -------------------------------------------------------------------------------- /app/data/ufoSightings.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "year": "1945", 4 | "sightings": 6 5 | }, 6 | { 7 | "year": "1946", 8 | "sightings": 8 9 | }, 10 | { 11 | "year": "1947", 12 | "sightings": 34 13 | }, 14 | { 15 | "year": "1948", 16 | "sightings": 8 17 | }, 18 | { 19 | "year": "1949", 20 | "sightings": 15 21 | }, 22 | { 23 | "year": "1950", 24 | "sightings": 25 25 | }, 26 | { 27 | "year": "1951", 28 | "sightings": 20 29 | }, 30 | { 31 | "year": "1952", 32 | "sightings": 48 33 | }, 34 | { 35 | "year": "1953", 36 | "sightings": 34 37 | }, 38 | { 39 | "year": "1954", 40 | "sightings": 50 41 | }, 42 | { 43 | "year": "1955", 44 | "sightings": 31 45 | }, 46 | { 47 | "year": "1956", 48 | "sightings": 38 49 | }, 50 | { 51 | "year": "1957", 52 | "sightings": 67 53 | }, 54 | { 55 | "year": "1958", 56 | "sightings": 40 57 | }, 58 | { 59 | "year": "1959", 60 | "sightings": 47 61 | }, 62 | { 63 | "year": "1960", 64 | "sightings": 64 65 | }, 66 | { 67 | "year": "1961", 68 | "sightings": 39 69 | }, 70 | { 71 | "year": "1962", 72 | "sightings": 55 73 | }, 74 | { 75 | "year": "1963", 76 | "sightings": 75 77 | }, 78 | { 79 | "year": "1964", 80 | "sightings": 77 81 | }, 82 | { 83 | "year": "1965", 84 | "sightings": 167 85 | }, 86 | { 87 | "year": "1966", 88 | "sightings": 169 89 | }, 90 | { 91 | "year": "1967", 92 | "sightings": 178 93 | }, 94 | { 95 | "year": "1968", 96 | "sightings": 183 97 | }, 98 | { 99 | "year": "1969", 100 | "sightings": 138 101 | }, 102 | { 103 | "year": "1970", 104 | "sightings": 126 105 | }, 106 | { 107 | "year": "1971", 108 | "sightings": 110 109 | }, 110 | { 111 | "year": "1972", 112 | "sightings": 146 113 | }, 114 | { 115 | "year": "1973", 116 | "sightings": 209 117 | }, 118 | { 119 | "year": "1974", 120 | "sightings": 241 121 | }, 122 | { 123 | "year": "1975", 124 | "sightings": 279 125 | }, 126 | { 127 | "year": "1976", 128 | "sightings": 246 129 | }, 130 | { 131 | "year": "1977", 132 | "sightings": 239 133 | }, 134 | { 135 | "year": "1978", 136 | "sightings": 301 137 | }, 138 | { 139 | "year": "1979", 140 | "sightings": 221 141 | }, 142 | { 143 | "year": "1980", 144 | "sightings": 211 145 | }, 146 | { 147 | "year": "1981", 148 | "sightings": 146 149 | }, 150 | { 151 | "year": "1982", 152 | "sightings": 182 153 | }, 154 | { 155 | "year": "1983", 156 | "sightings": 132 157 | }, 158 | { 159 | "year": "1984", 160 | "sightings": 172 161 | }, 162 | { 163 | "year": "1985", 164 | "sightings": 192 165 | }, 166 | { 167 | "year": "1986", 168 | "sightings": 173 169 | }, 170 | { 171 | "year": "1987", 172 | "sightings": 193 173 | }, 174 | { 175 | "year": "1988", 176 | "sightings": 203 177 | }, 178 | { 179 | "year": "1989", 180 | "sightings": 220 181 | }, 182 | { 183 | "year": "1990", 184 | "sightings": 217 185 | }, 186 | { 187 | "year": "1991", 188 | "sightings": 210 189 | }, 190 | { 191 | "year": "1992", 192 | "sightings": 228 193 | }, 194 | { 195 | "year": "1993", 196 | "sightings": 285 197 | }, 198 | { 199 | "year": "1994", 200 | "sightings": 381 201 | }, 202 | { 203 | "year": "1995", 204 | "sightings": 1336 205 | }, 206 | { 207 | "year": "1996", 208 | "sightings": 862 209 | }, 210 | { 211 | "year": "1997", 212 | "sightings": 1248 213 | }, 214 | { 215 | "year": "1998", 216 | "sightings": 1812 217 | }, 218 | { 219 | "year": "1999", 220 | "sightings": 2906 221 | }, 222 | { 223 | "year": "2000", 224 | "sightings": 2780 225 | }, 226 | { 227 | "year": "2001", 228 | "sightings": 3105 229 | }, 230 | { 231 | "year": "2002", 232 | "sightings": 3176 233 | }, 234 | { 235 | "year": "2003", 236 | "sightings": 3896 237 | }, 238 | { 239 | "year": "2004", 240 | "sightings": 4208 241 | }, 242 | { 243 | "year": "2005", 244 | "sightings": 3996 245 | }, 246 | { 247 | "year": "2006", 248 | "sightings": 3590 249 | }, 250 | { 251 | "year": "2007", 252 | "sightings": 4195 253 | }, 254 | { 255 | "year": "2008", 256 | "sightings": 4705 257 | }, 258 | { 259 | "year": "2009", 260 | "sightings": 4297 261 | }, 262 | { 263 | "year": "2010", 264 | "sightings": 2531 265 | } 266 | ] -------------------------------------------------------------------------------- /app/helpers/format.ts: -------------------------------------------------------------------------------- 1 | const percent = new Intl.NumberFormat(undefined, { 2 | style: 'percent', 3 | minimumFractionDigits: 2, 4 | maximumFractionDigits: 2 5 | }) 6 | 7 | const compact = new Intl.NumberFormat(undefined, { 8 | notation: 'compact' 9 | }) 10 | 11 | const decimal = new Intl.NumberFormat(undefined, { 12 | minimumFractionDigits: 2, 13 | maximumFractionDigits: 2 14 | }) 15 | 16 | export const formatDecimal = (number: number) => decimal.format(number) 17 | 18 | export const formatDate = (date: Date) => date.toISOString().substring(0, 10) 19 | 20 | export const formatPercent = (number: number) => percent.format(number) 21 | 22 | export const formatCompact = (number: number) => compact.format(number) 23 | -------------------------------------------------------------------------------- /app/next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/basic-features/typescript for more information. 6 | -------------------------------------------------------------------------------- /app/next.config.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-var-requires */ 2 | /** @type {import('next').NextConfig} */ 3 | 4 | const withMDX = require('@next/mdx')({ 5 | extension: /\.mdx?$/, 6 | options: { 7 | remarkPlugins: [], 8 | rehypePlugins: [require('@mapbox/rehype-prism')] 9 | // If you use `MDXProvider`, uncomment the following line. 10 | // providerImportSource: "@mdx-js/react", 11 | } 12 | }) 13 | module.exports = withMDX({ 14 | // Append the default value with md extensions 15 | pageExtensions: ['ts', 'tsx', 'js', 'jsx', 'md', 'mdx'], 16 | reactStrictMode: true 17 | }) 18 | -------------------------------------------------------------------------------- /app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "metrics-graphics-app", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build && next export", 8 | "start": "next start", 9 | "lint": "next lint" 10 | }, 11 | "dependencies": { 12 | "@mapbox/rehype-prism": "^0.8.0", 13 | "@mdx-js/loader": "^2.1.1", 14 | "@next/mdx": "^12.1.6", 15 | "classnames": "^2.3.1", 16 | "metrics-graphics": "3.0.1", 17 | "next": "^12.1.6", 18 | "react": "^17.0.2", 19 | "react-dom": "^17.0.2" 20 | }, 21 | "devDependencies": { 22 | "@tailwindcss/typography": "^0.5.2", 23 | "@types/node": "^17.0.35", 24 | "@types/react": "^18.0.9", 25 | "@types/react-dom": "^18.0.4", 26 | "autoprefixer": "^10.4.7", 27 | "postcss": "^8.4.14", 28 | "tailwindcss": "^3.0.24", 29 | "typescript": "^4.6.4" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /app/pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import '../styles/globals.css' 2 | import 'metrics-graphics/dist/mg.css' 3 | import type { AppProps } from 'next/app' 4 | import Link from 'next/link' 5 | import NavLink from '../components/NavLink' 6 | import Logo from '../components/Logo' 7 | 8 | function MyApp({ Component, pageProps }: AppProps) { 9 | return ( 10 |
11 |
12 |
13 | 14 |
15 | 16 |

MetricsGraphics

17 |
18 | 19 |
20 | Lines 21 | Scatterplots 22 | Histograms 23 | API 24 |
25 |
26 |
27 |
28 |
29 | 30 |
31 |
32 |
33 | ) 34 | } 35 | 36 | export default MyApp 37 | -------------------------------------------------------------------------------- /app/pages/_document.tsx: -------------------------------------------------------------------------------- 1 | import Document, { Html, Head, Main, NextScript } from 'next/document' 2 | 3 | class MyDocument extends Document { 4 | render() { 5 | return ( 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | ) 16 | } 17 | } 18 | 19 | export default MyDocument 20 | -------------------------------------------------------------------------------- /app/pages/histogram.mdx: -------------------------------------------------------------------------------- 1 | import Layout from '../components/Layout' 2 | import ParameterTable from '../components/ParameterTable' 3 | import Simple from '../components/charts/histogram/Simple' 4 | 5 | 6 | 7 | # Histograms 8 | 9 | ## API 10 | 11 | Extends the [base chart options](./mg-api). All options below are optional. 12 | 13 | 18 | 19 | ## Examples 20 | 21 | ### Difference in UFO Sighting and Reporting Dates (in months) 22 | 23 | Semi-real data about the reported differences between the supposed sighting of a UFO and the date it was reported. 24 | 25 | 26 | 27 | ```js 28 | new HistogramChart({ 29 | data: ufoData.map((date) => date / 30).sort(), 30 | width: 600, 31 | height: 200, 32 | binCount: 150, 33 | target: '#my-div', 34 | brush: 'x', 35 | yAxis: { 36 | extendedTicks: true 37 | }, 38 | tooltipFunction: (bar) => `${bar.time} months, volume ${bar.count}` 39 | }) 40 | ``` 41 | 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /app/pages/index.tsx: -------------------------------------------------------------------------------- 1 | import type { NextPage } from 'next' 2 | import Head from 'next/head' 3 | import { useEffect, useRef } from 'react' 4 | import { LineChart } from 'metrics-graphics' 5 | import sightings from '../data/ufoSightings.json' 6 | 7 | const Home: NextPage = () => { 8 | const chartRef = useRef(null) 9 | useEffect(() => { 10 | if (!chartRef.current) return 11 | const lineChart = new LineChart({ 12 | data: [sightings], 13 | markers: [{ year: 1964, label: '"The Creeping Terror" released' }], 14 | width: 650, 15 | height: 180, 16 | target: chartRef.current as any, 17 | xAccessor: 'year', 18 | yAccessor: 'sightings', 19 | area: true, 20 | yScale: { 21 | minValue: 0 22 | }, 23 | xAxis: { 24 | extendedTicks: true, 25 | label: 'Year', 26 | tickFormat: '.4r' 27 | }, 28 | yAxis: { 29 | label: 'Count' 30 | } 31 | }) 32 | }, [chartRef]) 33 | return ( 34 |
35 | 36 | MetricsGraphics 37 | 38 | 39 | 40 |

41 | MetricsGraphics is a library built on top of D3 that is optimized for visualizing and laying out time-series 42 | data. It provides a simple way to produce common types of graphics in a principled, consistent and responsive 43 | way. 44 |

45 |
46 |
47 | ) 48 | } 49 | 50 | export default Home 51 | -------------------------------------------------------------------------------- /app/pages/line.mdx: -------------------------------------------------------------------------------- 1 | import ParameterTable from '../components/ParameterTable' 2 | import Layout from '../components/Layout' 3 | import Simple from '../components/charts/line/Simple' 4 | import Confidence from '../components/charts/line/Confidence' 5 | import Multi from '../components/charts/line/Multi' 6 | import Aggregated from '../components/charts/line/Aggregated' 7 | import Broken from '../components/charts/line/Broken' 8 | import Active from '../components/charts/line/Active' 9 | import Baseline from '../components/charts/line/Baseline' 10 | 11 | 12 | 13 | # Line Charts 14 | 15 | ## API 16 | 17 | Extends the [base chart options](./mg-api). All options below are optional. 18 | 19 | | boolean', 22 | description: 'Specifies for which sub-array of data an area should be shown. If the chart is only one line, you can set it to true.' 23 | }, { 24 | name: 'confidenceBand', 25 | type: '[Accessor, Accessor]', 26 | description: 'Two-element array specifying how to access the lower (first) and upper (second) value for the confidence band. The two elements work like accessors (either a string or a function).' 27 | }, { 28 | name: 'voronoi', 29 | type: 'Partial', 30 | description: 'Custom parameters passed to the voronoi generator.' 31 | }, { 32 | name: 'defined', 33 | type: '(point: Data) => boolean', 34 | description: 'Function specifying whether or not to show a given datapoint. This is mainly used to create partially defined graphs.' 35 | }, { 36 | name: 'activeAccessor', 37 | type: 'Accessor', 38 | description: 'Accessor that defines whether or not a given data point should be shown as active' 39 | }, { 40 | name: 'activePoint', 41 | type: 'Partial', 42 | description: 'Custom parameters passed to the active point generator.' 43 | }]} /> 44 | 45 | ## Examples 46 | 47 | ### Simple Line Chart 48 | 49 | This is a simple line chart. You can remove the area portion by adding `area: false` to the arguments list. 50 | 51 | 52 | 53 | ```js 54 | new LineChart({ 55 | data: [fakeUsers.map(({ date, value }) => ({ date: new Date(date), value }))], 56 | width: 600, 57 | height: 200, 58 | yScale: { 59 | minValue: 0 60 | }, 61 | target: '#my-div', 62 | brush: 'xy', 63 | area: true, 64 | xAccessor: 'date', 65 | yAccessor: 'value', 66 | tooltipFunction: (point) => `${formatDate(point.date)}: ${formatCompact(point.value)}` 67 | }) 68 | ``` 69 | 70 | 71 | 72 | ### Confidence Band 73 | 74 | This is an example of a graph with a confidence band and extended x-axis ticks enabled. 75 | 76 | 77 | 78 | ```js 79 | new LineChart({ 80 | data: [ 81 | confidence.map((entry) => ({ 82 | ...entry, 83 | date: new Date(entry.date) 84 | })) 85 | ], 86 | xAxis: { 87 | extendedTicks: true 88 | }, 89 | yAxis: { 90 | tickFormat: 'percentage' 91 | }, 92 | width: 600, 93 | height: 200, 94 | target: '#my-div', 95 | confidenceBand: ['l', 'u'], 96 | tooltipFunction: (point) => `${formatDate(point.date)}: ${formatPercent(point.value)}` 97 | }) 98 | ``` 99 | 100 | 101 | ### Multiple Lines 102 | 103 | This line chart contains multiple lines. 104 | 105 | 106 | ```js 107 | new LineChart({ 108 | data: fakeUsers.map((fakeArray) => 109 | fakeArray.map((fakeEntry) => ({ 110 | ...fakeEntry, 111 | date: new Date(fakeEntry.date) 112 | })) 113 | ), 114 | width: 600, 115 | height: 200, 116 | target: '#my-div', 117 | xAccessor: 'date', 118 | yAccessor: 'value', 119 | legend: ['Line 1', 'Line 2', 'Line 3'], 120 | tooltipFunction: (point) => `${formatDate(point.date)}: ${formatCompact(point.value)}` 121 | }) 122 | ``` 123 | 124 | 125 | ### Aggregate Rollover 126 | 127 | One rollover for all lines. 128 | 129 | 130 | 131 | ```js 132 | new LineChart({ 133 | data: fakeUsers.map((fakeArray) => 134 | fakeArray.map((fakeEntry) => ({ 135 | ...fakeEntry, 136 | date: new Date(fakeEntry.date) 137 | })) 138 | ), 139 | width: 600, 140 | height: 200, 141 | target: '#my-div', 142 | xAccessor: 'date', 143 | yAccessor: 'value', 144 | legend: ['Line 1', 'Line 2', 'Line 3'], 145 | voronoi: { 146 | aggregate: true 147 | }, 148 | tooltipFunction: (point) => `${formatDate(point.date)}: ${formatCompact(point.value)}` 149 | }) 150 | ``` 151 | 152 | 153 | ### Broken lines (missing data points) 154 | 155 | You can hide individual data points on a particular attribute by setting the defined accessor (which has to return true for visible points). Data points whose y-accessor values are null are also hidden. 156 | 157 | 158 | 159 | ```js 160 | new LineChart({ 161 | data: [missing.map((e) => ({ ...e, date: new Date(e.date) }))], 162 | width: 600, 163 | height: 200, 164 | target: '#my-div', 165 | defined: (d) => !d.dead, 166 | area: true, 167 | tooltipFunction: (point) => `${formatDate(point.date)}: ${point.value}` 168 | }) 169 | ``` 170 | 171 | 172 | 173 | ### Active Points 174 | 175 | This line chart displays pre-defined active points. 176 | 177 | 178 | 179 | ```js 180 | new LineChart({ 181 | data: [ 182 | fakeUsers.map((entry, i) => ({ 183 | ...entry, 184 | date: new Date(entry.date), 185 | active: i % 5 === 0 186 | })) 187 | ], 188 | width: 600, 189 | height: 200, 190 | target: '#my-div', 191 | activeAccessor: 'active', 192 | activePoint: { 193 | radius: 2 194 | }, 195 | tooltipFunction: (point) => `${formatDate(point.date)}: ${formatCompact(point.value)}` 196 | }) 197 | ``` 198 | 199 | 200 | ### Baseline 201 | 202 | Baselines are horizontal lines that can added at arbitrary points. 203 | 204 | 205 | 206 | ```js 207 | new LineChart({ 208 | data: [ 209 | fakeUsers.map((entry) => ({ 210 | ...entry, 211 | date: new Date(entry.date) 212 | })) 213 | ], 214 | baselines: [{ value: 160000000, label: 'a baseline' }], 215 | width: 600, 216 | height: 200, 217 | target: '#my-div', 218 | tooltipFunction: (point) => `${formatDate(point.date)}: ${formatCompact(point.value)}` 219 | }) 220 | ``` 221 | 222 | 223 | 224 | -------------------------------------------------------------------------------- /app/pages/mg-api.mdx: -------------------------------------------------------------------------------- 1 | import ParameterTable from '../components/ParameterTable' 2 | import Layout from '../components/Layout' 3 | 4 | 5 | 6 | # API 7 | 8 | All MetricsGraphics charts are classes that can be instantiated with a set of parameters (e.g. `new LineChart({ ... })`). The chart is then mounted to the given `target` (see below), which is for example the `id` of an empty `div` in your DOM or a React `ref`. 9 | 10 | ## Data formats 11 | 12 | MetricsGraphics assumes that your data is either an array of objects or an array of arrays of objects. For example, your data could look like this: 13 | 14 | ```js 15 | [{ 16 | date: '2020-02-01', 17 | value: 10 18 | }, { 19 | date: '2020-02-02', 20 | value: 12 21 | }] 22 | ``` 23 | 24 | ## Common Parameters 25 | 26 | All charts inherit from an abstract chart, which has the following parameters (optional parameters marked with `?`): 27 | 28 | ', 31 | description: 'Data that is to be visualized.' 32 | }, { 33 | name: 'target', 34 | type: 'string', 35 | description: 'DOM node to which the graph will be mounted (compatible D3 selection or D3 selection specifier).' 36 | }, { 37 | name: 'width', 38 | type: 'number', 39 | description: 'Total width of the graph.' 40 | }, { 41 | name: 'height', 42 | type: 'number', 43 | description: 'Total height of the graph.' 44 | }, { 45 | name: 'markers?', 46 | type: 'Array', 47 | description: 'Markers that should be added to the chart. Each marker object should be accessible through the xAccessor and contain a label field.' 48 | }, { 49 | name: 'baselines?', 50 | type: 'Array', 51 | description: 'Baselines that should be added to the chart. Each baseline object should be accessible through the yAccessor and contain a label field.' 52 | }, { 53 | name: 'xAccessor?', 54 | type: 'string | Accessor', 55 | default: 'date', 56 | description: 'Either the name of the field that contains the x value or a function that receives a data object and returns its x value.' 57 | }, { 58 | name: 'yAccessor?', 59 | type: 'string | Accessor', 60 | default: 'value', 61 | description: 'Either the name of the field that contains the y value or a function that receives a data object and returns its y value.' 62 | }, { 63 | name: 'margin?', 64 | type: 'Margin', 65 | default: 'top: 10, left: 60, right: 20, bottom: 40', 66 | description: 'Margin around the chart for labels.' 67 | }, { 68 | name: 'buffer?', 69 | type: 'number', 70 | default: '10', 71 | description: 'Amount of buffer space between the axes and the actual graph.' 72 | }, { 73 | name: 'colors?', 74 | type: 'Array', 75 | default: 'd3.schemeCategory10', 76 | description: 'Custom color scheme for the graph.' 77 | }, { 78 | name: 'xScale?', 79 | type: 'Partial', 80 | description: 'Overwrite parameters of the auto-generated x scale.' 81 | }, { 82 | name: 'yScale?', 83 | type: 'Partial', 84 | description: 'Overwrite parameters of the auto-generated y scale.' 85 | }, { 86 | name: 'xAxis?', 87 | type: 'Partial', 88 | description: 'Overwrite parameters of the auto-generated x axis.' 89 | }, { 90 | name: 'yAxis?', 91 | type: 'Partial', 92 | description: 'Overwrite parameters of the auto-generated y axis.' 93 | }, { 94 | name: 'showTooltip?', 95 | type: 'boolean', 96 | default: 'true', 97 | description: 'Whether or not to show a tooltip.' 98 | }, { 99 | name: 'tooltipFunction', 100 | type: 'Accessor', 101 | description: 'Generate a custom tooltip string.' 102 | }, { 103 | name: 'legend?', 104 | type: 'Array', 105 | description: 'Used if data is an array of arrays. Names of the sub-arrays of data, used as legend labels.' 106 | }, { 107 | name: 'brush?', 108 | type: '"xy" | "x" | "y"', 109 | description: 'Adds either a one- or two-dimensional brush to the chart.' 110 | }]} /> 111 | 112 | ## Common Types 113 | 114 | ```ts 115 | type Accessor = (dataObject: X) => Y 116 | 117 | type Margin = { 118 | left: number 119 | right: number 120 | bottom: number 121 | top: number 122 | } 123 | 124 | type Scale = { 125 | type: 'linear' // this will be extended in the future 126 | range?: [number, number] 127 | domain?: [number, number] 128 | } 129 | 130 | type Axis = { 131 | scale: Scale 132 | buffer: number 133 | show?: boolean 134 | orientation?: 'top' | 'bottom' | 'left' | 'right' 135 | label?: string 136 | labelOffset?: number 137 | top?: number 138 | left?: number 139 | 140 | // a function to format a given tick, or one of the standard types (date, number, percentage), or string for d3-format 141 | tickFormat?: TextFunction | AxisFormat | string 142 | 143 | // defaults to 3 for vertical and 6 for horizontal axes 144 | tickCount?: number 145 | 146 | compact?: boolean 147 | 148 | // tick label prefix 149 | prefix?: string 150 | 151 | // tick label suffix 152 | suffix?: string 153 | 154 | // overwrite d3's default tick lengths 155 | tickLength?: number 156 | 157 | // draw extended tick lines 158 | extendedTicks?: boolean 159 | } 160 | ``` 161 | 162 | -------------------------------------------------------------------------------- /app/pages/scatter.mdx: -------------------------------------------------------------------------------- 1 | import Layout from '../components/Layout' 2 | import ParameterTable from '../components/ParameterTable' 3 | import Simple from '../components/charts/scatter/Simple' 4 | import Categories from '../components/charts/scatter/Categories' 5 | import Complex from '../components/charts/scatter/Complex' 6 | 7 | 8 | 9 | # Scatterplots 10 | 11 | ## API 12 | 13 | Extends the [base chart options](./mg-api). All options below are optional. 14 | 15 | 3' 20 | }, { 21 | name: 'xRug', 22 | type: 'boolean', 23 | description: 'Whether or not to generate a rug for the x axis.' 24 | }, { 25 | name: 'yRug', 26 | type: 'boolean', 27 | description: 'Whether or not to generate a rug for the y axis.' 28 | }]} /> 29 | 30 | ## Examples 31 | 32 | ### Simple Scatterplot 33 | 34 | This is an example scatterplot, in which we have enabled rug plots on the y-axis by setting the rug option to `true`. 35 | 36 | 37 | 38 | ```js 39 | new ScatterChart({ 40 | data: [points1], 41 | width: 500, 42 | height: 200, 43 | target: '#my-div', 44 | xAccessor: 'x', 45 | yAccessor: 'y', 46 | brush: 'xy', 47 | xRug: true, 48 | tooltipFunction: (point) => `${formatDecimal(point.x)} - ${formatDecimal(point.y)}` 49 | }) 50 | ``` 51 | 52 | 53 | 54 | ### Multi-Category Scatterplot 55 | 56 | This scatterplot contains data of multiple categories. 57 | 58 | 59 | 60 | ```js 61 | new ScatterChart({ 62 | data: points2.map((x: any) => x.values), 63 | legend: points2.map((x: any) => x.key), 64 | width: 500, 65 | height: 200, 66 | xAccessor: 'x', 67 | yAccessor: 'y', 68 | yRug: true, 69 | target: '#my-div', 70 | tooltipFunction: (point) => `${formatDecimal(point.x)} - ${formatDecimal(point.y)}` 71 | }) 72 | ``` 73 | 74 | 75 | ### Scatterplot with Size and Color 76 | 77 | Scatterplots have xAccessor, yAccessor and sizeAccessor. 78 | 79 | 80 | 81 | ```js 82 | new ScatterChart({ 83 | data: points2.map((x: any) => x.values), 84 | legend: points2.map((x: any) => x.key), 85 | width: 500, 86 | height: 200, 87 | target: '#my-div', 88 | xAccessor: 'x', 89 | yAccessor: 'y', 90 | sizeAccessor: (x: any) => Math.abs(x.w) * 3, 91 | tooltipFunction: (point) => `${formatDecimal(point.x)} - ${formatDecimal(point.y)}: ${formatDecimal(point.w)}` 92 | }) 93 | ``` 94 | 95 | 96 | 97 | 98 | -------------------------------------------------------------------------------- /app/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {} 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /app/public/favicon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 8 | -------------------------------------------------------------------------------- /app/styles/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | .token.prolog, 6 | .token.doctype, 7 | .token.cdata { 8 | @apply text-gray-700; 9 | } 10 | 11 | .token.comment { 12 | @apply text-gray-500; 13 | } 14 | 15 | .token.punctuation { 16 | @apply text-gray-700; 17 | } 18 | 19 | .token.property, 20 | .token.tag, 21 | .token.boolean, 22 | .token.number, 23 | .token.constant, 24 | .token.symbol, 25 | .token.deleted { 26 | @apply text-green-500; 27 | } 28 | 29 | .token.selector, 30 | .token.attr-name, 31 | .token.string, 32 | .token.char, 33 | .token.builtin, 34 | .token.inserted { 35 | @apply text-purple-500; 36 | } 37 | 38 | .token.operator, 39 | .token.entity, 40 | .token.url, 41 | .language-css .token.string, 42 | .style .token.string { 43 | @apply text-yellow-500; 44 | } 45 | 46 | .token.atrule, 47 | .token.attr-value, 48 | .token.keyword { 49 | @apply text-blue-500; 50 | } 51 | 52 | .token.function, 53 | .token.class-name { 54 | @apply text-pink-500; 55 | } 56 | 57 | .token.regex, 58 | .token.important, 59 | .token.variable { 60 | @apply text-yellow-500; 61 | } 62 | 63 | code[class*='language-'], 64 | pre[class*='language-'] { 65 | @apply text-gray-800; 66 | } 67 | 68 | pre::-webkit-scrollbar { 69 | display: none; 70 | } 71 | 72 | pre { 73 | @apply bg-gray-50; 74 | -ms-overflow-style: none; /* IE and Edge */ 75 | scrollbar-width: none; /* Firefox */ 76 | } -------------------------------------------------------------------------------- /app/tailwind.config.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line @typescript-eslint/no-var-requires 2 | const defaultTheme = require('tailwindcss/defaultTheme') 3 | 4 | module.exports = { 5 | content: ['./pages/**/*.{js,ts,jsx,tsx}', './components/**/*.{js,ts,jsx,tsx}'], 6 | theme: { 7 | extend: { 8 | typography: ({ theme }) => ({ 9 | DEFAULT: { 10 | css: { 11 | pre: { 12 | backgroundColor: theme('colors.indigo[50]'), 13 | fontSize: '0.7rem' 14 | } 15 | } 16 | } 17 | }), 18 | fontFamily: { 19 | sans: ['Inter var', ...defaultTheme.fontFamily.sans] 20 | } 21 | } 22 | }, 23 | plugins: [require('@tailwindcss/typography')] 24 | } 25 | -------------------------------------------------------------------------------- /app/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "noEmit": true, 10 | "esModuleInterop": true, 11 | "module": "esnext", 12 | "moduleResolution": "node", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "jsx": "preserve", 16 | "incremental": true 17 | }, 18 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], 19 | "exclude": ["node_modules"] 20 | } 21 | -------------------------------------------------------------------------------- /lib/.gitignore: -------------------------------------------------------------------------------- 1 | # Dependencies 2 | bower_components 3 | node_modules 4 | 5 | # Logs 6 | npm-debug.log 7 | 8 | # IDE 9 | .idea 10 | 11 | # FS 12 | .DS_Store 13 | 14 | # Others 15 | other/divider.psd 16 | other/htaccess.txt 17 | bare.html 18 | yarn.lock 19 | yarn-error.log 20 | 21 | # Dist files 22 | dist 23 | build 24 | -------------------------------------------------------------------------------- /lib/esbuild.mjs: -------------------------------------------------------------------------------- 1 | import esbuild from 'esbuild' 2 | 3 | const baseConfig = { 4 | entryPoints: ['src/index.ts'], 5 | bundle: true, 6 | sourcemap: true, 7 | target: 'esnext' 8 | } 9 | 10 | // esm 11 | esbuild.build({ 12 | ...baseConfig, 13 | outdir: 'dist/esm', 14 | splitting: true, 15 | format: 'esm' 16 | }) 17 | 18 | // cjs 19 | esbuild.build({ 20 | ...baseConfig, 21 | outdir: 'dist/cjs', 22 | format: 'cjs' 23 | }) 24 | -------------------------------------------------------------------------------- /lib/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "metrics-graphics", 3 | "version": "3.0.1", 4 | "description": "A library optimized for concise, principled data graphics and layouts", 5 | "main": "dist/cjs/index.js", 6 | "module": "dist/esm/index.js", 7 | "types": "dist/index.d.ts", 8 | "scripts": { 9 | "ts-types": "tsc --emitDeclarationOnly --outDir dist", 10 | "build": "rimraf dist && concurrently \"node ./esbuild.mjs\" \"npm run ts-types\" && cp src/mg.css dist/mg.css", 11 | "lint": "eslint src", 12 | "test": "echo \"no tests set up, will do later\"", 13 | "analyze": "source-map-explorer dist/esm/index.js" 14 | }, 15 | "repository": { 16 | "type": "git", 17 | "url": "git://github.com/metricsgraphics/metrics-graphics.git" 18 | }, 19 | "files": [ 20 | "dist" 21 | ], 22 | "keywords": [ 23 | "metrics-graphics", 24 | "metricsgraphicsjs", 25 | "metricsgraphics", 26 | "metricsgraphics.js", 27 | "d3 charts" 28 | ], 29 | "author": "Mozilla", 30 | "contributors": [ 31 | "Ali Almossawi", 32 | "Hamilton Ulmer", 33 | "William Lachance", 34 | "Jens Ochsenmeier" 35 | ], 36 | "license": "MPL-2.0", 37 | "bugs": { 38 | "url": "https://github.com/metricsgraphics/metrics-graphics/issues" 39 | }, 40 | "engines": { 41 | "node": ">=0.8.0" 42 | }, 43 | "homepage": "http://metricsgraphicsjs.org", 44 | "dependencies": { 45 | "d3": "^7.4.4" 46 | }, 47 | "devDependencies": { 48 | "@types/d3": "^7.1.0", 49 | "concurrently": "^7.2.0", 50 | "deepmerge": "^4.2.2", 51 | "esbuild": "^0.14.39", 52 | "rimraf": "^3.0.2", 53 | "source-map-explorer": "^2.5.2" 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /lib/src/charts/abstractChart.ts: -------------------------------------------------------------------------------- 1 | import { select, extent, max, brush as d3brush, brushX, brushY } from 'd3' 2 | import { randomId, makeAccessorFunction } from '../misc/utility' 3 | import Scale from '../components/scale' 4 | import Axis, { IAxis, AxisOrientation } from '../components/axis' 5 | import Tooltip from '../components/tooltip' 6 | import Legend from '../components/legend' 7 | import constants from '../misc/constants' 8 | import Point, { IPoint } from '../components/point' 9 | import { 10 | SvgD3Selection, 11 | AccessorFunction, 12 | Margin, 13 | GenericD3Selection, 14 | BrushType, 15 | DomainObject, 16 | Domain, 17 | LegendSymbol 18 | } from '../misc/typings' 19 | 20 | type TooltipFunction = (datapoint: any) => string 21 | 22 | export interface IAbstractChart { 23 | /** data that is to be visualized */ 24 | data: Array 25 | 26 | /** DOM node to which the graph will be mounted (D3 selection or D3 selection specifier) */ 27 | target: string 28 | 29 | /** total width of the graph */ 30 | width: number 31 | 32 | /** total height of the graph */ 33 | height: number 34 | 35 | /** markers that should be added to the chart. Each marker object should be accessible through the xAccessor and contain a label field */ 36 | markers?: Array 37 | 38 | /** baselines that should be added to the chart. Each baseline object should be accessible through the yAccessor and contain a label field */ 39 | baselines?: Array 40 | 41 | /** either name of the field that contains the x value or function that receives a data object and returns its x value */ 42 | xAccessor?: string | AccessorFunction 43 | 44 | /** either name of the field that contains the y value or function that receives a data object and returns its y value */ 45 | yAccessor?: string | AccessorFunction 46 | 47 | /** margins of the visualization for labels */ 48 | margin?: Margin 49 | 50 | /** amount of buffer between the axes and the graph */ 51 | buffer?: number 52 | 53 | /** custom color scheme for the graph */ 54 | colors?: Array 55 | 56 | /** overwrite parameters of the auto-generated x scale */ 57 | xScale?: Partial 58 | 59 | /** overwrite parameters of the auto-generated y scale */ 60 | yScale?: Partial 61 | 62 | /** overwrite parameters of the auto-generated x axis */ 63 | xAxis?: Partial 64 | 65 | /** overwrite parameters of the auto-generated y axis */ 66 | yAxis?: Partial 67 | 68 | /** whether or not to show a tooltip */ 69 | showTooltip?: boolean 70 | 71 | /** generate a custom tooltip string */ 72 | tooltipFunction?: (datapoint: any) => string 73 | 74 | /** names of the sub-arrays of data, used as legend labels */ 75 | legend?: Array 76 | 77 | /** add an optional brush */ 78 | brush?: BrushType 79 | 80 | /** custom domain computations */ 81 | computeDomains?: () => DomainObject 82 | } 83 | 84 | /** 85 | * This abstract chart class implements all functionality that is shared between all available chart types. 86 | */ 87 | export default abstract class AbstractChart { 88 | id: string 89 | 90 | // base chart fields 91 | data: Array 92 | markers: Array 93 | baselines: Array 94 | target: SvgD3Selection 95 | svg?: GenericD3Selection 96 | content?: GenericD3Selection 97 | container?: GenericD3Selection 98 | 99 | // accessors 100 | xAccessor: AccessorFunction 101 | yAccessor: AccessorFunction 102 | colors: Array 103 | 104 | // scales 105 | xDomain: Domain 106 | yDomain: Domain 107 | xScale: Scale 108 | yScale: Scale 109 | 110 | // axes 111 | xAxis?: Axis 112 | xAxisParams: any 113 | yAxis?: Axis 114 | yAxisParams: any 115 | 116 | // tooltip and legend stuff 117 | showTooltip: boolean 118 | tooltipFunction?: TooltipFunction 119 | tooltip?: Tooltip 120 | legend?: Array 121 | 122 | // dimensions 123 | width: number 124 | height: number 125 | 126 | // margins 127 | margin: Margin 128 | buffer: number 129 | 130 | // brush 131 | brush?: BrushType 132 | idleDelay = 350 133 | idleTimeout: unknown 134 | 135 | constructor({ 136 | data, 137 | target, 138 | markers, 139 | baselines, 140 | xAccessor, 141 | yAccessor, 142 | margin, 143 | buffer, 144 | width, 145 | height, 146 | colors, 147 | xScale, 148 | yScale, 149 | xAxis, 150 | yAxis, 151 | showTooltip, 152 | tooltipFunction, 153 | legend, 154 | brush, 155 | computeDomains 156 | }: IAbstractChart) { 157 | // convert string accessors to functions if necessary 158 | this.xAccessor = makeAccessorFunction(xAccessor ?? 'date') 159 | this.yAccessor = makeAccessorFunction(yAccessor ?? 'value') 160 | 161 | // set parameters 162 | this.data = data 163 | this.target = select(target) 164 | this.markers = markers ?? [] 165 | this.baselines = baselines ?? [] 166 | this.legend = legend ?? this.legend 167 | this.brush = brush ?? undefined 168 | this.xAxisParams = xAxis ?? this.xAxisParams 169 | this.yAxisParams = yAxis ?? this.yAxisParams 170 | this.showTooltip = showTooltip ?? true 171 | this.tooltipFunction = tooltipFunction 172 | 173 | this.margin = margin ?? { top: 10, left: 60, right: 20, bottom: 40 } 174 | this.buffer = buffer ?? 10 175 | 176 | // set unique id for chart 177 | this.id = randomId() 178 | 179 | // compute dimensions 180 | this.width = width 181 | this.height = height 182 | 183 | // normalize color and colors arguments 184 | this.colors = colors ?? constants.defaultColors 185 | 186 | // clear target 187 | this.target.selectAll('*').remove() 188 | 189 | // attach base elements to svg 190 | this.mountSvg() 191 | 192 | // set up scales 193 | this.xScale = new Scale({ range: [0, this.innerWidth], ...xScale }) 194 | this.yScale = new Scale({ range: [this.innerHeight, 0], ...yScale }) 195 | 196 | // compute domains and set them 197 | const { x, y } = computeDomains ? computeDomains() : this.computeDomains() 198 | this.xDomain = x 199 | this.yDomain = y 200 | this.xScale.domain = x 201 | this.yScale.domain = y 202 | 203 | this.abstractRedraw() 204 | } 205 | 206 | /** 207 | * Draw the abstract chart. 208 | */ 209 | abstractRedraw(): void { 210 | // if not drawn yet, abort 211 | if (!this.content) return 212 | 213 | // clear 214 | this.content.selectAll('*').remove() 215 | 216 | // set up axes if not disabled 217 | this.mountXAxis(this.xAxisParams) 218 | this.mountYAxis(this.yAxisParams) 219 | 220 | // pre-attach tooltip text container 221 | this.mountTooltip(this.showTooltip, this.tooltipFunction) 222 | 223 | // set up main container 224 | this.mountContainer() 225 | } 226 | 227 | /** 228 | * Draw the actual chart. 229 | * This is meant to be overridden by chart implementations. 230 | */ 231 | abstract redraw(): void 232 | 233 | mountBrush(whichBrush?: BrushType): void { 234 | // if no brush is specified, there's nothing to mount 235 | if (!whichBrush) return 236 | 237 | // brush can only be mounted after content is set 238 | if (!this.content || !this.container) { 239 | console.error('error: content not set yet') 240 | return 241 | } 242 | 243 | const brush = whichBrush === 'x' ? brushX() : whichBrush === 'y' ? brushY() : d3brush() 244 | brush.on('end', ({ selection }) => { 245 | // if no content is set, do nothing 246 | if (!this.content) { 247 | console.error('error: content is not set yet') 248 | return 249 | } 250 | // compute domains and re-draw 251 | if (selection === null) { 252 | if (!this.idleTimeout) { 253 | this.idleTimeout = setTimeout(() => { 254 | this.idleTimeout = null 255 | }, 350) 256 | return 257 | } 258 | 259 | // set original domains 260 | this.xScale.domain = this.xDomain 261 | this.yScale.domain = this.yDomain 262 | } else { 263 | if (this.brush === 'x') { 264 | this.xScale.domain = [selection[0], selection[1]].map(this.xScale.scaleObject.invert) 265 | } else if (this.brush === 'y') { 266 | this.yScale.domain = [selection[0], selection[1]].map(this.yScale.scaleObject.invert) 267 | } else { 268 | this.xScale.domain = [selection[0][0], selection[1][0]].map(this.xScale.scaleObject.invert) 269 | this.yScale.domain = [selection[1][1], selection[0][1]].map(this.yScale.scaleObject.invert) 270 | } 271 | this.content.select('.brush').call((brush as any).move, null) 272 | } 273 | 274 | // re-draw abstract elements 275 | this.abstractRedraw() 276 | 277 | // re-draw specific chart 278 | this.redraw() 279 | }) 280 | this.container.append('g').classed('brush', true).call(brush) 281 | } 282 | 283 | /** 284 | * Mount a new legend if necessary 285 | * @param {String} symbolType symbol type (circle, square, line) 286 | */ 287 | mountLegend(symbolType: LegendSymbol): void { 288 | if (!this.legend || !this.legend.length) return 289 | const legend = new Legend({ 290 | legend: this.legend, 291 | colorScheme: this.colors, 292 | symbolType 293 | }) 294 | legend.mountTo(this.target) 295 | } 296 | 297 | /** 298 | * Mount new x axis. 299 | * 300 | * @param xAxis object that can be used to overwrite parameters of the auto-generated x {@link Axis}. 301 | */ 302 | mountXAxis(xAxis: Partial): void { 303 | // axis only mountable after content is mounted 304 | if (!this.content) { 305 | console.error('error: content needs to be mounted first') 306 | return 307 | } 308 | 309 | if (typeof xAxis?.show !== 'undefined' && !xAxis.show) return 310 | this.xAxis = new Axis({ 311 | scale: this.xScale, 312 | orientation: AxisOrientation.BOTTOM, 313 | top: this.bottom, 314 | left: this.left, 315 | height: this.innerHeight, 316 | buffer: this.buffer, 317 | ...xAxis 318 | }) 319 | if (!xAxis?.tickFormat) this.computeXAxisType() 320 | 321 | // attach axis 322 | if (this.xAxis) this.xAxis.mountTo(this.content) 323 | } 324 | 325 | /** 326 | * Mount new y axis. 327 | * 328 | * @param yAxis object that can be used to overwrite parameters of the auto-generated y {@link Axis}. 329 | */ 330 | mountYAxis(yAxis: Partial): void { 331 | // axis only mountable after content is mounted 332 | if (!this.content) { 333 | console.error('error: content needs to be mounted first') 334 | return 335 | } 336 | 337 | if (typeof yAxis?.show !== 'undefined' && !yAxis.show) return 338 | this.yAxis = new Axis({ 339 | scale: this.yScale, 340 | orientation: AxisOrientation.LEFT, 341 | top: this.top, 342 | left: this.left, 343 | height: this.innerWidth, 344 | buffer: this.buffer, 345 | ...yAxis 346 | }) 347 | if (!yAxis?.tickFormat) this.computeYAxisType() 348 | if (this.yAxis) this.yAxis.mountTo(this.content) 349 | } 350 | 351 | /** 352 | * Mount a new tooltip if necessary. 353 | * 354 | * @param showTooltip whether or not to show a tooltip. 355 | * @param tooltipFunction function that receives a data object and returns the string displayed as tooltip. 356 | */ 357 | mountTooltip(showTooltip?: boolean, tooltipFunction?: TooltipFunction): void { 358 | // only mount of content is defined 359 | if (!this.content) { 360 | console.error('error: content is not defined yet') 361 | return 362 | } 363 | 364 | if (typeof showTooltip !== 'undefined' && !showTooltip) return 365 | this.tooltip = new Tooltip({ 366 | top: this.buffer, 367 | left: this.width - 2 * this.buffer, 368 | xAccessor: this.xAccessor, 369 | yAccessor: this.yAccessor, 370 | textFunction: tooltipFunction, 371 | colors: this.colors, 372 | legend: this.legend 373 | }) 374 | this.tooltip.mountTo(this.content) 375 | } 376 | 377 | /** 378 | * Mount the main container. 379 | */ 380 | mountContainer(): void { 381 | // content needs to be mounted first 382 | if (!this.content) { 383 | console.error('content needs to be mounted first') 384 | return 385 | } 386 | 387 | const width = max(this.xScale.range) 388 | const height = max(this.yScale.range) 389 | 390 | if (!width || !height) { 391 | console.error(`error: width or height is null (width: "${width}", height: "${height}")`) 392 | return 393 | } 394 | 395 | this.container = this.content 396 | .append('g') 397 | .attr('transform', `translate(${this.left},${this.top})`) 398 | .attr('clip-path', `url(#mg-plot-window-${this.id})`) 399 | .append('g') 400 | .attr('transform', `translate(${this.buffer},${this.buffer})`) 401 | this.container 402 | .append('rect') 403 | .attr('x', 0) 404 | .attr('y', 0) 405 | .attr('opacity', 0) 406 | .attr('pointer-events', 'all') 407 | .attr('width', width) 408 | .attr('height', height) 409 | } 410 | 411 | /** 412 | * This method is called by the abstract chart constructor. 413 | * Append the local svg node to the specified target, if necessary. 414 | * Return existing svg node if it's already present. 415 | */ 416 | mountSvg(): void { 417 | const svg = this.target.select('svg') 418 | 419 | // warn user if svg is not empty 420 | if (!svg.empty()) { 421 | console.warn('Warning: SVG is not empty. Rendering might be unnecessary.') 422 | } 423 | 424 | // clear svg 425 | svg.remove() 426 | 427 | this.svg = this.target.append('svg').classed('mg-graph', true).attr('width', this.width).attr('height', this.height) 428 | 429 | // prepare clip path 430 | this.svg.select('.mg-clip-path').remove() 431 | this.svg 432 | .append('defs') 433 | .attr('class', 'mg-clip-path') 434 | .append('clipPath') 435 | .attr('id', `mg-plot-window-${this.id}`) 436 | .append('svg:rect') 437 | .attr('width', this.width - this.margin.left - this.margin.right) 438 | .attr('height', this.height - this.margin.top - this.margin.bottom) 439 | 440 | // set viewbox 441 | this.svg.attr('viewBox', `0 0 ${this.width} ${this.height}`) 442 | 443 | // append content 444 | this.content = this.svg.append('g').classed('mg-content', true) 445 | } 446 | 447 | /** 448 | * If needed, charts can implement data normalizations, which are applied when instantiating a new chart. 449 | * TODO this is currently unused 450 | */ 451 | // abstract normalizeData(): void 452 | 453 | /** 454 | * Usually, the domains of the chart's scales depend on the chart type and the passed data, so this should usually be overwritten by chart implementations. 455 | * @returns domains for x and y axis as separate properties. 456 | */ 457 | computeDomains(): DomainObject { 458 | const flatData = this.data.flat() 459 | const x = extent(flatData, this.xAccessor) 460 | const y = extent(flatData, this.yAccessor) 461 | return { x: x as [number, number], y: y as [number, number] } 462 | } 463 | 464 | /** 465 | * Set tick format of the x axis. 466 | */ 467 | computeXAxisType(): void { 468 | // abort if no x axis is used 469 | if (!this.xAxis) { 470 | console.error('error: no x axis set') 471 | return 472 | } 473 | 474 | const flatData = this.data.flat() 475 | const xValue = this.xAccessor(flatData[0]) 476 | 477 | if (xValue instanceof Date) { 478 | this.xAxis.tickFormat = 'date' 479 | } else if (Number(xValue) === xValue) { 480 | this.xAxis.tickFormat = 'number' 481 | } 482 | } 483 | 484 | /** 485 | * Set tick format of the y axis. 486 | */ 487 | computeYAxisType(): void { 488 | // abort if no y axis is used 489 | if (!this.yAxis) { 490 | console.error('error: no y axis set') 491 | return 492 | } 493 | 494 | const flatData = this.data.flat() 495 | const yValue = this.yAccessor(flatData[0]) 496 | 497 | if (yValue instanceof Date) { 498 | this.yAxis.tickFormat = constants.axisFormat.date 499 | } else if (Number(yValue) === yValue) { 500 | this.yAxis.tickFormat = constants.axisFormat.number 501 | } 502 | } 503 | 504 | generatePoint(args: Partial): Point { 505 | return new Point({ 506 | ...args, 507 | xAccessor: this.xAccessor, 508 | yAccessor: this.yAccessor, 509 | xScale: this.xScale, 510 | yScale: this.yScale 511 | }) 512 | } 513 | 514 | get top(): number { 515 | return this.margin.top 516 | } 517 | 518 | get left(): number { 519 | return this.margin.left 520 | } 521 | 522 | get bottom(): number { 523 | return this.height - this.margin.bottom 524 | } 525 | 526 | // returns the pixel location of the respective side of the plot area. 527 | get plotTop(): number { 528 | return this.top + this.buffer 529 | } 530 | 531 | get plotLeft(): number { 532 | return this.left + this.buffer 533 | } 534 | 535 | get innerWidth(): number { 536 | return this.width - this.margin.left - this.margin.right - 2 * this.buffer 537 | } 538 | 539 | get innerHeight(): number { 540 | return this.height - this.margin.top - this.margin.bottom - 2 * this.buffer 541 | } 542 | } 543 | -------------------------------------------------------------------------------- /lib/src/charts/histogram.ts: -------------------------------------------------------------------------------- 1 | import { max, bin } from 'd3' 2 | import Delaunay from '../components/delaunay' 3 | import Rect from '../components/rect' 4 | import { TooltipSymbol } from '../components/tooltip' 5 | import { LegendSymbol, InteractionFunction } from '../misc/typings' 6 | import AbstractChart, { IAbstractChart } from './abstractChart' 7 | 8 | interface IHistogramChart extends IAbstractChart { 9 | binCount?: number 10 | } 11 | 12 | /** 13 | * Creates a new histogram graph. 14 | * 15 | * @param {Object} args argument object. See {@link AbstractChart} for general parameters. 16 | * @param {Number} [args.binCount] approximate number of bins that should be used for the histogram. Defaults to what d3.bin thinks is best. 17 | */ 18 | export default class HistogramChart extends AbstractChart { 19 | bins: Array 20 | rects?: Array 21 | delaunay?: any 22 | delaunayBar?: any 23 | _activeBar = -1 24 | 25 | constructor({ binCount, ...args }: IHistogramChart) { 26 | super({ 27 | ...args, 28 | computeDomains: () => { 29 | // set up histogram 30 | const dataBin = bin() 31 | if (binCount) dataBin.thresholds(binCount) 32 | const bins = dataBin(args.data) 33 | 34 | // update domains 35 | return { 36 | x: [0, bins.length], 37 | y: [0, max(bins, (bin: Array) => +bin.length)!] 38 | } 39 | } 40 | }) 41 | 42 | // set up histogram 43 | const dataBin = bin() 44 | if (binCount) dataBin.thresholds(binCount) 45 | this.bins = dataBin(this.data) 46 | 47 | this.redraw() 48 | } 49 | 50 | redraw(): void { 51 | // set up histogram rects 52 | this.mountRects() 53 | 54 | // set tooltip type 55 | if (this.tooltip) { 56 | this.tooltip.update({ legendObject: TooltipSymbol.SQUARE }) 57 | this.tooltip.hide() 58 | } 59 | 60 | // generate delaunator 61 | this.mountDelaunay() 62 | 63 | // mount legend if any 64 | this.mountLegend(LegendSymbol.SQUARE) 65 | 66 | // mount brush if necessary 67 | this.mountBrush(this.brush) 68 | } 69 | 70 | /** 71 | * Mount the histogram rectangles. 72 | */ 73 | mountRects(): void { 74 | this.rects = this.bins.map((bin) => { 75 | const rect = new Rect({ 76 | data: bin, 77 | xScale: this.xScale, 78 | yScale: this.yScale, 79 | color: this.colors[0], 80 | fillOpacity: 0.5, 81 | strokeWidth: 0, 82 | xAccessor: (bin) => bin.x0, 83 | yAccessor: (bin) => bin.length, 84 | widthAccessor: (bin) => this.xScale.scaleObject(bin.x1)! - this.xScale.scaleObject(bin.x0)!, 85 | heightAccessor: (bin) => -bin.length 86 | }) 87 | rect.mountTo(this.container!) 88 | return rect 89 | }) 90 | } 91 | 92 | /** 93 | * Handle move events from the delaunay triangulation. 94 | * 95 | * @returns handler function. 96 | */ 97 | onPointHandler(): InteractionFunction { 98 | return ([point]) => { 99 | this.activeBar = point.index 100 | 101 | // set tooltip if necessary 102 | if (!this.tooltip) return 103 | this.tooltip.update({ data: [point] }) 104 | } 105 | } 106 | 107 | /** 108 | * Handle leaving the delaunay triangulation area. 109 | * 110 | * @returns handler function. 111 | */ 112 | onLeaveHandler() { 113 | return () => { 114 | this.activeBar = -1 115 | if (this.tooltip) this.tooltip.hide() 116 | } 117 | } 118 | 119 | /** 120 | * Mount new delaunay triangulation. 121 | */ 122 | mountDelaunay(): void { 123 | this.delaunayBar = new Rect({ 124 | xScale: this.xScale, 125 | yScale: this.yScale, 126 | xAccessor: (bin) => bin.x0, 127 | yAccessor: (bin) => bin.length, 128 | widthAccessor: (bin) => bin.x1 - bin.x0, 129 | heightAccessor: (bin) => -bin.length 130 | }) 131 | this.delaunay = new Delaunay({ 132 | points: this.bins.map((bin) => ({ 133 | x: (bin.x1 + bin.x0) / 2, 134 | y: 0, 135 | time: bin.x0, 136 | count: bin.length 137 | })), 138 | xAccessor: (d) => d.x, 139 | yAccessor: (d) => d.y, 140 | xScale: this.xScale, 141 | yScale: this.yScale, 142 | onPoint: this.onPointHandler(), 143 | onLeave: this.onLeaveHandler() 144 | }) 145 | this.delaunay.mountTo(this.container) 146 | } 147 | 148 | get activeBar() { 149 | return this._activeBar 150 | } 151 | 152 | set activeBar(i: number) { 153 | // if rexts are not set yet, abort 154 | if (!this.rects) { 155 | console.error('error: can not set active bar, rects are empty') 156 | return 157 | } 158 | 159 | // if a bar was previously set, de-set it 160 | if (this._activeBar !== -1) { 161 | this.rects[this._activeBar].update({ fillOpacity: 0.5 }) 162 | } 163 | 164 | // set state 165 | this._activeBar = i 166 | 167 | // set point to active 168 | if (i !== -1) this.rects[i].update({ fillOpacity: 1 }) 169 | } 170 | } 171 | -------------------------------------------------------------------------------- /lib/src/charts/line.ts: -------------------------------------------------------------------------------- 1 | import Line from '../components/line' 2 | import Area from '../components/area' 3 | import constants from '../misc/constants' 4 | import Delaunay, { IDelaunay } from '../components/delaunay' 5 | import { makeAccessorFunction } from '../misc/utility' 6 | import { AccessorFunction, LegendSymbol, InteractionFunction, EmptyInteractionFunction } from '../misc/typings' 7 | import { IPoint } from '../components/point' 8 | import { TooltipSymbol } from '../components/tooltip' 9 | import AbstractChart, { IAbstractChart } from './abstractChart' 10 | 11 | type ConfidenceBand = [AccessorFunction | string, AccessorFunction | string] 12 | 13 | interface ILineChart extends IAbstractChart { 14 | /** specifies for which sub-array of data an area should be shown. Boolean if data is a simple array */ 15 | area?: Array | boolean 16 | 17 | /** array with two elements specifying how to access the lower (first) and upper (second) value for the confidence band. The two elements work like accessors and are either a string or a function */ 18 | confidenceBand?: ConfidenceBand 19 | 20 | /** custom parameters passed to the voronoi generator */ 21 | voronoi?: Partial 22 | 23 | /** function specifying whether or not to show a given datapoint */ 24 | defined?: (point: any) => boolean 25 | 26 | /** accessor specifying for a given data point whether or not to show it as active */ 27 | activeAccessor?: AccessorFunction | string 28 | 29 | /** custom parameters passed to the active point generator. See {@see Point} for a list of parameters */ 30 | activePoint?: Partial 31 | } 32 | 33 | export default class LineChart extends AbstractChart { 34 | delaunay?: Delaunay 35 | defined?: (point: any) => boolean 36 | activeAccessor?: AccessorFunction 37 | activePoint?: Partial 38 | area?: Array | boolean 39 | confidenceBand?: ConfidenceBand 40 | delaunayParams?: Partial 41 | 42 | // one delaunay point per line 43 | delaunayPoints: Array = [] 44 | 45 | constructor({ area, confidenceBand, voronoi, defined, activeAccessor, activePoint, ...args }: ILineChart) { 46 | super(args) 47 | 48 | // if data is not a 2d array, die 49 | if (!Array.isArray(args.data[0])) throw new Error('data is not a 2-dimensional array.') 50 | 51 | if (defined) this.defined = defined 52 | if (activeAccessor) this.activeAccessor = makeAccessorFunction(activeAccessor) 53 | this.activePoint = activePoint ?? this.activePoint 54 | this.area = area ?? this.area 55 | this.confidenceBand = confidenceBand ?? this.confidenceBand 56 | this.delaunayParams = voronoi ?? this.delaunayParams 57 | 58 | this.redraw() 59 | } 60 | 61 | redraw(): void { 62 | this.mountLines() 63 | this.mountActivePoints(this.activePoint ?? {}) 64 | 65 | // generate areas if necessary 66 | this.mountAreas(this.area || false) 67 | 68 | // set tooltip type 69 | if (this.tooltip) { 70 | this.tooltip.update({ legendObject: TooltipSymbol.LINE }) 71 | this.tooltip.hide() 72 | } 73 | 74 | // generate confidence band if necessary 75 | if (this.confidenceBand) { 76 | this.mountConfidenceBand(this.confidenceBand) 77 | } 78 | 79 | // add markers and baselines 80 | this.mountMarkers() 81 | this.mountBaselines() 82 | 83 | // set up delaunay triangulation 84 | this.mountDelaunay(this.delaunayParams ?? {}) 85 | 86 | // mount legend if any 87 | this.mountLegend(LegendSymbol.LINE) 88 | 89 | // mount brush if necessary 90 | this.mountBrush(this.brush) 91 | } 92 | 93 | /** 94 | * Mount lines for each array of data points. 95 | */ 96 | mountLines(): void { 97 | // abort if container is not defined yet 98 | if (!this.container) { 99 | console.error('error: container is not defined yet') 100 | return 101 | } 102 | 103 | // compute lines and delaunay points 104 | this.data.forEach((lineData, index) => { 105 | const line = new Line({ 106 | data: lineData, 107 | xAccessor: this.xAccessor, 108 | yAccessor: this.yAccessor, 109 | xScale: this.xScale, 110 | yScale: this.yScale, 111 | color: this.colors[index], 112 | defined: this.defined 113 | }) 114 | this.delaunayPoints[index] = this.generatePoint({ radius: 3 }) 115 | line.mountTo(this.container!) 116 | }) 117 | } 118 | 119 | /** 120 | * If an active accessor is specified, mount active points. 121 | * @param params custom parameters for point generation. See {@see Point} for a list of options. 122 | */ 123 | mountActivePoints(params: Partial): void { 124 | // abort if container is not defined yet 125 | if (!this.container) { 126 | console.error('error: container is not defined yet') 127 | return 128 | } 129 | 130 | if (!this.activeAccessor) return 131 | this.data.forEach((pointArray, index) => { 132 | pointArray.filter(this.activeAccessor).forEach((data: any) => { 133 | const point = this.generatePoint({ 134 | data, 135 | color: this.colors[index], 136 | radius: 3, 137 | ...params 138 | }) 139 | point.mountTo(this.container!) 140 | }) 141 | }) 142 | } 143 | 144 | /** 145 | * Mount all specified areas. 146 | * 147 | * @param area specifies for which sub-array of data an area should be shown. Boolean if data is a simple array. 148 | */ 149 | mountAreas(area: Array | boolean): void { 150 | if (typeof area === 'undefined') return 151 | 152 | let areas: Array = [] 153 | const areaGenerator = (lineData: any, index: number) => 154 | new Area({ 155 | data: lineData, 156 | xAccessor: this.xAccessor, 157 | yAccessor: this.yAccessor, 158 | xScale: this.xScale, 159 | yScale: this.yScale, 160 | color: this.colors[index], 161 | defined: this.defined 162 | }) 163 | 164 | // if area is boolean and truthy, generate areas for each line 165 | if (typeof area === 'boolean' && area) { 166 | areas = this.data.map(areaGenerator) 167 | 168 | // if area is array, only show areas for the truthy lines 169 | } else if (Array.isArray(area)) { 170 | areas = this.data.filter((lineData, index) => area[index]).map(areaGenerator) 171 | } 172 | 173 | // mount areas 174 | areas.forEach((area) => area.mountTo(this.container)) 175 | } 176 | 177 | /** 178 | * Mount the confidence band specified by two accessors. 179 | * 180 | * @param lowerAccessor for the lower confidence bound. Either a string (specifying the property of the object representing the lower bound) or a function (returning the lower bound when given a data point). 181 | * @param upperAccessor for the upper confidence bound. Either a string (specifying the property of the object representing the upper bound) or a function (returning the upper bound when given a data point). 182 | */ 183 | mountConfidenceBand([lowerAccessor, upperAccessor]: ConfidenceBand): void { 184 | // abort if container is not set 185 | if (!this.container) { 186 | console.error('error: container not defined yet') 187 | return 188 | } 189 | 190 | const confidenceBandGenerator = new Area({ 191 | data: this.data[0], // confidence band only makes sense for one line 192 | xAccessor: this.xAccessor, 193 | y0Accessor: makeAccessorFunction(lowerAccessor), 194 | yAccessor: makeAccessorFunction(upperAccessor), 195 | xScale: this.xScale, 196 | yScale: this.yScale, 197 | color: '#aaa' 198 | }) 199 | confidenceBandGenerator.mountTo(this.container) 200 | } 201 | 202 | /** 203 | * Mount markers, if any. 204 | */ 205 | mountMarkers(): void { 206 | // abort if content is not set yet 207 | if (!this.content) { 208 | console.error('error: content container not set yet') 209 | return 210 | } 211 | 212 | const markerContainer = this.content.append('g').attr('transform', `translate(${this.left},${this.top})`) 213 | this.markers.forEach((marker) => { 214 | const x = this.xScale.scaleObject(this.xAccessor(marker)) 215 | markerContainer 216 | .append('line') 217 | .classed('line-marker', true) 218 | .attr('x1', x!) 219 | .attr('x2', x!) 220 | .attr('y1', this.yScale.range[0] + this.buffer) 221 | .attr('y2', this.yScale.range[1] + this.buffer) 222 | markerContainer.append('text').classed('text-marker', true).attr('x', x!).attr('y', 8).text(marker.label) 223 | }) 224 | } 225 | 226 | mountBaselines(): void { 227 | // abort if content is not set yet 228 | if (!this.content) { 229 | console.error('error: content container not set yet') 230 | return 231 | } 232 | 233 | const baselineContainer = this.content.append('g').attr('transform', `translate(${this.left},${this.top})`) 234 | this.baselines.forEach((baseline) => { 235 | const y = this.yScale.scaleObject(this.yAccessor(baseline)) 236 | baselineContainer 237 | .append('line') 238 | .classed('line-baseline', true) 239 | .attr('x1', this.xScale.range[0] + this.buffer) 240 | .attr('x2', this.xScale.range[1] + this.buffer) 241 | .attr('y1', y!) 242 | .attr('y2', y!) 243 | baselineContainer 244 | .append('text') 245 | .classed('text-baseline', true) 246 | .attr('x', this.xScale.range[1] + this.buffer) 247 | .attr('y', y! - 2) 248 | .text(baseline.label) 249 | }) 250 | } 251 | 252 | /** 253 | * Handle incoming points from the delaunay move handler. 254 | * 255 | * @returns handler function. 256 | */ 257 | onPointHandler(): InteractionFunction { 258 | return (points) => { 259 | // pre-hide all points 260 | this.delaunayPoints.forEach((dp) => dp.dismount()) 261 | 262 | points.forEach((point) => { 263 | const index = point.arrayIndex || 0 264 | 265 | // set hover point 266 | this.delaunayPoints[index].update({ 267 | data: point, 268 | color: this.colors[index] 269 | }) 270 | this.delaunayPoints[index].mountTo(this.container) 271 | }) 272 | 273 | // set tooltip if necessary 274 | if (!this.tooltip) return 275 | this.tooltip.update({ data: points }) 276 | } 277 | } 278 | 279 | /** 280 | * Handles leaving the delaunay area. 281 | * 282 | * @returns handler function. 283 | */ 284 | onLeaveHandler(): EmptyInteractionFunction { 285 | return () => { 286 | this.delaunayPoints.forEach((dp) => dp.dismount()) 287 | if (this.tooltip) this.tooltip.hide() 288 | } 289 | } 290 | 291 | /** 292 | * Mount a new delaunay triangulation instance. 293 | * 294 | * @param customParameters custom parameters for {@link Delaunay}. 295 | */ 296 | mountDelaunay(customParameters: Partial): void { 297 | // abort if container is not set yet 298 | if (!this.container) { 299 | console.error('error: container not set yet') 300 | return 301 | } 302 | 303 | this.delaunay = new Delaunay({ 304 | points: this.data, 305 | xAccessor: this.xAccessor, 306 | yAccessor: this.yAccessor, 307 | xScale: this.xScale, 308 | yScale: this.yScale, 309 | onPoint: this.onPointHandler(), 310 | onLeave: this.onLeaveHandler(), 311 | defined: this.defined, 312 | ...customParameters 313 | }) 314 | this.delaunay.mountTo(this.container) 315 | } 316 | 317 | computeYAxisType(): void { 318 | // abort if no y axis is used 319 | if (!this.yAxis) { 320 | console.error('error: no y axis set') 321 | return 322 | } 323 | 324 | const flatData = this.data.flat() 325 | const yValue = this.yAccessor(flatData[0]) 326 | 327 | if (yValue instanceof Date) { 328 | this.yAxis.tickFormat = constants.axisFormat.date 329 | } else if (Number(yValue) === yValue) { 330 | this.yAxis.tickFormat = constants.axisFormat.number 331 | } 332 | } 333 | } 334 | -------------------------------------------------------------------------------- /lib/src/charts/scatter.ts: -------------------------------------------------------------------------------- 1 | import Delaunay from '../components/delaunay' 2 | import Rug, { RugOrientation } from '../components/rug' 3 | import { makeAccessorFunction } from '../misc/utility' 4 | import { AccessorFunction, LegendSymbol, InteractionFunction, EmptyInteractionFunction } from '../misc/typings' 5 | import Point from '../components/point' 6 | import { TooltipSymbol } from '../components/tooltip' 7 | import AbstractChart, { IAbstractChart } from './abstractChart' 8 | 9 | interface IScatterChart extends IAbstractChart { 10 | /** accessor specifying the size of a data point. Can be either a string (name of the size field) or a function (receiving a data point and returning its size) */ 11 | sizeAccessor?: string | AccessorFunction 12 | 13 | /** whether or not to generate a rug for the x axis */ 14 | xRug?: boolean 15 | 16 | /** whether or not to generate a rug for the x axis */ 17 | yRug?: boolean 18 | } 19 | 20 | interface ActivePoint { 21 | i: number 22 | j: number 23 | } 24 | 25 | export default class ScatterChart extends AbstractChart { 26 | points?: Array 27 | delaunay?: Delaunay 28 | delaunayPoint?: Point 29 | sizeAccessor: AccessorFunction 30 | showXRug: boolean 31 | xRug?: Rug 32 | showYRug: boolean 33 | yRug?: Rug 34 | _activePoint: ActivePoint = { i: -1, j: -1 } 35 | 36 | constructor({ sizeAccessor, xRug, yRug, ...args }: IScatterChart) { 37 | super(args) 38 | this.showXRug = xRug ?? false 39 | this.showYRug = yRug ?? false 40 | 41 | this.sizeAccessor = sizeAccessor ? makeAccessorFunction(sizeAccessor) : () => 3 42 | 43 | this.redraw() 44 | } 45 | 46 | redraw(): void { 47 | // set up rugs if necessary 48 | this.mountRugs() 49 | 50 | // set tooltip type 51 | if (this.tooltip) { 52 | this.tooltip.update({ legendObject: TooltipSymbol.CIRCLE }) 53 | this.tooltip.hide() 54 | } 55 | 56 | // set up points 57 | this.mountPoints() 58 | 59 | // generate delaunator 60 | this.mountDelaunay() 61 | 62 | // mount legend if any 63 | this.mountLegend(LegendSymbol.CIRCLE) 64 | 65 | // mount brush if necessary 66 | this.mountBrush(this.brush) 67 | } 68 | 69 | /** 70 | * Mount new rugs. 71 | */ 72 | mountRugs(): void { 73 | // if content is not set yet, abort 74 | if (!this.content) { 75 | console.error('error: content not set yet') 76 | return 77 | } 78 | 79 | if (this.showXRug) { 80 | this.xRug = new Rug({ 81 | accessor: this.xAccessor, 82 | scale: this.xScale, 83 | colors: this.colors, 84 | data: this.data, 85 | left: this.plotLeft, 86 | top: this.innerHeight + this.plotTop + this.buffer, 87 | orientation: RugOrientation.HORIZONTAL // TODO how to pass tickLength etc? 88 | }) 89 | this.xRug.mountTo(this.content) 90 | } 91 | if (this.showYRug) { 92 | this.yRug = new Rug({ 93 | accessor: this.yAccessor, 94 | scale: this.yScale, 95 | colors: this.colors, 96 | data: this.data, 97 | left: this.left, 98 | top: this.plotTop, 99 | orientation: RugOrientation.VERTICAL 100 | }) 101 | this.yRug.mountTo(this.content) 102 | } 103 | } 104 | 105 | /** 106 | * Mount scatter points. 107 | */ 108 | mountPoints(): void { 109 | // if container is not set yet, abort 110 | if (!this.container) { 111 | console.error('error: container not set yet') 112 | return 113 | } 114 | 115 | this.points = this.data.map((pointSet, i) => 116 | pointSet.map((data: any) => { 117 | const point = this.generatePoint({ 118 | data, 119 | color: this.colors[i], 120 | radius: this.sizeAccessor(data) as number, 121 | fillOpacity: 0.3, 122 | strokeWidth: 1 123 | }) 124 | point.mountTo(this.container!) 125 | return point 126 | }) 127 | ) 128 | } 129 | 130 | /** 131 | * Handle incoming points from the delaunay triangulation. 132 | * 133 | * @returns handler function 134 | */ 135 | onPointHandler(): InteractionFunction { 136 | return ([point]) => { 137 | this.activePoint = { i: point.arrayIndex ?? 0, j: point.index } 138 | 139 | // set tooltip if necessary 140 | if (!this.tooltip) return 141 | this.tooltip.update({ data: [point] }) 142 | } 143 | } 144 | 145 | /** 146 | * Handle leaving the delaunay area. 147 | * 148 | * @returns handler function 149 | */ 150 | onLeaveHandler(): EmptyInteractionFunction { 151 | return () => { 152 | this.activePoint = { i: -1, j: -1 } 153 | if (this.tooltip) this.tooltip.hide() 154 | } 155 | } 156 | 157 | /** 158 | * Mount new delaunay triangulation instance. 159 | */ 160 | mountDelaunay(): void { 161 | // if container is not set yet, abort 162 | if (!this.container) { 163 | console.error('error: container not set yet') 164 | return 165 | } 166 | 167 | this.delaunayPoint = this.generatePoint({ radius: 3 }) 168 | this.delaunay = new Delaunay({ 169 | points: this.data, 170 | xAccessor: this.xAccessor, 171 | yAccessor: this.yAccessor, 172 | xScale: this.xScale, 173 | yScale: this.yScale, 174 | nested: true, 175 | onPoint: this.onPointHandler(), 176 | onLeave: this.onLeaveHandler() 177 | }) 178 | this.delaunay.mountTo(this.container) 179 | } 180 | 181 | get activePoint() { 182 | return this._activePoint 183 | } 184 | 185 | set activePoint({ i, j }: ActivePoint) { 186 | // abort if points are not set yet 187 | if (!this.points) { 188 | console.error('error: cannot set point, as points are not set') 189 | return 190 | } 191 | 192 | // if a point was previously set, de-set it 193 | if (this._activePoint.i !== -1 && this._activePoint.j !== -1) { 194 | this.points[this._activePoint.i][this._activePoint.j].update({ 195 | fillOpacity: 0.3 196 | }) 197 | } 198 | 199 | // set state 200 | this._activePoint = { i, j } 201 | 202 | // set point to active 203 | if (i !== -1 && j !== -1) this.points[i][j].update({ fillOpacity: 1 }) 204 | } 205 | } 206 | -------------------------------------------------------------------------------- /lib/src/components/abstractShape.ts: -------------------------------------------------------------------------------- 1 | import { SvgD3Selection } from '../misc/typings' 2 | import Scale from './scale' 3 | 4 | export interface IAbstractShape { 5 | /** datapoint used to generate shape */ 6 | data?: any 7 | 8 | /** scale used to compute x values */ 9 | xScale: Scale 10 | 11 | /** scale used to compute y values */ 12 | yScale: Scale 13 | 14 | /** color used for fill and strokes */ 15 | color?: string 16 | 17 | /** opacity of the shape fill */ 18 | fillOpacity?: number 19 | 20 | /** width of the stroke around the shape */ 21 | strokeWidth?: number 22 | } 23 | 24 | export default abstract class AbstractShape { 25 | data: any 26 | shapeObject: any 27 | xScale: Scale 28 | yScale: Scale 29 | color: string 30 | fillOpacity = 1 31 | strokeWidth = 0 32 | 33 | constructor({ data, xScale, yScale, color, fillOpacity, strokeWidth }: IAbstractShape) { 34 | this.data = data 35 | this.xScale = xScale 36 | this.yScale = yScale 37 | this.color = color ?? 'black' 38 | this.fillOpacity = fillOpacity ?? this.fillOpacity 39 | this.strokeWidth = strokeWidth ?? this.strokeWidth 40 | } 41 | 42 | /** 43 | * Render the shape and mount it to the given node. 44 | * Implemented by classes extending AbstractShape. 45 | * 46 | * @param svg D3 node to mount the shape to 47 | */ 48 | abstract mountTo(svg: SvgD3Selection): void 49 | 50 | /** 51 | * Hide the shape by setting the opacity to 0. This doesn't remove the shape. 52 | */ 53 | hide(): void { 54 | if (this.shapeObject) this.shapeObject.attr('opacity', 0) 55 | } 56 | 57 | /** 58 | * Update the given parameters of the object. 59 | * Implemented by classes extending AbstractShape. 60 | * 61 | * @param args parameters to be updated 62 | */ 63 | abstract update(args: any): void 64 | 65 | /** 66 | * Update generic properties of the shape. 67 | * This method can be used in the implementations of {@link AbstractShape#update}. 68 | * 69 | * @param color new color of the shape. 70 | * @param fillOpacity new fill opacity of the shape. 71 | * @param strokeWidth new stroke width of the shape. 72 | */ 73 | updateGeneric({ 74 | color, 75 | fillOpacity, 76 | strokeWidth 77 | }: Pick): void { 78 | if (color) this.updateColor(color) 79 | if (fillOpacity) this.updateOpacity(fillOpacity) 80 | if (strokeWidth) this.updateStroke(strokeWidth) 81 | } 82 | 83 | /** 84 | * Update the color of the shape. 85 | * 86 | * @param color new color of the shape. 87 | */ 88 | updateColor(color: string): void { 89 | this.color = color 90 | this.updateProp('fill', color) 91 | } 92 | 93 | /** 94 | * Update the fill opacity of the shape. 95 | * 96 | * @param fillOpacity new fill opacity of the shape. 97 | */ 98 | updateOpacity(fillOpacity: number): void { 99 | this.fillOpacity = fillOpacity 100 | this.updateProp('fill-opacity', fillOpacity) 101 | } 102 | 103 | /** 104 | * Update the stroke width of the shape. 105 | * 106 | * @param strokeWidth new stroke width of the shape. 107 | */ 108 | updateStroke(strokeWidth: number): void { 109 | this.strokeWidth = strokeWidth 110 | this.updateProp('stroke-width', strokeWidth) 111 | } 112 | 113 | /** 114 | * Update an attribute of the raw shape node. 115 | * 116 | * @param name attribute name 117 | * @param value new value 118 | */ 119 | updateProp(name: string, value: number | string): void { 120 | if (this.shapeObject) this.shapeObject.attr(name, value) 121 | } 122 | 123 | /** 124 | * Remove the shape. 125 | */ 126 | dismount(): void { 127 | if (this.shapeObject) this.shapeObject.remove() 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /lib/src/components/area.ts: -------------------------------------------------------------------------------- 1 | import { area, curveCatmullRom, CurveFactory } from 'd3' 2 | import { AccessorFunction, DefinedFunction, SvgD3Selection } from '../misc/typings' 3 | import Scale from './scale' 4 | 5 | interface IArea { 6 | /** data for which the area should be created */ 7 | data: Array 8 | 9 | /** x accessor function */ 10 | xAccessor: AccessorFunction 11 | 12 | /** y accessor function */ 13 | yAccessor: AccessorFunction 14 | 15 | /** y base accessor function (defaults to 0) */ 16 | y0Accessor?: AccessorFunction 17 | 18 | /** alternative to yAccessor */ 19 | y1Accessor?: AccessorFunction 20 | 21 | /** scale used to scale elements in x direction */ 22 | xScale: Scale 23 | 24 | /** scale used to scale elements in y direction */ 25 | yScale: Scale 26 | 27 | /** curving function. See {@link https://github.com/d3/d3-shape#curves} for available curves in d3 */ 28 | curve?: CurveFactory 29 | 30 | /** color of the area */ 31 | color?: string 32 | 33 | /** specifies whether or not to show a given datapoint */ 34 | defined?: DefinedFunction 35 | } 36 | 37 | export default class Area { 38 | data: Array 39 | areaObject?: any 40 | index = 0 41 | color = 'none' 42 | 43 | constructor({ data, xAccessor, yAccessor, y0Accessor, y1Accessor, xScale, yScale, curve, color, defined }: IArea) { 44 | this.data = data 45 | this.color = color ?? this.color 46 | 47 | const y0 = y0Accessor ?? ((d) => 0) 48 | const y1 = y1Accessor ?? yAccessor 49 | 50 | // set up line object 51 | this.areaObject = area() 52 | .defined((d) => { 53 | if (y0(d) === null || y1(d) === null) return false 54 | return !defined ? true : defined(d) 55 | }) 56 | .x((d) => xScale.scaleObject(xAccessor(d))) 57 | .y1((d) => yScale.scaleObject(y1(d))) 58 | .y0((d) => yScale.scaleObject(y0(d))) 59 | .curve(curve ?? curveCatmullRom) 60 | } 61 | 62 | /** 63 | * Mount the area to a given d3 node. 64 | * 65 | * @param svg d3 node to mount the area to. 66 | */ 67 | mountTo(svg: SvgD3Selection): void { 68 | svg.append('path').classed('mg-area', true).attr('fill', this.color).datum(this.data).attr('d', this.areaObject) 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /lib/src/components/axis.ts: -------------------------------------------------------------------------------- 1 | import { axisTop, axisLeft, axisRight, axisBottom, format, timeFormat } from 'd3' 2 | import constants from '../misc/constants' 3 | import { GD3Selection, LineD3Selection, TextD3Selection, TextFunction } from '../misc/typings' 4 | import Scale from './scale' 5 | 6 | const DEFAULT_VERTICAL_OFFSET = 35 7 | const DEFAULT_HORIZONTAL_OFFSET = 45 8 | 9 | type NumberFormatFunction = (x: number) => string 10 | type DateFormatFunction = (x: Date) => string 11 | type FormatFunction = NumberFormatFunction | DateFormatFunction 12 | 13 | export enum AxisOrientation { 14 | TOP = 'top', 15 | BOTTOM = 'bottom', 16 | RIGHT = 'right', 17 | LEFT = 'left' 18 | } 19 | 20 | enum AxisFormat { 21 | DATE = 'date', 22 | NUMBER = 'number', 23 | PERCENTAGE = 'percentage' 24 | } 25 | 26 | export interface IAxis { 27 | /** scale of the axis */ 28 | scale: Scale 29 | 30 | /** buffer used by the chart, necessary to compute margins */ 31 | buffer: number 32 | 33 | /** whether or not to show the axis */ 34 | show?: boolean 35 | 36 | /** orientation of the axis */ 37 | orientation?: AxisOrientation 38 | 39 | /** optional label to place beside the axis */ 40 | label?: string 41 | 42 | /** offset between label and axis */ 43 | labelOffset?: number 44 | 45 | /** translation from the top of the chart's box to render the axis */ 46 | top?: number 47 | 48 | /** translation from the left of the chart's to render the axis */ 49 | left?: number 50 | 51 | /** can be 1) a function to format a given tick or a specifier, or 2) one of the available standard formatting types (date, number, percentage) or a string for d3-format */ 52 | tickFormat?: TextFunction | AxisFormat | string 53 | 54 | /** number of ticks to render, defaults to 3 for vertical and 6 for horizontal axes */ 55 | tickCount?: number 56 | 57 | /** whether or not to render a compact version of the axis (clamps the main axis line at the outermost ticks) */ 58 | compact?: boolean 59 | 60 | /** prefix for tick labels */ 61 | prefix?: string 62 | 63 | /** suffix for tick labels */ 64 | suffix?: string 65 | 66 | /** overwrite d3's default tick lengths */ 67 | tickLength?: number 68 | 69 | /** draw extended ticks into the graph (used to make a grid) */ 70 | extendedTicks?: boolean 71 | 72 | /** if extended ticks are used, this parameter specifies the inner length of ticks */ 73 | height?: number 74 | } 75 | 76 | export default class Axis { 77 | label = '' 78 | labelOffset = 0 79 | top = 0 80 | left = 0 81 | scale: Scale 82 | orientation = AxisOrientation.BOTTOM 83 | axisObject: any 84 | compact = false 85 | extendedTicks = false 86 | buffer = 0 87 | height = 0 88 | prefix = '' 89 | suffix = '' 90 | 91 | constructor({ 92 | orientation, 93 | label, 94 | labelOffset, 95 | top, 96 | left, 97 | height, 98 | scale, 99 | tickFormat, 100 | tickCount, 101 | compact, 102 | buffer, 103 | prefix, 104 | suffix, 105 | tickLength, 106 | extendedTicks 107 | }: IAxis) { 108 | this.scale = scale 109 | this.label = label ?? this.label 110 | this.buffer = buffer ?? this.buffer 111 | this.top = top ?? this.top 112 | this.left = left ?? this.left 113 | this.height = height ?? this.height 114 | this.orientation = orientation ?? this.orientation 115 | this.compact = compact ?? this.compact 116 | this.prefix = prefix ?? this.prefix 117 | this.suffix = suffix ?? this.suffix 118 | if (typeof tickLength !== 'undefined') this.tickLength = tickLength 119 | this.extendedTicks = extendedTicks ?? this.extendedTicks 120 | this.setLabelOffset(labelOffset) 121 | 122 | this.setupAxisObject() 123 | 124 | // set or compute tickFormat 125 | if (tickFormat) this.tickFormat = tickFormat 126 | this.tickCount = tickCount ?? (this.isVertical ? 3 : 6) 127 | } 128 | 129 | /** 130 | * Set the label offset. 131 | * 132 | * @param labelOffset offset of the label. 133 | */ 134 | setLabelOffset(labelOffset?: number): void { 135 | this.labelOffset = 136 | typeof labelOffset !== 'undefined' 137 | ? labelOffset 138 | : this.isVertical 139 | ? DEFAULT_HORIZONTAL_OFFSET 140 | : DEFAULT_VERTICAL_OFFSET 141 | } 142 | 143 | /** 144 | * Set up the main axis object. 145 | */ 146 | setupAxisObject(): void { 147 | switch (this.orientation) { 148 | case constants.axisOrientation.top: 149 | this.axisObject = axisTop(this.scale.scaleObject) 150 | break 151 | case constants.axisOrientation.left: 152 | this.axisObject = axisLeft(this.scale.scaleObject) 153 | break 154 | case constants.axisOrientation.right: 155 | this.axisObject = axisRight(this.scale.scaleObject) 156 | break 157 | default: 158 | this.axisObject = axisBottom(this.scale.scaleObject) 159 | break 160 | } 161 | } 162 | 163 | /** 164 | * Get the domain object call function. 165 | * @returns that mounts the domain when called. 166 | */ 167 | domainObject() { 168 | return (g: GD3Selection): LineD3Selection => 169 | g 170 | .append('line') 171 | .classed('domain', true) 172 | .attr('x1', this.isVertical ? 0.5 : this.compact ? this.buffer : 0) 173 | .attr('x2', this.isVertical ? 0.5 : this.compact ? this.scale.range[1] : this.scale.range[1] + 2 * this.buffer) 174 | .attr('y1', this.isVertical ? (this.compact ? this.top + 0.5 : 0.5) : 0) 175 | .attr( 176 | 'y2', 177 | this.isVertical ? (this.compact ? this.scale.range[0] + 0.5 : this.scale.range[0] + 2 * this.buffer + 0.5) : 0 178 | ) 179 | } 180 | 181 | /** 182 | * Get the label object call function. 183 | * @returns {Function} that mounts the label when called. 184 | */ 185 | labelObject(): (node: GD3Selection) => TextD3Selection { 186 | const value = Math.abs(this.scale.range[0] - this.scale.range[1]) / 2 187 | const xValue = this.isVertical ? -this.labelOffset : value 188 | const yValue = this.isVertical ? value : this.labelOffset 189 | return (g) => 190 | g 191 | .append('text') 192 | .attr('x', xValue) 193 | .attr('y', yValue) 194 | .attr('text-anchor', 'middle') 195 | .classed('label', true) 196 | .attr('transform', this.isVertical ? `rotate(${-90} ${xValue},${yValue})` : '') 197 | .text(this.label) 198 | } 199 | 200 | get isVertical(): boolean { 201 | return [constants.axisOrientation.left, constants.axisOrientation.right].includes(this.orientation) 202 | } 203 | 204 | get innerLeft(): number { 205 | return this.isVertical ? 0 : this.buffer 206 | } 207 | 208 | get innerTop(): number { 209 | return this.isVertical ? this.buffer : 0 210 | } 211 | 212 | get tickAttribute(): string { 213 | return this.isVertical ? 'x1' : 'y1' 214 | } 215 | 216 | get extendedTickLength(): number { 217 | const factor = this.isVertical ? 1 : -1 218 | return factor * (this.height + 2 * this.buffer) 219 | } 220 | 221 | /** 222 | * Mount the axis to the given d3 node. 223 | * @param svg d3 node. 224 | */ 225 | mountTo(svg: GD3Selection): void { 226 | // set up axis container 227 | const axisContainer = svg 228 | .append('g') 229 | .attr('transform', `translate(${this.left},${this.top})`) 230 | .classed('mg-axis', true) 231 | 232 | // if no extended ticks are used, draw the domain line 233 | if (!this.extendedTicks) axisContainer.call(this.domainObject()) 234 | 235 | // mount axis but remove default-generated domain 236 | axisContainer 237 | .append('g') 238 | .attr('transform', `translate(${this.innerLeft},${this.innerTop})`) 239 | .call(this.axisObject) 240 | .call((g) => g.select('.domain').remove()) 241 | 242 | // if necessary, make ticks longer 243 | if (this.extendedTicks) { 244 | axisContainer.call((g) => 245 | g.selectAll('.tick line').attr(this.tickAttribute, this.extendedTickLength).attr('opacity', 0.3) 246 | ) 247 | } 248 | 249 | // if necessary, add label 250 | if (this.label !== '') axisContainer.call(this.labelObject()) 251 | } 252 | 253 | /** 254 | * Compute the time formatting function based on the time domain. 255 | * @returns d3 function for formatting time. 256 | */ 257 | diffToTimeFormat(): FormatFunction { 258 | const diff = Math.abs(this.scale.domain[1] - this.scale.domain[0]) / 1000 259 | 260 | const millisecondDiff = diff < 1 261 | const secondDiff = diff < 60 262 | const dayDiff = diff / (60 * 60) < 24 263 | const fourDaysDiff = diff / (60 * 60) < 24 * 4 264 | const manyDaysDiff = diff / (60 * 60 * 24) < 60 265 | const manyMonthsDiff = diff / (60 * 60 * 24) < 365 266 | 267 | return millisecondDiff 268 | ? timeFormat('%M:%S.%L') 269 | : secondDiff 270 | ? timeFormat('%M:%S') 271 | : dayDiff 272 | ? timeFormat('%H:%M') 273 | : fourDaysDiff || manyDaysDiff || manyMonthsDiff 274 | ? timeFormat('%b %d') 275 | : timeFormat('%Y') 276 | } 277 | 278 | /** 279 | * Get the d3 number formatting function for an abstract number type. 280 | * 281 | * @param formatType abstract format to be converted (number, date, percentage) 282 | * @returns d3 formatting function for the given abstract number type. 283 | */ 284 | stringToFormat(formatType: AxisFormat | string): FormatFunction { 285 | switch (formatType) { 286 | case constants.axisFormat.number: 287 | return this.isVertical ? format('~s') : format('') 288 | case constants.axisFormat.date: 289 | return this.diffToTimeFormat() 290 | case constants.axisFormat.percentage: 291 | return format('.0%') 292 | default: 293 | return format(formatType) 294 | } 295 | } 296 | 297 | get tickFormat() { 298 | return this.axisObject.tickFormat() 299 | } 300 | 301 | set tickFormat(tickFormat: FormatFunction | string) { 302 | // if tickFormat is a function, apply it directly 303 | const formatFunction = typeof tickFormat === 'function' ? tickFormat : this.stringToFormat(tickFormat) 304 | 305 | this.axisObject.tickFormat((d: any) => `${this.prefix}${formatFunction(d)}${this.suffix}`) 306 | } 307 | 308 | get tickCount() { 309 | return this.axisObject.ticks() 310 | } 311 | 312 | set tickCount(tickCount: number) { 313 | this.axisObject.ticks(tickCount) 314 | } 315 | 316 | get tickLength() { 317 | return this.axisObject.tickSize() 318 | } 319 | 320 | set tickLength(length: number) { 321 | this.axisObject.tickSize(length) 322 | } 323 | } 324 | -------------------------------------------------------------------------------- /lib/src/components/delaunay.ts: -------------------------------------------------------------------------------- 1 | import { Delaunay as DelaunayObject, pointer } from 'd3' 2 | import { 3 | AccessorFunction, 4 | InteractionFunction, 5 | EmptyInteractionFunction, 6 | DefinedFunction, 7 | GenericD3Selection 8 | } from '../misc/typings' 9 | import Scale from './scale' 10 | 11 | export interface IDelaunay { 12 | /** raw data basis for delaunay computations, can be nested */ 13 | points: Array 14 | 15 | /** function to access the x value for a given data point */ 16 | xAccessor: AccessorFunction 17 | 18 | /** function to access the y value for a given data point */ 19 | yAccessor: AccessorFunction 20 | 21 | /** scale used to scale elements in x direction */ 22 | xScale: Scale 23 | 24 | /** scale used to scale elements in y direction */ 25 | yScale: Scale 26 | 27 | /** function called with the array of nearest points on mouse movement -- if aggregate is false, the array will contain at most one element */ 28 | onPoint?: InteractionFunction 29 | 30 | /** function called when the delaunay area is left */ 31 | onLeave?: EmptyInteractionFunction 32 | 33 | /** function called with the array of nearest points on mouse click in the delaunay area -- if aggregate is false, the array will contain at most one element */ 34 | onClick?: InteractionFunction 35 | 36 | /** whether or not the points array contains sub-arrays */ 37 | nested?: boolean 38 | 39 | /** if multiple points have the same x value and should be shown together, aggregate can be set to true */ 40 | aggregate?: boolean 41 | 42 | /** optional function specifying whether or not to show a given datapoint */ 43 | defined?: DefinedFunction 44 | } 45 | 46 | export default class Delaunay { 47 | points?: Array 48 | aggregatedPoints: any 49 | delaunay: any 50 | xScale: Scale 51 | yScale: Scale 52 | xAccessor: AccessorFunction 53 | yAccessor: AccessorFunction 54 | aggregate = false 55 | onPoint: InteractionFunction 56 | onClick?: InteractionFunction 57 | onLeave: EmptyInteractionFunction 58 | 59 | constructor({ 60 | points, 61 | xAccessor, 62 | yAccessor, 63 | xScale, 64 | yScale, 65 | onPoint, 66 | onLeave, 67 | onClick, 68 | nested, 69 | aggregate, 70 | defined 71 | }: IDelaunay) { 72 | this.xAccessor = xAccessor 73 | this.yAccessor = yAccessor 74 | this.xScale = xScale 75 | this.yScale = yScale 76 | this.onPoint = onPoint ?? (() => null) 77 | this.onLeave = onLeave ?? (() => null) 78 | this.onClick = onClick ?? this.onClick 79 | this.aggregate = aggregate ?? this.aggregate 80 | 81 | // normalize passed points 82 | const isNested = nested ?? (Array.isArray(points[0]) && points.length > 1) 83 | this.normalizePoints({ points, nested: isNested, aggregate, defined }) 84 | 85 | // set up delaunay 86 | this.mountDelaunay(isNested, this.aggregate) 87 | } 88 | 89 | /** 90 | * Create a new delaunay triangulation. 91 | * 92 | * @param isNested whether or not the data is nested 93 | * @param aggregate whether or not to aggregate points based on their x value 94 | */ 95 | mountDelaunay(isNested: boolean, aggregate: boolean): void { 96 | // if points are not set yet, stop 97 | if (!this.points) { 98 | console.error('error: points not defined') 99 | return 100 | } 101 | 102 | this.delaunay = DelaunayObject.from( 103 | this.points.map((point) => [ 104 | this.xAccessor(point) as number, 105 | (isNested && !aggregate ? this.yAccessor(point) : 0) as number 106 | ]) 107 | ) 108 | } 109 | 110 | /** 111 | * Normalize the passed data points. 112 | * 113 | * @param {Object} args argument object 114 | * @param {Array} args.points raw data array 115 | * @param {Boolean} args.isNested whether or not the points are nested 116 | * @param {Boolean} args.aggregate whether or not to aggregate points based on their x value 117 | * @param {Function} [args.defined] optional function specifying whether or not to show a given datapoint. 118 | */ 119 | normalizePoints({ 120 | points, 121 | nested, 122 | aggregate, 123 | defined 124 | }: Pick): void { 125 | this.points = nested 126 | ? points 127 | .map((pointArray, arrayIndex) => 128 | pointArray 129 | .filter((p: any) => !defined || defined(p)) 130 | .map((point: any, index: number) => ({ 131 | ...point, 132 | index, 133 | arrayIndex 134 | })) 135 | ) 136 | .flat(Infinity) 137 | : points 138 | .flat(Infinity) 139 | .filter((p) => !defined || defined(p)) 140 | .map((p, index) => ({ ...p, index })) 141 | 142 | // if points should be aggregated, hash-map them based on their x accessor value 143 | if (!aggregate) return 144 | this.aggregatedPoints = this.points.reduce((acc, val) => { 145 | const key = JSON.stringify(this.xAccessor(val)) 146 | if (!acc.has(key)) { 147 | acc.set(key, [val]) 148 | } else { 149 | acc.set(key, [val, ...acc.get(key)]) 150 | } 151 | return acc 152 | }, new Map()) 153 | } 154 | 155 | /** 156 | * Handle raw mouse movement inside the delaunay rect. 157 | * Finds the nearest data point(s) and calls onPoint. 158 | * 159 | * @param rawX raw x coordinate of the cursor. 160 | * @param rawY raw y coordinate of the cursor. 161 | */ 162 | gotPoint(rawX: number, rawY: number): void { 163 | // if points are empty, return 164 | if (!this.points) { 165 | console.error('error: points are empty') 166 | return 167 | } 168 | 169 | const x = this.xScale.scaleObject.invert(rawX) 170 | const y = this.yScale.scaleObject.invert(rawY) 171 | 172 | // find nearest point 173 | const index = this.delaunay.find(x, y) 174 | 175 | // if points should be aggregated, get all points with the same x value 176 | if (this.aggregate) { 177 | this.onPoint(this.aggregatedPoints.get(JSON.stringify(this.xAccessor(this.points[index])))) 178 | } else { 179 | this.onPoint([this.points[index]]) 180 | } 181 | } 182 | 183 | /** 184 | * Handle raw mouse clicks inside the delaunay rect. 185 | * Finds the nearest data point(s) and calls onClick. 186 | * 187 | * @param rawX raw x coordinate of the cursor. 188 | * @param rawY raw y coordinate of the cursor. 189 | */ 190 | clickedPoint(rawX: number, rawY: number): void { 191 | // if points empty, abort 192 | if (!this.points) { 193 | console.error('error: points empty') 194 | return 195 | } 196 | 197 | const x = this.xScale.scaleObject.invert(rawX) 198 | const y = this.yScale.scaleObject.invert(rawY) 199 | 200 | // find nearest point 201 | const index = this.delaunay.find(x, y) 202 | if (this.onClick) this.onClick({ ...this.points[index], index }) 203 | } 204 | 205 | /** 206 | * Mount the delaunator to a given d3 node. 207 | * 208 | * @param svg d3 selection to mount the delaunay elements to. 209 | */ 210 | mountTo(svg: GenericD3Selection): void { 211 | svg.on('mousemove', (event) => { 212 | const coords = pointer(event) 213 | this.gotPoint(coords[0], coords[1]) 214 | }) 215 | svg.on('mouseleave', () => { 216 | this.onLeave() 217 | }) 218 | svg.on('click', (event) => { 219 | const coords = pointer(event) 220 | this.clickedPoint(coords[0], coords[1]) 221 | }) 222 | } 223 | } 224 | -------------------------------------------------------------------------------- /lib/src/components/legend.ts: -------------------------------------------------------------------------------- 1 | import { select } from 'd3' 2 | import constants from '../misc/constants' 3 | import { LegendSymbol } from '../misc/typings' 4 | 5 | interface ILegend { 6 | /** array of descriptive legend strings */ 7 | legend: Array 8 | 9 | /** colors used for the legend -- will be darkened for better visibility */ 10 | colorScheme: Array 11 | 12 | /** symbol used in the legend */ 13 | symbolType: LegendSymbol 14 | } 15 | 16 | export default class Legend { 17 | legend: Array 18 | colorScheme: Array 19 | symbolType: LegendSymbol 20 | 21 | constructor({ legend, colorScheme, symbolType }: ILegend) { 22 | this.legend = legend 23 | this.colorScheme = colorScheme 24 | this.symbolType = symbolType 25 | } 26 | 27 | /** 28 | * Darken a given color by a given amount. 29 | * 30 | * @see https://css-tricks.com/snippets/javascript/lighten-darken-color/ 31 | * @param color hex color specifier 32 | * @param amount how much to darken the color 33 | * @returns darkened color in hex representation. 34 | */ 35 | darkenColor(color: string, amount: number): string { 36 | // remove hash 37 | color = color.slice(1) 38 | 39 | const num = parseInt(color, 16) 40 | 41 | const r = this.clamp((num >> 16) + amount) 42 | const b = this.clamp(((num >> 8) & 0x00ff) + amount) 43 | const g = this.clamp((num & 0x0000ff) + amount) 44 | 45 | return '#' + (g | (b << 8) | (r << 16)).toString(16) 46 | } 47 | 48 | /** 49 | * Clamp a number between 0 and 255. 50 | * 51 | * @param number number to be clamped. 52 | * @returns clamped number. 53 | */ 54 | clamp(number: number): number { 55 | return number > 255 ? 255 : number < 0 ? 0 : number 56 | } 57 | 58 | /** 59 | * Mount the legend to the given node. 60 | * 61 | * @param node d3 specifier or d3 node to mount the legend to. 62 | */ 63 | mountTo(node: any) { 64 | const symbol = constants.symbol[this.symbolType] 65 | 66 | // create d3 selection if necessary 67 | const target = typeof node === 'string' ? select(node).append('div') : node.append('div') 68 | target.classed('mg-legend', true) 69 | 70 | this.legend.forEach((item, index) => { 71 | target 72 | .append('span') 73 | .classed('text-legend', true) 74 | .style('color', this.darkenColor(this.colorScheme[index], -10)) 75 | .text(`${symbol} ${item}`) 76 | }) 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /lib/src/components/line.ts: -------------------------------------------------------------------------------- 1 | import { line, curveCatmullRom, CurveFactory } from 'd3' 2 | import { AccessorFunction, SvgD3Selection } from '../misc/typings' 3 | import Scale from './scale' 4 | 5 | interface ILine { 6 | /** array of datapoints used to create the line */ 7 | data: Array 8 | 9 | /** function to access the x value for a given datapoint */ 10 | xAccessor: AccessorFunction 11 | 12 | /** function to access the y value for a given datapoint */ 13 | yAccessor: AccessorFunction 14 | 15 | /** scale used to compute x values */ 16 | xScale: Scale 17 | 18 | /** scale used to compute y values */ 19 | yScale: Scale 20 | 21 | /** curving function used to draw the line. See {@link https://github.com/d3/d3-shape#curves} for curves available in d3 */ 22 | curve?: CurveFactory 23 | 24 | /** color of the line */ 25 | color?: string 26 | 27 | /** function specifying whether or not to show a given datapoint */ 28 | defined?: (datapoint: any) => boolean 29 | } 30 | 31 | export default class Line { 32 | lineObject?: any 33 | data: Array 34 | color: string 35 | 36 | constructor({ data, xAccessor, yAccessor, xScale, yScale, curve, color, defined }: ILine) { 37 | // cry if no data was passed 38 | if (!data) throw new Error('line needs data') 39 | this.data = data 40 | this.color = color ?? 'black' 41 | 42 | // set up line object 43 | this.lineObject = line() 44 | .defined((d) => { 45 | if (yAccessor(d) === null) return false 46 | if (typeof defined === 'undefined') return true 47 | return defined(d) 48 | }) 49 | .x((d) => xScale.scaleObject(xAccessor(d))) 50 | .y((d) => yScale.scaleObject(yAccessor(d))) 51 | .curve(curve ?? curveCatmullRom) 52 | } 53 | 54 | /** 55 | * Mount the line to the given d3 node. 56 | * 57 | * @param svg d3 node to mount the line to. 58 | */ 59 | mountTo(svg: SvgD3Selection): void { 60 | svg.append('path').classed('mg-line', true).attr('stroke', this.color).datum(this.data).attr('d', this.lineObject) 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /lib/src/components/point.ts: -------------------------------------------------------------------------------- 1 | import { AccessorFunction, SvgD3Selection } from '../misc/typings' 2 | import AbstractShape, { IAbstractShape } from './abstractShape' 3 | 4 | export interface IPoint extends IAbstractShape { 5 | /** function to access the x value of the point */ 6 | xAccessor: AccessorFunction 7 | 8 | /** function to access the y value of the point */ 9 | yAccessor: AccessorFunction 10 | 11 | /** radius of the point */ 12 | radius?: number 13 | } 14 | 15 | export default class Point extends AbstractShape { 16 | xAccessor: AccessorFunction 17 | yAccessor: AccessorFunction 18 | radius = 1 19 | 20 | constructor({ xAccessor, yAccessor, radius, ...args }: IPoint) { 21 | super(args) 22 | this.xAccessor = xAccessor 23 | this.yAccessor = yAccessor 24 | this.radius = radius ?? this.radius 25 | } 26 | 27 | get cx(): number { 28 | return this.xScale.scaleObject(this.xAccessor(this.data)) 29 | } 30 | 31 | get cy(): number { 32 | return this.yScale.scaleObject(this.yAccessor(this.data)) 33 | } 34 | 35 | /** 36 | * Mount the point to the given node. 37 | * 38 | * @param svg d3 node to mount the point to. 39 | */ 40 | mountTo(svg: SvgD3Selection): void { 41 | this.shapeObject = svg 42 | .append('circle') 43 | .attr('cx', this.cx) 44 | .attr('pointer-events', 'none') 45 | .attr('cy', this.cy) 46 | .attr('r', this.radius) 47 | .attr('fill', this.color) 48 | .attr('stroke', this.color) 49 | .attr('fill-opacity', this.fillOpacity) 50 | .attr('stroke-width', this.strokeWidth) 51 | } 52 | 53 | /** 54 | * Update the point. 55 | * 56 | * @param data point object 57 | */ 58 | update({ data, ...args }: IAbstractShape): void { 59 | this.updateGeneric(args) 60 | if (data) { 61 | this.data = data 62 | if (!this.shapeObject) return 63 | this.shapeObject.attr('cx', this.cx).attr('cy', this.cy).attr('opacity', 1) 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /lib/src/components/rect.ts: -------------------------------------------------------------------------------- 1 | import { AccessorFunction, GenericD3Selection } from '../misc/typings' 2 | import AbstractShape, { IAbstractShape } from './abstractShape' 3 | 4 | interface IRect extends IAbstractShape { 5 | /** function to access the x value of the rectangle */ 6 | xAccessor: AccessorFunction 7 | 8 | /** function to access the y value of the rectangle */ 9 | yAccessor: AccessorFunction 10 | 11 | /** function to access the width of the rectangle */ 12 | widthAccessor: AccessorFunction 13 | 14 | /** function to access the height of the rectangle */ 15 | heightAccessor: AccessorFunction 16 | } 17 | 18 | export default class Rect extends AbstractShape { 19 | xAccessor: AccessorFunction 20 | yAccessor: AccessorFunction 21 | widthAccessor: AccessorFunction 22 | heightAccessor: AccessorFunction 23 | 24 | constructor({ xAccessor, yAccessor, widthAccessor, heightAccessor, ...args }: IRect) { 25 | super(args) 26 | this.xAccessor = xAccessor 27 | this.yAccessor = yAccessor 28 | this.widthAccessor = widthAccessor 29 | this.heightAccessor = heightAccessor 30 | } 31 | 32 | get x(): number { 33 | return this.xScale.scaleObject(this.xAccessor(this.data)) 34 | } 35 | 36 | get y(): number { 37 | return this.yScale.scaleObject(this.yAccessor(this.data)) 38 | } 39 | 40 | get width(): number { 41 | return Math.max(0, Math.abs(this.widthAccessor(this.data))) 42 | } 43 | 44 | get height(): number { 45 | return Math.max(0, this.yScale.scaleObject(this.heightAccessor(this.data))) 46 | } 47 | 48 | /** 49 | * Mount the rectangle to the given node. 50 | * 51 | * @param svg d3 node to mount the rectangle to. 52 | */ 53 | mountTo(svg: GenericD3Selection): void { 54 | this.shapeObject = svg 55 | .append('rect') 56 | .attr('x', this.x) 57 | .attr('y', this.y) 58 | .attr('width', this.width) 59 | .attr('height', this.height) 60 | .attr('pointer-events', 'none') 61 | .attr('fill', this.color) 62 | .attr('stroke', this.color) 63 | .attr('fill-opacity', this.fillOpacity) 64 | .attr('stroke-width', this.strokeWidth) 65 | } 66 | 67 | /** 68 | * Update the rectangle. 69 | * 70 | * @param data updated data object. 71 | */ 72 | update({ data, ...args }: Partial): void { 73 | this.updateGeneric(args) 74 | if (data) { 75 | this.data = data 76 | if (!this.shapeObject) return 77 | this.shapeObject 78 | .attr('x', this.x) 79 | .attr('y', this.y) 80 | .attr('width', this.width) 81 | .attr('height', this.height) 82 | .attr('opacity', 1) 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /lib/src/components/rug.ts: -------------------------------------------------------------------------------- 1 | import constants from '../misc/constants' 2 | import { AccessorFunction, GenericD3Selection } from '../misc/typings' 3 | import Scale from './scale' 4 | 5 | export enum RugOrientation { 6 | HORIZONTAL = 'horizontal', 7 | VERTICAL = 'vertical' 8 | } 9 | 10 | interface IRug { 11 | /** accessor used to get the rug value for a given datapoint */ 12 | accessor: AccessorFunction 13 | 14 | /** scale function of the rug */ 15 | scale: Scale 16 | 17 | /** data to be rugged */ 18 | data: Array 19 | 20 | /** length of the rug's ticks */ 21 | tickLength?: number 22 | 23 | /** color scheme of the rug ticks */ 24 | colors?: Array 25 | 26 | /** orientation of the rug */ 27 | orientation?: RugOrientation 28 | 29 | /** left margin of the rug */ 30 | left?: number 31 | 32 | /** top margin of the rug */ 33 | top?: number 34 | } 35 | 36 | export default class Rug { 37 | accessor: AccessorFunction 38 | scale: Scale 39 | rugObject: any 40 | data: Array 41 | left = 0 42 | top = 0 43 | tickLength = 5 44 | colors = constants.defaultColors 45 | orientation = RugOrientation.HORIZONTAL 46 | 47 | constructor({ accessor, scale, data, tickLength, colors, orientation, left, top }: IRug) { 48 | this.accessor = accessor 49 | this.scale = scale 50 | this.data = data 51 | this.tickLength = tickLength ?? this.tickLength 52 | this.colors = colors ?? this.colors 53 | this.orientation = orientation ?? this.orientation 54 | this.left = left ?? this.left 55 | this.top = top ?? this.top 56 | } 57 | 58 | get isVertical(): boolean { 59 | return this.orientation === constants.orientation.vertical 60 | } 61 | 62 | /** 63 | * Mount the rug to the given node. 64 | * 65 | * @param svg d3 node to mount the rug to. 66 | */ 67 | mountTo(svg: GenericD3Selection): void { 68 | // add container 69 | const top = this.isVertical ? this.top : this.top - this.tickLength 70 | const container = svg.append('g').attr('transform', `translate(${this.left},${top})`) 71 | 72 | // add lines 73 | this.data.forEach((dataArray, i) => 74 | dataArray.forEach((datum: any) => { 75 | const value = this.scale.scaleObject(this.accessor(datum)) 76 | container 77 | .append('line') 78 | .attr(this.isVertical ? 'y1' : 'x1', value!) 79 | .attr(this.isVertical ? 'y2' : 'x2', value!) 80 | .attr(this.isVertical ? 'x1' : 'y1', 0) 81 | .attr(this.isVertical ? 'x2' : 'y2', this.tickLength) 82 | .attr('stroke', this.colors[i]) 83 | }) 84 | ) 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /lib/src/components/scale.ts: -------------------------------------------------------------------------------- 1 | import { scaleLinear, ScaleLinear } from 'd3' 2 | import { Domain, Range } from '../misc/typings' 3 | 4 | enum ScaleType { 5 | LINEAR = 'linear' 6 | } 7 | 8 | type SupportedScale = ScaleLinear 9 | 10 | interface IScale { 11 | /** type of scale (currently only linear) */ 12 | type?: ScaleType 13 | 14 | /** scale range */ 15 | range?: Range 16 | 17 | /** scale domain */ 18 | domain?: Domain 19 | 20 | /** overwrites domain lower bound */ 21 | minValue?: number 22 | 23 | /** overwrites domain upper bound */ 24 | maxValue?: number 25 | } 26 | 27 | export default class Scale { 28 | type: ScaleType 29 | scaleObject: SupportedScale 30 | minValue?: number 31 | maxValue?: number 32 | 33 | constructor({ type, range, domain, minValue, maxValue }: IScale) { 34 | this.type = type ?? ScaleType.LINEAR 35 | this.scaleObject = this.getScaleObject(this.type) 36 | 37 | // set optional custom ranges and domains 38 | if (range) this.range = range 39 | if (domain) this.domain = domain 40 | 41 | // set optional min and max 42 | this.minValue = minValue 43 | this.maxValue = maxValue 44 | } 45 | 46 | /** 47 | * Get the d3 scale object for a given scale type. 48 | * 49 | * @param {String} type scale type 50 | * @returns {Object} d3 scale type 51 | */ 52 | getScaleObject(type: ScaleType): SupportedScale { 53 | switch (type) { 54 | default: 55 | return scaleLinear() 56 | } 57 | } 58 | 59 | get range(): Range { 60 | return this.scaleObject.range() 61 | } 62 | 63 | set range(range: Range) { 64 | this.scaleObject.range(range) 65 | } 66 | 67 | get domain(): Domain { 68 | return this.scaleObject.domain() 69 | } 70 | 71 | set domain(domain: Domain) { 72 | // fix custom domain values if necessary 73 | if (typeof this.minValue !== 'undefined') domain[0] = this.minValue 74 | if (typeof this.maxValue !== 'undefined') domain[1] = this.maxValue 75 | this.scaleObject.domain(domain) 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /lib/src/components/tooltip.ts: -------------------------------------------------------------------------------- 1 | import constants from '../misc/constants' 2 | import { TextFunction, AccessorFunction, GenericD3Selection } from '../misc/typings' 3 | 4 | export enum TooltipSymbol { 5 | CIRCLE = 'circle', 6 | LINE = 'line', 7 | SQUARE = 'square' 8 | } 9 | 10 | interface ITooltip { 11 | /** symbol to show in the tooltip (defaults to line) */ 12 | legendObject?: TooltipSymbol 13 | 14 | /** description of the different data arrays shown in the legend */ 15 | legend?: Array 16 | 17 | /** array of colors for the different data arrays, defaults to schemeCategory10 */ 18 | colors?: Array 19 | 20 | /** custom text formatting function -- generated from accessors if not defined */ 21 | textFunction?: TextFunction 22 | 23 | /** entries to show in the tooltip, usually empty when first instantiating */ 24 | data?: Array 25 | 26 | /** margin to the left of the tooltip */ 27 | left?: number 28 | 29 | /** margin to the top of the tooltip */ 30 | top?: number 31 | 32 | /** if no custom text function is specified, specifies how to get the x value from a specific data point */ 33 | xAccessor?: AccessorFunction 34 | 35 | /** if no custom text function is specified, specifies how to get the y value from a specific data point */ 36 | yAccessor?: AccessorFunction 37 | } 38 | 39 | export default class Tooltip { 40 | legendObject = TooltipSymbol.LINE 41 | legend: Array 42 | colors = constants.defaultColors 43 | data: Array 44 | left = 0 45 | top = 0 46 | node: any 47 | textFunction = (x: any) => 'bla' 48 | 49 | constructor({ legendObject, legend, colors, textFunction, data, left, top, xAccessor, yAccessor }: ITooltip) { 50 | this.legendObject = legendObject ?? this.legendObject 51 | this.legend = legend ?? [] 52 | this.colors = colors ?? this.colors 53 | this.setTextFunction(textFunction, xAccessor, yAccessor) 54 | this.data = data ?? [] 55 | this.left = left ?? this.left 56 | this.top = top ?? this.top 57 | } 58 | 59 | /** 60 | * Sets the text function for the tooltip. 61 | * 62 | * @param textFunction custom text function for the tooltip text. Generated from xAccessor and yAccessor if not 63 | * @param xAccessor if no custom text function is specified, this function specifies how to get the x value from a specific data point. 64 | * @param yAccessor if no custom text function is specified, this function specifies how to get the y value from a specific data point. 65 | */ 66 | setTextFunction(textFunction?: TextFunction, xAccessor?: AccessorFunction, yAccessor?: AccessorFunction): void { 67 | this.textFunction = 68 | textFunction || (xAccessor && yAccessor ? this.baseTextFunction(xAccessor, yAccessor) : this.textFunction) 69 | } 70 | 71 | /** 72 | * If no textFunction was specified when creating the tooltip instance, this method generates a text function based on the xAccessor and yAccessor. 73 | * 74 | * @param xAccessor returns the x value of a given data point. 75 | * @param yAccessor returns the y value of a given data point. 76 | * @returns base text function used to render the tooltip for a given datapoint. 77 | */ 78 | baseTextFunction(xAccessor: AccessorFunction, yAccessor: AccessorFunction): TextFunction { 79 | return (point: any) => `${xAccessor(point)}: ${yAccessor(point)}` 80 | } 81 | 82 | /** 83 | * Update the tooltip. 84 | */ 85 | update({ data, legendObject, legend }: Pick): void { 86 | this.data = data ?? this.data 87 | this.legendObject = legendObject ?? this.legendObject 88 | this.legend = legend ?? this.legend 89 | this.addText() 90 | } 91 | 92 | /** 93 | * Hide the tooltip (without destroying it). 94 | */ 95 | hide(): void { 96 | this.node.attr('opacity', 0) 97 | } 98 | 99 | /** 100 | * Mount the tooltip to the given d3 node. 101 | * 102 | * @param svg d3 node to mount the tooltip to. 103 | */ 104 | mountTo(svg: GenericD3Selection): void { 105 | this.node = svg 106 | .append('g') 107 | .style('font-size', '0.7rem') 108 | .attr('transform', `translate(${this.left},${this.top})`) 109 | .attr('opacity', 0) 110 | this.addText() 111 | } 112 | 113 | /** 114 | * Adds the text to the tooltip. 115 | * For each datapoint in the data array, one line is added to the tooltip. 116 | */ 117 | addText(): void { 118 | // first, clear existing content 119 | this.node.selectAll('*').remove() 120 | 121 | // second, add one line per data entry 122 | this.node.attr('opacity', 1) 123 | this.data.forEach((datum, index) => { 124 | const symbol = constants.symbol[this.legendObject] 125 | const realIndex = datum.arrayIndex ?? index 126 | const color = this.colors[realIndex] 127 | const node = this.node 128 | .append('text') 129 | .attr('text-anchor', 'end') 130 | .attr('y', index * 12) 131 | 132 | // category 133 | node.append('tspan').classed('text-category', true).attr('fill', color).text(this.legend[realIndex]) 134 | 135 | // symbol 136 | node.append('tspan').attr('dx', '6').attr('fill', color).text(symbol) 137 | 138 | // text 139 | node 140 | .append('tspan') 141 | .attr('dx', '6') 142 | .text(`${this.textFunction(datum)}`) 143 | }) 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /lib/src/index.ts: -------------------------------------------------------------------------------- 1 | export { default as LineChart } from './charts/line' 2 | export { default as ScatterChart } from './charts/scatter' 3 | export { default as HistogramChart } from './charts/histogram' 4 | -------------------------------------------------------------------------------- /lib/src/mg.css: -------------------------------------------------------------------------------- 1 | .mg-graph .domain { 2 | stroke: #b3b2b2; 3 | } 4 | 5 | .mg-graph .tick line { 6 | stroke: #b3b2b2; 7 | } 8 | 9 | .mg-graph .tick text { 10 | font-size: 0.7rem; 11 | fill: black; 12 | opacity: 0.6; 13 | } 14 | 15 | .mg-graph .label { 16 | font-size: 0.8rem; 17 | font-weight: 600; 18 | opacity: 0.8; 19 | } 20 | 21 | .mg-graph .line-marker { 22 | stroke: #ccc; 23 | stroke-width: 1px; 24 | stroke-dasharray: 2; 25 | } 26 | 27 | .mg-graph .line-baseline { 28 | stroke: #ccc; 29 | stroke-width: 1px; 30 | } 31 | 32 | .mg-graph .text-marker { 33 | text-anchor: middle; 34 | font-size: 0.7rem; 35 | fill: black; 36 | } 37 | 38 | .mg-graph .text-baseline { 39 | text-anchor: end; 40 | font-size: 0.7rem; 41 | fill: black; 42 | } 43 | 44 | .mg-graph .text-category { 45 | font-weight: bold; 46 | } 47 | 48 | .mg-line { 49 | stroke-width: 1; 50 | fill: none; 51 | } 52 | 53 | .mg-area { 54 | fill-opacity: 0.3; 55 | } 56 | 57 | .text-legend { 58 | margin-right: 1em; 59 | font-size: 0.7rem; 60 | } 61 | -------------------------------------------------------------------------------- /lib/src/misc/constants.ts: -------------------------------------------------------------------------------- 1 | const constants = { 2 | chartType: { 3 | line: 'line', 4 | histogram: 'histogram', 5 | bar: 'bar', 6 | point: 'point' 7 | }, 8 | axisOrientation: { 9 | top: 'top', 10 | bottom: 'bottom', 11 | left: 'left', 12 | right: 'right' 13 | }, 14 | scaleType: { 15 | categorical: 'categorical', 16 | linear: 'linear', 17 | log: 'log' 18 | }, 19 | axisFormat: { 20 | date: 'date', 21 | number: 'number', 22 | percentage: 'percentage' 23 | }, 24 | legendObject: { 25 | circle: 'circle', 26 | line: 'line', 27 | square: 'square' 28 | }, 29 | symbol: { 30 | line: '—', 31 | circle: '•', 32 | square: '■' 33 | }, 34 | orientation: { 35 | vertical: 'vertical', 36 | horizontal: 'horizontal' 37 | }, 38 | defaultColors: [ 39 | '#1f77b4', 40 | '#ff7f0e', 41 | '#2ca02c', 42 | '#d62728', 43 | '#9467bd', 44 | '#8c564b', 45 | '#e377c2', 46 | '#7f7f7f', 47 | '#bcbd22', 48 | '#17becf' 49 | ] 50 | } 51 | 52 | export default constants 53 | -------------------------------------------------------------------------------- /lib/src/misc/typings.ts: -------------------------------------------------------------------------------- 1 | import { Selection } from 'd3' 2 | 3 | export interface AccessorFunction { 4 | (dataObject: X): Y 5 | } 6 | 7 | export interface TextFunction { 8 | (dataObject: unknown): string 9 | } 10 | 11 | export interface InteractionFunction { 12 | (pointArray: Array): void 13 | } 14 | 15 | export interface EmptyInteractionFunction { 16 | (): void 17 | } 18 | 19 | export interface DefinedFunction { 20 | (dataObject: unknown): boolean 21 | } 22 | 23 | export interface Margin { 24 | left: number 25 | right: number 26 | bottom: number 27 | top: number 28 | } 29 | 30 | export interface DomainObject { 31 | x: Domain 32 | y: Domain 33 | } 34 | 35 | export enum LegendSymbol { 36 | LINE = 'line', 37 | CIRCLE = 'circle', 38 | SQUARE = 'square' 39 | } 40 | 41 | export type BrushType = 'xy' | 'x' | 'y' 42 | 43 | export type Domain = number[] 44 | export type Range = number[] 45 | 46 | export type GenericD3Selection = Selection 47 | export type SvgD3Selection = Selection 48 | export type GD3Selection = Selection 49 | export type LineD3Selection = Selection 50 | export type TextD3Selection = Selection 51 | -------------------------------------------------------------------------------- /lib/src/misc/utility.ts: -------------------------------------------------------------------------------- 1 | import { AccessorFunction } from './typings' 2 | 3 | /** 4 | * Handle cases where the user specifies an accessor string instead of an accessor function. 5 | * 6 | * @param functionOrString accessor string/function to be made an accessor function 7 | * @returns accessor function 8 | */ 9 | export function makeAccessorFunction(functionOrString: AccessorFunction | string): AccessorFunction { 10 | return typeof functionOrString === 'string' ? (d: any) => d[functionOrString] : functionOrString 11 | } 12 | 13 | /** 14 | * Generate a random id. 15 | * Used to create ids for clip paths, which need to be referenced by id. 16 | * 17 | * @returns random id string. 18 | */ 19 | export function randomId(): string { 20 | return Math.random().toString(36).substring(2, 15) 21 | } 22 | -------------------------------------------------------------------------------- /lib/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "module": "ESNext", 5 | "moduleResolution": "node", 6 | "lib": ["ES2020", "DOM"], 7 | "strict": true, 8 | "sourceMap": true, 9 | "declaration": true, 10 | "esModuleInterop": true, 11 | "forceConsistentCasingInFileNames": true, 12 | "outDir": "dist" 13 | }, 14 | "include": ["src/**/*.ts"] 15 | } 16 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "workspaces": [ 4 | "lib", 5 | "app" 6 | ], 7 | "repository": "github:metricsgraphics/metrics-graphics", 8 | "contributors": [ 9 | "Ali Almossawi", 10 | "Hamilton Ulmer", 11 | "William Lachance", 12 | "Jens Ochsenmeier" 13 | ], 14 | "license": "MPL-2.0", 15 | "bugs": { 16 | "url": "https://github.com/metricsgraphics/metrics-graphics/issues" 17 | }, 18 | "homepage": "http://metricsgraphicsjs.org", 19 | "dependencies": {}, 20 | "devDependencies": { 21 | "@typescript-eslint/eslint-plugin": "^5.25.0", 22 | "@typescript-eslint/parser": "^5.25.0", 23 | "eslint": "^8.15.0", 24 | "eslint-config-prettier": "^8.5.0", 25 | "eslint-config-standard": "^17.0.0", 26 | "eslint-plugin-import": "^2.26.0", 27 | "eslint-plugin-n": "^15.2.0", 28 | "eslint-plugin-prettier": "^4.0.0", 29 | "eslint-plugin-promise": "^6.0.0", 30 | "eslint-plugin-react": "^7.30.0", 31 | "eslint-plugin-react-hooks": "^4.5.0", 32 | "prettier": "^2.6.2", 33 | "typescript": "^4.6.4" 34 | } 35 | } 36 | --------------------------------------------------------------------------------