├── src ├── pages │ ├── 404 │ │ └── index.tsx │ ├── index.less │ ├── index.tsx │ ├── dashboard-a │ │ ├── index.module.less │ │ └── index.tsx │ ├── monitor-template │ │ ├── index.tsx │ │ └── index.module.less │ └── home │ │ └── index.tsx ├── templates │ ├── example.module.less │ └── example.tsx ├── static │ └── images │ │ ├── favicon.png │ │ ├── theme-dark.svg │ │ └── theme-light.svg ├── typing.d.ts ├── utils │ ├── location.ts │ └── get-data.ts ├── components │ ├── x-plot │ │ ├── not-found.tsx │ │ ├── common │ │ │ ├── header.less │ │ │ ├── legend-table.less │ │ │ ├── explaination.less │ │ │ ├── explaination.tsx │ │ │ ├── use-g2plot.tsx │ │ │ ├── parse.tsx │ │ │ ├── register.ts │ │ │ ├── header.tsx │ │ │ ├── parse.less │ │ │ └── legend-table.tsx │ │ ├── statistic.less │ │ ├── ring-progress.less │ │ ├── liquid.tsx │ │ ├── pie.tsx │ │ ├── statistic.tsx │ │ ├── column.tsx │ │ ├── gauge.tsx │ │ ├── scatter.tsx │ │ ├── radial-bar.tsx │ │ ├── line.tsx │ │ ├── sankey.tsx │ │ ├── area.tsx │ │ ├── bar.tsx │ │ └── ring-progress.tsx │ ├── base │ │ ├── page-loading.tsx │ │ ├── try-it-page.module.less │ │ ├── code-loading.tsx │ │ ├── home.less │ │ ├── code-loading.less │ │ ├── try-it-page.tsx │ │ └── home.tsx │ ├── RedirectIndex.tsx │ ├── x-container │ │ └── panel.tsx │ └── Seo.tsx ├── styles │ ├── dashboard.module.less │ ├── base.less │ └── dark.less ├── themes │ └── index.ts ├── layouts │ ├── footer.tsx │ ├── layout.tsx │ ├── seo.tsx │ ├── header.tsx │ └── layout.less ├── types │ └── index.ts ├── examples │ ├── meta.json │ ├── waffle.ts │ ├── emoji-waffle.ts │ ├── html-tooltip.ts │ └── pizza.ts └── dashboards │ ├── monitor-template.tsx │ └── dashboard-a.ts ├── .prettierignore ├── .prettierrc ├── gatsby-browser.js ├── tsconfig.json ├── .github └── workflows │ ├── preview-start.yml │ ├── deploy.yml │ ├── preview-build.yml │ └── preview-deploy.yml ├── api-extractor.json ├── LICENSE ├── .gitignore ├── .all-contributorsrc ├── gatsby-node.js ├── package.json ├── gatsby-config.js └── README.md /src/pages/index.less: -------------------------------------------------------------------------------- 1 | @import '@/styles/base.less'; -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | .cache 2 | package.json 3 | package-lock.json 4 | public 5 | -------------------------------------------------------------------------------- /src/templates/example.module.less: -------------------------------------------------------------------------------- 1 | .footer { 2 | display: none; 3 | } 4 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "arrowParens": "avoid", 3 | "singleQuote": true, 4 | "semi": true 5 | } 6 | -------------------------------------------------------------------------------- /src/static/images/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/antvis/vis-dashboard/HEAD/src/static/images/favicon.png -------------------------------------------------------------------------------- /src/typing.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.module.less'; 2 | 3 | declare module '*.json'; 4 | 5 | declare module '@antv/data-set'; -------------------------------------------------------------------------------- /src/utils/location.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 从 url location 获取是否编辑态 3 | */ 4 | export function isEditMode() { 5 | if (window) { 6 | return /mode=edit/.test(window.location.hash); 7 | } 8 | return false; 9 | } 10 | -------------------------------------------------------------------------------- /src/pages/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import RedirectIndex from '../components/RedirectIndex'; 3 | import './index.less'; 4 | 5 | const Page: React.FC & { noLayout: boolean } = () => ; 6 | 7 | Page.noLayout = false; 8 | 9 | export default Page; 10 | -------------------------------------------------------------------------------- /gatsby-browser.js: -------------------------------------------------------------------------------- 1 | window.g2plot = require('@antv/g2plot'); 2 | window.g2 = require('@antv/g2'); 3 | window.dataSet = require('@antv/data-set'); 4 | window.antd = require('antd'); 5 | window.lodash = require('lodash'); 6 | window.insertCss = require('insert-css'); 7 | 8 | require('antd/lib/message/style/index.css'); -------------------------------------------------------------------------------- /src/pages/404/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Layout from '@/layouts/layout'; 3 | 4 | const NotFoundPage = () => ( 5 | 6 |

404: Not Found

7 |

You just hit a route that doesn't exist... the sadness.

8 |
9 | ); 10 | 11 | export default NotFoundPage; 12 | -------------------------------------------------------------------------------- /src/components/x-plot/not-found.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { XComponentProps } from '@/types'; 3 | 4 | const Not_Found: React.FC = props => { 5 | return ( 6 |
7 | 找不到组件 {props.tag} 8 |
9 | ); 10 | }; 11 | 12 | export default Not_Found; 13 | -------------------------------------------------------------------------------- /src/utils/get-data.ts: -------------------------------------------------------------------------------- 1 | export function getData(data?: object[] | string): Promise { 2 | if (typeof data === 'string' && data.startsWith('http')) { 3 | const url = data; 4 | return fetch(url).then(data => data.json()); 5 | } else if (typeof data === 'object') { 6 | return Promise.resolve(data); 7 | } 8 | 9 | return Promise.resolve([]); 10 | } 11 | -------------------------------------------------------------------------------- /src/components/x-plot/common/header.less: -------------------------------------------------------------------------------- 1 | .grid-drag-handler { 2 | position: relative; 3 | z-index: 10; 4 | 5 | width: 100%; 6 | 7 | &.header-no-title { 8 | position: absolute; 9 | } 10 | } 11 | 12 | .draggable .grid-drag-handler { 13 | &:hover { 14 | cursor: move; 15 | transition: background-color 0.1s ease-in-out; 16 | background-color: #f1f5f9; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/styles/dashboard.module.less: -------------------------------------------------------------------------------- 1 | @main-max-width: 1232px; 2 | @main-padding: 32px; 3 | 4 | .header { 5 | padding: 1rem @main-padding; 6 | 7 | :global { 8 | .site-title { 9 | max-width: calc(@main-max-width - @main-padding * 2); 10 | } 11 | } 12 | } 13 | 14 | .main { 15 | height: auto; 16 | margin: 16px auto; 17 | max-width: @main-max-width; 18 | padding: 0 @main-padding; 19 | 20 | overflow: auto; 21 | } 22 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "es5", 5 | "outDir": "lib", 6 | "declaration": true, 7 | "importHelpers": true, 8 | "moduleResolution": "node", 9 | "allowSyntheticDefaultImports": true, 10 | "esModuleInterop": true, 11 | "resolveJsonModule": true, 12 | "skipLibCheck": true, 13 | "lib": ["esnext", "dom"], 14 | "jsx": "react", 15 | "paths": { 16 | "@/*": ["./src/*"] 17 | } 18 | }, 19 | "include": ["src"] 20 | } 21 | -------------------------------------------------------------------------------- /src/components/base/page-loading.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from 'react'; 2 | import CodeLoading from './code-loading'; 3 | 4 | const PageLoading: React.FC = () => { 5 | useEffect(() => { 6 | // 为了确保遮罩层下,没有滚动事件 7 | const oldOverflow = document.body.style.overflow; 8 | document.body.style.overflow = 'hidden'; 9 | return () => { 10 | document.body.style.overflow = oldOverflow; 11 | }; 12 | }, []); 13 | 14 | return ( 15 |
16 | 17 |
18 | ); 19 | }; 20 | 21 | export default PageLoading; 22 | -------------------------------------------------------------------------------- /src/templates/example.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Layout from '@/layouts/layout'; 3 | import TryItPage from '@/components/base/try-it-page'; 4 | import styles from './example.module.less'; 5 | 6 | type Props = { 7 | pageContext: { 8 | source: string; 9 | } 10 | } 11 | 12 | export default function Example({ 13 | pageContext, 14 | ...props 15 | }: Props) { 16 | const { source } = pageContext; 17 | 18 | return ( 19 | 20 | 21 | 22 | ); 23 | } 24 | -------------------------------------------------------------------------------- /src/themes/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 主题常量 3 | */ 4 | export const THEME_COLORS = [ 5 | '#009CFF', 6 | '#FF8BDD', 7 | '#41C195', 8 | '#F6BD16', 9 | '#62687D', 10 | 11 | // 马卡龙 12 | // "#025DF4", 13 | // '#DB6BCF', 14 | // '#2498D1', 15 | // '#BBBDE6', 16 | // '#4045B2', 17 | // '#21A97A', 18 | // '#FF745A', 19 | // '#007E99', 20 | // '#FFA8A8', 21 | // '#2391FF', 22 | 23 | // 主题色 24 | // '#5B8FF9', 25 | // '#61DDAA', 26 | // '#65789B', 27 | // '#F6BD16', 28 | // '#7262fd', 29 | '#78D3F8', 30 | '#9661BC', 31 | '#F6903D', 32 | '#008685', 33 | '#F08BB4', 34 | ]; 35 | -------------------------------------------------------------------------------- /src/layouts/footer.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | type Props = { 4 | /** 作者 */ 5 | author: string; 6 | /** 联系方式 */ 7 | contact: string; 8 | className?: string; 9 | }; 10 | 11 | const Footer: React.FC = ({ contact, author, className }) => { 12 | return ( 13 | 22 | ); 23 | }; 24 | 25 | export default Footer; 26 | -------------------------------------------------------------------------------- /.github/workflows/preview-start.yml: -------------------------------------------------------------------------------- 1 | 2 | name: Preview Start 3 | 4 | on: pull_request_target 5 | 6 | jobs: 7 | preview: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - name: create 11 | uses: actions-cool/maintain-one-comment@v1.1.0 12 | with: 13 | token: ${{ secrets.GITHUB_TOKEN }} 14 | body: | 15 | ⚡️ Deploying PR Preview... 16 | 17 | 18 | body-include: '' 19 | -------------------------------------------------------------------------------- /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | name: github pages 2 | 3 | on: 4 | push: 5 | branches: 6 | - master # Set a branch name to trigger deployment 7 | 8 | jobs: 9 | deploy: 10 | runs-on: ubuntu-18.04 11 | steps: 12 | - uses: actions/checkout@v2 13 | - uses: actions/setup-node@v2 14 | with: 15 | node-version: 12 16 | 17 | - run: npm install 18 | - run: npm run build 19 | 20 | - name: Deploy 21 | uses: peaceiris/actions-gh-pages@v3 22 | with: 23 | github_token: ${{ secrets.GITHUB_TOKEN }} 24 | publish_dir: ./public 25 | cname: vis-dashboard.antv.vision 26 | -------------------------------------------------------------------------------- /src/styles/base.less: -------------------------------------------------------------------------------- 1 | @import '~react-grid-layout/css/styles.css'; 2 | @import '~react-resizable/css/styles.css'; 3 | 4 | .full { 5 | width: 100%; 6 | height: 100%; 7 | 8 | position: relative; 9 | } 10 | 11 | .move-handler-placeholder { 12 | position: absolute; 13 | height: 20px; 14 | left: 0; 15 | right: 0; 16 | // 提高到最顶层 17 | z-index: 100; 18 | 19 | &:hover { 20 | background-color: rgba(0, 0, 0, 0.05); 21 | } 22 | } 23 | 24 | // 自定义 react-grid-layout 样式 25 | .react-grid-item:not(.react-grid-placeholder) { 26 | border: 1px solid #efefef; 27 | } 28 | 29 | // 开发中,发现存在部分 antd 样式没走暗黑主题, 待确认原因 30 | td.ant-table-column-sort { 31 | background-color: inherit; 32 | } 33 | -------------------------------------------------------------------------------- /src/types/index.ts: -------------------------------------------------------------------------------- 1 | import { Layouts } from 'react-grid-layout'; 2 | 3 | export type LooseObject = Record; 4 | 5 | export type Attributes = LooseObject & { 6 | readonly id?: string; 7 | // 主题 8 | readonly theme?: string | object; 9 | }; 10 | 11 | export type ReportNode = { 12 | /** 组件类型,小写开头 */ 13 | tag: string; 14 | /** 组件 id */ 15 | id: string; 16 | /** 组件属性 */ 17 | attributes: A; 18 | children?: ReportNode[]; 19 | }; 20 | 21 | export type Report = { 22 | // 主题 23 | theme?: string; 24 | layouts: Record; 25 | content: ReportNode; 26 | }; 27 | 28 | export type XComponentProps = Omit< 29 | ReportNode, 30 | 'children' 31 | >; 32 | -------------------------------------------------------------------------------- /src/components/x-plot/statistic.less: -------------------------------------------------------------------------------- 1 | .ant-statistic { 2 | &:not(:last-child) { 3 | margin-right: 4px; 4 | } 5 | 6 | .ant-statistic-title { 7 | overflow: hidden; 8 | text-overflow: ellipsis; 9 | white-space: nowrap; 10 | } 11 | 12 | @media (max-width: 414px) { 13 | .ant-statistic-content { 14 | font-size: 1.4rem; 15 | } 16 | } 17 | } 18 | 19 | // 对应暗黑主题 20 | body[data-theme='dark'] { 21 | .ant-statistic-title { 22 | color: rgba(255, 255, 255, 0.45); 23 | } 24 | 25 | .ant-statistic-content { 26 | color: rgba(255, 255, 255, 0.65); 27 | } 28 | } 29 | 30 | .statistic-item { 31 | display: flex; 32 | align-items: center; 33 | width: 100%; 34 | height: 100%; 35 | 36 | .icon { 37 | margin-right: 24px; 38 | } 39 | } 40 | 41 | -------------------------------------------------------------------------------- /api-extractor.json: -------------------------------------------------------------------------------- 1 | { 2 | "mainEntryPointFilePath": "/node_modules/@antv/g2plot/lib/index.d.ts", 3 | "apiReport": { 4 | "enabled": false 5 | }, 6 | "docModel": { 7 | "enabled": false 8 | }, 9 | "dtsRollup": { 10 | "enabled": true 11 | }, 12 | "tsdocMetadata": { 13 | "enabled": false 14 | }, 15 | "messages": { 16 | "extractorMessageReporting": { 17 | "ae-missing-release-tag": { 18 | "logLevel": "none" 19 | }, 20 | "ae-forgotten-export": { 21 | "logLevel": "none" 22 | } 23 | }, 24 | "tsdocMessageReporting": { 25 | "tsdoc-param-tag-missing-hyphen": { 26 | "logLevel": "none" 27 | }, 28 | "tsdoc-escape-greater-than": { 29 | "logLevel": "none" 30 | } 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/components/x-plot/common/legend-table.less: -------------------------------------------------------------------------------- 1 | .legend-table { 2 | height: 100%; 3 | overflow: auto; 4 | 5 | .index-column { 6 | display: flex; 7 | align-items: center; 8 | 9 | .marker { 10 | width: 10px; 11 | height: 2px; 12 | margin-right: 8px; 13 | } 14 | } 15 | 16 | .row { 17 | cursor: pointer; 18 | } 19 | 20 | .unselected-row { 21 | opacity: 0.3; 22 | } 23 | 24 | // 重写table 样式 25 | .ant-table-content { 26 | font-size: 12px; 27 | white-space: nowrap; 28 | 29 | .ant-table-thead { 30 | opacity: 0.85; 31 | background: #F5F7FA; 32 | 33 | .ant-table-cell { 34 | padding: 5px 8px; 35 | } 36 | } 37 | 38 | } 39 | } 40 | 41 | @media(max-width: 500px) { 42 | .legend-table { 43 | max-height: 180px; 44 | width: 100%; 45 | } 46 | } -------------------------------------------------------------------------------- /src/components/x-plot/ring-progress.less: -------------------------------------------------------------------------------- 1 | .ring-plot-container.plot-container { 2 | display: grid; 3 | align-items: center; 4 | justify-content: center; 5 | grid-template-columns: 1fr 1fr 1fr 1fr; 6 | grid-template-rows: 120px; 7 | 8 | .ring-progress-container { 9 | height: 100%; 10 | 11 | .statistic-icon-container { 12 | svg { 13 | width: 100%; 14 | height: 100%; 15 | } 16 | svg:not(:root) { 17 | overflow: visible; 18 | } 19 | } 20 | } 21 | } 22 | 23 | @media (max-width: 680px) { 24 | .ring-plot-container.plot-container { 25 | grid-template-columns: 1fr 1fr; 26 | grid-template-rows: 120px 120px; 27 | } 28 | } 29 | 30 | // 对应暗黑主题 31 | body[data-theme='dark'] { 32 | .ring-progress-container .g2-html-annotation { 33 | color: rgba(255, 255, 255, 0.85) !important; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /.github/workflows/preview-build.yml: -------------------------------------------------------------------------------- 1 | name: Preview Build 2 | 3 | on: 4 | pull_request: 5 | types: [opened, synchronize, reopened] 6 | 7 | jobs: 8 | build-preview: 9 | runs-on: ubuntu-latest 10 | 11 | steps: 12 | - uses: actions/checkout@v2 13 | with: 14 | ref: ${{ github.event.pull_request.head.sha }} 15 | 16 | - name: build 17 | run: | 18 | yarn 19 | NODE_OPTIONS='--max-old-space-size=4096' yarn run build 20 | 21 | - name: upload dist artifact 22 | uses: actions/upload-artifact@v2 23 | with: 24 | name: dist 25 | path: public/ 26 | retention-days: 5 27 | 28 | - name: Save PR number 29 | if: ${{ always() }} 30 | run: echo ${{ github.event.number }} > ./pr-id.txt 31 | 32 | - name: Upload PR number 33 | if: ${{ always() }} 34 | uses: actions/upload-artifact@v2 35 | with: 36 | name: pr 37 | path: ./pr-id.txt 38 | -------------------------------------------------------------------------------- /src/components/RedirectIndex.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from 'react'; 2 | import { navigate, graphql, StaticQuery } from 'gatsby'; 3 | import Seo from './Seo'; 4 | import PageLoading from './base/page-loading'; 5 | 6 | const RedirectIndex = () => { 7 | useEffect(() => { 8 | navigate('home'); 9 | }, []); 10 | 11 | const renderIndex = (data: { 12 | site: { 13 | siteMetadata: { 14 | title?: string; 15 | }; 16 | }; 17 | }) => { 18 | const { 19 | site: { 20 | siteMetadata: { title = '' }, 21 | }, 22 | } = data; 23 | return ( 24 | <> 25 | 26 | 27 | 28 | ); 29 | }; 30 | 31 | return ( 32 | 44 | ); 45 | }; 46 | 47 | export default RedirectIndex; 48 | -------------------------------------------------------------------------------- /src/components/x-plot/liquid.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | import { Liquid, LiquidOptions } from '@antv/g2plot'; 3 | import { XComponentProps } from '@/types'; 4 | import { Header } from '@/components/x-plot/common/header'; 5 | import { UseG2Plot } from '@/components/x-plot/common/use-g2plot'; 6 | import * as _ from 'lodash'; 7 | 8 | type XLiquidProps = XComponentProps; 9 | 10 | const DEFAULT_OPTIONS = { 11 | // 添加默认水波图样式(不然只显示中心文案) 12 | liquidStyle: () => ({ 13 | fill: '#6193FA', 14 | stroke: '#6193FA', 15 | }), 16 | }; 17 | 18 | export const XLiquid: React.FC = props => { 19 | const { attributes } = props; 20 | 21 | const [options, updateOptions] = useState({ percent: 0 }); 22 | 23 | useEffect(() => { 24 | updateOptions(_.assign({}, DEFAULT_OPTIONS, attributes)); 25 | }, [attributes]); 26 | 27 | return ( 28 |
29 |
30 | 31 |
32 | ); 33 | }; 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Visiky 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/components/x-plot/common/explaination.less: -------------------------------------------------------------------------------- 1 | @title-height: 42px; 2 | 3 | .explaination { 4 | height: 100%; 5 | max-width: 250px; 6 | 7 | &>.title { 8 | height: @title-height; 9 | line-height: @title-height; 10 | font-size: 16px; 11 | opacity: 0.85; 12 | } 13 | 14 | .detail { 15 | overflow-y: auto; 16 | height: 100%; 17 | 18 | .detail-item { 19 | &:not(:first-child) { 20 | margin-top: 24px; 21 | } 22 | 23 | .title { 24 | margin-bottom: 12px; 25 | opacity: 0.85; 26 | font-family: PingFangSC-Medium; 27 | font-size: 14px; 28 | } 29 | .description { 30 | opacity: 0.65; 31 | } 32 | } 33 | } 34 | } 35 | 36 | @media(max-width: 500px) { 37 | .explaination { 38 | width: 100%; 39 | max-height: 180px; 40 | max-width: unset; 41 | margin-top: 12px !important; 42 | 43 | .detail { 44 | height: calc(100% - @title-height); 45 | 46 | .detail-item { 47 | &:not(:first-child) { 48 | margin-top: 16px; 49 | } 50 | 51 | .title { 52 | margin-bottom: 10px; 53 | } 54 | } 55 | } 56 | } 57 | } -------------------------------------------------------------------------------- /src/components/base/try-it-page.module.less: -------------------------------------------------------------------------------- 1 | @header-height: 56px; 2 | @footer-height: 52px; 3 | .try-it-page { 4 | height: calc(100vh - @header-height); 5 | display: grid; 6 | grid-template-columns: 1fr 1fr; 7 | 8 | .playground-result-wrapper { 9 | background: #fff; 10 | border-right: 0.5px solid rgba(0, 0, 0, 0.05); 11 | display: flex; 12 | align-items: center; 13 | justify-content: center; 14 | } 15 | 16 | :global { 17 | .react-monaco-editor-container { 18 | position: relative; 19 | } 20 | // 用于滚动 21 | .react-monaco-editor-container::after { 22 | content: ' '; 23 | width: 56px; 24 | position: absolute; 25 | top: 0; 26 | bottom: 0; 27 | right: 0; 28 | } 29 | } 30 | } 31 | 32 | @media only screen and (max-width: 1024px) { 33 | @playground-result-height: 400px; 34 | .try-it-page { 35 | width: 100%; 36 | min-height: @playground-result-height; 37 | height: calc(50vh - @header-height / 2); 38 | grid-template-columns: 1fr; 39 | } 40 | .playground-result-wrapper { 41 | border-bottom: 0.5px solid rgba(0, 0, 0, 0.1); 42 | border-right: none; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/components/x-plot/common/explaination.tsx: -------------------------------------------------------------------------------- 1 | import React, { ReactNode } from 'react'; 2 | import * as _ from 'lodash'; 3 | import { Col, Row } from 'antd'; 4 | import "./explaination.less"; 5 | 6 | interface ExplainationDetailType { 7 | icon?: ReactNode; 8 | title: string | ReactNode; 9 | description: string | ReactNode; 10 | } 11 | export interface ExplainationProps { 12 | className: string; 13 | title: string | ReactNode; 14 | details: Array; 15 | } 16 | 17 | export const Explaination: React.FC = (props) => { 18 | const { title, details, className = '' } = props; 19 | return ( 20 |
21 |
{title}
22 |
23 | {_.map(details, (item, index) => ( 24 | 25 | {item.icon && {item.icon}} 26 | 27 | {item.title} 28 | {item.description} 29 | 30 | 31 | ))} 32 |
33 |
34 | ) 35 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (http://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # Typescript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # dotenv environment variable files 55 | .env* 56 | 57 | # gatsby files 58 | .cache/ 59 | public 60 | 61 | # Mac files 62 | .DS_Store 63 | 64 | # Yarn 65 | yarn-error.log 66 | .pnp/ 67 | .pnp.js 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | dist 72 | package-lock.json -------------------------------------------------------------------------------- /src/static/images/theme-dark.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/x-plot/common/use-g2plot.tsx: -------------------------------------------------------------------------------- 1 | import React, { useRef, useEffect } from 'react'; 2 | import { Plot, G2 } from '@antv/g2plot'; 3 | 4 | type Props = { 5 | Ctor: new (dom: HTMLDivElement, options: Partial) => Plot; 6 | options: Partial; 7 | onReady?: (P: Plot) => void; 8 | className?: string; 9 | style?: object; 10 | }; 11 | 12 | export function UseG2Plot({ 13 | className, 14 | Ctor, 15 | options, 16 | style = {}, 17 | onReady, 18 | }: Props) { 19 | const container = useRef(); 20 | const plotRef = useRef>(); 21 | 22 | const listenEvents = () => { 23 | if (plotRef.current) { 24 | plotRef.current.on(G2.VIEW_LIFE_CIRCLE.AFTER_RENDER, () => { 25 | onReady && onReady(plotRef.current); 26 | }); 27 | } 28 | }; 29 | 30 | useEffect(() => { 31 | if (plotRef.current) { 32 | plotRef.current.destroy(); 33 | } 34 | if (container.current) { 35 | const plot = new Ctor(container.current, options); 36 | plotRef.current = plot; 37 | listenEvents(); 38 | plot.render(); 39 | // fixMe: 折线图不能触发 afterRender 事件,暂时手动触发; 40 | plot.emit(G2.VIEW_LIFE_CIRCLE.AFTER_RENDER); 41 | } 42 | }, [container]); 43 | 44 | useEffect(() => { 45 | if (plotRef.current) { 46 | plotRef.current.update(options); 47 | } 48 | }, [options]); 49 | 50 | return
; 51 | } 52 | -------------------------------------------------------------------------------- /.all-contributorsrc: -------------------------------------------------------------------------------- 1 | { 2 | "projectName": "vis-dashboard", 3 | "projectOwner": "antvis", 4 | "repoType": "github", 5 | "repoHost": "https://github.com", 6 | "files": [ 7 | "README.md" 8 | ], 9 | "imageSize": 32, 10 | "contributorTemplate": "\">\" width=\"<%= options.imageSize %>px;\" alt=\"\"/>
<%= contributor.name %>
", 11 | "commit": true, 12 | "commitConvention": "none", 13 | "contributors": [ 14 | { 15 | "login": "visiky", 16 | "name": "Visiky", 17 | "avatar_url": "https://avatars.githubusercontent.com/u/15646325?v=4", 18 | "profile": "https://github.com/visiky", 19 | "contributions": [ 20 | "code", 21 | "design", 22 | "ideas" 23 | ] 24 | }, 25 | { 26 | "login": "yp0413150120", 27 | "name": "banli", 28 | "avatar_url": "https://avatars.githubusercontent.com/u/24318174?v=4", 29 | "profile": "https://github.com/yp0413150120", 30 | "contributions": [ 31 | "code" 32 | ] 33 | }, 34 | { 35 | "login": "joriewong", 36 | "name": "Jared Wang", 37 | "avatar_url": "https://avatars.githubusercontent.com/u/11408040?v=4", 38 | "profile": "https://wongjorie.top/", 39 | "contributions": [ 40 | "code" 41 | ] 42 | } 43 | ], 44 | "contributorsPerLine": 10 45 | } 46 | -------------------------------------------------------------------------------- /src/pages/dashboard-a/index.module.less: -------------------------------------------------------------------------------- 1 | @import '@/styles/base.less'; 2 | 3 | .header { 4 | background-color: #009cff; 5 | } 6 | 7 | .footer a { 8 | color: #009cff; 9 | 10 | &:hover { 11 | color: rgba(0, 156, 255, 0.85); 12 | } 13 | } 14 | 15 | .x-canvas { 16 | display: flex; 17 | flex-wrap: wrap; 18 | min-height: 90vh; 19 | width: 100%; 20 | overflow: hidden; 21 | 22 | :global { 23 | // 自定义 plot-container 样式 24 | .x-plot { 25 | display: flex; 26 | flex-direction: column; 27 | 28 | @header-height: 48px; 29 | // 带头 30 | .header + .plot-container { 31 | height: calc(100% - @header-height) !important; 32 | } 33 | 34 | .plot-container { 35 | width: 100%; 36 | flex: 1; 37 | } 38 | } 39 | } 40 | } 41 | 42 | // 屏幕适配下 43 | @media (max-width: 414px) { 44 | :global { 45 | .g2-html-annotation, 46 | .g2-html-annotation :not(.ring-annotation) * { 47 | font-size: 1rem !important; 48 | } 49 | 50 | .g2-html-annotation.custom-rank-annotation, 51 | .g2-html-annotation.custom-rank-annotation *, 52 | .g2-html-annotation .custom-tooltip, 53 | .g2-html-annotation .custom-tooltip * { 54 | font-size: 0.8rem !important; 55 | } 56 | } 57 | } 58 | 59 | @media (max-width: 375px) { 60 | :global { 61 | .g2-html-annotation, 62 | .g2-html-annotation :not(.ring-annotation) * { 63 | font-size: 0.8rem !important; 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/pages/dashboard-a/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | import cx from 'classnames'; 3 | import Layout from '@/layouts/layout'; 4 | import { parse } from '@/components/x-plot/common/parse'; 5 | import { reportJSON } from '@/dashboards/dashboard-a'; 6 | import dashboardStyles from '@/styles/dashboard.module.less'; 7 | import '@/styles/dark.less'; 8 | import styles from './index.module.less'; 9 | 10 | const SecondPage = () => { 11 | const [themeMode, setThemeMode] = useState(); 12 | const [json, updateJson] = useState(reportJSON); 13 | 14 | useEffect(() => { 15 | setThemeMode(document.body.dataset.theme); 16 | 17 | const observer = new MutationObserver(([record]) => { 18 | if ( 19 | record.target.nodeName === 'BODY' && 20 | record.attributeName === 'data-theme' 21 | ) { 22 | setThemeMode(document.body.dataset.theme); 23 | } 24 | }); 25 | 26 | observer.observe(document.body, { attributes: true }); 27 | }, []); 28 | 29 | useEffect(() => { 30 | updateJson({ ...json, theme: themeMode }); 31 | }, [themeMode]); 32 | 33 | return ( 34 | 40 |
{parse(json)}
41 |
42 | ); 43 | }; 44 | 45 | export default SecondPage; 46 | -------------------------------------------------------------------------------- /src/components/x-plot/common/parse.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import * as _ from 'lodash'; 3 | import { ReportNode, Report } from '@/types'; 4 | import { THEME_COLORS } from '@/themes'; 5 | import { getXComponent } from './register'; 6 | import './parse.less'; 7 | 8 | /** 9 | * 解析报表结构,递归生成 react 节点 10 | 11 | * @returns {ReactElement} 12 | */ 13 | export const parse = (report: Report) => { 14 | const { layouts, content: reportNode, theme } = report; 15 | let subComponents = []; 16 | const { children } = reportNode; 17 | if (children?.length > 0) { 18 | subComponents = children.map((child: ReportNode) => { 19 | return ( 20 |
21 | {parse({ layouts, content: child, theme })} 22 |
23 | ); 24 | }); 25 | } 26 | 27 | const ConcreteXComponent = getXComponent(reportNode.tag); // 根据不同的type,注入不同装饰器 28 | 29 | // 这里根据type决定组件具有什么装饰器(功能),至于实际渲染成什么组件由ReactLoader根据tag,Device的值去决定 30 | const c = React.createElement( 31 | ConcreteXComponent, 32 | { 33 | // 每个组件以id为key,保证id变化后,组件就unmount,并且这里key一定要放到第一个位置 34 | key: `${reportNode.id}-d`, 35 | id: `${reportNode.id}-d`, 36 | tag: reportNode.tag, 37 | attributes: _.assign( 38 | {}, 39 | { color: THEME_COLORS, theme }, 40 | reportNode.attributes, 41 | { 42 | layout: _.get(layouts, reportNode.id, {}), 43 | } 44 | ), 45 | }, 46 | subComponents 47 | ); 48 | 49 | return c; 50 | }; 51 | -------------------------------------------------------------------------------- /src/components/x-plot/pie.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import { Pie, PieOptions } from '@antv/g2plot'; 3 | import * as _ from 'lodash'; 4 | import { Header } from '@/components/x-plot/common/header'; 5 | import { UseG2Plot } from '@/components/x-plot/common/use-g2plot'; 6 | import { getData } from '@/utils/get-data'; 7 | import { XComponentProps } from '@/types'; 8 | 9 | type XPieProps = XComponentProps; 10 | 11 | export const XPie: React.FC = props => { 12 | const { attributes } = props; 13 | 14 | let chart: Pie; 15 | const [config, updateConfig] = useState({ 16 | data: [], 17 | angleField: '', 18 | colorField: '', 19 | }); 20 | 21 | useEffect(() => { 22 | getData(attributes.data).then(data => { 23 | updateConfig(_.assign({}, config, attributes, { data })); 24 | }); 25 | }, [attributes]); 26 | 27 | useEffect(() => { 28 | if (chart) { 29 | getData(attributes.data).then(data => { 30 | chart.changeData(data); 31 | }); 32 | } 33 | }, [attributes.data]); 34 | 35 | return ( 36 |
37 |
38 | {!_.isEmpty(config.data) && config.angleField && config.colorField && ( 39 | (chart = chartInstance)} 45 | /> 46 | )} 47 |
48 | ); 49 | }; 50 | -------------------------------------------------------------------------------- /src/components/x-plot/statistic.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Statistic } from 'antd'; 3 | import * as _ from 'lodash'; 4 | import { XComponentProps } from '@/types'; 5 | import { Header } from '@/components/x-plot/common/header'; 6 | import './statistic.less'; 7 | 8 | type XPlotProps = XComponentProps<{ 9 | /** 指标 */ 10 | measures: string[]; 11 | /** 元信息 */ 12 | meta: Record; 13 | /** 数据 */ 14 | data: object[]; 15 | style?: object; 16 | }>; 17 | 18 | export const XStatistic: React.FC = props => { 19 | const { measures, style, data, meta } = props.attributes; 20 | 21 | return ( 22 |
23 |
24 |
25 | {measures.map((measure, idx) => { 26 | const dataValue = _.get(data, ['0', measure]); 27 | const dataName = _.get(meta, [measure, 'alias']); 28 | const icon = _.get(meta, [measure, 'icon']); 29 | return ( 30 |
31 | {icon &&
{icon}
} 32 | 39 |
40 | ); 41 | })} 42 |
43 |
44 | ); 45 | }; 46 | -------------------------------------------------------------------------------- /src/pages/monitor-template/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | import cx from 'classnames'; 3 | import Layout from '@/layouts/layout'; 4 | import { parse } from '@/components/x-plot/common/parse'; 5 | import { monitorJson } from '@/dashboards/monitor-template'; 6 | import dashboardStyles from '@/styles/dashboard.module.less'; 7 | import '@/styles/dark.less'; 8 | import styles from './index.module.less'; 9 | 10 | const isBrowser = typeof document !== 'undefined'; 11 | 12 | const SecondPage = () => { 13 | const [themeMode, setThemeMode] = useState(); 14 | const [json, updateJson] = useState(monitorJson); 15 | 16 | useEffect(() => { 17 | setThemeMode(isBrowser ? document.body.dataset.theme : ''); 18 | 19 | const observer = new MutationObserver(([record]) => { 20 | if ( 21 | record.target.nodeName === 'BODY' && 22 | record.attributeName === 'data-theme' 23 | ) { 24 | setThemeMode(isBrowser ? document.body.dataset.theme : ''); 25 | } 26 | }); 27 | 28 | observer.observe(document.body, { attributes: true }); 29 | }, []); 30 | 31 | useEffect(() => { 32 | updateJson({ ...json, theme: themeMode }); 33 | }, [themeMode]); 34 | 35 | return ( 36 | 42 |
{parse(json)}
43 |
44 | ); 45 | }; 46 | 47 | export default SecondPage; 48 | -------------------------------------------------------------------------------- /src/components/x-plot/common/register.ts: -------------------------------------------------------------------------------- 1 | import { XPanel } from '@/components/x-container/panel'; 2 | import Not_Found from '../not-found'; 3 | import { XPie } from '../pie'; 4 | import { XBar } from '../bar'; 5 | import { XColumn } from '../column'; 6 | import { XLine } from '../line'; 7 | import { XRadialBar } from '../radial-bar'; 8 | import { XRingProgress } from '../ring-progress'; 9 | import { XArea } from '../area'; 10 | import { XSankey } from '../sankey'; 11 | import { XScatter } from '../scatter'; 12 | import { XGauge } from '../gauge'; 13 | import { XStatistic } from '../statistic'; 14 | import { XLiquid } from '../liquid'; 15 | 16 | /** 17 | * 组件池子 18 | */ 19 | const COMPONENTS_POOL = {}; 20 | 21 | /** 22 | * 注册组件 23 | */ 24 | function registerComponent(type: string, XComponent) { 25 | COMPONENTS_POOL[type] = XComponent; 26 | } 27 | 28 | /** 29 | * 获取组件 30 | */ 31 | export function getXComponent(type: string) { 32 | return COMPONENTS_POOL[type] || Not_Found; 33 | } 34 | 35 | registerComponent('pie', XPie); 36 | registerComponent('line', XLine); 37 | registerComponent('column', XColumn); 38 | registerComponent('bar', XBar); 39 | registerComponent('ring-progress', XRingProgress); 40 | registerComponent('radial-bar', XRadialBar); 41 | registerComponent('area', XArea); 42 | registerComponent('sankey', XSankey); 43 | registerComponent('scatter', XScatter); 44 | registerComponent('gauge', XGauge); 45 | registerComponent('statistic', XStatistic); 46 | registerComponent('liquid', XLiquid); 47 | 48 | /** 49 | * 容器,可以创建 react-grid-layout 50 | */ 51 | registerComponent('panel', XPanel); 52 | -------------------------------------------------------------------------------- /src/components/x-container/panel.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import cx from 'classnames'; 3 | import * as _ from 'lodash'; 4 | import { WidthProvider, Responsive, Layouts } from 'react-grid-layout'; 5 | import { XComponentProps } from '@/types'; 6 | import { isEditMode } from '@/utils/location'; 7 | import PageLoading from '../base/page-loading'; 8 | 9 | const ReactGridLayout = WidthProvider(Responsive); 10 | type XProps = XComponentProps<{ 11 | layout: Layouts; 12 | }>; 13 | 14 | export const XPanel: React.FC = ({ attributes, children }) => { 15 | const [layout, setLayout] = useState({}); 16 | 17 | useEffect(() => { 18 | setLayout(attributes.layout); 19 | }, [attributes.layout]); 20 | 21 | const onLayoutChange = (...args) => { 22 | console.log(...args); 23 | }; 24 | 25 | if (_.isEmpty(layout)) { 26 | return ; 27 | } 28 | return ( 29 |
30 | 44 | {children} 45 | 46 |
47 | ); 48 | }; 49 | -------------------------------------------------------------------------------- /src/components/base/code-loading.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import './code-loading.less'; 3 | 4 | const CodeLoading: React.FC = () => ( 5 |
6 |
7 |
8 |
9 | 10 | 11 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 |
28 |
29 | 30 | 31 | 32 |
33 |
34 | 35 | 36 | 37 |
38 |
39 |

Loading...

40 |
41 |
42 |
43 |
44 | ); 45 | 46 | export default CodeLoading; 47 | -------------------------------------------------------------------------------- /src/components/x-plot/column.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import { Column, ColumnOptions } from '@antv/g2plot'; 3 | import * as _ from 'lodash'; 4 | import { Header } from '@/components/x-plot/common/header'; 5 | import { UseG2Plot } from '@/components/x-plot/common/use-g2plot'; 6 | import { getData } from '@/utils/get-data'; 7 | import { XComponentProps } from '@/types'; 8 | import { getColor } from './area'; 9 | 10 | type XColumnProps = XComponentProps; 11 | 12 | export const XColumn: React.FC = props => { 13 | const { attributes } = props; 14 | 15 | let chart: Column; 16 | const [config, updateConfig] = useState({ 17 | data: [], 18 | xField: '', 19 | yField: '', 20 | }); 21 | 22 | useEffect(() => { 23 | getData(attributes.data).then(data => { 24 | updateConfig( 25 | _.assign({}, config, attributes, { data, color: getColor(attributes) }) 26 | ); 27 | }); 28 | }, [attributes]); 29 | 30 | useEffect(() => { 31 | if (chart) { 32 | getData(attributes.data).then(data => { 33 | chart.changeData(data); 34 | }); 35 | } 36 | }, [attributes.data]); 37 | 38 | return ( 39 |
40 |
41 | {!_.isEmpty(config.data) && config.xField && config.yField && ( 42 | (chart = chartInstance)} 48 | /> 49 | )} 50 |
51 | ); 52 | }; 53 | -------------------------------------------------------------------------------- /src/components/x-plot/common/header.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import * as cx from 'classnames'; 3 | import { XComponentProps } from '@/types'; 4 | import { Dropdown, Menu } from 'antd'; 5 | import * as _ from 'lodash'; 6 | import { DownOutlined } from '@ant-design/icons'; 7 | import './header.less'; 8 | 9 | type HeaderProps = { 10 | changeOption?: (number) => void; 11 | selectedOption?: number; 12 | } & XComponentProps; 13 | 14 | export const Header: React.FC = ({ 15 | attributes, 16 | changeOption, 17 | selectedOption = 0, 18 | }) => { 19 | const { title, options } = attributes; 20 | 21 | const noTitle = !attributes.title; 22 | 23 | const onMenuClick = e => { 24 | const key = e.key; 25 | changeOption && changeOption(key); 26 | }; 27 | 28 | const overlay = _.isEmpty(options) ? null : ( 29 | 34 | {_.map(options, (option, idx) => ( 35 | {`样式${ 36 | idx + 1 37 | }`} 38 | ))} 39 | 40 | ); 41 | 42 | return ( 43 |
48 | {title} 49 | {_.size(options) > 1 && ( 50 | 51 | 52 | 切换样式 53 | 54 | 55 | 56 | )} 57 |
58 | ); 59 | }; 60 | -------------------------------------------------------------------------------- /src/components/x-plot/gauge.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | import { Gauge, GaugeOptions } from '@antv/g2plot'; 3 | import * as _ from 'lodash'; 4 | import { Header } from '@/components/x-plot/common/header'; 5 | import { UseG2Plot } from '@/components/x-plot/common/use-g2plot'; 6 | import { Attributes, XComponentProps } from '@/types'; 7 | 8 | type GaugeAttributes = Attributes & { 9 | style?: object; 10 | options: GaugeOptions[]; 11 | }; 12 | 13 | type XGaugeProps = XComponentProps; 14 | 15 | export const XGauge: React.FC = props => { 16 | const { attributes } = props; 17 | 18 | const [options, updateConfig] = useState({ percent: null }); 19 | const [selectedIdx, changeSelectedIdx] = useState(0); 20 | 21 | useEffect(() => { 22 | const selectOptions = attributes.options[selectedIdx]; 23 | let range = selectOptions.range; 24 | const theme = attributes.theme; 25 | if (_.isString(range?.color)) { 26 | range = { 27 | ticks: [0, selectOptions.percent, 1], 28 | color: [ 29 | range.color, 30 | theme === 'dark' ? 'rgba(255,255,255,0.15)' : 'rgba(0,0,0,0.05)', 31 | ], 32 | }; 33 | } 34 | updateConfig(_.assign({}, options, selectOptions, { theme, range })); 35 | }, [attributes, selectedIdx]); 36 | 37 | return ( 38 |
43 |
48 | 49 |
50 | ); 51 | }; 52 | -------------------------------------------------------------------------------- /gatsby-node.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | const _ = require('lodash'); 4 | const MonacoWebpackPlugin = require(`monaco-editor-webpack-plugin`); 5 | 6 | exports.onCreateWebpackConfig = ({ actions, loaders, stage, getConfig }) => { 7 | const config = getConfig(); 8 | 9 | if (config.resolve) { 10 | config.resolve.alias = { 11 | ...config.resolve.alias, 12 | '@': path.resolve(__dirname, 'src'), 13 | }; 14 | } else { 15 | config.resolve = { 16 | alias: { '@': path.resolve(__dirname, 'src') }, 17 | }; 18 | } 19 | 20 | if (!config.plugins) { 21 | config.plugins = []; 22 | } 23 | config.plugins.push( 24 | new MonacoWebpackPlugin({ 25 | languages: ['javascript', 'json', 'typescript', 'html'], 26 | }) 27 | ); 28 | 29 | // This will completely replace the webpack config with the modified object. 30 | actions.replaceWebpackConfig(config); 31 | }; 32 | 33 | exports.createPages = async function ({ actions }) { 34 | const { createPage } = actions; 35 | 36 | let meta; 37 | try { 38 | meta = JSON.parse( 39 | fs.readFileSync(path.resolve(__dirname, 'src/examples/meta.json'), 'utf8') || 40 | '{}' 41 | ); 42 | } catch (e) { 43 | meta = {}; 44 | } 45 | 46 | const exampleTemplate = require.resolve('./src/templates/example.tsx'); 47 | 48 | _.each(meta.demos, demo => { 49 | const source = fs.readFileSync( 50 | `${path.resolve(__dirname, `src/examples/${demo.filename}`)}`, 51 | 'utf8' 52 | ); 53 | createPage({ 54 | path: `/gallery/${demo.pathname}`, // required 55 | component: exampleTemplate, 56 | context: { 57 | source, 58 | }, 59 | }); 60 | }); 61 | }; 62 | -------------------------------------------------------------------------------- /src/components/x-plot/common/parse.less: -------------------------------------------------------------------------------- 1 | @font-family: Avenir, -apple-system, BlinkMacSystemFont, 'Segoe UI', 2 | 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', 'Helvetica Neue', 3 | Helvetica, Arial, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 4 | sans-serif; 5 | 6 | // 每一个卡片都有一个 .x-component 的类名(.header + .plot-container 组成) 7 | .x-component { 8 | background-color: #fff; 9 | border-radius: 2px; 10 | 11 | @media (max-width: 640px) { 12 | .plot-container { 13 | padding: 16px; 14 | } 15 | } 16 | 17 | .header { 18 | display: flex; 19 | justify-content: space-between; 20 | align-items: center; 21 | 22 | .title { 23 | font-family: @font-family; 24 | font-size: 16px; 25 | line-height: 16px; 26 | color: rgba(0, 0, 0, 0.85); 27 | letter-spacing: -0.2px; 28 | font-weight: 500; 29 | } 30 | 31 | .style-switcher { 32 | color: rgba(0, 0, 0, 0.45); 33 | font-size: 12px; 34 | display: flex; 35 | align-items: center; 36 | cursor: pointer; 37 | :last-child { 38 | margin-left: 4px; 39 | } 40 | } 41 | } 42 | 43 | .header { 44 | padding: 16px 24px; 45 | } 46 | .plot-container { 47 | padding: 0 24px 24px; 48 | } 49 | } 50 | 51 | /** 拖拽部位的样式 start */ 52 | :not(.draggable) .header { 53 | cursor: default; 54 | } 55 | 56 | /** 拖拽部位的样式 end */ 57 | 58 | // 头部样式切换按钮 59 | .header-style-switcher-menu { 60 | padding: 0; 61 | 62 | .menu-item { 63 | font-size: 12px; 64 | padding: 4px 8px; 65 | } 66 | 67 | .ant-dropdown-menu-item { 68 | color: rgba(0, 0, 0, 0.45); 69 | } 70 | 71 | .ant-dropdown-menu-item-selected { 72 | color: #009cff !important; 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/pages/home/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import _ from 'lodash'; 3 | import Layout from '@/layouts/layout'; 4 | import { Home } from '@/components/base/home'; 5 | import GalleryMeta from '@/examples/meta.json'; 6 | 7 | export default () => { 8 | const lang = 'zh'; 9 | const dashboards = [ 10 | { 11 | image: 12 | 'https://gw.alipayobjects.com/zos/antfincdn/BPnCLgmtzC/1096*560_light.png', 13 | darkImage: 14 | 'https://gw.alipayobjects.com/zos/antfincdn/0GDWIEjbkO/1094*560.png', 15 | name: 'C 端场景', 16 | path: 'dashboard-a', 17 | }, 18 | { 19 | image: 20 | 'https://gw.alipayobjects.com/zos/antfincdn/n%268Jqw3vKF/1096*560_light.png', 21 | darkImage: 22 | 'https://gw.alipayobjects.com/zos/antfincdn/YI%241xJfKMf/2094*560.png', 23 | name: '监控场景', 24 | path: 'monitor-template', 25 | }, 26 | { 27 | image: 28 | 'https://gw.alipayobjects.com/zos/antfincdn/wuk2prOsEL/placeholder.png', 29 | name: '建设中', 30 | path: null, 31 | }, 32 | { 33 | image: 34 | 'https://gw.alipayobjects.com/zos/antfincdn/wuk2prOsEL/placeholder.png', 35 | name: '建设中', 36 | path: null, 37 | }, 38 | ]; 39 | 40 | const charts = GalleryMeta.demos.map((d: any) => ({ 41 | ..._.omit(d, ['screenshots', 'title', 'pathname']), 42 | image: d.screenshots.default, 43 | darkImage: d.screenshots.dark, 44 | name: d.title[lang], 45 | path: `gallery/${d.pathname}`, 46 | })); 47 | 48 | /** 三方图表 */ 49 | const thirdPartyCharts = [...GalleryMeta.third]; 50 | 51 | return ( 52 | 53 | 58 | 59 | ); 60 | }; 61 | -------------------------------------------------------------------------------- /src/components/x-plot/scatter.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import { Scatter, ScatterOptions } from '@antv/g2plot'; 3 | import * as _ from 'lodash'; 4 | import { Header } from '@/components/x-plot/common/header'; 5 | import { UseG2Plot } from '@/components/x-plot/common/use-g2plot'; 6 | import { XComponentProps } from '@/types'; 7 | import { getData } from '@/utils/get-data'; 8 | 9 | type XScatterProps = XComponentProps; 10 | 11 | export const XScatter: React.FC = props => { 12 | const { attributes } = props; 13 | 14 | let chart: Scatter; 15 | const [config, updateConfig] = useState({ 16 | data: [], 17 | xField: '', 18 | yField: '', 19 | }); 20 | 21 | useEffect(() => { 22 | getData(attributes.data).then(data => { 23 | const activeStateStyle = { shadowColor: '#E8EDF3' }; 24 | if (attributes.theme === 'dark') { 25 | activeStateStyle.shadowColor = 'rgba(255,255,255,0.2)'; 26 | } 27 | updateConfig( 28 | _.assign({}, config, attributes, { 29 | data, 30 | state: { active: { style: activeStateStyle } }, 31 | }) 32 | ); 33 | }); 34 | }, [attributes]); 35 | 36 | useEffect(() => { 37 | if (chart) { 38 | getData(attributes.data).then(data => { 39 | chart.changeData(data); 40 | }); 41 | } 42 | }, [attributes.data]); 43 | 44 | return ( 45 |
46 |
47 | {!_.isEmpty(config.data) && config.xField && config.yField && ( 48 | (chart = chartInstance)} 54 | /> 55 | )} 56 |
57 | ); 58 | }; 59 | -------------------------------------------------------------------------------- /src/layouts/layout.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Layout component that queries for data 3 | * with Gatsby's useStaticQuery component 4 | * 5 | * See: https://www.gatsbyjs.com/docs/use-static-query/ 6 | */ 7 | 8 | import React from 'react'; 9 | import { useStaticQuery, graphql } from 'gatsby'; 10 | import cx from 'classnames'; 11 | import Header from './header'; 12 | import Footer from './footer'; 13 | import Seo from './seo'; 14 | import './layout.less'; 15 | 16 | type Props = { 17 | hideSiteTitle?: boolean; 18 | siteTitle?: string; 19 | /** 网站布局 · 头部 classname */ 20 | headerClassName?: string; 21 | /** 网站布局 · 主体 classname */ 22 | mainClassName?: string; 23 | /** 网站布局 · 脚部 classname */ 24 | footerClassName?: string; 25 | /** 是否开启主题模式切换 */ 26 | themeModeSwitcher?: boolean; 27 | }; 28 | 29 | const Layout: React.FC = ({ 30 | children, 31 | siteTitle, 32 | hideSiteTitle, 33 | headerClassName, 34 | mainClassName, 35 | footerClassName, 36 | themeModeSwitcher, 37 | }) => { 38 | const data = useStaticQuery(graphql` 39 | query SiteTitleQuery { 40 | site { 41 | siteMetadata { 42 | title 43 | githubUrl 44 | author 45 | contact 46 | } 47 | } 48 | } 49 | `); 50 | 51 | const { title, githubUrl, contact, author } = data.site.siteMetadata; 52 | 53 | return ( 54 | <> 55 | 56 | {!hideSiteTitle && ( 57 |
63 | )} 64 |
{children}
65 |