Coder Social home page Coder Social logo

cldellow / datasette-ui-extras Goto Github PK

View Code? Open in Web Editor NEW
12.0 12.0 1.0 1.55 MB

Add editing UI and other power-user features to Datasette.

License: Apache License 2.0

Python 67.20% CSS 4.96% Shell 0.25% JavaScript 27.25% HTML 0.33%
datasette datasette-io datasette-plugin

datasette-ui-extras's Introduction

I like all things data. I've worked with data my whole career, from things that would fit in a Tweet all the way up to multi-petabyte data warehouses running in the cloud.

Currently, I'm a founder at SyncWith. We want anyone to be able to get their data into Google Sheets and Looker Studio.

As a hobby, I'm dabbling with making small datasets much more accessible via SQLite and Datasette plugins.

You can find me on the web at cldellow.com or as @cldellow on Hachyderm.

datasette-ui-extras's People

Contributors

cldellow avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar

Forkers

meowcat

datasette-ui-extras's Issues

add `current_actor()` SQLite function

I'm not sure how achievable this is. #49 explores creating something like updatable views for the turker use case. It'd be handy to have a trigger that can set last_edited_by = current_actor(), where current_actor() is evaluated in the context of the current request to get the user's ID.

Can ... can we do that?

Simon mentioned maybe you can abuse asgi_wrapper to get access to the request: https://discord.com/channels/823971286308356157/823971286941302908/1070385463640215702

serve concatenated js/css

We currently serve N CSS/JS files. Instead, we should transparently concatenate them into a single, fingerprinted file with a long-lived expiry date.

facets: make `Facet by this` column option smarter

Split from #21

If we remove facet suggestions, we no longer have a way to facet by date, or by array.

It would be nice if we could customize the cog to show those options -- right now it always forces column faceting.

I think there isn't an official way to hook column actions yet: simonw/datasette#983 (comment)

We might be able to party in https://github.com/simonw/datasette/blob/main/datasette/static/table.js#L1

...although discovering which column we're connected too will be a pain. I think we'd have to parse the absolute positioning then reverse engineer which column it is

This also means mobile users won't be able to control facets, I'm ok with that for now -- plus we might make the omnibar smarter, which would mitigate this a bit.

re-jig facets

Proposal

We'll monkey-patch the existing ColumnFacet, DateFacet, ArrayFacet.

A pragmatic way to get started:

  • ui: move facets to sidebar, see #9
  • ui: when rendering in the HTML context, don't compute facets
  • ui: add some JS that fetches facets via the JSON API and updates the sidebar
    • ideally, we only compute facets? I think we can pass nocount, nosuggest to disable a bunch fos tuff, maybe we can also pass pagesize=0? see code

      • passing _size=0&_nocount=1 is a start, although the size gets bumped up to 1 (to discover pagination? special case of 0!)
    • ideally we make 1 call per facet, so we can begin rendering as soon as any data is available

    • it'd be nice if we could reuse _facet_results.html -- but not necessary for an MVP

  • ui: rewrite toggle_url - strip .json, _nocount, _size
  • ui: support facet truncation
  • ui: make facets OR (eg you can pick multiple options for each, via a checkbox)
  • ui: format numbers with thousands separator
  • ui: show spinner when loading facets (...need to make facets slower to compute :)
  • backend: make the list of __dux_facets driven by metadata and qs params
  • ui: insert facets in order, even if responses come back out of order
  • ui: add explicit column options to facet by each of the facet types
  • backend: compute facet suggestions once per table; re-use them regardless of WHERE clause, OFFSET, LIMIT, etc
    • if we could trace lineage of columns through arbitrary queries (see simonw/datasette#1293), this could be re-used for deciding how to facet columns in such queries
    • this might be as simple as tracking COUNT(*), COUNT(column), COUNT(DISTINCT column), MIN(column), MAX(column), SUM(column), TOTAL(column), AVG(column). Or it could be as complex as tracking the actual sets of distinct values (needed for #3, #14). Probably start with the simple summary statistics.
  • backend: normalize WHERE clause to remove rowid filter when computing facet results
    • might be able to do this in the UI actually, by stripping out the _next parameter
  • backend: cache facet results (insight: maybe TTL or willingness to stale read should be a function of how long it took to compute the result)

Background

Facets are really cool. They're complex and present many tradeoffs. There's no one right set of choices that can satisfy everyone. I think Datasette's current approach with them is fairly conservative, which is a sensible choice for Datasette, the platform. For my own tastes, I'd like them to behave a bit differently. And since this is a plug-in, YOLO, let's take a wildly different approach.

What I like:

  • Plugin interface: register_facet_classes and filters_from_request permit a wide array of shenanigans.
  • Plumbing: there's a ton of machinery to make facets work, to serialize parameters, to render it. I think most of this can be re-used. In fact, the exposure of facets via the JSON API will be key to enabling lazy-loading of facets.

What I'd like to adjust:

  • Facets being opt-in: I'd rather see all the facets, and maybe hide them if needed.
  • Add some extra facets: faceting by month or by year might be nice; faceting by buckets for numeric
  • Be able to sort facets either by frequency or by label: humans likely expect some things to be sorted by label. You might also do a blend - sort by frequency for the top 3 items, then sort by label.
  • Facets being above-the-fold, in-line in content: I'd rather it be in a sidebar
  • Eager loading blocks render of the main data: I'd be OK with them loading in after-the-fact as an Ajax call -- if they were in a sidebar, this wouldn't cause reflow of the main table.
  • Facets results include their filters - if you have a Country filter with 5 options, and you select 1 of them, the other 4 go away. I'd like the country selection to limit the results shown by other facets (eg the list of states), but it should still let you add other countries.
  • Pagination affecting facets: eg compare page 1 and page 2. IMO, the facet counts should be the same.
  • Facets are slow: 3 facets on global-power-plants takes ~2 seconds to render. If you enable tracing, you can see that it's the facet queries that are slow. I think there are a few culprits:
    • doing a query for each facet is slow. SQLite's VM is very naive, stepping over each row is very expensive. Scanning the table N times for N columns, 1 at a time is much slower than scanning the table once for N columns.
    • the facet suggestion queries are slow. In some cases, I think this is because they're buggy -- they're meant to look at the first 100 rows, but are actually doing a full table scan in the common negative case because the LIMIT clause applies after the WHERE clause, see DateFacet
    • stepping back - deciding if a column is a candidate for a facet is information that never or rarely changes. Doing it on every page view seems very inefficient, especially if it blocks render.
  • Facet queries aren't cached - For my use case, a common scenario is landing on the main table page. This is probably 80% of views. If we cached only this, and ignored the long tail of filter permutations, we'd get a big boost. Another good reason to cache it: the main table page is unfiltered, so it's also the largest set of data, and slowest for which to compute facets
  • Facets aren't guaranteed to make progress - the FARA table has a different set of pages time out each time I refresh it. :( It'd be nice if the progress that was made contributed towards future refreshes, or if a background thread could make progress on it so a future refresh found it.

That's a big list! They don't all depend on each other. This ticket is primarily to explore the performance side of things.

Edit UI (DML)

This issue lays out the vision for the editing UI that datasette-ui-extras will provide. See #54 for the DDL version.

Editing is only for SQLite databases, not DuckDB.

Editing will use the write API introduced in Datasette 1.0, so you'll need to be on Datasette 1.0.

My goal is to bring an automatic, pluggable, user-friendly UI that enables these use cases: traditional data entry, turker mode, and forms.

Non-goals: supporting JavaScript-disabled browsers.

UI Attributes

Automatic

Don't make users define redundant mappings. Lean into the structure that SQL provides us.

Use SQL foreign keys and CHECK constraints to define what is permissible and drive UI control selection.

Because SQLite is untyped, we'll have to sniff rows and/or use heuristics in some cases.

Things we'll aim to support:

  • you can't edit primary keys
  • foreign keys
  • CHECK constraints of the form x IN (...)
  • ISO dates
  • ISO timestamps
  • DEFAULT values (eg, when inserting a row, we'll try to prepopulate with what the DEFAULT would be, to hint that you don't need to fill it in)
  • JSON string arrays

You can declare a SQL VIEW to further control the user experience. Imagine that you have:

CREATE TABLE reviews(
  id integer primary key,
  url text not null,
  review text not null,
  rating text (check rating in ('negative', 'positive')),
  rated_at text,
  rated_by text
)

You'd like to have some contractors fill out the rating field. They shouldn't have access to the url field. They should only have access to rows that still need a rating. When they rate something, you'd like to automatically track who rated it and when. Oh, and you'd like to give some instructions.

You can do this by creating a view, and giving them access to that:

CREATE VIEW needs_rating AS
/* set rated_by=current_actor() */
/* set rated_at=datetime() */
SELECT
  id,
  'What is the sentiment of the review? If unsure, choose positive.' AS instructions,
  review AS review_readonly,
  rating
FROM reviews 
WHERE rating IS NULL

Only two unaliased columns from the base table are present, and thus candidates to be editable. id, however, is part of the primary key, and so only rating is editable.

When the user submits their entry, the set statements are automatically executed, tracking who edited the row, and when.

Pluggable

We'll try to render sensible controls. Sometimes we might get it wrong -- perhaps we'll render an input field that expects a number, when it really ought to have been a checkbox that stored 1 for checked and 0 for unchecked.

You can override us by implementing a plugin hook:

@hookimpl
def edit_control(datasette, database, table, column):
  if column == 'name':
    return 'ShoutyControl'

ShoutyControl must be a JavaScript class that is available to the page. This can be a pre-defined one provided by datasette-ui-extras or one you author via a file loaded by extra_js_urls or inlined by extra_body_script

The class should conform to this interface:

class ShoutyControl {
  constructor(db, table, column, initialValue) {
    this.initialValue = initialValue;
    this.el = null;
  }

  // Return a DOM element that will be shown to the user to edit this column's value
  createControl() {
    this.el = document.createElement('input');
    this.el.value = this.initialValue;
    return this.el;
  }

  get value() {
    // Be shouty.
    return this.el.value.toUpperCase();
  }
}

TODO: consider if the interface should have an isValid function, and a way to signal that its value has changed (for example, to permit "autocommit on blur" modes)

TODO: document how you might reference other columns. eg, say you have text The quick brown fox jumped over the lazy red dog. in column A, and you want the user to annotate it and have those annotations show up in column B as [{"substring": "jumped", "label": "verb"}]

User friendly

We'll try to show a good control. For small, closed sets, we'll use a drop-down. For larger sets or open sets, an autocomplete combobox.

For the turker use case, we might render a space-inefficient control that has key-bindings that permit you to quickly advance through a dataset, eg from https://prodi.gy:

Selection_405

Use cases

The use cases don't assume any particular access scheme. Some scenarios may be only authenticated users, some may permit anonymous users (eg forms). See the Authentication, authorization and auditing section for more.

These are imagined as alternative layouts for the row view, eg the pages located at /db/table/1, /db/table/2 and so on.

Traditional (add new + edit existing)

It's the typical vertical layout of column name, UI-control-to-specify value.

You might be in auto commit mode, or you might have to click an explicit Save button.

Clicking Save keeps you on the current row page.

Example: curation of detailed data, ability to deep-link in workflows.

Forms (add new)

You can submit new rows, but not read, edit or delete existing rows.

After submitting you are redirected to a customizable URL. By default, you are sent to the new form submission page.

Example: collecting feedback from the general public.

Turker (edit existing)

You can edit a subset of fields on a subset of existing rows, but can not read or delete other rows.

After submitting an edit, you are advanced to the next row that needs editing.

You might want affordances to enable very fast editing -- for example, focus the first control, permit keyboard shortcuts to auto-select an answer and move on.

Example: contractors who are doing piecemeal data entry, trusted internal staff doing manual annotations.

Authentication, authorization, auditing

This is all delegated to other systems.

Authentication is handled by Datasette's actor system.

Authorization is handled by Datasette's permission system.

There's no built-in support for auditing. If you'd like to track which users created/updated rows, use the datasette-current-actor plugin and create suitable DEFAULT values or trigger functions.

Open questions

Implementation notes

  • You can configure which tables are editable by default in metadata.json, or activate edit mode with ?_dux_edit=1

  • We'll need a hook to create/update INSTEAD OF triggers for editable views.

Rejected ideas

Spreadsheet mode

The table view would let you update values in-place, without navigating to the row page for each entry.

Rejected because I think I can't do a sufficiently good job on the UI: it likely won't be an actual spreadsheet view with a seamless grid and resizeable columns. More likely, it would have inline controls that automatically commit changes via ajax.

facets: make them behave like e-commerce facets?

Split from #21. I'm not convinced this is a good idea yet.

Imagine two facets, gender and country. They start like:

Gender
 - Male (90)
 - Female (70)

Country
  - Canada (100)
  - USA (50)
  - Mexico (10)

If you pick USA, the facets change to:

Gender
 - Male (25)
 - Female (25)

Country
  X USA (50)

Could they instead change to:

Gender
 - Male (25)
 - Female (25)

Country
  - Canada (100)
  X USA (50)
  - Mexico (10)

i.e. show what you have selected, but also show the other options within the same facet.

This would let you create ORs, by picking multiple countries, eg USA and Mexico.

This might get pretty weird if you have multiple facets active.

search omnibox

When we have stats for the table in dux_column_stats, let's enhance the search box to help users build structured queries.

We'll render it if it's absent -- extra_template_vars could pass supports_search=True).

The spirit of what I want, using https://dux.fly.dev/superuser/posts as an example:

Typing linu suggests:

Typing 2022 (or 2022-01-08, or 2022-03, with language tweaked as needed) suggests:

  • ...creation_date in 2022
  • ...creation_date after 2022
  • ...creation_date before 2022
  • ...and other date time columns

Prioritizing the options to show might be hard--probably want max 10 (this is what google.com uses). We'll try to do something reasonable about which columns to include (maybe we should use the currently displayed columns as a starting point?), and we should let the user override the default case.

table: hide filters by default

There is already an attempt to show a read only summary of them. Editing them is rare, plus you can edit some of them by facets today, and by the omni bar eventually.

Maybe a pencil icon to display them, and it toggles the presence of a _dux_edit_filters params.

column cog icon - only show on hover

The cog takes up a lot of horizontal space.

Maybe:

  • display: none by default
  • display:inline-block on hover (maybe on hover of the whole row?)
  • position:absolute so it doesn't reflow
  • have the cog on the first column always visible as a hint that you can do things

add statistical summary facet

Show min, p50, p90, p95, p99, max

When you add it, we won't be able to correctly tell that's what you did, unless we do something special. e.g. adding response_time > 500 will change the population on which we're calculating the statistics. We could add a __dux_stats=XXX parameter that we use to freeze the facets.

But then that won't update if you refine the other filters. Maybe that's OK?

Also unclear what the default action should be, eg filter to things higher than it? Lower than it? Wouldn't you just... sort?

Maybe the toggle URL should just be # and we never return selected = True?

filters: autosuggest filter values

related to #3 - #3 is about when you have to consult an fkey table for the label, this is about when the raw value is present

https://global-power-plants.datasettes.com/global-power-plants/global-power-plants?_sort=rowid&primary_fuel__exact=Solar&country_long__in=Canada%2C+United+States+of+America

When typing in the filter box, it'd be nice if it auto-suggested options

This might require some storage in order to remember candidate values (running SELECT DISTINCT xxx FROM table is likely slow)

explore: can the row page support views?

As part of #48, it might be valuable to let users define a subset of a table as a view. For example, if you have:

CREATE TABLE data(
  id integer primary key,
  value text,
  approved integer,
  approved_at text,
  approved_by text
)

You might want to create a queue of records-to-be-reviewed, like:

CREATE VIEW needs_review AS
SELECT
  id,
  'Look closely at the value. Do you approve it?' AS instructions,
  value AS value_aliased_so_as_not_to_be_editable,
  approved
FROM data
WHERE approved IS NULL

That view declares that you should be able to edit the approved field. id isn't editable, as it's part of the pkey. The other two columns are aliased, and so aren't editable.

Then, you'd use SQLite's INSTEAD OF trigger to permit updates against the view. You'd probably want some jazz to create these triggers automatically, so users can get by with basic SQL understanding.

Questions:

  • Could we inject outside context into the update trigger? eg say we wanted to set approved_at and approved_by in a somewhat natural way
  • Can we coerce Datasette into supporting the row page for views, eg https://dux.fly.dev/cooking/posts/1 works but https://dux.fly.dev/cooking/questions/1 gives Row not found: ['1']. It looks like these two things need to be fixed:
    • It tries to search for rowid = 1, because it falls back to rowid, see row_sql_params_pks, we'd need to teach db.primary_keys(table) to be smarter, which is really detect_primary_keys. I think that's doable.
    • It fails to find foreign keys in views/row#foreign_key_tables. We'd want to teach this to be a bit smarter - every view should have an entry, views that are simple selects from a single table should have the columns in common.
  • Can the JSON API support views? The code seems to check that it's a table
    • RowUpdateView#update_row delegates to sqlite-utils, which fails with 'View' object has no attribute 'update'. Compare View vs Table#update
    • ...I tried to patch this to re-use the update from Table... I get an AssertionError with an empty message when the exit functon of the sqlite3.Connection context manager runs. What's going on. Ah, it's the assert rowcount == 1, which I guess doesn't apply for INSTEAD OF triggers

facets: permit collapsing facets

Related to #9, although could be done with facets in the ATF position, too.

Have a control that hides the facet pickers. Persist whether it's hidden in localStorage, perhaps per database/table.

mvp of Edit UI: update existing rows

Build out the MVP of #48:

  • know if the page is editable
    • URL must be a row page
    • ?_dux_edit=1 query parameter present (in the future we can define via metadata)
    • user has update-row permission on this table
  • render an edit-row template instead of the row template
    • we can re-use much of the row template via jinja magic, see Datasette docs, {% extends "default:row.html" %}
    • it should emit a <form> that wraps all the controls
    • the form's submit event is intercepted and handled by us
    • there is a submit button
    • clicking the submit button tries to send a write API update call
  • render_cell should emit an edit control if the control is editable
  • implement a StringControl that is just a vanilla text input
  • write a function that determines the set of columns that are editable (for tables: all but pkeys; for views: all unaliases columns from base table except pkeys)

better support for wide tables (set a view as default)

simonw/datasette#596 (comment)

Once you've customized your view, eg https://global-power-plants.datasettes.com/global-power-plants/global-power-plants?_nocol=gppd_idnr&_nocol=other_fuel2&_nocol=owner&_nocol=other_fuel3&_sort_desc=capacity_mw&_nocol=other_fuel1

It'd be neat if you could do two things:

  1. as an end-user, flag it so that going to https://global-power-plants.datasettes.com/global-power-plants/global-power-plants in the future redirected you to that customized view
  2. as the Datasette owner, flag it so that becomes the default view for users

(1) could be done with localStorage if we're willing to be inefficient (we'd load the page; then immediately redirect the user on the client-side); or cookies (but we might be likely to blow through the 4KB limit pretty fast). It would be best to do this with persistent storage. The preference could be linked either to an ephemeral identifier stored in a cookie, or your identity as an actor.

(2) would require persistent storage in the DB, maybe a hidden table

track statistics about columns in `_dux_column_stats`

I'd like non-programmers to be able to use datasette-ui-extras successfully to build edit UIs. A challenge with SQLite is that its untyped/flexibly typed.

We'd like to recognize:

  • Dates like 2021-12-03 (eg date())
  • Date/times like 2021-12-03 01:02:03 (eg datetime())
  • Date/times with UTC flag like 2021-01-04T21:26:30Z
  • Date/times with more precision and no timezone: 2020-05-01T16:10:12.469751
  • Date/times like 2022-11-11T21:44:21+00:00
  • Seconds since the epoch like 1575765142 (question: is this a date or a date time? If it's a date, what TZ is it in?)
  • Array of strings (these should also be summarized as fixed sets)
  • Array of ints
  • Fixed sets smaller than 100 unique items
    • Store popularity info, and whether we observed more than 100 items
    • The threshold of 100 should be configurable

Proposal:

  1. We'll collect stats on every column. We'll exclude virtual tables and tables that start with _.
CREATE TABLE _dux_column_stats(
  table text not null,
  column text not null,
  type text not null,
  nullable boolean not null,
  min any,
  max any,
  computed_at text not null default (datetime()),
  limit integer, -- Was this based on SELECT *, or SELECT * LIMIT N ?
  distinct_limit int not null, -- How many distinct examples were we willing to capture?
  distincts text not null, -- eg [{ value: 123.1, count: 123}]
  json_each_distincts text not null, -- same shape as distincts, but the contents of JSON arrays
  nulls integer not null, -- the output of COUNT(*) FILTER (WHERE TYPEOF(column) == 'null')
  integers integer not null, -- as above, but integer
  reals integer not null, -- as above, but real
  texts integer not null, -- as above, but text
  blobs integer not null, -- as above, but blob
  primary key (table, column)
);

That ought to be enough for us to determine the serialization format of a column, eg whether it's seconds since the epoch or ISO timestamp with T or with space.

  1. On startup, we'll ensure the table exists for every attached writable database. If the schema isn't exactly what we expect, we'll drop and recreate it. This is super opinionated! Maybe we'll have an opt-out knob later, but even then, it should be on by default so that it's easy to use.

  2. We'll be able to fetch stats on demand. If there's no entry in the table, we'll do a minimal scan based on WITH small AS (SELECT "column" FROM table LIMIT 1000) SELECT ...

  3. In the absence of stats, we'll assume that BOOLEAN is a checkbox (0/1), DATE is DATE() and DATETIME is DATETIME().

  4. Whenever we do a minimal scan, we'll also queue a full scan to happen in a separate thread. Basically, things should trend towards being accurate. To start, we'll just pick a high enough N that this generally works for common end-user scenarios.

  5. This table should be configured as a hidden table so it does not appear for the end-user.

Improve cacheability of facet requests

We compute facets by making JSON requests, one per each facet.

For example, faceting by creation_date and tags results in these two requests:

https://dux-demo.fly.dev/superuser/posts.json
  ?_sort=id
  &_facet_year=creation_date
  &_facet_array=tags
  &post_type__exact=question
  &_size=0
  &_nocount=1
  &_dux_facet=_facet_year           <--
  &_dux_facet_column=creation_date  <--
https://dux-demo.fly.dev/superuser/posts.json
  ?_sort=id
  &_facet_year=creation_date
  &_facet_array=tags
  &post_type__exact=question
  &_size=0
  &_nocount=1
  &_dux_facet=_facet_array <--
  &_dux_facet_column=tags  <--

Even though each request is only for a single facet, we include both _facet_year=creation_date and _facet_array=tags in both requests so that the URLs that get constructed are correct.

If you remove the creation_date facet, the request for the tags facet will have a different set of parameters:

https://dux-demo.fly.dev/superuser/posts.json
  ?_sort=id
  &_facet_array=tags
  &post_type__exact=question
  &_size=0
  &_nocount=1
  &_dux_facet=_facet_array
  &_dux_facet_column=tags

This means that even though we have a perfectly good cached response for this facet, we won't be able to use it.

A possible alternative would be to omit all _facet_* parameters except the facet under consideration, and then teach fixupToggleUrl how to re-add the other columns.

A good solution would also re-add the deleted parameters in the same location to ensure a high cache rate. This feels pretty involved for what is probably not that big of a problem.

be able to see table/view definition

I hid this in #22

...and now I regret it, as it's useful to see a view definition. eg on https://dux.fly.dev/cooking/questions

If the default HTML had an ID like schema-definitions, we could expose it via a similar trick as the advanced export, eg make it so https://dux.fly.dev/cooking/questions#schema-definitions showed the schema.

That would require a change in Datasette's default template.

Alternative idea: if we do #54, we'll expose a way to edit views. You'll be able to see the schema there.

facets: mobile ui

This is half baked, but I'm thinking something like a drawer that you can pull out, then you can swipe left/right to see facet counts.

Maybe you get a preview of the data with a scrim that obscures it unless you click?

edit UI is incorrectly shown to a user w/o update-row

https://dux.fly.dev/cooking/badges/2?_dux_edit=1 shows the UI. Since the actor doesn't actually have the update-row permission, I would have expected

visible, private = await datasette.check_visibility(
request.actor,
permissions=[
("update-row", (database, table)),
],
)
if not visible:
return None
to prevent it from appearing

I'm pretty sure the actor doesn't have the update-row permission, as trying to submit an edit results in a failure, and that checks the same thing (see: https://github.com/simonw/datasette/blob/8b9d7fdbd8de7e74414cc29e3005382669a812dc/datasette/views/row.py#L204-L206)

I don't see it locally if I write a permission_allowed hook that prevents update-row... so what's going on? I actually can see this locally -- I was testing with False, but if I test with None, it repros

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.