{"id":51311439,"url":"https://github.com/plotly/dash-sunburst","last_synced_at":"2026-07-01T04:03:46.685Z","repository":{"id":48029959,"uuid":"150488111","full_name":"plotly/dash-sunburst","owner":"plotly","description":"Dash / React + D3 tutorial: Sunburst diagrams","archived":false,"fork":false,"pushed_at":"2023-01-13T22:45:00.000Z","size":1929,"stargazers_count":47,"open_issues_count":17,"forks_count":11,"subscribers_count":8,"default_branch":"master","last_synced_at":"2024-05-12T00:40:43.397Z","etag":null,"topics":["dash","data-visualization","plotly","plotly-dash","python","sunburst","sunburst-chart"],"latest_commit_sha":null,"homepage":"","language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/plotly.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2018-09-26T20:46:12.000Z","updated_at":"2024-03-07T01:43:17.000Z","dependencies_parsed_at":"2023-02-09T17:31:36.998Z","dependency_job_id":null,"html_url":"https://github.com/plotly/dash-sunburst","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/plotly/dash-sunburst","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/plotly%2Fdash-sunburst","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/plotly%2Fdash-sunburst/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/plotly%2Fdash-sunburst/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/plotly%2Fdash-sunburst/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/plotly","download_url":"https://codeload.github.com/plotly/dash-sunburst/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/plotly%2Fdash-sunburst/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":34992075,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-07-01T02:00:05.325Z","response_time":130,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["dash","data-visualization","plotly","plotly-dash","python","sunburst","sunburst-chart"],"created_at":"2026-07-01T04:03:44.538Z","updated_at":"2026-07-01T04:03:46.659Z","avatar_url":"https://github.com/plotly.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"_Please note: this package is intended as a tutorial. It is not recommended for actual use in your apps - for that purpose we have the sunburst trace type in plotly.js / dcc.Graph https://plotly.com/python/sunburst-charts/_\n\n# Dash Sunburst\n\n![sunburst chart](dash-sunburst.png)\n\nThis repository demonstrates the principles of combining D3 with React, using a Sunburst chart as an example, and was created from the [`dash-component-boilerplate` template](https://github.com/plotly/dash-component-boilerplate). The Demo uses this `Sunburst` component to show the contents of a house, with items being added, removed, and resized over time, and letting you zoom in and out of the rooms and items both from within the component itself and from another control.\n\nThis component was created primarily as a _D3.JS + Dash_ tutorial. You can use this component in your projects but we are not maintaining it. In fact, we built a first-class Sunburst chart as part of plotly.js and we recommend using this sunburst chart instead: https://plot.ly/python/sunburst-charts/\n\nTo run the Dash demo:\n1. Clone this repo\n2. Run the demo app\n```\npython usage.py\n```\n3. Open your web browser to http://localhost:8050\n![sunburst chart in Python](readme_usage_py.png)\n\n# Code walkthrough - JavaScript side\n\nFollowing the structure laid out in the [D3 + React tutorial](https://gist.github.com/alexcjohnson/a4b714eee8afd2123ee00cb5b3278a5f) we make two files: [`d3/sunburst.js`](src/lib/d3/sunburst.js) for the D3 component and [`components/Sunburst.react.js`](src/lib/components/Sunburst.react.js) for its React/Dash wrapper. Following the `dash-component-boilerplate` example, this component is then exported using [`index.js`](src/lib/index.js) which is imported by the main component in [`App.js`](src/demo/App.js).\n\n## Sunburst.react.js\n\nThis wrapper simply connects the React component API to the similar structures we create in the D3 component. Excerpting from this file out of order, we see:\n\n```js\nSunburst.propTypes = {\n    /**\n     * id and setProps are standard for Dash components\n     */\n    id: PropTypes.string,\n    setProps: PropTypes.func,\n\n    /**\n     * All the rest are the state of the figure. See the full source for details\n     */\n    width: PropTypes.number,\n    height: PropTypes.number,\n    padding: PropTypes.number,\n    innerRadius: PropTypes.number,\n    transitionDuration: PropTypes.number,\n    data: PropTypes.object.isRequired,\n    dataVersion: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),\n    selectedPath: PropTypes.arrayOf(PropTypes.string),\n    interactive: PropTypes.bool\n};\n```\n\nIn addition to the standard `id` and `setProps` props, we insert all the state needed by the D3 component as props of the React wrapper. This gives us type validation - for the most part anyway; this example doesn't validate the structure of `data`, nor put limits on the numeric fields, but a more production-ready version may want to do this. Note in particular the `dataVersion` prop. We will use this to avoid having to copy - and diff - the entire `data` object, which may be large and tedious. Also `selectedPath`, which is connected to the state of the user interaction with the sunburst, as different parts of the subtree are selected. `interactive` lets you disable click-to-select nodes, when you want that managed elsewhere.\n\n```js\nrender() {\n    return \u003cdiv id={this.props.id} ref={el =\u003e {this.el = el}} /\u003e;\n}\n```\n\nIn `render` we just create an empty `\u003cdiv\u003e` and store a reference to it in `this.el`.\n\n```js\ncomponentDidMount() {\n    this.sunburst = new SunburstD3(this.el, this.props, figure =\u003e {\n        const {setProps} = this.props;\n        const {selectedPath} = figure;\n\n        if (setProps) { setProps({selectedPath}); }\n        else { this.setState({selectedPath}); }\n    });\n}\n```\n\n`componentDidMount` instantiates our D3 component, giving it the element to render into, the props for initial render (it will ignore the Dash-specific ones), and a callback to respond to changes from inside that component. A more complex component might emit a variety of events depending on different user interactions, but in the end all that really matters is the current state of the component, not what specifically changed with this event. Here we know the only thing that changed is `selectedPath` but it would be just as well to call `setProps(figure)` no matter what event was emitted.\n\n```js\ncomponentDidUpdate() {\n    this.sunburst.update(this.props);\n}\n```\n\nWhenever the React component gets new props, it simply forwards them on to the D3 component.\n\n## App.js\n\nWe don't need to know anything about the D3 component in order to use the `Sunburst` React component in our app - just what's encapsulated in the `Sunburst` component itself:\n\n```js\nconstructor() {\n    super();\n    this.state = {\n        transitionDuration: 1000,\n        selectedPath: ['living room'],\n        dataVersion: 1,\n        data: {\n            ...\n        }\n    }\n    this.setProps = this.setProps.bind(this);\n    this.mutateData = this.mutateData.bind(this);\n\n    this.period = 3;\n    this.updateInterval = setInterval(this.mutateData, 1000 * this.period);\n}\n```\n\nIn the `App` constructor we start with a seed state for the `Sunburst` - because this is a simple app where everything is related to that `Sunburst`, its state is stored in the top level of `App.state`, but a more complex app would nest it. We also initialize the interval that will periodically edit the data. We don't need to be concerned with the `mutateData` method, except to know that all it does is call `this.setState({data: updatedData})`\n\n```js\nrender() {\n    const {data, selectedPath} = this.state;\n    const selectedPathStr = selectedPath.join(',');\n    const paths = getPathStrs(data, '');\n    const options = paths.map(path =\u003e (\n        \u003coption value={path} key={path}\u003e\n            {path.split(',').join('-\u003e') || 'root'}\n        \u003c/option\u003e\n    ));\n    const selectChange = e =\u003e {\n        this.setState({selectedPath: e.target.value.split(',')})\n    };\n\n    return (\n        \u003cdiv\u003e\n            \u003ch2\u003eSunburst Demo\u003c/h2\u003e\n            \u003cp\u003eClick a node, or select it in the dropdown, to select a subtree.\u003c/p\u003e\n            \u003cp\u003eEvery {this.period} seconds a node will be added, removed, resized, or renamed\u003c/p\u003e\n            \u003cSunburst\n                setProps={this.setProps}\n                {...this.state}\n            /\u003e\n            \u003cselect value={selectedPathStr} onChange={selectChange}\u003e\n                {options}\n            \u003c/select\u003e\n        \u003c/div\u003e\n    )\n}\n```\n\n`App` renders some introductory notes, our `Sunburst` component, and a dropdown menu that pulls the complete list of paths out of the same state that's used by the `Sunburst`. You'll notice that whether we select an item by clicking on it directly or via this dropdown, both the dropdown and the `Sunburst` update.\n\n## sunburst.js\n\nFinally, here's the D3 code, all contained in a class we export as `SunburstD3`.\n\n```js\nconstructor(el, figure, onChange) {\n    const self = this;\n    self.update = self.update.bind(self);\n    self._update = self._update.bind(self);\n\n    self.svg = d3.select(el).append('svg');\n    self.pathGroup = self.svg.append('g');\n    self.textGroup = self.svg.append('g')\n        .style('pointer-events', 'none');\n\n    self.angularScale = d3.scale.linear().range([0, Tau]);\n    self.radialScale = d3.scale.sqrt();\n    self.colorScale = d3.scale.category20();\n    self.partition = d3.layout.partition()\n        .value(d =\u003e !d.children \u0026\u0026 d.size)\n        .sort((a, b) =\u003e a.i - b.i);\n\n    self.arc = d3.svg.arc()\n        .startAngle(d =\u003e constrain(self.angularScale(d.x), 0, Tau))\n        .endAngle(d =\u003e constrain(self.angularScale(d.x + d.dx), 0, Tau))\n        .innerRadius(d =\u003e Math.max(0, self.radialScale(d.y)))\n        .outerRadius(d =\u003e Math.max(0, self.radialScale(d.y + d.dy)));\n\n    self.figure = {};\n\n    self.onChange = onChange;\n\n    self.initialized = false;\n\n    self._promise = Promise.resolve();\n\n    self.update(figure);\n}\n```\n\nOur constructor does 3 things:\n1. Creates the container elements that we'll need no matter what specific diagram we render inside: `self.svg` is the `\u003csvg\u003e` element, `self.pathGroup` will contain the sunburst arcs, and `self.textGroup` will hold text, added as a separate group so the text will always be in front of the arcs.\n2. Pre-calculates d3 helpers that won't change later (`self.angularScale` through `self.arc`)\n3. Sends the initial figure to `self.update`.\nThere's also a bit of complication around updating potentially during animations. `self._promise` is a chain that's added on to whenever a new animation is scheduled, and `self.update` is an async wrapper around the synchronous `self._update`, ensuring a new figure is applied only after that chain is complete.\n\n`self._update` is the meat, so we'll tackle it in pieces:\n\n### Figure setup\n```js\nconst oldFigure = self.figure;\n\n// fill defaults in the new figure\nconst width = figure.width || dflts.width;\nconst height = figure.height || dflts.height;\n// interactive: undefined defaults to true\nconst interactive = figure.interactive !== false;\nconst padding = figure.padding || dflts.padding;\nconst innerRadius = figure.innerRadius || dflts.innerRadius;\nconst transitionDuration = figure.transitionDuration || dflts.transitionDuration;\nconst {data, dataVersion} = figure;\nconst selectedPath = figure.selectedPath || [];\n\nconst newFigure = self.figure = {\n    width,\n    height,\n    interactive,\n    padding,\n    innerRadius,\n    transitionDuration,\n    data,\n    dataVersion,\n    selectedPath\n};\n```\n\nHere we stash the previous figure as `oldFigure` and create a new one, inserting default values where values were not provided.\n\nNext comes functions containing our standard D3 code (which was inspired by https://bl.ocks.org/mbostock/4348373 but has been heavily modified, as you can see. Notice that I'm using D3V3 here so some things will change if you're using V4 or V5), but we've broken up the activity by purpose, `transitionToNode`, `updatePaths`, and `setSize`. We'll use these depending on the observed changes. The only items I want to call out within this block are\n1. `transitionToNode` is used in the `click` callback for our nodes (wrapped up with animation management code).\n2. At the end of `transitionToNode` is the block:\n```js\nif(self.onChange) {\n    self.figure.selectedPath = getPath(node);\n    self.onChange(self.figure);\n}\n```\nSo when this is called on a click, it updates the `figure` and we pass it back up the React chain of command. But it's also called during drawing, in which case the figure we pass back up will be the same one we just received. Which makes the next section extremely important...\n\n### Diffing\n```js\nconst change = diff(oldFigure, newFigure);\nif(!change) { return; }\n\nconst sizeChange = change.width || change.height || change.padding;\nconst dataChange = change.data;\n```\n\nWe compare the old and new figures to determine what changed. Here we're concerned with three things:\n1) Are there any changes at all? If not, we can bail out now, without running any DOM manipulations. This will happen regularly due to `transitionToNode` as described above.\n2) Did the size of the figure change? If so there are more extensive things we need to do, that will require updating the size and position of all our paths and text elements.\n3) Did the data change? Inside `diff` we look for `dataVersion`, and if we find it we skip comparing `data` itself between the old and new figures, instead reporting changes in `dataVersion` as `change.data`.\n\nThere can be other changes that lead to a truthy `change` without setting either `sizeChange` or `dataChange` - such as `innerRadius` and `selectedPath`, and in general if we added styling properties (colors, line widths, font sizes...) they would fall into this category too. Those can follow the minimal update pathway below.\n\n### Drawing\n```js\nif(sizeChange) { setSize(); }\n\nlet paths = self.pathGroup.selectAll('path');\nlet texts = self.textGroup.selectAll('text');\n\nif(dataChange) {\n    // clone data before partitioning, since this mutates the data\n    self.nodes = self.partition.nodes(addIndices(JSON.parse(JSON.stringify(data))));\n    paths = paths.data(self.nodes, getPathStr);\n    texts = texts.data(self.nodes, getPathStr);\n\n    // exit paths at the beginning of the transition\n    // enters will happen at the end\n    paths.exit().remove();\n    texts.exit().remove();\n}\n\nconst selectedNode = getNode(self.nodes[0], selectedPath);\n// no node: path is wrong, probably because we received a new selectedPath\n// before the data it belongs with\nif(!selectedNode) { return retVal; }\n\n// immediate redraw rather than transition if:\nconst shouldAnimate =\n    // first draw\n    self.initialized \u0026\u0026\n    // new root node\n    (newRootName === oldRootName) \u0026\u0026\n    // not a pure up/down transition\n    sameHead(oldSelectedPath, newSelectedPath) \u0026\u0026\n    // the previous data didn't contain the new selected node\n    // this can happen if we transition selectedPath first, then data\n    (!dataChange || getNode(oldFigure.data, newSelectedPath));\n\nconsole.log(shouldAnimate, oldSelectedPath, newSelectedPath);\n\nif(shouldAnimate) {\n    retVal = new Promise(resolve =\u003e {\n        transitionToNode(selectedNode)\n            .each('end', () =\u003e {\n                updatePaths(paths, texts, dataChange);\n                self.transitioning = false;\n                resolve();\n            });\n    });\n}\nelse {\n    // first draw has no animation, and initializes the scales\n    self.angularScale.domain(selectedX(selectedNode));\n    self.radialScale.domain(selectedY(selectedNode))\n    self.radialScale.range(selectedRadius(selectedNode));\n\n    updatePaths(paths, texts, dataChange);\n\n    self.initialized = true;\n}\n```\n\nIf the size and data did not change, all we do is select the paths and texts, find the selected node, transition to it, and, upon finishing that transition, update the paths - and `updatePaths` knows about `dataChange` so it can skip the `enter()` steps.\n\nThe logic for whether the state transition is amenable to animation or not is handled here, in `shouldAnimate`. This is important for Dash - and for React integration in general - because it means this is the *only* place we need to worry about edge detection. Dash apps are stateless, so it's particularly tricky to determine this on the Python side, and React apps are best written the same way as far down the tree as possible. D3 to a certain extent *can* work similarly, but for finer control we explicitly calculate what kind of change has been made and tell D3 whether to animate.\n\nNow lets open the JavaScript demo environment:\n```\nnpm run start\n```\n\nLo and behold, we have a zoomable sunburst chart, connected to changing data and sibling UI controls, drawn with D3 and React :tada: There are of course bits of polish to be added if this component were to be used in production - shrinking or removing text that's too big for its arc, and creating style props, for example, and nicer tooltips than the built-in `\u003ctitle\u003e` elements. But the principles are the same.\n\n# Code Walkthrough - Python side\n\n`dash-component-boilerplate` makes it super easy to connect the React component we just made to Python. As in its [README](https://github.com/plotly/dash-component-boilerplate), run:\n```\nnpm run build:js-dev\nnpm run build:py\n```\nFor these build steps to run without warnings, the `lib/components` directory should contain *only* React components, which is why we moved the D3 code into its own directory, `lib/d3`. Now we can use the component in our Dash app [`usage.py`](usage.py):\n```py\nfrom dash_sunburst import Sunburst\n```\n\nWe'll make a simple app using this component: Feeding some static data to the component, we'll display the selected path elsewhere, and create a plotly.js graph that calculates some statistics based on the displayed data and selected path. First the static data and the app layout:\n\n```py\nsunburst_data = { ... }\n\napp.layout = html.Div([\n    html.Div(\n        [Sunburst(id='sun', data=sunburst_data)],\n        style={'width': '49%', 'display': 'inline-block', 'float': 'left'}),\n    dcc.Graph(\n        id='graph',\n        style={'width': '49%', 'display': 'inline-block', 'float': 'left'}),\n    html.Div(id='output', style={'clear': 'both'})\n])\n```\nOur `Sunburst` component doesn't support `style`, so we wrap it in an `html.Div`. The `Graph` and `Div#output` are initially blank, but our callbacks will fill them in on load. The content of these callbacks is straightforward Python - check out `usage.py` for the complete code - the key is simply to identify the dependencies of each one using the `@app.callback` decorator:\n```py\n@app.callback(Output('output', 'children'), [Input('sun', 'selectedPath')])\ndef display_selected(selected_path):\n    # format the selected path for display as text\n    ...\n\n@app.callback(Output('graph', 'figure'), [Input('sun', 'data'), Input('sun', 'selectedPath')])\ndef display_graph(data, selected_path):\n    # crawl the sunburst data, along with its selected path,\n    # to create the related plotly.js graph\n    ...\n```\n\nAnd that's it! `python usage.py` gives us our D3 sunburst diagram, connected through Dash to whatever else we choose.\n\n![usage.py running](readme_usage_py.png)\n\nFurther examples expanding on server-side updates can be found in [`usage_backend_update_via_controls.py`](usage_backend_update_via_controls.py) and [`usage_backend_update_via_selections.py`](usage_backend_update_via_selections.py)\n\n# More Resources\n- Learn more about Dash: https://dash.plot.ly\n- View the original component boilerplate: https://github.com/plotly/dash-component-boilerplate\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fplotly%2Fdash-sunburst","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fplotly%2Fdash-sunburst","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fplotly%2Fdash-sunburst/lists"}