spstreets / gtfs_manager Goto Github PK
View Code? Open in Web Editor NEWA GUI for viewing and editing GTFS data
License: MIT License
A GUI for viewing and editing GTFS data
License: MIT License
Currently, for trips that have an associated shape, if a stop within that trip is changed, we disregard the entire shape and simply interpolate between each individual stop. An improvement would be to use the shape between each stop which has not been edited and only interpolate between stops where one has been edited.
There are problems with this however.
What if the user wants to continue to use the existing shape because the new stop added is on this shape?
What to do if the user wants to save the file not having updated the shape? Just have that one particular shape missing? Use the old one? Automatically create a new one interpolating between the stops?
Will need to make it clear that changes to a stop will affect other routes also referencing that stop. Maybe want editing functionality that will automatically create a new copy of the original stop, allowing a stop to be easily updated only for a specific route.
This is an meta-issue discuss what the outcome after 2 months, 3 months, and beyond. Idea: split these out into sub-issues possibly linked with time-dependent milestones. In rough time/difficulty order:
Each of these can be split out as an issue and assigned to a milestone.
OLD: Provide functionality to filter which trips to have in the list, which makes it easier to navigate to certain trips, and display certain trips on the map.
NEW: The filtered items will be displayed on the map, not the list. This is because the map will be the main point of user interaction and is much easier for users to discover/identify a range of different items.
Might also want to use the filter as a condition for certain analysis, but this is probably a much more advanced feature that would only happen in the future.
Simply show all trips on map.
Want to know eg specifically what Stops are being used at that point? Can instantly see this on the map.
Want to know eg specifically what Agencies or Routes are running at that point? No direct support for now, just click the different trips to find out.
To display/identify only deleted items which eg include an agency, a stop, and a stop_time. Can simply show all of the Agencies trips on the map, but this will not make clear that an agency has been deleted vs a number of stops. Maybe the map is used to get an overview and geographic sense, and provide a separate list the same as edited below to detail the actual items which have been deleted
To display/identify only not deleted items which eg include an agency, a stop, and a stop_time. Can simply show all of the Agencies trips on the map, but this will not make clear that an agency has been deleted vs a number of stops. Maybe the map is used to get an overview and geographic sense, and provide a separate list the same as edited below to detail the actual items which have been deleted
To display/identify only edited items which eg include an agency, a stop, and a stop_time. How to display all these distinctly on the map? Not possible? Just have a separate list of items which can be of different types, with each widget showing minimal info but are clickable to reveal the item on the list and map?
(Mostly) fixed by setting the appropriate flags in .layout()
to indicate that paths/circles should be recalucated when window/widget is resized. The problem is that the with current design this requires recreating all the boxed groups, which is far too slow. Really we need to redesign so that paths and boxes are created for some fixed canvas dimensions (eg the bitmap dimensions) and then to draw on the actual widget sized canvas we transform all the paths/circles with the appropriate scale.
An easier temporary solution is to simply accept that there will be a delay. This is reasonably fine currently for a single size change, eg hitting full screen, but not suitable for click and drag resizing, need to ensure the only do paths/circles recalculations once the drag resizing if finished to avoid doing it multiple times.
It seems redundant to display each trip in a separate list when most of the information is the same, usually just a different ordering of stop sequence, and displaying them separately obfiscates this and makes it hard to see any differences between the trips, if any.
Add a background to the map, using either raster or vector tiles.
The map currently draws all the routes on the map with every paint, which is slow, and doesn't use any form of caching. This is exacerbated by the fact the a bug/quirk in the Either widget (linebender/druid#2205) means there is often a full screen refresh, and thus map paint, even when not interacting with the map.
Initially this will likely be solved by drawing the lines once, saving to a bitmap, and then reusing this bitmap on subsequent paints, only drawing individual for hover/selection hightlighting, and recreating the bitmap when routes are shown/hidden. See https://xi.zulipchat.com/#narrow/stream/259397-piet/topic/Apply.20transform.20to.20GPU.20buffer
Hopefully in the future it will be possible to cache the paths themselves and apply transformations directly to them, and avoid having to use a bitmap.
One question that has likely practical value to bus operators/planners is figuring out how many buses are physically needed to provide service for some day. It's trivial to calculate an upper bound -- just count the number of trips occurring on the given day. But of course one vehicle can perform multiple non-simultaneous trips. And in practice, probably the same bus finishes one direction of a route and starts a trip going in the opposite direction.
Maybe v1 of answering this just looks at non-overlapping time intervals for trips and calculates the max happening through the day. Then v2 would get a bit fancier and stop assuming a bus can just warp from the end of one trip to the start of another in some amount of time, and make sure there's enough time to drive between the endpoints.
This isn't a high priority thing to work on, just one example of an interesting question that can be asked about GTFS. I haven't done any searching around to see if it's been solved in an existing library.
The most user friendly way would be to just click on the map, which is easy to implement. But will eventually probably want a way to do this purely in the list, rather than needing to use the map? In which case this is TBD.
Is it possible to clearly display all the corresponding routes/trips/stop_times for a particular stop? Possibly not in the list, only on the map? But even on the map it will not be clear since different routes for the same stop will of course overlap and not be easily differentiable - but only for that section of the route? Also just showing on the map doesn't make it easy to find route names etc. Maybe displaying a list of all route names that use that stop in the stop widget would be best? Should at least display the number of trips which use that stop (either on the map, list, or both) and maybe provide a button which just cycles through these trips displaying eg 3/12. This might make it easier to quickly understand what trips there are and also allows selecting single trips at a time to make it easier to select the corresponding trip you are interested in.
Usually clicking a stop selects that stop. If (single?) trip has already been selected, clicking instead selects the stop time (and stop?)
It's often good to have small input datasets for testing and minimal reproducible examples that run fast.
Anyone no of any such GTFS files? Thinking it would be idea to have one for SP the case study city.
Currently using some Cairo specifc types in map code which means the app only works on Linux, need to work out what the cross platform equivalent is.
Click point should set the center of the viewport.
All fields can be directly individually edited. Some will require format checking or specialised input controls, eg for dates, etc. Could even have user specified formats to enforce things like naming conventions for routes. Reference/relational fields like trip_id, stop_id, service_id, agency_id, route_id, shape_id, etc are updated by selecting from a list to ensure only valid ids are used.
For updating fields related to ordering like route_sort_order or stop_sequence, will need to update by moving the item to a new position (using keyboard shortcuts initially, but maybe drag and drop in the future), rather updating the numeric value directly to:
It would be useful to still see deleted items in the main item list, but greyed out. In general, I see deleting more as a flag that the data has been removed (but is still accessible), until the updated data is actually exported from the app to a GTFS file, rather than actually removing the data and making it inaccessible until restored. This makes it easier to display deletions in context, easily see if there has been a deletion, and reuse/copy their data for other items.
Deleting any of these will also delete all "child items" eg deleting a route also delete all it's trips.
Can all be deleted directly with no knock on effect. Deleting a trip will delete all of it's stop_times
, frequencies
, and shapes
.
Stops and calendar items cannot be deleted unless they are not referenced by any stop_times or trips respectively. When all referencing trip
s/stop_time
s are deleted, the referenced calendar
/stop
s won't be automatically deleted, but will now be deletable individually. Will allso provide functionality to clean up all orphaned stops and calendars in one go. This cleanup functionality is related to validation functionality which should be reasonably distinct, and users can do it all in one big lump before saving/publishing.
For each type of item, eg route, trip, stop, agency, etc, provide the following "edit types":
Create new
Delete
Update
It should be possible to delete edits, which will restore the app data to what is in the gtfs file.
If there are multiple types of edits for the same item, they will be all displayed together. Eg if an item has had a field updated, and was then subsequenttly delete, both edits should be displayed together. If a new item is created and then subsequently deleted, it makes no sense to delete the created edit with also deleting the delete edit. However, edits will need to be stored separately in order to function as an edit/undo stack.
Currently displaying edits in a separate list, but it probably makes sense to just show them in the actual item list and just provide a filter to only show items which have been edited as an equivalent to the edits list. Or at least have a flag for items in the item list to signal that they have been edited, with link to the edit. This might be better since we can't we display delete edits in the item list, unless we just displayed them greyed out or something, but would need to distinguish that from items that are simply deselected from the map.
Any other topics we should cover in the user testing session?
This is a catch all for all performance issues.
Launching the app in it's current state with the full Sao Paulo dataset will max out my 16GB RAM. This problem is entirely solved by limiting the data from around 20-50 stop_times per trip, to 2. The memory usage then drops to around 1GB.
For now, simply limit the number of routes (this is crude and only works well for SP, since there is 1 agency and always only 2 trips) so I can focus on making progress with features, and then revisit when I will understand Druid better, have a clearer list of architecture requirements necessary for features, can share a reasonably complete example with the Druid community to make it easier to ask for suggestions for ways to handle the large data and performance.
Pagination could work but wouldn't be straight forward to know what number to group the routes by since different routes can have vastly different amounts of trips and stop_times.
A better solution might to not nest stop_times in trips in routes in agencies and just have everything eg in separate columns (but probably still all in the same column), so only one eg trips list can be viewed at a time (similar to how the stop works). This would also make it easier to achieve the goals of having sticky headers. This is likely to be the best solution and could just go ahead with this now but means we need to spend time building the functionality to update the AppState during runtime, so will postpone for the time being.
Feel free to edit/comment:
Prototype of #opensource GTFS data viewer + editor. Early stages of development. Anyone interested in public transport and building #rustlang skills? We're looking for contributors! For further info get in touch, me and @CarlinoDustin will point in right direction! https://github.com/spstreets/gtfs_manager
This is a summary issue to list all the features that are additonal to simply display the data as is in the app, in list form, and a simple map displaying the route. For example, analysis and statistics type questions users might want to ask of the dataset that isn't just simple summary statistics of the data, ie requires some significant calculation, simulation, additional data, etc.
Heads-up @maxwell8888 I got this error trying to install it just now:
Compiling gtfs_manager v0.1.0 (/home/robin/.cargo/git/checkouts/gtfs_manager-428d2c6132e01227/67f20d1)
error: there is no argument named `children_type_name`
--> src/views.rs:923:44
|
923 | format!("Number of {children_type_name}s")
| ^^^^^^^^^^^^^^^^^^^^
error: there is no argument named `name2`
--> src/views.rs:1925:51
|
1925 | .with_child(Label::new(format!("Number of {name2}s")).with_font(ANNOTATION))
| ^^^^^^^
error: could not compile `gtfs_manager` due to 2 previous errors
warning: build failed, waiting for other jobs to finish...
error: failed to compile `gtfs_manager v0.1.0 (https://github.com/spstreets/gtfs_manager#67f20d18)`, intermediate artifacts can be found at `/tmp/cargo-installqFCjqt`
Caused by:
build failed
currently just using stop positions. Use actual shapes when available.
Will need to be able to fall back to just interpolating stops not just for when shapes.txt is not provided, but when a new stop is added, or arguably any changes to the stops in a trip is made (since we can't necessarily assume the bus will take the same route).
Show distribution of buses on along a route at a given time. For SP this will be estimated since specific depart times are not provided, only headways.
This could become messy and hard to display for overlapping routes, eg in the city centre.
Add functionality which would allow dragging to move the individual coordinate points that make up the shape, and also adding or removing points.
In the same way that selecting a route on the map will scroll to that route in the list, users should be able to click on a stop on the map which will make visible a corresponding stop widget in the list.
Only show 1 stop at a time, the selected stop. Being able to scroll through a long list of stops doesn't seem to add much value like a long list routes does.
Though it would be easy to allow users to select multiple stops (Ctrl + click) if they wanted to build a longer list for comparison, though this doesn't seem very important and means more UI space would need to be reserved for a list of stops rather than singluar. Will definitely want to support users being able to select exactly two stops for specifying journeys, though this still doesn't mean we need to display both stops simulatanesouly and will probably use a different UI for this type of journey planning.
Selecting a different stop for a stop_time: The most user friendly way would be to just click on the map, which is easy to implement. But will eventually probably want a way to do this purely in the list, rather than needing to use the map? In which case this is TBD.
When clicking on a stop used by multiple routes, there is no clear associated route or stop_time to display, unless maybe a route has already been selected so the user is specifically selecting a stop for that route.
Having a long list of stops is one option, and clicking on that stop could scroll to the corresponding widget. However, the number of stops can be very large and even if we hide most of the widget behind expanders as described in #8 there is still likely to be performance problems. It also makes more sense to initially show the stop_time from which the stop can be naviagted to, since the inverse is not really possible - nope it is not always clear what route clicking on a stop relates to. There doesn't seem to be any other sensible way to group/subset stops other than by route, or maybe alphbetically by first letter of name?
There doesn't seem to be much need for having all stops in one list, and makes more sense to only display individual stops
The only exception is selecting a new stop for a stop time.
It seems like it would be nicer to be able to add a stop at the route or shape level, and then trips/stop_times select from the routes/shapes subset of stops when updating stops, but the gtfs data structure doesn't really support this which would likely make it hard and convoluted to implement.
Is it possible to clearly display all the corresponding routes/trips/stop_times for a particular stop? Possibly not in the list, only on the map? But even on the map it will not be clear since different routes for the same stop will of course overlap and not be easily differentiable - but only for that section of the route? Maybe displaying a list of all route names that use that stop in the stop widget would be best?
Will need to make it clear that changes to a stop will affect other routes also referencing that stop. Maybe want editing functionality that will automatically create a new copy of the original stop, allowing a stop to be easily updated only for a specific route.
Avoid recalculating paths every time zoom level changes.
We will be able to use the same paths for drawing to bitmaps for caching and canvas for immediate mode. The problem is that for the bitmaps, each zoom level requires a bitmap with a different path width, so the bitmap needs to be recreated. This will avoid recalcuating the binning (though the binning relies on knowing the path width, we can just make bins using the max path width), but will still be too slow for continuous scrolling (before immediate mode kicks in). Could possibly have 10 or so cached bitmaps at different zoom levels to allow somewhat smooth scrolling.
If we add a new stop outside of the original latlong coordinate bounds, it gets clipped when making the bitmap since they are sized to the original bounds.
This isn't immediately important until we have a background providing a reference as to where to add a stop.
!visible
or deleted
, then so are all children of that item. but the children won't automatically have them same flags applied.!visible
or deleted
), draw selected, else draw normal (or hover but don't need flag for this)so filtering can be like applying (!
)visible
to trips, or a selection?
With filtering we might want to filter the trips, and then select one, in which case filtering can't just be multi-selection
sometimes we might want a subset, where we can still select items, but also see all trips so we can see the filtered relative to the other trips (automatically hiding the other trips would be annoying for users in this case). But sometimes we do actually want to hide the non filtered stops to make it easier to concentrate on a subset.
filtering to all trips in an agency/route is equivalent to selecting that agency/route?
Do we want to support multiple simultaneous filters? eg trips from a specific agency that run on a certain day.
For (each?) filters, just simply have a switch to control whether the non filtered trips are displayed.
The selection of a stop, will also apply a filter for related routes, from which individual trips can be selected. Would want selecting a trip
types of filter:
Trips that run on a certain day - subset - but also might be nice to show everything so we can see the filter relative to all trips
Trips that are running at a specific point in time - subset
Deleted trips - subset - or maybe grey out the deleted ones
Not deleted trips - subset (because then want to be able to select other trips)
Trips which use a certain stop - subset (but would be the same as selecting a stop?)
The trips required to make a journey between two stops - maybe subset but more ideally a special type of selection
Look into clipping the ctx so the map nevers paints outside it's layout size.
Say you open the list of routes for an agency, then scroll down through those routes, the agency should remain visible, similary for trips and the stop_times.
The max resolution bitmap it seems to be possible to use to cache the map is around 5000 * 5000, anything over this and performance will degrade or the app will crash. Zooming into the bitmap past around 5x we loose clean lines, however at this point it should be possible to draw the map directly on every frame instead of using a bitmap cache, since only a small proportion of the map is being drawn.
Just tried to install it and I get:
cargo install --git https://github.com/spstreets/gtfs_manager
Updating git repository `https://github.com/spstreets/gtfs_manager`
Installing gtfs_manager v0.1.0 (https://github.com/spstreets/gtfs_manager#e101b0d3)
Updating crates.io index
Updating git repository `https://github.com/linebender/druid`
error: failed to compile `gtfs_manager v0.1.0 (https://github.com/spstreets/gtfs_manager#e101b0d3)`, intermediate artifacts can be found at `/tmp/cargo-installt2x6qJ`
Caused by:
failed to get `gtfs-structures` as a dependency of package `gtfs_manager v0.1.0 (/home/robin/.cargo/git/checkouts/gtfs_manager-428d2c6132e01227/e101b0d)`
Caused by:
failed to load source for dependency `gtfs-structures`
Caused by:
Unable to update /home/robin/.cargo/git/checkouts/gtfs_manager-428d2c6132e01227/gtfs-structure
Caused by:
failed to read `/home/robin/.cargo/git/checkouts/gtfs_manager-428d2c6132e01227/gtfs-structure/Cargo.toml`
Caused by:
No such file or directory (os error 2)
This is normal and expected behaviour when interacting with maps. It is also especially noticable for double click zooming, so is required before that can be added.
Hovering a trip should highlight it and bring up sufficient information to help identify it, clicking it "selects" that trip and should that trip in the list. It is much easier to just ask the user to click a trip to bring up it's info, and avoids the situation where multiple trips are being hovered simultaneously.
Selecting an agency/route/trip/stop_time/stop in the list will also select it on the map and cause the previous item to be deselected.
Currently if multiple trips are being hovered and the user clicks, the "first" arbitrary trip is the one selected. This makes it harder to select the desired trip, especially if it follows the same path as another trip which comes "first". Maybe better to select all trips, then let the user narrow down from the list?
Provide some controls for resizing/zooming the map around the selected route, but don't do this automatically. Maybe double clicking the route zooms to it?
Esc (and clicking outside the map?) delselects any selected route.
Interaction with the line drawn on the map will not differentiate between trips, since in most cases all trips have the exact same shape, therefore any functionality that tries to differentiate between different trips would ususally be redundant. In order to only display a specific trip on the map (which provides more info than the route display, ie direction of travel and only the stops for that trip) users select the route first, and then use controls to choose which trip they want to highlight (either from the route widget in the list which is auto-scrolled to, or the route info that is brought up on the map itself).
Hovering a list item should highlight it on the map.
Will need to confirm with user testing, but I suspect that users will largely use the tool in a map driven way, so discovery of eg routes from the list will be far less important since mostly routes will be discovered from selecting on the map, or found from being associated with a selected stop. In this case the focus of the list should be optimising for presenting info clearly, rather than discoverability. However, should still be able to navigate and investigate data easily in the list alone? This would be especially useful on small screen where the map becomes less usable, also would likely be necessary for accessibility.
Nested layout forces us to only show hierarchical subsets. With columns, we would be able to list for example all trips, which could be useful for certain use cases, but would need virtual scrolling eg for stop_times.
Why/how would you use the list for navigation? Info used for navigation:
location, eg selecting an agency/route/trip/stop based on it's position on the map, so list is not used.
Agency: name, also how many routes?
Routes: short_name or long_name and color (sometimes), how many trips?
Trips: ?headsign, ?short_name, how many stops times?
Stop times: stop sequence
The problem with separate columns is they will often be redundant, eg only 1 agency, 2 trips don't requrie entire columns so will need to make use of space below some how. could just have map below, but then might have a long list of stop_times bang in the middle of the map... of course some datasets have many agencies and many trips, which is when the nested list becomes less usable, also as long as the columns are narrow stop_time's needn't be in the middle of the screen.
Nested list is compact, only using one column. Using separate columns uses up much more space, but could potentially find a clever way to hide them when required
Should work the same in both cases?
Trips is the main thing we will want to filter, but the current design for columns won't allow display multiple trips from different agencies/routes. Even though the nested list would support this it would be very user unfriendly since it would potentially require scrolling through many agencies/routes to get to the next trip; all selected trips will not be immediately visible.
Maybe don't apply filters to list (for the time being?), only to map map, from which users can select individual trips for more info?
How would sticky headers work with multiple trips?
Which would work best on a small laptop sreen or even a mobile phone?
Users could find nested list confusing to navigate since only parts are visible at a time, even with sticky headers, which might also be very hard to implement. Separate lists are very clear and easy to understand what is selected, to what level (eg only a route vs a stop_time in a trip in a route), what the parents are, and to quickly change to a different parent at any level.
Nested lists are nice in that they make clear the hierarchical nature of the data, but this is probably already known/common sense/immediately obvious for most users anyway, and the column strucutre also is probably pretty intuitive.
In general either allowing multiple expansions for distinct items, or auto collapsing previous expansion, are both clunky so I prefer separate columns.
Having separate columns means they could be labelled: agency - route - trip - stop time, which would be handy because it will not always be clear what you are looking at just by having ids. Could probably work something similar into a nested layout.
A nested layout makes it clear that a list of trips is only the trips for the previously selected route, not all trips.
When a trip(s) is selected on the map, it will need to be revealed in the list. Implementing this in the nested list is harder but should be very doable. More importantly is can we instantly show all the data/info the user might be interested in, eg parents, children, without needing more clicks/expanding.
Separate columns only have one sublist of each item type shows at a time, which simplifies the required data structure and load/drop mechanism.
Users expanding to many sections could use too much memory and crash the app, would need to find a solution to mitigate this is the long term.
Add a web version hosted on GH Pages to make it easier to share progress and for others to demo the app.
Note: Piet doesn't currently support .capture_image_area()
for the web backend so will need to either implement it myself, or not use .capture_image_area()
for the web build.
Consider doing this after the end of the project since I will end up doing a lot of the prerequite learning anyway for my other projects, which avoid using up project time on this learning.
Storing the full vector of ~90k MyStopTime
s, using plain String
s and not Arc<String>
in the AppData is not problem.
Filtering this list for a given trip_id on demand is also fine and doesn't produce any perceptable delay. Whether this is done by reassigning a new list using .iter().filter() in a AppDelegate, or using a FilteredList widget make no perceptible difference for stop_times.
So there is no performance problem with storing or filtering the lists, the performance problem seems to come entirely from creating the widgets which is why the routes list is the only one with a perceptible delay because the list is around ~1300 long of semi complex widgets. Where as stop_times is only ~30 items long. Widget complexity * length of list is the critical thing to manage.
Currently, opening the Sao Paulo data with the app will cause it to crash. This is because the widget for a route is now more complex, containing lot's of Textbox's etc to allow updaing route attributes, and the number of routes in the SP dataset is around 1300, and the app becomes unusable at about 200 routes.
making inital data appears to only use around 200mb memory, implying the problem is with launching the app and creating widgets etc. This might not be right as 200mb seems quite low.
TLDR: Chosen solution is to keep the long list of routes but hide most of the widget controls behind dropdowns.
Storing all the routes and their attibute data in the app state doesn't seem to be a problem, only creating too many widgets for the data, which is convenient because the former would be a much harder problem to fix.
A simple solution would be to paginate long lists like routes. This is flexible and doesn't put any contrainsts on how complex the lists can be since we can reduce the length of each page until sufficient performance is achieved. The downside is that it would complicate syncing selections of items on the map with selections of items in the lists.
The second solution is to keep the long lists but reduce the complexity of the default route widget to the extent that displaying 1000's of them is still performant. This less flexible than the first solution but is easier to implemenent and is reasonable since the initial list of routes shouldn't display too much information or provide too much interaction anyway.
Arguably, very long lists don't provide a good user experience any because at a certain point they become too long to scroll manually with say a scroll wheel, and the scroll bar control becomes too small/sensitive to be used easily. However, the lists of routes should be the longest list displayed by the app and it is unlikely that other datasets will exceed SP's ~1300 (I believe London only has around 600, and larger areas will likely be spread across different agencies), and this number is perfectly usable.
In any case users will probably want some way of more easily finding a route they are interested anyway, rather than scrolling through and eyeballing 1300 cases, like selecting it from the map, or searching by name or some other criteria.
I'm uncertain whether widgets are dropped when they loose their visibility in an Either
widget. If they don't then the app could end up crashing after the user has opened enough lists, even if opening a new list automatically closes the previous list.
Initially a journey will just be selecting two stops, and then extra information can be provided calculated from these two stops.
eg:
which combinations of trips can be used to travel between them
which is the fastest route - will depend on time of day?
what are the stops along the best route between them
Seems like the problem is the search radius is too large so we are picking up too many stops and only the first stop is highlighted, which might not be the one expected/closest to the mouse.
Make sure a panning drag which ends in minimap does not result in the minimap handling the mouse up event and changing the focal point.
A declarative, efficient, and flexible JavaScript library for building user interfaces.
๐ Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. ๐๐๐
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google โค๏ธ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.